feat: Task 1.1 - Enforce key as primary identifier in Serie class
- Add validation in Serie.__init__ to prevent empty/whitespace keys - Add validation in Serie.key setter to prevent empty values - Automatically strip whitespace from key values - Add comprehensive docstrings explaining key as unique identifier - Document folder property as metadata only (not for lookups) - Create comprehensive test suite with 16 tests in test_serie_class.py - All 56 Serie-related tests pass successfully - Update instructions.md to mark Task 1.1 as completed This is the first task in the Series Identifier Standardization effort to establish 'key' as the single source of truth for series identification throughout the codebase.
This commit is contained in:
parent
e42e223f28
commit
048434d49c
@ -175,10 +175,10 @@ For each task completed:
|
|||||||
|
|
||||||
**Success Criteria:**
|
**Success Criteria:**
|
||||||
|
|
||||||
- [ ] `key` property has validation preventing empty values
|
- [x] `key` property has validation preventing empty values
|
||||||
- [ ] Docstrings clearly state `key` is the unique identifier
|
- [x] Docstrings clearly state `key` is the unique identifier
|
||||||
- [ ] `folder` is documented as metadata only
|
- [x] `folder` is documented as metadata only
|
||||||
- [ ] All existing tests for `Serie` still pass
|
- [x] All existing tests for `Serie` still pass
|
||||||
|
|
||||||
**Test Command:**
|
**Test Command:**
|
||||||
|
|
||||||
@ -186,6 +186,25 @@ For each task completed:
|
|||||||
conda run -n AniWorld python -m pytest tests/unit/test_anime_models.py -v
|
conda run -n AniWorld python -m pytest tests/unit/test_anime_models.py -v
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ COMPLETED
|
||||||
|
|
||||||
|
**Implementation Details:**
|
||||||
|
|
||||||
|
- Added comprehensive validation in `__init__` to ensure `key` is never empty or whitespace
|
||||||
|
- Added validation in `key` setter to prevent setting empty values
|
||||||
|
- Key values are automatically stripped of whitespace
|
||||||
|
- Added detailed docstrings explaining `key` as the unique identifier
|
||||||
|
- Documented `folder` as "filesystem folder name (metadata only, not used for lookups)"
|
||||||
|
- Created comprehensive test suite in `tests/unit/test_serie_class.py` with 16 tests covering:
|
||||||
|
- Key validation (empty, whitespace, valid values)
|
||||||
|
- Key setter validation
|
||||||
|
- Whitespace stripping behavior
|
||||||
|
- String representation
|
||||||
|
- Serialization/deserialization (to_dict/from_dict)
|
||||||
|
- File save/load functionality
|
||||||
|
- Documentation completeness
|
||||||
|
- All 56 Serie-related tests pass successfully
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Task 1.2: Update SerieList to Use Key for Lookups
|
#### Task 1.2: Update SerieList to Use Key for Lookups
|
||||||
|
|||||||
@ -1,23 +1,65 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
class Serie:
|
class Serie:
|
||||||
|
"""
|
||||||
|
Represents an anime series with metadata and episode information.
|
||||||
|
|
||||||
|
The `key` property is the unique identifier for the series (provider-assigned, URL-safe).
|
||||||
|
The `folder` property is the filesystem folder name (metadata only, not used for lookups).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Unique series identifier from provider (e.g., "attack-on-titan"). Cannot be empty.
|
||||||
|
name: Display name of the series
|
||||||
|
site: Provider site URL
|
||||||
|
folder: Filesystem folder name (metadata only, e.g., "Attack on Titan (2013)")
|
||||||
|
episodeDict: Dictionary mapping season numbers to lists of episode numbers
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If key is None or empty string
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, key: str, name: str, site: str, folder: str, episodeDict: dict[int, list[int]]):
|
def __init__(self, key: str, name: str, site: str, folder: str, episodeDict: dict[int, list[int]]):
|
||||||
self._key = key
|
if not key or not key.strip():
|
||||||
|
raise ValueError("Serie key cannot be None or empty")
|
||||||
|
|
||||||
|
self._key = key.strip()
|
||||||
self._name = name
|
self._name = name
|
||||||
self._site = site
|
self._site = site
|
||||||
self._folder = folder
|
self._folder = folder
|
||||||
self._episodeDict = episodeDict
|
self._episodeDict = episodeDict
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""String representation of Serie object"""
|
"""String representation of Serie object"""
|
||||||
return f"Serie(key='{self.key}', name='{self.name}', site='{self.site}', folder='{self.folder}', episodeDict={self.episodeDict})"
|
return f"Serie(key='{self.key}', name='{self.name}', site='{self.site}', folder='{self.folder}', episodeDict={self.episodeDict})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self) -> str:
|
def key(self) -> str:
|
||||||
|
"""
|
||||||
|
Unique series identifier (primary identifier for all lookups).
|
||||||
|
|
||||||
|
This is the provider-assigned, URL-safe identifier used throughout the application
|
||||||
|
for series identification, lookups, and operations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The unique series key
|
||||||
|
"""
|
||||||
return self._key
|
return self._key
|
||||||
|
|
||||||
@key.setter
|
@key.setter
|
||||||
def key(self, value: str):
|
def key(self, value: str):
|
||||||
self._key = value
|
"""
|
||||||
|
Set the unique series identifier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: New key value
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If value is None or empty string
|
||||||
|
"""
|
||||||
|
if not value or not value.strip():
|
||||||
|
raise ValueError("Serie key cannot be None or empty")
|
||||||
|
self._key = value.strip()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -37,10 +79,26 @@ class Serie:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def folder(self) -> str:
|
def folder(self) -> str:
|
||||||
|
"""
|
||||||
|
Filesystem folder name (metadata only, not used for lookups).
|
||||||
|
|
||||||
|
This property contains the local directory name where the series
|
||||||
|
files are stored. It should NOT be used as an identifier for
|
||||||
|
series lookups - use `key` instead.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The filesystem folder name
|
||||||
|
"""
|
||||||
return self._folder
|
return self._folder
|
||||||
|
|
||||||
@folder.setter
|
@folder.setter
|
||||||
def folder(self, value: str):
|
def folder(self, value: str):
|
||||||
|
"""
|
||||||
|
Set the filesystem folder name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Folder name for the series
|
||||||
|
"""
|
||||||
self._folder = value
|
self._folder = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
244
tests/unit/test_serie_class.py
Normal file
244
tests/unit/test_serie_class.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for Serie class to verify key validation and identifier usage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.core.entities.series import Serie
|
||||||
|
|
||||||
|
|
||||||
|
class TestSerieValidation:
|
||||||
|
"""Test Serie class validation logic."""
|
||||||
|
|
||||||
|
def test_serie_creation_with_valid_key(self):
|
||||||
|
"""Test creating Serie with valid key."""
|
||||||
|
serie = Serie(
|
||||||
|
key="attack-on-titan",
|
||||||
|
name="Attack on Titan",
|
||||||
|
site="https://aniworld.to/anime/stream/attack-on-titan",
|
||||||
|
folder="Attack on Titan (2013)",
|
||||||
|
episodeDict={1: [1, 2, 3], 2: [1, 2]}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert serie.key == "attack-on-titan"
|
||||||
|
assert serie.name == "Attack on Titan"
|
||||||
|
assert serie.site == "https://aniworld.to/anime/stream/attack-on-titan"
|
||||||
|
assert serie.folder == "Attack on Titan (2013)"
|
||||||
|
assert serie.episodeDict == {1: [1, 2, 3], 2: [1, 2]}
|
||||||
|
|
||||||
|
def test_serie_creation_with_empty_key_raises_error(self):
|
||||||
|
"""Test that creating Serie with empty key raises ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="key cannot be None or empty"):
|
||||||
|
Serie(
|
||||||
|
key="",
|
||||||
|
name="Test Series",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1]}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serie_creation_with_whitespace_key_raises_error(self):
|
||||||
|
"""Test that creating Serie with whitespace-only key raises error."""
|
||||||
|
with pytest.raises(ValueError, match="key cannot be None or empty"):
|
||||||
|
Serie(
|
||||||
|
key=" ",
|
||||||
|
name="Test Series",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1]}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serie_key_is_stripped(self):
|
||||||
|
"""Test that Serie key is stripped of whitespace."""
|
||||||
|
serie = Serie(
|
||||||
|
key=" attack-on-titan ",
|
||||||
|
name="Attack on Titan",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Attack on Titan (2013)",
|
||||||
|
episodeDict={1: [1]}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert serie.key == "attack-on-titan"
|
||||||
|
|
||||||
|
def test_serie_key_setter_with_valid_value(self):
|
||||||
|
"""Test setting key property with valid value."""
|
||||||
|
serie = Serie(
|
||||||
|
key="initial-key",
|
||||||
|
name="Test",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1]}
|
||||||
|
)
|
||||||
|
|
||||||
|
serie.key = "new-key"
|
||||||
|
assert serie.key == "new-key"
|
||||||
|
|
||||||
|
def test_serie_key_setter_with_empty_value_raises_error(self):
|
||||||
|
"""Test that setting key to empty string raises ValueError."""
|
||||||
|
serie = Serie(
|
||||||
|
key="initial-key",
|
||||||
|
name="Test",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1]}
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="key cannot be None or empty"):
|
||||||
|
serie.key = ""
|
||||||
|
|
||||||
|
def test_serie_key_setter_with_whitespace_raises_error(self):
|
||||||
|
"""Test that setting key to whitespace raises ValueError."""
|
||||||
|
serie = Serie(
|
||||||
|
key="initial-key",
|
||||||
|
name="Test",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1]}
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="key cannot be None or empty"):
|
||||||
|
serie.key = " "
|
||||||
|
|
||||||
|
def test_serie_key_setter_strips_whitespace(self):
|
||||||
|
"""Test that key setter strips whitespace."""
|
||||||
|
serie = Serie(
|
||||||
|
key="initial-key",
|
||||||
|
name="Test",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1]}
|
||||||
|
)
|
||||||
|
|
||||||
|
serie.key = " new-key "
|
||||||
|
assert serie.key == "new-key"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSerieProperties:
|
||||||
|
"""Test Serie class properties and methods."""
|
||||||
|
|
||||||
|
def test_serie_str_representation(self):
|
||||||
|
"""Test string representation of Serie."""
|
||||||
|
serie = Serie(
|
||||||
|
key="test-key",
|
||||||
|
name="Test Series",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1, 2]}
|
||||||
|
)
|
||||||
|
|
||||||
|
str_repr = str(serie)
|
||||||
|
assert "key='test-key'" in str_repr
|
||||||
|
assert "name='Test Series'" in str_repr
|
||||||
|
assert "folder='Test Folder'" in str_repr
|
||||||
|
|
||||||
|
def test_serie_to_dict(self):
|
||||||
|
"""Test conversion of Serie to dictionary."""
|
||||||
|
serie = Serie(
|
||||||
|
key="test-key",
|
||||||
|
name="Test Series",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1, 2], 2: [1, 2, 3]}
|
||||||
|
)
|
||||||
|
|
||||||
|
data = serie.to_dict()
|
||||||
|
|
||||||
|
assert data["key"] == "test-key"
|
||||||
|
assert data["name"] == "Test Series"
|
||||||
|
assert data["site"] == "https://example.com"
|
||||||
|
assert data["folder"] == "Test Folder"
|
||||||
|
assert "1" in data["episodeDict"]
|
||||||
|
assert data["episodeDict"]["1"] == [1, 2]
|
||||||
|
|
||||||
|
def test_serie_from_dict(self):
|
||||||
|
"""Test creating Serie from dictionary."""
|
||||||
|
data = {
|
||||||
|
"key": "test-key",
|
||||||
|
"name": "Test Series",
|
||||||
|
"site": "https://example.com",
|
||||||
|
"folder": "Test Folder",
|
||||||
|
"episodeDict": {"1": [1, 2], "2": [1, 2, 3]}
|
||||||
|
}
|
||||||
|
|
||||||
|
serie = Serie.from_dict(data)
|
||||||
|
|
||||||
|
assert serie.key == "test-key"
|
||||||
|
assert serie.name == "Test Series"
|
||||||
|
assert serie.folder == "Test Folder"
|
||||||
|
assert serie.episodeDict == {1: [1, 2], 2: [1, 2, 3]}
|
||||||
|
|
||||||
|
def test_serie_save_and_load_from_file(self):
|
||||||
|
"""Test saving and loading Serie from file."""
|
||||||
|
serie = Serie(
|
||||||
|
key="test-key",
|
||||||
|
name="Test Series",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Test Folder",
|
||||||
|
episodeDict={1: [1, 2, 3]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
delete=False,
|
||||||
|
suffix='.json'
|
||||||
|
) as f:
|
||||||
|
temp_filename = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Save to file
|
||||||
|
serie.save_to_file(temp_filename)
|
||||||
|
|
||||||
|
# Load from file
|
||||||
|
loaded_serie = Serie.load_from_file(temp_filename)
|
||||||
|
|
||||||
|
# Verify all properties match
|
||||||
|
assert loaded_serie.key == serie.key
|
||||||
|
assert loaded_serie.name == serie.name
|
||||||
|
assert loaded_serie.site == serie.site
|
||||||
|
assert loaded_serie.folder == serie.folder
|
||||||
|
assert loaded_serie.episodeDict == serie.episodeDict
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if os.path.exists(temp_filename):
|
||||||
|
os.remove(temp_filename)
|
||||||
|
|
||||||
|
def test_serie_folder_is_mutable(self):
|
||||||
|
"""Test that folder property can be changed (it's metadata only)."""
|
||||||
|
serie = Serie(
|
||||||
|
key="test-key",
|
||||||
|
name="Test",
|
||||||
|
site="https://example.com",
|
||||||
|
folder="Old Folder",
|
||||||
|
episodeDict={1: [1]}
|
||||||
|
)
|
||||||
|
|
||||||
|
serie.folder = "New Folder"
|
||||||
|
assert serie.folder == "New Folder"
|
||||||
|
# Key should remain unchanged
|
||||||
|
assert serie.key == "test-key"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSerieDocumentation:
|
||||||
|
"""Test that Serie class has proper documentation."""
|
||||||
|
|
||||||
|
def test_serie_class_has_docstring(self):
|
||||||
|
"""Test that Serie class has a docstring."""
|
||||||
|
assert Serie.__doc__ is not None
|
||||||
|
assert "unique identifier" in Serie.__doc__.lower()
|
||||||
|
|
||||||
|
def test_key_property_has_docstring(self):
|
||||||
|
"""Test that key property has descriptive docstring."""
|
||||||
|
assert Serie.key.fget.__doc__ is not None
|
||||||
|
assert "unique" in Serie.key.fget.__doc__.lower()
|
||||||
|
assert "identifier" in Serie.key.fget.__doc__.lower()
|
||||||
|
|
||||||
|
def test_folder_property_has_docstring(self):
|
||||||
|
"""Test that folder property documents it's metadata only."""
|
||||||
|
assert Serie.folder.fget.__doc__ is not None
|
||||||
|
assert "metadata" in Serie.folder.fget.__doc__.lower()
|
||||||
|
assert "not used for lookups" in Serie.folder.fget.__doc__.lower()
|
||||||
Loading…
x
Reference in New Issue
Block a user