feat(scraper): add LoginResult model and abstract login() interface

Problem: `:CP <platform> 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`.
This commit is contained in:
Barrett Ruth 2026-03-05 14:59:46 -05:00
parent a202725cc5
commit dfb648531b
Signed by: barrett
GPG key ID: A6C96C9349D2FC81
2 changed files with 24 additions and 1 deletions

View file

@ -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 <id> | tests <id> | contests"
return f"Usage: {name}.py metadata <id> | tests <id> | 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(

View file

@ -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 = ""