diff --git a/src/codealmanac/core/models.py b/src/codealmanac/core/models.py index dc918e90..2e60f1ba 100644 --- a/src/codealmanac/core/models.py +++ b/src/codealmanac/core/models.py @@ -1,5 +1,17 @@ -from pydantic import BaseModel, ConfigDict +from typing import Annotated + +from pydantic import AfterValidator, BaseModel, ConfigDict class CodeAlmanacModel(BaseModel): model_config = ConfigDict(frozen=True, extra="forbid") + + +def _require_non_negative_limit(value: int | None) -> int | None: + if value is not None and value < 0: + raise ValueError("limit must be non-negative") + return value + + +NonNegativeLimit = Annotated[int | None, AfterValidator(_require_non_negative_limit)] +RequiredNonNegativeLimit = Annotated[int, AfterValidator(_require_non_negative_limit)] diff --git a/src/codealmanac/core/text.py b/src/codealmanac/core/text.py index 5b3df526..0420fda8 100644 --- a/src/codealmanac/core/text.py +++ b/src/codealmanac/core/text.py @@ -3,3 +3,9 @@ def required_text(value: str, field_name: str) -> str: if not text: raise ValueError(f"{field_name} must not be empty") return text + + +def optional_text(value: str | None, field_name: str) -> str | None: + if value is None: + return None + return required_text(value, field_name) diff --git a/src/codealmanac/services/index/requests.py b/src/codealmanac/services/index/requests.py index fe19ef22..8f2aa686 100644 --- a/src/codealmanac/services/index/requests.py +++ b/src/codealmanac/services/index/requests.py @@ -1,8 +1,6 @@ from pathlib import Path -from pydantic import field_validator - -from codealmanac.core.models import CodeAlmanacModel +from codealmanac.core.models import CodeAlmanacModel, NonNegativeLimit from codealmanac.services.repositories.models import RepositoryName @@ -10,14 +8,7 @@ class SearchIndexRequest(CodeAlmanacModel): query: str | None = None topics: tuple[str, ...] = () mentions: str | None = None - limit: int | None = None - - @field_validator("limit") - @classmethod - def non_negative_limit(cls, value: int | None) -> int | None: - if value is not None and value < 0: - raise ValueError("limit must be non-negative") - return value + limit: NonNegativeLimit = None class ReindexRequest(CodeAlmanacModel): diff --git a/src/codealmanac/services/runs/models.py b/src/codealmanac/services/runs/models.py index 9780753c..3d724053 100644 --- a/src/codealmanac/services/runs/models.py +++ b/src/codealmanac/services/runs/models.py @@ -5,7 +5,7 @@ from pydantic import StringConstraints, field_validator, model_validator from codealmanac.core.models import CodeAlmanacModel -from codealmanac.core.text import required_text +from codealmanac.core.text import optional_text, required_text from codealmanac.services.harnesses.models import ( HarnessEvent, HarnessKind, @@ -187,9 +187,7 @@ def require_ingest_input_text(cls, value: tuple[str, ...]) -> tuple[str, ...]: @field_validator("model", "title", "guidance") @classmethod def require_optional_text(cls, value: str | None) -> str | None: - if value is None: - return None - return required_text(value, "run spec text") + return optional_text(value, "run spec text") @model_validator(mode="after") def validate_kind_payload(self) -> "RunSpec": diff --git a/src/codealmanac/services/runs/requests.py b/src/codealmanac/services/runs/requests.py index ba9788a6..17ec9c85 100644 --- a/src/codealmanac/services/runs/requests.py +++ b/src/codealmanac/services/runs/requests.py @@ -3,7 +3,7 @@ from pydantic import field_validator, model_validator -from codealmanac.core.models import CodeAlmanacModel +from codealmanac.core.models import CodeAlmanacModel, NonNegativeLimit from codealmanac.core.text import required_text from codealmanac.services.harnesses.models import HarnessEvent, HarnessTranscriptRef from codealmanac.services.repositories.models import RepositoryName @@ -21,14 +21,7 @@ class ListRunsRequest(CodeAlmanacModel): repository_name: RepositoryName | None = None - limit: int | None = None - - @field_validator("limit") - @classmethod - def non_negative_limit(cls, value: int | None) -> int | None: - if value is not None and value < 0: - raise ValueError("limit must be non-negative") - return value + limit: NonNegativeLimit = None class ShowRunRequest(CodeAlmanacModel): diff --git a/src/codealmanac/services/search/requests.py b/src/codealmanac/services/search/requests.py index 7f9402c4..1d866735 100644 --- a/src/codealmanac/services/search/requests.py +++ b/src/codealmanac/services/search/requests.py @@ -1,8 +1,6 @@ from pathlib import Path -from pydantic import field_validator - -from codealmanac.core.models import CodeAlmanacModel +from codealmanac.core.models import CodeAlmanacModel, NonNegativeLimit from codealmanac.services.repositories.models import RepositoryName @@ -12,11 +10,4 @@ class SearchPagesRequest(CodeAlmanacModel): query: str | None = None topics: tuple[str, ...] = () mentions: str | None = None - limit: int | None = None - - @field_validator("limit") - @classmethod - def non_negative_limit(cls, value: int | None) -> int | None: - if value is not None and value < 0: - raise ValueError("limit must be non-negative") - return value + limit: NonNegativeLimit = None diff --git a/src/codealmanac/services/viewer/requests.py b/src/codealmanac/services/viewer/requests.py index 5478e9f8..6ea0e742 100644 --- a/src/codealmanac/services/viewer/requests.py +++ b/src/codealmanac/services/viewer/requests.py @@ -2,7 +2,11 @@ from pydantic import field_validator -from codealmanac.core.models import CodeAlmanacModel +from codealmanac.core.models import ( + CodeAlmanacModel, + NonNegativeLimit, + RequiredNonNegativeLimit, +) from codealmanac.core.text import required_text from codealmanac.services.repositories.models import RepositoryName from codealmanac.services.runs.models import RunId @@ -41,21 +45,14 @@ class ViewerSearchRequest(CodeAlmanacModel): cwd: Path repository_name: RepositoryName | None = None query: str | None = None - limit: int = 50 - - @field_validator("limit") - @classmethod - def non_negative_limit(cls, value: int) -> int: - if value < 0: - raise ValueError("limit must be non-negative") - return value + limit: RequiredNonNegativeLimit = 50 class ViewerFileRequest(CodeAlmanacModel): cwd: Path path: str repository_name: RepositoryName | None = None - limit: int = 50 + limit: RequiredNonNegativeLimit = 50 @field_validator("path") @classmethod @@ -69,13 +66,6 @@ def normalize_file_path(cls, value: str) -> str: raise ValueError("file path must be repo-relative") return normalized - @field_validator("limit") - @classmethod - def non_negative_limit(cls, value: int) -> int: - if value < 0: - raise ValueError("limit must be non-negative") - return value - class ViewerTopicRequest(CodeAlmanacModel): cwd: Path @@ -92,14 +82,7 @@ def require_slug(cls, value: str) -> str: class ViewerJobsRequest(CodeAlmanacModel): cwd: Path repository_name: RepositoryName | None = None - limit: int | None = None - - @field_validator("limit") - @classmethod - def non_negative_limit(cls, value: int | None) -> int | None: - if value is not None and value < 0: - raise ValueError("limit must be non-negative") - return value + limit: NonNegativeLimit = None class ViewerJobRequest(CodeAlmanacModel): diff --git a/src/codealmanac/workflows/build/requests.py b/src/codealmanac/workflows/build/requests.py index 3a7b3dd2..0827aaaa 100644 --- a/src/codealmanac/workflows/build/requests.py +++ b/src/codealmanac/workflows/build/requests.py @@ -3,7 +3,7 @@ from pydantic import field_validator from codealmanac.core.models import CodeAlmanacModel -from codealmanac.core.text import required_text +from codealmanac.core.text import optional_text from codealmanac.services.harnesses.models import HarnessKind from codealmanac.services.repositories.models import RepositoryName from codealmanac.services.runs.models import RunId @@ -22,9 +22,7 @@ class BuildRequest(CodeAlmanacModel): @field_validator("model", "title", "guidance") @classmethod def require_optional_text(cls, value: str | None) -> str | None: - if value is None: - return None - return required_text(value, "build request text") + return optional_text(value, "build request text") class StartedBuildRequest(CodeAlmanacModel): @@ -40,6 +38,4 @@ class StartedBuildRequest(CodeAlmanacModel): @field_validator("model", "title", "guidance") @classmethod def require_optional_text(cls, value: str | None) -> str | None: - if value is None: - return None - return required_text(value, "build request text") + return optional_text(value, "build request text") diff --git a/src/codealmanac/workflows/garden/requests.py b/src/codealmanac/workflows/garden/requests.py index 736daa41..e63a847d 100644 --- a/src/codealmanac/workflows/garden/requests.py +++ b/src/codealmanac/workflows/garden/requests.py @@ -3,7 +3,7 @@ from pydantic import field_validator from codealmanac.core.models import CodeAlmanacModel -from codealmanac.core.text import required_text +from codealmanac.core.text import optional_text from codealmanac.services.harnesses.models import HarnessKind from codealmanac.services.repositories.models import RepositoryName from codealmanac.services.runs.models import RunId @@ -21,9 +21,7 @@ class GardenRequest(CodeAlmanacModel): @field_validator("model", "title", "guidance") @classmethod def require_optional_text(cls, value: str | None) -> str | None: - if value is None: - return None - return required_text(value, "garden request text") + return optional_text(value, "garden request text") class StartedGardenRequest(GardenRequest): diff --git a/src/codealmanac/workflows/ingest/requests.py b/src/codealmanac/workflows/ingest/requests.py index 7e40f00b..4678a800 100644 --- a/src/codealmanac/workflows/ingest/requests.py +++ b/src/codealmanac/workflows/ingest/requests.py @@ -3,7 +3,7 @@ from pydantic import field_validator from codealmanac.core.models import CodeAlmanacModel -from codealmanac.core.text import required_text +from codealmanac.core.text import optional_text from codealmanac.services.harnesses.models import HarnessKind from codealmanac.services.repositories.models import RepositoryName from codealmanac.services.runs.models import RunId @@ -29,9 +29,7 @@ def require_inputs(cls, value: tuple[str, ...]) -> tuple[str, ...]: @field_validator("model", "title", "guidance") @classmethod def require_optional_text(cls, value: str | None) -> str | None: - if value is None: - return None - return required_text(value, "ingest request text") + return optional_text(value, "ingest request text") class StartedIngestRequest(IngestRequest):