feat: migrate persistence from JSON to SQLModel (Phase 1)
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled

- Integrated SQLModel with SQLite for robust data persistence
- Refactored UserManager and WatchlistManager to use SQL queries
- Migrated models to SQLModel with relationships and primary keys
- Updated test suite with in-memory database isolation
- Removed deprecated JSON storage files
This commit is contained in:
root
2026-03-24 10:40:36 +00:00
parent d4d8d8a3b6
commit 29c7040b20
13 changed files with 596 additions and 1165 deletions
+36 -14
View File
@@ -1,15 +1,41 @@
"""Authentication models for user management"""
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
"""Authentication models for user management with SQLModel support"""
import uuid
from pydantic import BaseModel, EmailStr, Field as PydanticField
from typing import Optional, List
from datetime import datetime
from sqlmodel import SQLModel, Field, Relationship
class UserCreate(BaseModel):
"""Schema for user registration"""
username: str = Field(..., min_length=3, max_length=50)
email: Optional[EmailStr] = None
password: str = Field(..., min_length=6)
class UserBase(SQLModel):
"""Base schema for user data"""
username: str = Field(index=True, unique=True, min_length=3, max_length=50)
email: Optional[str] = Field(default=None, index=True)
full_name: Optional[str] = None
is_active: bool = Field(default=True)
class UserTable(UserBase, table=True):
"""Database table for users"""
__tablename__ = "users"
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
primary_key=True,
index=True,
nullable=False
)
hashed_password: str
created_at: datetime = Field(default_factory=datetime.now)
last_login: Optional[datetime] = None
# Relationships
watchlist_items: List["WatchlistItemTable"] = Relationship(back_populates="user")
class UserCreate(UserBase):
"""Schema for user registration"""
password: str = PydanticField(..., min_length=6)
email: Optional[EmailStr] = None
class UserLogin(BaseModel):
@@ -18,13 +44,9 @@ class UserLogin(BaseModel):
password: str
class User(BaseModel):
"""Schema for user data"""
class User(UserBase):
"""Schema for user data (API Response)"""
id: str
username: str
email: Optional[str] = None
full_name: Optional[str] = None
is_active: bool = True
created_at: datetime
last_login: Optional[datetime] = None
+82 -46
View File
@@ -1,8 +1,11 @@
"""Pydantic models for Watchlist and Auto-Download system"""
from pydantic import BaseModel, Field
from typing import Optional, Literal
"""Models for Watchlist and Auto-Download system with SQLModel support"""
import uuid
import json
from pydantic import BaseModel, Field as PydanticField
from typing import Optional, Literal, List
from datetime import datetime
from enum import Enum
from sqlmodel import SQLModel, Field, Relationship, Column, String
class WatchlistStatus(str, Enum):
@@ -21,34 +24,80 @@ class QualityPreference(str, Enum):
P480 = "480p" # SD
class WatchlistItem(BaseModel):
"""An anime being tracked for automatic episode downloads"""
id: str = Field(..., description="Unique identifier (UUID)")
user_id: str = Field(..., description="User ID who owns this watchlist item")
anime_title: str = Field(..., description="Title of the anime")
anime_url: str = Field(..., description="URL to the anime page")
provider_id: str = Field(..., description="Provider ID (animesama, nekosama, etc.)")
lang: Literal["vostfr", "vf"] = Field(default="vostfr", description="Language preference")
class WatchlistItemBase(SQLModel):
"""Base schema for watchlist items"""
anime_title: str = Field(index=True)
anime_url: str
provider_id: str
lang: str = Field(default="vostfr")
# Tracking state
last_checked: Optional[datetime] = Field(None, description="Last time we checked for new episodes")
last_episode_downloaded: int = Field(default=0, description="Last episode number downloaded")
total_episodes: Optional[int] = Field(None, description="Total episodes if known")
last_checked: Optional[datetime] = None
last_episode_downloaded: int = Field(default=0)
total_episodes: Optional[int] = None
# Settings
auto_download: bool = Field(default=True, description="Automatically download new episodes")
quality_preference: QualityPreference = Field(default=QualityPreference.AUTO, description="Preferred quality")
status: WatchlistStatus = Field(default=WatchlistStatus.ACTIVE, description="Tracking status")
auto_download: bool = Field(default=True)
quality_preference: QualityPreference = Field(default=QualityPreference.AUTO)
status: WatchlistStatus = Field(default=WatchlistStatus.ACTIVE)
# Metadata
poster_image: Optional[str] = Field(None, description="URL to poster image")
cover_image: Optional[str] = Field(None, description="URL to cover image")
synopsis: Optional[str] = Field(None, description="Anime synopsis")
genres: list[str] = Field(default_factory=list, description="Anime genres")
poster_image: Optional[str] = None
cover_image: Optional[str] = None
synopsis: Optional[str] = None
# Timestamps
added_at: datetime = Field(default_factory=datetime.now, description="When added to watchlist")
updated_at: datetime = Field(default_factory=datetime.now, description="Last update time")
added_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
class WatchlistItemTable(WatchlistItemBase, table=True):
"""Database table for watchlist items"""
__tablename__ = "watchlist_items"
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
primary_key=True,
index=True,
nullable=False
)
user_id: str = Field(foreign_key="users.id", index=True)
# Store list as JSON string in SQLite
genres_json: Optional[str] = Field(default="[]", sa_column=Column(String))
@property
def genres(self) -> List[str]:
return json.loads(self.genres_json or "[]")
@genres.setter
def genres(self, value: List[str]):
self.genres_json = json.dumps(value or [])
# Relationships
user: Optional["UserTable"] = Relationship(back_populates="watchlist_items")
class WatchlistItem(BaseModel):
"""An anime being tracked for automatic episode downloads (API Response)"""
id: str
user_id: str
anime_title: str
anime_url: str
provider_id: str
lang: str
last_checked: Optional[datetime] = None
last_episode_downloaded: int = 0
total_episodes: Optional[int] = None
auto_download: bool = True
quality_preference: QualityPreference = QualityPreference.AUTO
status: WatchlistStatus = WatchlistStatus.ACTIVE
poster_image: Optional[str] = None
cover_image: Optional[str] = None
synopsis: Optional[str] = None
genres: List[str] = []
added_at: datetime
updated_at: datetime
class Config:
json_encoders = {
@@ -64,12 +113,10 @@ class WatchlistItemCreate(BaseModel):
lang: Literal["vostfr", "vf"] = "vostfr"
auto_download: bool = True
quality_preference: QualityPreference = QualityPreference.AUTO
# Optional metadata
poster_image: Optional[str] = None
cover_image: Optional[str] = None
synopsis: Optional[str] = None
genres: list[str] = []
genres: List[str] = []
class WatchlistItemUpdate(BaseModel):
@@ -96,26 +143,15 @@ class AutoDownloadResult(BaseModel):
watchlist_item_id: str
anime_title: str
new_episodes_found: int
episodes_downloaded: list[int] = Field(default_factory=list)
episodes_failed: list[tuple[int, str]] = Field(default_factory=list) # (episode_number, error_message)
checked_at: datetime = Field(default_factory=datetime.now)
episodes_downloaded: list[int] = PydanticField(default_factory=list)
episodes_failed: list[tuple[int, str]] = PydanticField(default_factory=list)
checked_at: datetime = PydanticField(default_factory=datetime.now)
class WatchlistSettings(BaseModel):
"""Global watchlist settings"""
check_interval_hours: int = Field(default=6, ge=1, le=168, description="Check interval (1-168 hours)")
auto_download_enabled: bool = Field(default=True, description="Global auto-download toggle")
max_concurrent_auto_downloads: int = Field(default=2, ge=1, le=10, description="Max concurrent auto-downloads")
notify_on_new_episodes: bool = Field(default=False, description="Send notifications for new episodes")
include_completed_anime: bool = Field(default=False, description="Check completed anime too")
class Config:
json_schema_extra = {
"example": {
"check_interval_hours": 6,
"auto_download_enabled": True,
"max_concurrent_auto_downloads": 2,
"notify_on_new_episodes": False,
"include_completed_anime": False
}
}
check_interval_hours: int = PydanticField(default=6, ge=1, le=168)
auto_download_enabled: bool = PydanticField(default=True)
max_concurrent_auto_downloads: int = PydanticField(default=2, ge=1, le=10)
notify_on_new_episodes: bool = PydanticField(default=False)
include_completed_anime: bool = PydanticField(default=False)