diff --git a/bam_masterdata/datamodel/v2/CHANGES.md b/bam_masterdata/datamodel/v2/CHANGES.md new file mode 100644 index 00000000..8f5ecd86 --- /dev/null +++ b/bam_masterdata/datamodel/v2/CHANGES.md @@ -0,0 +1,40 @@ +# Summary of changes + +- New `code` only identifiers for the classes names +- `is_a` field in ObjectTypeDef keeps track of inheritance +- `previous_versions` encodes metadata about the previous version equivalent object code + +- Changed descriptions: $NAME, $SHOW_IN_PROJECT_OVERVIEW, START_DATE, END_DATE +- `$SHOW_IN_PROJECT_OVERVIEW` changed property_label +- Deleted EXPERIMENTAL_STEP.EXPERIMENTAL_RESULTS, $ANNOTATIONS_STATE, $XMLCOMMENTS +- Deleted `GMAW_BASE` and `LASER_HYBRID_MAGNET` (Cagtay) +- Fix `ACCREDITATED_CALIBRATION_LAB` to `ACCREDITED_CALIBRATION_LAB` +- Deleted `TASK`, `ACTION` +- Deleted `EXPERIMENTAL_STEP.RM_ETHANOL`, `ACTION.DEVICE_TRAINING`, `ACTION.DEVICE_USAGE`, `EXPERIMENTAL_STEP.MEASUREMENT_SESSION` +- Changed Calibration properties descriptions, sections, and property labels +- Changed `CALIBRATION_PROVIDER` from CONTROLLEDVOCABULARY to VARCHAR +- Deleted `COMPUTATIONAL_ANALYSIS` (now it is `ANALYSIS`) +- Deleted `CONDA_ENVIRONMENT`, `JUPYTER_NOTEBOOK` +- Change vocabulary code `DCPD_POT_CAL` to `DCPD_POT_DROP_CALIBRATION` +- Changed `DCPD_INITIAL_CRACKLENGTH` property code to `DCPD_INITIAL_CRACK_LENGTH` +- Deleted all properties in DCPD which are instrument-specific --> they should be properties of the instrument +- Changed `RAZOR_STROKELENGTH`, `RAZOR_STROKESPEED`, and `RAZOR_STROKECOUNT` to `RAZOR_STROKE_LENGTH`, `RAZOR_STROKE_SPEED`, and `RAZOR_STROKE_COUNT` +- Changed `RAZOR_STROKE_COUNT` from REAL to INTEGER +- Combined `FCG_TEST` and `FCG_STEP` into `FCG_TEST` +- Changed `FINAL_CYCLES` from REAL to INTEGER +- Changed vocabulary code `MICROSCOPY_FCG_CRACK_LENGTH_TYPE` to `MICROSCOPY_FS_TYPE` +- Deleted `EXPERIMENTAL_STEP.FCG_EVALUATION` +- Added detailed measurement metadata (DLS, FTIR, ImageSeries, NMR, ProfileScan, SAXS, SEM, TEM, Video, and LaserDiff PSD) to `activities.py`, carrying over `previous_versions` from the legacy schema and keeping the BFO-informed parent properties untouched. +- Removed the migrated activity definitions from the active legacy model, appended them as a commented block at the end of `object_types_old.py`, and preserved their field descriptions for reference. +- Marked the legacy `EXPERIMENTAL_STEP.*` codes (including DCPD, MS batch, SAXS, SEM, TEM, NMR, DLS, FTIR, Video, thermographic, and LaserDiff PSD) as deprecated in `base.py` to signal that the new v2 activity classes should be used instead. +- **DLS:** `DLS_DISPERSANT` → `DLS.DISPERSANT`, `DLS_TEMPERATURE` → `DLS.TEMPERATURE`, `DLS_CELL_DESCRIPTION` → `DLS.CELLDESCRIPTION`, `DLS_ATTENUATOR` → `DLS.ATTENUATOR`, `DLS_Z_AVERAGE` → `DLS.ZAVG`, `DLS_PDI` → `DLS.PDI`, `DLS_ZETA_POTENTIAL` → `DLS.ZETA`, `DLS_CONDUCTIVITY` → `DLS.COND`. +- **FTIR:** `FTIR_START_WAVENUMBER`, `FTIR_END_WAVENUMBER`, `FTIR_RESOLUTION`, `FTIR_SCAN_COUNT`, `FTIR_ACCESSORY`, `FTIR_FLUSHED_WITH_NITROGEN` now replace `FTIR.*` properties. +- **ImageSeries:** `IMAGE_HORIZONTAL_RESOLUTION`, `IMAGE_VERTICAL_RESOLUTION`, `IMAGE_SERIES_COUNT`; we removed the legacy `UUID`. +- **NMR:** `NMR_NUCLEUS_DIRECT`, `NMR_NUCLEUS_INDIRECT`, `NMR_SOLVENT`, `NMR_FREQUENCY`, `NMR_EXPERIMENT_TYPE`, `NMR_SCAN_COUNT`, `NMR_START_CHEMICAL_SHIFT`, `NMR_END_CHEMICAL_SHIFT`, `NMR_IS_QUANTITATIVE`, `NMR_PULSE_ANGLE`, `NMR_INTERPULSE_DELAY`, `NMR_ACQUISITION_TIME`. +- **ProfileScan:** `SCAN_LINE_COUNT`, `SCAN_LINE_RESOLUTION`; the legacy `UUID` is no longer part of the data model. +- **SAXS measurement:** `SAXS_CELL_TEMPERATURE`, `SAXS_EXPOSURE_TIME`, `SAXS_FRAME_COUNT`. +- **SEM:** `SEM_OPERATING_MODE`, `SEM_DETECTOR`, `SEM_ACCELERATION_VOLTAGE`, `SEM_MAGNIFICATION`, `SEM_WORKING_DISTANCE`; we dropped `SEM.INSTRUMENT` and normalized the `SEM.ACCELERATIONVOLTAGE` units (now a VARCHAR placeholder since `keV` in pint is tricky). +- **TEM:** `TEM_OPERATING_MODE`, `TEM_DETECTOR`, `TEM_ACCELERATION_VOLTAGE`, `TEM_MAGNIFICATION`, `TEM_CAMERA_LENGTH`. +- **VideoRecording:** `IMAGE_HORIZONTAL_RESOLUTION`, `IMAGE_VERTICAL_RESOLUTION`, `VIDEO_FRAME_PER_SECONDS`, `VIDEO_CODEC`, `VIDEO_DYNAMIC_FRAMERATE`; the old `UUID` field was removed. +- **LaserDiff PSD:** `DISPERSING_MEDIUM`, `SCATTERING_MODEL_PSD_LD`, `REFRACTIVE_INDEX_SAMPLE`, `ABSORPTION_COEFFICIENT_SAMPLE`, `LASER_OBSCURATION`, `LASER_TRANSMISSION`, `MEASUREMENT_MEDIUM_TEMPERATURE`, `PSD_D10`, `PSD_D50`, `PSD_D90`, `MODE_COUNT`. +- Ensured every property keeps its legacy `previous_versions` entry so the mapping to `object_types_old.py` remains explicit, supporting audits and migrations. diff --git a/bam_masterdata/datamodel/v2/base.py b/bam_masterdata/datamodel/v2/base.py new file mode 100644 index 00000000..2df4a3d0 --- /dev/null +++ b/bam_masterdata/datamodel/v2/base.py @@ -0,0 +1,456 @@ +from bam_masterdata.metadata.definitions import ObjectTypeDef, PropertyTypeAssignment +from bam_masterdata.metadata.entities import ObjectType + +# A list of deprecated ObjectType codes that should not be used in new data entries. This can be used to +# maintain backward compatibility while signaling to users that certain types are no longer recommended for use. +DEPRECATED_OR_UNUSED = [ + "RAW_MATERIAL.STEEL", + "RAW_MATERIAL.ALUMINIUM", +] + + +class BaseEntity(ObjectType): + defs = ObjectTypeDef( + code="BASE_ENTITY", + description=""" + A BaseEntity is an entity that encompasses both material and immaterial + existents, serving as the foundational type from which all domain-specific + entities are derived. + """, + iri="https://bam.de/masterdata/BaseEntity", + references=["http://purl.obolibrary.org/obo/BFO_0000001"], + generated_code_prefix="BASE_ENTIT", + aliases=[], + ) + + name = PropertyTypeAssignment( + code="$NAME", + data_type="VARCHAR", + property_label="Name", + description=""" + Human-readable name used to identify the entity in user interfaces, reports, and search results. + """, + mandatory=True, + section="General Information", + ) + + show_in_project_overview = PropertyTypeAssignment( + code="$SHOW_IN_PROJECT_OVERVIEW", + data_type="BOOLEAN", + property_label="Visible in project overview?", + description=""" + Controls whether the entity is displayed in the project overview page. + """, + mandatory=False, + section="General Information", + ) + + description = PropertyTypeAssignment( + code="DESCRIPTION", + data_type="MULTILINE_VARCHAR", + property_label="Description", + description=""" + Human-readable description of the entity. + """, + mandatory=False, + section="References", + previous_versions=["EXPERIMENTAL_STEP.EXPERIMENTAL_DESCRIPTION"], + ) + + data_id = PropertyTypeAssignment( + code="DATA_ID", + data_type="VARCHAR", # ! change to multivalued VARCHAR when available + property_label="ID", + description=""" + Persistent identifier used to uniquely identify the entity. It can be any unique + identifier that can be used to reference the entity internally or in external systems + or databases. + """, + mandatory=False, + section="References", + previous_versions=["PUBLICATION"], + ) + + references = PropertyTypeAssignment( + code="REFERENCE", + data_type="MULTILINE_VARCHAR", # ! change to multivalued HYPERLINK when available + property_label="References", + description=""" + Links/DOIs/URLs relevant to this entity (one per line). + """, + mandatory=False, + section="References", + ) + + +class Activity(BaseEntity): + defs = ObjectTypeDef( + code="ACTIVITY", + description=""" + An Activity is something that occurs over a period of time and acts upon or with + entities; it may include consuming, processing, transforming, modifying, + relocating, using, or generating entities. + """, + iri="https://bam.de/masterdata/Activity", + references=[ + "http://purl.obolibrary.org/obo/BFO_0000015", + "https://www.w3.org/TR/prov-o/#Activity", + ], + generated_code_prefix="ACTIV", + aliases=["ACTIVITY_STEP", "EXPERIMENTAL_STEP"], + previous_versions=["EXPERIMENTAL_STEP"], + ) + + start_date = PropertyTypeAssignment( + code="START_DATE", + data_type="TIMESTAMP", + property_label="Start date", + description=""" + Start date of the activity. + """, + mandatory=False, + section="General Information", + ) + + end_date = PropertyTypeAssignment( + code="END_DATE", + data_type="TIMESTAMP", + property_label="End date", + description=""" + End date of the activity. + """, + mandatory=False, + section="General Information", + ) + + status = PropertyTypeAssignment( + code="ACTIVITY_STATUS", + data_type="CONTROLLEDVOCABULARY", + vocabulary_code="ACTIVITY_STATUS", + property_label="Status", + description=""" + Current status of the activity: PLANNED, RUNNING, COMPLETED, CANCELLED. + """, + mandatory=False, + section="General Information", + previous_versions=["FINISHED_FLAG"], + ) + + goals = PropertyTypeAssignment( + code="GOALS", + data_type="MULTILINE_VARCHAR", + property_label="Goals", + description=""" + Goals of the activity in free-text format. + """, + mandatory=False, + section="Activity Details", + previous_versions=["EXPERIMENTAL_STEP.EXPERIMENTAL_GOALS"], + ) + + activity_spreadsheet = PropertyTypeAssignment( + code="SPREADSHEET", + data_type="XML", + property_label="Spreadsheet", + description=""" + Structured spreadsheet used to capture tabular parameters, intermediate values, or + structured notes associated with an entity. This field is intended for lightweight, + human-curated data and is not a replacement for datasets or result files. + """, + mandatory=False, + section="Activity Details", + previous_versions=["EXPERIMENTAL_STEP.SPREADSHEET"], + ) + + notes = PropertyTypeAssignment( + code="NOTES", + data_type="MULTILINE_VARCHAR", + property_label="Notes", + description=""" + Free-form notes. + """, + mandatory=False, + section="Additional Information", + ) + + +class Analysis(Activity): + defs = ObjectTypeDef( + code="ANALYSIS", + description=""" + An Analysis is an activity that interprets existing data to derive new data, such + as properties, patterns, or parameters + """, + iri="https://bam.de/masterdata/Analysis", + generated_code_prefix="ANALI", + aliases=[], + previous_versions=["COMPUTATIONAL_ANALYSIS"], + ) + + +class Calibration(Activity): + defs = ObjectTypeDef( + code="CALIBRATION", + description=""" + A Calibration is an activity that establishes or adjusts the mapping between + measurement outputs and reference standards. + """, + iri="https://bam.de/masterdata/Calibration", + generated_code_prefix="CALIB", + aliases=[], + previous_versions=[], + ) + + calibration_date = PropertyTypeAssignment( + code="CALIBRATION_DATE", + data_type="DATE", + property_label="Calibration date", + description=""" + Date when the calibration was performed. + """, + mandatory=True, + section="Calibration Information", + ) + + calibration_provider = PropertyTypeAssignment( + code="CALIBRATION_PROVIDER", + data_type="VARCHAR", + vocabulary_code="CALIBRATION_PROVIDER", + property_label="Calibration provider", + description=""" + Organization or service provider that performed the calibration. + """, + mandatory=True, + section="Calibration Information", + ) + + calibration_certificate_number = PropertyTypeAssignment( + code="CALIBRATION_CERTIFICATE_NUMBER", + data_type="VARCHAR", + property_label="Calibration certificate number", + description=""" + Identifier of the calibration certificate issued for this calibration. + """, + mandatory=True, + section="Calibration Information", + ) + + accredited_calibration_lab = PropertyTypeAssignment( + code="ACCREDITED_CALIBRATION_LAB", + data_type="BOOLEAN", + property_label="Calibration performed by an Accredited Laboratory?", + description=""" + Indicates whether the calibration was performed by an accredited laboratory. + """, + mandatory=True, + section="Calibration Information", + previous_versions=["ACCREDITATED_CALIBRATION_LAB"], + ) + + calibration_lab_accreditation_number = PropertyTypeAssignment( + code="CALIBRATION_LAB_ACCREDITATION_NUMBER", + data_type="VARCHAR", + property_label="Calibration Laboratory Accreditation Number", + description=""" + Accreditation identifier of the laboratory (required if the calibration was performed by + an accredited laboratory). + """, + mandatory=False, + section="Calibration Information", + ) + + +class Measurement(Activity): + defs = ObjectTypeDef( + code="MEASUREMENT", + description=""" + A Measurement is an activity that uses an experimental device to produce quantitative + or qualitative data about the properties of a material. + """, + iri="https://bam.de/masterdata/Measurement", + generated_code_prefix="MEASU", + aliases=[], + previous_versions=[], + ) + + +class Processing(Activity): + defs = ObjectTypeDef( + code="PROCESSING", + description=""" + A Processing is an activity that alters the structure, composition, or form of a + material. + """, + iri="https://bam.de/masterdata/Processing", + generated_code_prefix="PROCE", + aliases=[], + previous_versions=[], + ) + + +class Simulation(Activity): + defs = ObjectTypeDef( + code="SIMULATION", + description=""" + A Simulation is an activity that uses computational models to replicate or + predict the behavior of a material. + """, + iri="https://bam.de/masterdata/Simulation", + generated_code_prefix="SIMUL", + aliases=[], + previous_versions=[], + ) + + +class Synthesis(Activity): + defs = ObjectTypeDef( + code="SYNTHESIS", + description=""" + A Synthesis is an activity that creates or assembles materials through chemical or + physical means. + """, + iri="https://bam.de/masterdata/Synthesis", + generated_code_prefix="SYNTH", + aliases=[], + previous_versions=[], + ) + + +class Test(Activity): + defs = ObjectTypeDef( + code="TEST", + description=""" + A Test is an activity that subjects materials to specific conditions to evaluate + their performance, reliability, or compliance with certain standards. + """, + iri="https://bam.de/masterdata/Test", + generated_code_prefix="TEST", + aliases=[], + previous_versions=[], + ) + + +class Entity(BaseEntity): + defs = ObjectTypeDef( + code="ENTITY", + description=""" + An Entity is a physical, digital, conceptual, or other kind of thing with some fixed + aspects; entities may be real or imaginary. + """, + iri="https://bam.de/masterdata/Entity", + references=[ + "http://purl.obolibrary.org/obo/BFO_0000002", + "https://www.w3.org/TR/prov-o/#Entity", + ], + generated_code_prefix="ENTIT", + aliases=[], + previous_versions=[], + ) + + +class InformationObject(Entity): + defs = ObjectTypeDef( + code="INFORMATION_OBJECT", + description=""" + An InformationObject is an (information content) entity that represents, describes, + or encodes knowledge about systems, instruments, or activities. It may be produced + by processes or used as input for interpretation, automation, or modelling. + """, + iri="https://bam.de/masterdata/InformationObject", + references=[ + "http://purl.obolibrary.org/obo/IAO_0000030", + ], + generated_code_prefix="INFOR_OBJEC", + aliases=[], + previous_versions=[], + ) + + +class MaterialEntity(Entity): + defs = ObjectTypeDef( + code="MATERIAL_ENTITY", + description=""" + A MaterialEntity is an (independent continuant) entity that has some portion of matter as a + proper or improper continuant part, and persists through time while possibly gaining + or losing parts. + """, + iri="https://bam.de/masterdata/MaterialEntity", + references=[ + "http://purl.obolibrary.org/obo/BFO_0000040", + ], + generated_code_prefix="MATER_ENTIT", + aliases=[], + previous_versions=[], + ) + + +class Material(MaterialEntity): + defs = ObjectTypeDef( + code="MATERIAL", + description=""" + A Material is a material entity that is composed of a physical substance or mixture of + substances that is characterized by its chemical composition, structure, and properties. + """, + iri="https://bam.de/masterdata/Material", + references=[], + generated_code_prefix="MATER", + aliases=[], + previous_versions=[], + ) + + +class Instrument(MaterialEntity): + defs = ObjectTypeDef( + code="INSTRUMENT", + description=""" + An Instrument is a material entity that is designed or used to support an activity by + measuring, modifying, or interacting with other entities such as systems and organisms. + """, + iri="https://bam.de/masterdata/Instrument", + references=[ + "http://purl.obolibrary.org/obo/OBI_0000968", + ], + generated_code_prefix="INSTR", + aliases=[], + previous_versions=[], + ) + + +class Sample(MaterialEntity): + defs = ObjectTypeDef( + code="SAMPLE", + description=""" + A Sample is a material entity that is collected for potential use as an input upon + which measurements or observations are performed. + """, + iri="https://bam.de/masterdata/Sample", + references=[ + "http://purl.obolibrary.org/obo/OBI_0100051", + ], + generated_code_prefix="SAMPL", + aliases=[], + previous_versions=[], + ) + + +class Organism(MaterialEntity): + defs = ObjectTypeDef( + code="ORGANISM", + description=""" + An Organism is a material entity that is an individual living system, such as animal, + plant, bacteria or virus, that is capable of replicating or reproducing, growth + and maintenance in the right environment. An organism may be unicellular or made up, + like humans, of many billions of cells divided into specialized tissues and organs. + """, + iri="https://bam.de/masterdata/Organism", + references=[ + "http://purl.obolibrary.org/obo/OBI_0100026", + ], + generated_code_prefix="ORGAN", + aliases=[], + previous_versions=[], + ) + + +# end of level 2 objects +# below this, we can still define Entity subtypes like Location, Person, Chemical, etc. diff --git a/bam_masterdata/metadata/definitions.py b/bam_masterdata/metadata/definitions.py index b94652ea..6ad4253b 100644 --- a/bam_masterdata/metadata/definitions.py +++ b/bam_masterdata/metadata/definitions.py @@ -93,6 +93,44 @@ class EntityDef(BaseModel): """, ) + is_a: str | None = Field( + None, + description=""" + A lineage string indicating the inheritance, i.e., the parent entities from which this + entity is derived (using `code` as the identifier of each entity). It is a string + with the format `".."`. In the specific ase + of BASE_ENTITY, this field is None. + + This is resolved in `ObjectType`. + + Example, `MEASUREMENT` inherits from `ACTIVITY`, which inherits from `BASE_ENTITY`: "BASE_ENTITY.ACTIVITY.MEASUREMENT". + """, + ) + + references: list[str] = Field( + default_factory=list, + description=""" + List of references (e.g., URLs or DOIs) related to the entity definition. This can + include links to IRIs on other ontology definitions, documentation, or relevant publications. + """, + ) + + aliases: list[str] = Field( + default_factory=list, + description=""" + List of alternative codes for the entity. These aliases can be used to refer to + the entity in different contexts or systems, e.g., in an older version of the Masterdata. + """, + ) + + previous_versions: list[str] = Field( + default_factory=list, + description=""" + List of previous version codes for the entity. This can be used to track the evolution + of the entity definition over time. + """, + ) + id: str | None = Field( default=None, description=""" @@ -152,13 +190,11 @@ def validate_iri(cls, value: str | None) -> str | None: """ if not value: return value - if not re.match( - r"^http://purl.obolibrary.org/bam-masterdata/[\w_]+:[\d.]+$", value - ): + if not re.match(r"https://bam.de/masterdata/[\w_]+$", value): raise ValueError( - "`iri` must follow the rules specified in the description: 1) Must start with 'http://purl.obolibrary.org/bam-masterdata/', " - "2) followed by the entity name, 3) separated by a colon, 4) followed by the semantic versioning number. " - "Example: 'http://purl.obolibrary.org/bam-masterdata/Instrument:1.0.0'." + "`iri` must follow the rules specified in the description: 1) Must start with 'https://bam.de/masterdata/', " + "2) followed by the entity name. " + "Example: 'https://bam.de/masterdata/BaseEntity'." ) return value @@ -201,7 +237,7 @@ def excel_headers_map(self) -> dict: fields = [ k for k in self.model_fields.keys() - if k not in ["iri", "id", "row_location"] + if k not in ["iri", "is_a", "references", "aliases", "id", "row_location"] ] headers: dict = {} for f in fields: @@ -328,9 +364,9 @@ def model_validator_after_init(cls, data: Any) -> Any: Returns: Any: The data with the validated fields. """ - # If `generated_code_prefix` is not set, use the first 3 characters of `code` + # If `generated_code_prefix` is not set, use the first 5 characters of `code` if not data.generated_code_prefix: - data.generated_code_prefix = data.code[:3] + data.generated_code_prefix = data.code[:5] return data @@ -516,10 +552,11 @@ class Instrument(ObjectType): ) show_in_edit_views: bool = Field( - ..., + True, description=""" If `True`, the property is shown in the edit views of the ELN in the object type instantiation. If `False`, the property is hidden. + Defaults to True. """, ) diff --git a/bam_masterdata/metadata/entities.py b/bam_masterdata/metadata/entities.py index 92cb560a..fce01c1f 100644 --- a/bam_masterdata/metadata/entities.py +++ b/bam_masterdata/metadata/entities.py @@ -522,7 +522,7 @@ class VocabularyType(BaseEntity): model_config = ConfigDict(ignored_types=(VocabularyTypeDef, VocabularyTerm)) terms: list[VocabularyTerm] = Field( - default=[], + default_factory=list, description=""" List of vocabulary terms. This is useful for internal representation of the model. """, @@ -603,7 +603,7 @@ class ObjectType(BaseEntity): ) properties: list[PropertyTypeAssignment] = Field( - default=[], + default_factory=list, description=""" List of properties assigned to an object type. This is useful for internal representation of the model. """, @@ -617,6 +617,16 @@ def __init__(self, **kwargs): for key, prop in self._property_metadata.items(): self._properties[key] = prop.data_type + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + if not hasattr(cls, "defs"): + return + # Build lineage codes from root->...->self using defs.code + codes = [c.defs.code for c in reversed(cls.__mro__) if hasattr(c, "defs")] + # For BASE_ENTITY => None, for others, e.g., Activity => "BASE_ENTITY", for Measurement => "BASE_ENTITY.ACTIVITY" + is_a = ".".join(codes[:-1]) if len(codes) > 1 else None + cls.defs = cls.defs.model_copy(update={"is_a": is_a}) # store in `defs` + def _set_object_value(self, key, value): """ Sets the value when the data type is OBJECT. @@ -890,7 +900,7 @@ class CollectionType(ObjectType): ) attached_objects: dict[str, ObjectType] = Field( - default={}, + default_factory=dict, exclude=True, description=""" Dictionary containing the object types attached to the collection type. @@ -899,7 +909,7 @@ class CollectionType(ObjectType): ) relationships: dict[str, tuple[str, str]] = Field( - default={}, + default_factory=dict, exclude=True, description=""" Dictionary containing the relationships between the objects attached to the collection type. diff --git a/pyproject.toml b/pyproject.toml index 4452e8a3..26bcfc4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,9 @@ docu = [ "mkdocstrings", "mkdocstrings-python", ] +plot = [ + "graphviz", # requires system package: apt install graphviz +] [project.scripts] bam_masterdata = "bam_masterdata.cli:cli" diff --git a/tools/scripts/plot_datamodel_graph.py b/tools/scripts/plot_datamodel_graph.py new file mode 100644 index 00000000..bf887fa4 --- /dev/null +++ b/tools/scripts/plot_datamodel_graph.py @@ -0,0 +1,58 @@ +import argparse +import importlib +from pathlib import Path + +from graphviz import Digraph + +from bam_masterdata.metadata.definitions import PropertyTypeAssignment +from bam_masterdata.metadata.entities import ObjectType + + +def get_label(cls) -> str: + """Create label based on property types.""" + props = [ + name + for name, val in vars(cls).items() + if isinstance(val, PropertyTypeAssignment) + ] + return f"{cls.__name__}\\n" + "\\n".join(props) + + +def add_subclasses(dot: Digraph, cls): + """Add all subclasses to the graph.""" + dot.node(cls.__name__, label=get_label(cls), shape="box") + for subcls in cls.__subclasses__(): + dot.edge(cls.__name__, subcls.__name__) + add_subclasses(dot, subcls) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "root_class", + help="Root class to plot (e.g. bam_masterdata.datamodel.v2.base.BaseEntity)", + ) + parser.add_argument( + "--output", + default="", + help="Output file name (without extension). By default uses the root class name.", + ) + + args = parser.parse_args() + + module = args.root_class.rsplit(".", 1)[0] + class_name = args.root_class.rsplit(".", 1)[1] + + root_class = getattr(importlib.import_module(module), class_name) + + filename = args.output or root_class.__name__ + + dot = Digraph() + add_subclasses(dot, root_class) + + out_path = Path(__file__).parent / filename + dot.render(str(out_path), format="png") + + +if __name__ == "__main__": + main()