"""Pydantic response-envelope models.
Runtime-validated ArcGIS payloads consumed by restgdf, split into two
tiers by design (see :mod:`restgdf._models._drift`):
* Permissive: :class:`FieldSpec`, :class:`ErrorInfo`, :class:`Feature`,
:class:`LayerMetadata`, :class:`ServiceInfo`.
* Strict: :class:`ErrorResponse`.
Subsequent slices (query envelopes, crawl, credentials) add more models
alongside these in this module.
"""
from __future__ import annotations
from typing import Any
from pydantic import AliasChoices, Field, field_validator
from restgdf._models._drift import PermissiveModel, StrictModel
[docs]
class FieldSpec(PermissiveModel):
"""A field descriptor entry in a layer's ``fields`` list.
Real ArcGIS servers emit an open-ended set of keys here
(``sqlType``, ``defaultValue``, ``modelName``, ...). Permissive tier
preserves them via ``extra="allow"`` while declaring the handful of
keys restgdf actually consumes.
"""
name: str | None = None
type: str | None = None
alias: str | None = None
length: int | None = None
domain: dict | None = None
nullable: bool | None = None
editable: bool | None = None
[docs]
class ErrorInfo(PermissiveModel):
"""Inner error payload: ``{"code": int, "message": str, ...}``.
ArcGIS error payloads routinely carry diagnostic extras
(``messageCode``, ``errorCode``, ``details``) that restgdf does not
need but should not strip away.
"""
code: int | None = None
message: str | None = None
details: list[str] | None = None
[docs]
class ErrorResponse(StrictModel):
"""Top-level JSON error envelope: ``{"error": {...}}``.
Strict tier: callers branching on ``isinstance(obj, ErrorResponse)``
need the ``error`` key to actually be present. Missing-key drift on
this envelope indicates a protocol-level bug, not vendor variance.
"""
error: ErrorInfo = Field(...)
[docs]
class Feature(PermissiveModel):
"""A single feature in :attr:`FeaturesResponse.features`.
``attributes`` is declared as a dict but not typed further — ArcGIS
layer schemas are dynamic. ``geometry`` is optional because non-
spatial tables and ``returnGeometry=false`` queries omit it.
"""
attributes: dict[str, Any] | None = None
geometry: dict[str, Any] | None = None
[docs]
class ServiceInfo(PermissiveModel):
"""Root ``GET <services_root>?f=json`` envelope.
A narrower permissive view over the subset of keys a services-root
crawl consumes (``services`` and ``folders``). Unlike
:class:`LayerMetadata`, this model does not enrich nested ``services``
entries into typed objects — the crawl report keeps them as raw
dicts so per-service merge keys (``name``, ``type``) survive
unchanged.
"""
services: list[dict[str, Any]] | None = None
folders: list[str] | None = None
layers: list[LayerMetadata] | None = None
url: str | None = None
LayerMetadata.model_rebuild()
[docs]
class CountResponse(StrictModel):
"""Envelope for ``?returnCountOnly=true`` query results.
Strict tier: ArcGIS *always* returns ``count`` for this query shape,
so a missing/ill-typed key signals a protocol-level incident (for
example an HTML error page bodied as JSON). :func:`_parse_response`
surfaces those as :class:`RestgdfResponseError`.
"""
count: int = Field(...)
[docs]
class ObjectIdsResponse(StrictModel):
"""Envelope for ``?returnIdsOnly=true`` query results.
Strict tier. The response is operation-critical: chunked pagination
in :mod:`restgdf.utils.getgdf` requires both the OID field name and
the full id list. A zero-row match produces
``{"objectIdFieldName": "OBJECTID", "objectIds": null}`` in the
wild; the ``object_ids`` validator below coerces that ``None`` to an
empty list so consumers can unconditionally iterate.
"""
object_id_field_name: str = Field(
...,
alias="objectIdFieldName",
validation_alias=AliasChoices("objectIdFieldName", "object_id_field_name"),
)
object_ids: list[int] = Field(
default_factory=list,
alias="objectIds",
validation_alias=AliasChoices("objectIds", "object_ids"),
)
@field_validator("object_ids", mode="before")
@classmethod
def _coerce_null_to_empty(cls, value: Any) -> Any:
if value is None:
return []
return value
[docs]
class FeaturesResponse(PermissiveModel):
"""Envelope for ``?f=json`` feature queries.
Permissive tier: only the envelope keys restgdf consumes are
declared. ``features`` is kept as a ``list[dict]`` rather than
``list[Feature]`` on purpose — validating every feature of a large
batch with pydantic would be expensive and returns no value to the
downstream GeoPandas reader, which consumes raw ArcGIS JSON. Callers
that need typed features can validate them explicitly via
:class:`Feature`.
"""
object_id_field_name: str | None = Field(
default=None,
alias="objectIdFieldName",
validation_alias=AliasChoices("objectIdFieldName", "object_id_field_name"),
)
features: list[dict[str, Any]] = Field(default_factory=list)
exceeded_transfer_limit: bool | None = Field(
default=None,
alias="exceededTransferLimit",
validation_alias=AliasChoices(
"exceededTransferLimit",
"exceeded_transfer_limit",
),
)
[docs]
class TokenResponse(StrictModel):
"""Envelope for ArcGIS ``/generateToken`` responses.
Strict tier: token refresh is operation-critical; a missing
``token`` or ``expires`` key means a token cannot be used and any
downstream request will fail authentication. ArcGIS also returns
error envelopes through this same endpoint (``{"error": {...}}``);
those fail validation here and surface as
:class:`RestgdfResponseError`, leaving the original payload on
``exc.raw`` for operator triage.
"""
token: str = Field(...)
expires: int = Field(...)
ssl: bool | None = None
__all__ = [
"CountResponse",
"ErrorInfo",
"ErrorResponse",
"Feature",
"FeaturesResponse",
"FieldSpec",
"LayerMetadata",
"ObjectIdsResponse",
"ServiceInfo",
"TokenResponse",
]