229 lines
7 KiB
Python
229 lines
7 KiB
Python
|
|
"""
|
||
|
|
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"
|
||
|
|
)
|