From dfb648531b6a49aa55bcc04e003aff6a34a82b01 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 14:59:46 -0500 Subject: [PATCH] feat(scraper): add `LoginResult` model and abstract `login()` interface Problem: `:CP login` blindly caches credentials without validating them against the platform. Solution: add `LoginResult` to `models.py`, abstract `login()` method and `_login_error` helper to `BaseScraper`, and wire up the `"login"` CLI dispatch in `_run_cli_async`. --- scrapers/base.py | 19 ++++++++++++++++++- scrapers/models.py | 6 ++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/scrapers/base.py b/scrapers/base.py index c77e293..11ab8c6 100644 --- a/scrapers/base.py +++ b/scrapers/base.py @@ -9,6 +9,7 @@ from .language_ids import get_language_id from .models import ( CombinedTest, ContestListResult, + LoginResult, MetadataResult, SubmitResult, TestsResult, @@ -58,9 +59,12 @@ class BaseScraper(ABC): credentials: dict[str, str], ) -> SubmitResult: ... + @abstractmethod + async def login(self, credentials: dict[str, str]) -> LoginResult: ... + def _usage(self) -> str: name = self.platform_name - return f"Usage: {name}.py metadata | tests | contests" + return f"Usage: {name}.py metadata | tests | contests | login" def _metadata_error(self, msg: str) -> MetadataResult: return MetadataResult(success=False, error=msg, url="") @@ -82,6 +86,9 @@ class BaseScraper(ABC): def _submit_error(self, msg: str) -> SubmitResult: return SubmitResult(success=False, error=msg) + def _login_error(self, msg: str) -> LoginResult: + return LoginResult(success=False, error=msg) + async def _run_cli_async(self, args: list[str]) -> int: if len(args) < 2: print(self._metadata_error(self._usage()).model_dump_json()) @@ -133,6 +140,16 @@ class BaseScraper(ABC): print(result.model_dump_json()) return 0 if result.success else 1 + case "login": + creds_raw = os.environ.get("CP_CREDENTIALS", "{}") + try: + credentials = json.loads(creds_raw) + except json.JSONDecodeError: + credentials = {} + result = await self.login(credentials) + print(result.model_dump_json()) + return 0 if result.success else 1 + case _: print( self._metadata_error( diff --git a/scrapers/models.py b/scrapers/models.py index 68de9a9..4dafc64 100644 --- a/scrapers/models.py +++ b/scrapers/models.py @@ -64,6 +64,12 @@ class TestsResult(ScrapingResult): model_config = ConfigDict(extra="forbid") +class LoginResult(ScrapingResult): + credentials: dict[str, str] = Field(default_factory=dict) + + model_config = ConfigDict(extra="forbid") + + class SubmitResult(ScrapingResult): submission_id: str = "" verdict: str = ""