Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions apps/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -25815,6 +25815,93 @@
]
}
},
"/fixtures/{fixture_id}/pidinst": {
"get": {
"operationId": "get_fixture_pidinst_fixtures__fixture_id__pidinst_get",
"parameters": [
{
"description": "Target fixture's id.",
"in": "path",
"name": "fixture_id",
"required": true,
"schema": {
"description": "Target fixture's id.",
"format": "uuid",
"title": "Fixture Id",
"type": "string"
}
},
{
"description": "Legacy principal-id header (trust-the-proxy shape). When IDENTITY_PROVIDERS is configured (bearer-auth mode), this header is IGNORED and the verified bearer token from `BearerAuthMiddleware` (Authorization: Bearer) sets the principal. When no IdPs are configured (legacy mode), the application TRUSTS this header (no cryptographic verification) -- production deployments in legacy mode MUST front the API with an auth proxy that strips any client-supplied X-Principal-Id and sets it to the verified principal UUID. Behavior when absent: see Settings.require_authenticated_principal.",
"in": "header",
"name": "X-Principal-Id",
"required": false,
"schema": {
"anyOf": [
{
"format": "uuid",
"type": "string"
},
{
"type": "null"
}
],
"description": "Legacy principal-id header (trust-the-proxy shape). When IDENTITY_PROVIDERS is configured (bearer-auth mode), this header is IGNORED and the verified bearer token from `BearerAuthMiddleware` (Authorization: Bearer) sets the principal. When no IdPs are configured (legacy mode), the application TRUSTS this header (no cryptographic verification) -- production deployments in legacy mode MUST front the API with an auth proxy that strips any client-supplied X-Principal-Id and sets it to the verified principal UUID. Behavior when absent: see Settings.require_authenticated_principal.",
"title": "X-Principal-Id"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PidinstRecordResponse"
}
}
},
"description": "Successful Response"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Authorize port denied the query."
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "No fixture exists with the given id."
},
"409": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Fixture state is missing a mandatory PIDINST source: no bound Asset carries any owners (FixtureOwnerStateNotAvailableError), or no bound Asset carries any manufacturer (FixtureManufacturerStateNotAvailableError)."
},
"422": {
"description": "View preparation produced an empty landing page URL or fixture name; or the path parameter failed schema validation."
}
},
"summary": "Get the PIDINST v1.0 record for a fixture",
"tags": [
"equipment"
]
}
},
"/frames": {
"post": {
"operationId": "post_frames_frames_post",
Expand Down
204 changes: 204 additions & 0 deletions apps/api/src/cora/equipment/_pidinst_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"""Pydantic mirror of the PIDINST v1.0 record + conversion helper.

The aggregate kernel owns the slice-C `PidinstRecord` frozen dataclass
(in `_pidinst_types.py`). FastAPI's OpenAPI schema generator requires a
Pydantic-typed mirror to render `response_model`, so this module hosts
that mirror plus the `_record_to_response` helper that walks the
slice-C tree into Pydantic.

Lives at BC root rather than inside any one feature slice because both
the Asset-tier (`get_asset_pidinst`) and Fixture-tier
(`get_fixture_pidinst`) read routes return the same record shape. Per
the BC-flat-root layout convention private to this BC.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from pydantic import BaseModel

if TYPE_CHECKING:
from cora.equipment._pidinst_types import PidinstRecord


class PidinstIdentifierDTO(BaseModel):
"""PIDINST v1.0 Property 1: persistent identifier of the instrument."""

value: str
scheme: str


class OwnerDTO(BaseModel):
"""PIDINST v1.0 Property 5: a body owning or curating the instrument."""

name: str
contact: str | None = None
identifier: str | None = None
identifier_type: str | None = None


class ManufacturerDTO(BaseModel):
"""PIDINST v1.0 Property 6: the body manufacturing the instrument."""

name: str
identifier: str | None = None
identifier_type: str | None = None


class PidinstModelDTO(BaseModel):
"""PIDINST v1.0 Property 7: the model identification of the instrument."""

name: str
identifier: str
identifier_type: str


class InstrumentTypeDTO(BaseModel):
"""PIDINST v1.0 Property 9: a typology category for the instrument."""

name: str
identifier: str | None = None
identifier_type: str


class MeasuredVariableDTO(BaseModel):
"""PIDINST v1.0 Property 10: a physical quantity the instrument measures."""

name: str


class PidinstDateDTO(BaseModel):
"""PIDINST v1.0 Property 11: a date marker on the instrument lifecycle."""

value: str
date_type: str


class RelatedIdentifierDTO(BaseModel):
"""PIDINST v1.0 Property 12: a related identifier (parent asset, etc.)."""

value: str
identifier_type: str
relation_type: str


class PidinstAlternateIdentifierDTO(BaseModel):
"""PIDINST v1.0 Property 13: an alternate identifier under a known scheme."""

value: str
kind: str
name: str | None = None


class MeasurementTechniqueDTO(BaseModel):
"""PIDINST v1.0 Property 14: a measurement technique applied by the instrument."""

name: str


class PidinstRecordResponse(BaseModel):
"""Read-side DTO mirroring the slice-C `PidinstRecord` dataclass.

Pydantic mirror so FastAPI can generate an OpenAPI schema. Decouples
the wire format from the slice-C `PidinstRecord` so the two can
evolve independently. Field shapes verbatim from the slice-C tree.
"""

identifier: PidinstIdentifierDTO
schema_version: str
landing_page: str
name: str
publisher: str
publication_year: int | None
owners: list[OwnerDTO]
manufacturers: list[ManufacturerDTO]
model: PidinstModelDTO | None
description: str | None
instrument_types: list[InstrumentTypeDTO]
measured_variables: list[MeasuredVariableDTO]
dates: list[PidinstDateDTO]
related_identifiers: list[RelatedIdentifierDTO]
alternate_identifiers: list[PidinstAlternateIdentifierDTO]
measurement_techniques: list[MeasurementTechniqueDTO]


def record_to_response(record: PidinstRecord) -> PidinstRecordResponse:
"""Walk the slice-C `PidinstRecord` tree into the Pydantic mirror."""
return PidinstRecordResponse(
identifier=PidinstIdentifierDTO(
value=record.identifier.value,
scheme=record.identifier.scheme.value,
),
schema_version=record.schema_version.value,
landing_page=record.landing_page,
name=record.name,
publisher=record.publisher,
publication_year=record.publication_year,
owners=[
OwnerDTO(
name=owner.name,
contact=owner.contact,
identifier=owner.identifier,
identifier_type=owner.identifier_type,
)
for owner in record.owners
],
manufacturers=[
ManufacturerDTO(
name=manufacturer.name,
identifier=manufacturer.identifier,
identifier_type=(
manufacturer.identifier_type.value
if manufacturer.identifier_type is not None
else None
),
)
for manufacturer in record.manufacturers
],
model=(
PidinstModelDTO(
name=record.model.name,
identifier=record.model.identifier,
identifier_type=record.model.identifier_type,
)
if record.model is not None
else None
),
description=record.description,
instrument_types=[
InstrumentTypeDTO(
name=instrument_type.name,
identifier=instrument_type.identifier,
identifier_type=instrument_type.identifier_type,
)
for instrument_type in record.instrument_types
],
measured_variables=[
MeasuredVariableDTO(name=variable.name) for variable in record.measured_variables
],
dates=[
PidinstDateDTO(value=pidinst_date.value, date_type=pidinst_date.date_type.value)
for pidinst_date in record.dates
],
related_identifiers=[
RelatedIdentifierDTO(
value=related_identifier.value,
identifier_type=related_identifier.identifier_type,
relation_type=related_identifier.relation_type.value,
)
for related_identifier in record.related_identifiers
],
alternate_identifiers=[
PidinstAlternateIdentifierDTO(
value=alternate_identifier.value,
kind=alternate_identifier.kind.value,
name=alternate_identifier.name,
)
for alternate_identifier in record.alternate_identifiers
],
measurement_techniques=[
MeasurementTechniqueDTO(name=technique.name)
for technique in record.measurement_techniques
],
)
Loading
Loading