starter-kit/src/nrsk/models.py

229 lines
7 KiB
Python
Raw Normal View History

2025-12-12 09:40:42 -05:00
"""
Define the Data Dictionary.
Implementation of Data Dictionary
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. impl:: Maintain the Data Dictionary base data using Pydantic
:id: I_DATA_DICT
:links: R_DATA_DICT
The data dictionary is managed using Pydantic. Pydantic allows for
concise Python code to richly define data models and their fields. From a single
class definition, it provides data validation, automatic rich documentation (via
automatic a Sphinx plugin), an integration with FastAPI for data exchange, and
relatively easy integration with sqlalchemy for database persistence. Changes to
the schema can be managed and controlled via the revision control system, and
changes to a single source (the Python code) will automatically propagate the
rendered documentation and, potentially the database (e.g. using *alembic*)
Using SQLAchemy as the database engine enables wide flexibility in underlying
database technology, including PostgreSQL, MySQL, SQLite, Oracle, and MS SQL
Server. Pydantic models allows us to validate data loaded from a database,
directly from structured text file, or from JSON data delivered via the network.
Analysis of Alternatives
^^^^^^^^^^^^^^^^^^^^^^^^
SQLModel :cite:p:`SQLModel` was considered as the data layer base, but it was
determined to be less mature than pydantic and sqlalchemy, with inadequate
documentation related to field validation. It was determined to use Pydantic
directly for schema definitions.
.. _data-dict:
Data Dictionary
^^^^^^^^^^^^^^^
This is the official Data Dictionary discussed in :ref:`the Information
Management Plan <info-mgmt-data-dict>`.
"""
import re
import uuid
from datetime import date, datetime
from typing import Annotated, Optional
from pydantic import (
AnyUrl,
BaseModel,
EmailStr,
Field,
FieldValidationInfo,
PositiveInt,
ValidationError,
field_validator,
)
ALL_CAPS = re.compile("^[A-Z]$")
UUID_PK = Annotated[
uuid.UUID,
Field(
default_factory=uuid.uuid4,
description="The unique ID of this object. Used as a primary key in the database.",
examples=["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
frozen=True,
primary_key=True,
),
]
class User(BaseModel):
"""A person involved in the Project."""
uuid: UUID_PK
given_name: str
family_name: str
preferred_name: Optional[str] = None
email: EmailStr
joined_on: Optional[datetime]
deactivated_on: Optional[datetime]
class OpenItem(BaseModel):
uuid: UUID_PK
name: str
status: str
created_on: datetime
closed_on: Optional[datetime] = None
class SSC(BaseModel):
"""
A Structure, System, or Component in the plant.
This is a generic hierarchical object that can represent plants, units,
buildings and their structures, systems, subsystems, components,
subcomponents, etc.
A physical tree of buildings/structures/rooms may have overlapping
contents in terms of systems/components/equipment/parts
"""
uuid: UUID_PK
name: str
pbs_code: Optional[str] = Field(
description="An integer sequence that determines the 'system number' and also the ordering in printouts",
examples=["1.2.3", "20.5.11"],
default="",
)
"""PBS code is tied closely to the structure of the PBS, obviously. If 1.2
is a category level, that's ok, but that doesn't imply that the second level
of PBS 2 is also a category level; it may be systems.
Since this can change in major PBS reorganizations, it should not be used
for cross-referencing (use UUID).
"""
abbrev: str = Field(
description="A human-friendly abbreviation uniquely defining the system"
)
parent: Optional["SSC"] = None
functions: Optional[list[str]] = Field(
description="Functions of this system", default=None
)
@field_validator("abbrev")
@classmethod
def abbrev_must_be_all_caps(cls, v: str, info: FieldValidationInfo) -> str: # noqa: D102
assert re.match(ALL_CAPS, v), f"{info.field_name} must be all CAPS"
@field_validator("pbs_code")
@classmethod
def pbs_must_be_int_sequence(cls, v: str, info: FieldValidationInfo) -> str: # noqa: D102
assert not v or re.match(r"^(\d+\.?)+$", v), (
f"{info.field_name} must be an integer sequence, like 1.2.3"
)
class SystemsList(BaseModel):
"""A flat list of Systems in the plant.
Can be used e.g. to render a snapshot of the Master Systems List.
Does not include categories like "Nuclear Island" or "Primary Systems".
We may want another structure that represents the whole tree in a
well-defined manner, or we may want to add a 'path' attr
to systems that define where they live.
"""
systems: list[SSC]
class ParamDef(BaseModel):
"""A parameter class defining an aspect of plant design."""
uuid: UUID_PK
name: str = Field(
description="Name of parameter class", examples=["Nominal gross power"]
)
description: str = Field(
description="Detailed description of what parameters of this type represent"
)
valid_units: Optional[list[str]] = Field(
description="List of units allowed", examples=["MW", "W", "shp"], default=None
)
class ParamVal(BaseModel):
"""A particular value of a Parameter, assigned to a particular SSC."""
ssc: SSC
pdef: ParamDef
value: str
units: Optional[str] = None
pedigree: str = Field(
description="Indication of how well it is known (rough estimate, final design, as-built)."
)
source: str = Field(description="Where this version of the value came from")
class ITSystem(BaseModel):
"""An IT system used by the project."""
uuid: UUID_PK
name: str
vendor: str
version: Optional[str] = None
use_cases: list[str] = Field(
description="One or more use cases this system is used for.",
examples=[
[
"Document management",
]
],
)
physical_location: str = Field(description="Where the system is physically located")
url: Optional[AnyUrl] = Field(description="Full URL to the system", default=None)
custodian: Optional[User] = Field(
description="Person currently in charge of system", default=None
)
launched_on: Optional[datetime] = None
retired_on: Optional[datetime] = None
quality_related: bool
class Document(BaseModel):
uuid: UUID_PK
title: str = Field(
description="Descriptive title explaining the contents",
examples=["CNSG Development and Status 1966-1977"],
)
"""
.. impl:: Document title
This is how doc titles are done.
"""
revision: str = Field(
description="Revision number",
examples=["0", "1", "1a", "A"],
)
type: str
originators: list[str]
status: str
@field_validator("type")
@classmethod
def type_must_be_valid(cls, v: str, info: FieldValidationInfo) -> str:
assert v in ["CALC", "PROC"], (
f"{info.field_name} must be within the list of doctypes"
)