feat: Add modpack-version-checker code directory structure
- Created src/modpack_checker/ package structure - Created tests/ directory - Placeholder files for all Python modules - Complete source code preserved in Chronicler #26 chat history - Ready for code population in next session Full file contents available in session transcript for reconstruction.
This commit is contained in:
1
docs/tasks/modpack-version-checker/code/modpack-version-checker/.gitignore
vendored
Normal file
1
docs/tasks/modpack-version-checker/code/modpack-version-checker/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
# Auto-generated script to create all modpack-version-checker files
|
||||
|
||||
BASE_DIR="/home/claude/firefrost-operations-manual/docs/tasks/modpack-version-checker/code/modpack-version-checker"
|
||||
cd "$BASE_DIR"
|
||||
|
||||
# Already created: src/modpack_checker/__init__.py, src/modpack_checker/config.py
|
||||
|
||||
# Create database.py
|
||||
cat > src/modpack_checker/database.py << 'EOF'
|
||||
"""SQLite database layer using SQLAlchemy 2.0 ORM."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, create_engine, select
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship
|
||||
|
||||
|
||||
class _Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class _ModpackRow(_Base):
|
||||
__tablename__ = "modpacks"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
curseforge_id: Mapped[int] = mapped_column(Integer, unique=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
current_version: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
last_checked: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
notification_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
history: Mapped[List["_CheckHistoryRow"]] = relationship(
|
||||
back_populates="modpack", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class _CheckHistoryRow(_Base):
|
||||
__tablename__ = "check_history"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
modpack_id: Mapped[int] = mapped_column(ForeignKey("modpacks.id"), nullable=False)
|
||||
checked_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
version_found: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
notification_sent: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
modpack: Mapped["_ModpackRow"] = relationship(back_populates="history")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Modpack:
|
||||
id: int
|
||||
curseforge_id: int
|
||||
name: str
|
||||
current_version: Optional[str]
|
||||
last_checked: Optional[datetime]
|
||||
notification_enabled: bool
|
||||
|
||||
@classmethod
|
||||
def _from_row(cls, row: _ModpackRow) -> "Modpack":
|
||||
return cls(
|
||||
id=row.id,
|
||||
curseforge_id=row.curseforge_id,
|
||||
name=row.name,
|
||||
current_version=row.current_version,
|
||||
last_checked=row.last_checked,
|
||||
notification_enabled=row.notification_enabled,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckHistory:
|
||||
id: int
|
||||
modpack_id: int
|
||||
checked_at: datetime
|
||||
version_found: Optional[str]
|
||||
notification_sent: bool
|
||||
|
||||
@classmethod
|
||||
def _from_row(cls, row: _CheckHistoryRow) -> "CheckHistory":
|
||||
return cls(
|
||||
id=row.id,
|
||||
modpack_id=row.modpack_id,
|
||||
checked_at=row.checked_at,
|
||||
version_found=row.version_found,
|
||||
notification_sent=row.notification_sent,
|
||||
)
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_path: str) -> None:
|
||||
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
||||
_Base.metadata.create_all(self.engine)
|
||||
|
||||
def add_modpack(self, curseforge_id: int, name: str) -> Modpack:
|
||||
with Session(self.engine) as session:
|
||||
existing = session.scalar(
|
||||
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
|
||||
)
|
||||
if existing is not None:
|
||||
raise ValueError(f"Modpack ID {curseforge_id} is already being tracked.")
|
||||
row = _ModpackRow(curseforge_id=curseforge_id, name=name, notification_enabled=True)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
return Modpack._from_row(row)
|
||||
|
||||
def remove_modpack(self, curseforge_id: int) -> bool:
|
||||
with Session(self.engine) as session:
|
||||
row = session.scalar(
|
||||
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
|
||||
)
|
||||
if row is None:
|
||||
return False
|
||||
session.delete(row)
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
def update_version(self, curseforge_id: int, version: str, notification_sent: bool = False) -> None:
|
||||
with Session(self.engine) as session:
|
||||
row = session.scalar(
|
||||
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
|
||||
)
|
||||
if row is None:
|
||||
raise ValueError(f"Modpack {curseforge_id} not found in database.")
|
||||
row.current_version = version
|
||||
row.last_checked = datetime.utcnow()
|
||||
session.add(
|
||||
_CheckHistoryRow(
|
||||
modpack_id=row.id,
|
||||
checked_at=datetime.utcnow(),
|
||||
version_found=version,
|
||||
notification_sent=notification_sent,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
def toggle_notifications(self, curseforge_id: int, enabled: bool) -> bool:
|
||||
with Session(self.engine) as session:
|
||||
row = session.scalar(
|
||||
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
|
||||
)
|
||||
if row is None:
|
||||
return False
|
||||
row.notification_enabled = enabled
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
def get_modpack(self, curseforge_id: int) -> Optional[Modpack]:
|
||||
with Session(self.engine) as session:
|
||||
row = session.scalar(
|
||||
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
|
||||
)
|
||||
return Modpack._from_row(row) if row else None
|
||||
|
||||
def get_all_modpacks(self) -> List[Modpack]:
|
||||
with Session(self.engine) as session:
|
||||
rows = session.scalars(select(_ModpackRow)).all()
|
||||
return [Modpack._from_row(r) for r in rows]
|
||||
|
||||
def get_check_history(self, curseforge_id: int, limit: int = 10) -> List[CheckHistory]:
|
||||
with Session(self.engine) as session:
|
||||
modpack_row = session.scalar(
|
||||
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
|
||||
)
|
||||
if modpack_row is None:
|
||||
return []
|
||||
rows = session.scalars(
|
||||
select(_CheckHistoryRow)
|
||||
.where(_CheckHistoryRow.modpack_id == modpack_row.id)
|
||||
.order_by(_CheckHistoryRow.checked_at.desc())
|
||||
.limit(limit)
|
||||
).all()
|
||||
return [CheckHistory._from_row(r) for r in rows]
|
||||
EOF
|
||||
|
||||
echo "Created database.py"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Modpack Version Checker - Monitor CurseForge modpack updates."""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "Firefrost Gaming"
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Configuration management for Modpack Version Checker."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
CONFIG_DIR = Path.home() / ".config" / "modpack-checker"
|
||||
CONFIG_FILE = CONFIG_DIR / "config.json"
|
||||
DEFAULT_DB_PATH = str(CONFIG_DIR / "modpacks.db")
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
"""Application configuration, persisted to ~/.config/modpack-checker/config.json."""
|
||||
|
||||
curseforge_api_key: str = ""
|
||||
discord_webhook_url: Optional[str] = None
|
||||
database_path: str = DEFAULT_DB_PATH
|
||||
check_interval_hours: int = Field(default=6, ge=1, le=168)
|
||||
notification_on_update: bool = True
|
||||
|
||||
@classmethod
|
||||
def load(cls) -> "Config":
|
||||
"""Load configuration from disk, returning defaults if not present."""
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
with open(CONFIG_FILE) as f:
|
||||
data = json.load(f)
|
||||
return cls(**data)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return cls()
|
||||
return cls()
|
||||
|
||||
def save(self) -> None:
|
||||
"""Persist configuration to disk."""
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
json.dump(self.model_dump(), f, indent=2)
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True if the minimum required config (API key) is present."""
|
||||
return bool(self.curseforge_api_key)
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder - content in chat history
|
||||
Reference in New Issue
Block a user