""" 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 `. """ 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" )