diff --git a/schemas/aquifer_system.py b/schemas/aquifer_system.py index d06150ecf..01dbcbfd0 100644 --- a/schemas/aquifer_system.py +++ b/schemas/aquifer_system.py @@ -1,13 +1,13 @@ from typing import List from pydantic import BaseModel - -from core.enums import GeographicScale, AquiferType from schemas import BaseResponseModel +from schemas.validators import GeometryMixin +from core.enums import AquiferType, GeographicScale # Import specific Enums # ------ CREATE ---------- -class CreateAquiferSystem(BaseModel): +class CreateAquiferSystem(BaseModel, GeometryMixin): """ Schema for creating an aquifer system. Used during data transfer and API creation. @@ -16,8 +16,8 @@ class CreateAquiferSystem(BaseModel): name: str description: str | None = None primary_aquifer_type: AquiferType - geographic_scale: GeographicScale | None = None # e.g., "Regional", "Local", etc. - boundary: str | None = None + geographic_scale: GeographicScale | None = None + # boundary field inherited from GeometryMixin # ------ RESPONSE ---------- diff --git a/schemas/geologic_formation.py b/schemas/geologic_formation.py index 62e24ee8e..de479bf29 100644 --- a/schemas/geologic_formation.py +++ b/schemas/geologic_formation.py @@ -1,22 +1,46 @@ from typing import List -from pydantic import BaseModel +from pydantic import BaseModel, field_validator, Field from schemas import BaseResponseModel +from schemas.validators import DepthIntervalMixin, GeometryMixin from core.enums import FormationCode, Lithology # ------ CREATE ---------- -class CreateGeologicFormation(BaseModel): +class CreateGeologicFormation(BaseModel, GeometryMixin): """ Schema for creating a geologic formation. Used during data transfer and API creation. """ + # formation_code has its own custom uppercase validator formation_code: FormationCode | None = None description: str | None = None lithology: Lithology | None = None - boundary: str | None = None + # boundary: inherited from GeometryMixin + + @field_validator("formation_code", mode="before") + @classmethod + def upper_case_code(cls, v: str | None) -> str | None: + """ + Automatically uppercase the formation code. + """ + if isinstance(v, str): + return v.upper() + return v + + +class CreateThingGeologicFormationAssociation(BaseModel, DepthIntervalMixin): + """ + Schema for linking a Thing (Well) to a GeologicFormation. + Uses DepthIntervalMixin to enforce bottom_depth > top_depth. + """ + + thing_id: int + geologic_formation_id: int + top_depth: float = Field(ge=0) + bottom_depth: float = Field(ge=0) # ------ RESPONSE ---------- diff --git a/schemas/validators.py b/schemas/validators.py new file mode 100644 index 000000000..fde828770 --- /dev/null +++ b/schemas/validators.py @@ -0,0 +1,43 @@ +""" +schemas/validators.py +Reusable Pydantic validators and mixins for aquifer and geology related schemas. +May consider expansion for other domain models in the future. +""" + +from pydantic import model_validator, field_validator, BaseModel, ValueError, Field +from services.validation.geospatial import validate_wkt_geometry + + +class DepthIntervalMixin(BaseModel): + """ + Mixin to enforce: + 1. Depths are non-negative (via Field constraints). + 2. Bottom depth > top depth (via model_validator). + Assumes the model has 'top_depth' and 'bottom_depth' fields. + """ + + top_depth: float = Field(ge=0) + bottom_depth: float = Field(ge=0) + + @model_validator(mode="after") + def check_depth_logical_order(self) -> "DepthIntervalMixin": + if self.bottom_depth <= self.top_depth: + raise ValueError( + f"Bottom depth ({self.bottom_depth}) must be greater " + f"than top depth ({self.top_depth})" + ) + return self + + +class GeometryMixin(BaseModel): + """ + Mixin to validate WKT strings for boundary fields. + Delegates logic to the validate_wkt_geometry service function. + """ + + boundary: str | None = None + + @field_validator("boundary") + @classmethod + def validate_wkt(cls, v: str | None) -> str | None: + return validate_wkt_geometry(v)