Files
IfcOpenShell/src/bsdd/bsdd_json.py

250 lines
9.2 KiB
Python

from __future__ import annotations
import json
import weakref
from datetime import datetime
from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
from type_hints import *
def _lower_first(s: str) -> str:
return s[:1].lower() + s[1:] if s else s
class CaseInsensitiveModel(BaseModel):
model_config = ConfigDict(populate_by_name=True, alias_generator=_lower_first)
class BsddDictionary(CaseInsensitiveModel):
OrganizationCode: str
DictionaryCode: str
DictionaryVersion: str
LanguageIsoCode: LANGUAGE_ISO_CODE
LanguageOnly: bool
UseOwnUri: bool
DictionaryName: Optional[str] = None
DictionaryUri: Optional[str] = None
License: Optional[str] = "MIT"
LicenseUrl: Optional[str] = None
ChangeRequestEmailAddress: Optional[str] = None
ModelVersion: Optional[str] = "2.0"
MoreInfoUrl: Optional[str] = None
QualityAssuranceProcedure: Optional[str] = None
QualityAssuranceProcedureUrl: Optional[str] = None
ReleaseDate: Optional[datetime] = None
Status: Optional[STATUS] = None
Classes: list[BsddClass] = Field(default_factory=list)
Properties: list[BsddProperty] = Field(default_factory=list)
@classmethod
def load(cls, path) -> BsddDictionary:
"""Load from a JSON file and validate via the normalizer above."""
with open(path, "r", encoding="utf-8") as f:
raw = json.load(f)
# The model_validator(before) handles list/dict/nested shapes
return cls.model_validate(raw)
def save(self, path):
with open(path, "w") as file:
json.dump(self.model_dump(mode="json", exclude_none=True), file)
# add Parent to children after loading
def model_post_init(self, context):
for c in self.Classes:
c._set_parent(self)
for p in self.Properties:
p._set_parent(self)
class BsddClass(CaseInsensitiveModel):
Code: str
Name: str
ClassType: CLASS_TYPE = "Class"
Definition: Optional[str] = None
Description: Optional[str] = None
ParentClassCode: Optional[str | None] = None
RelatedIfcEntityNamesList: Optional[list[str]] = None
Synonyms: Optional[list[str]] = None
ActivationDateUtc: Optional[datetime] = None
ReferenceCode: Optional[str] = None
CountriesOfUse: Optional[list[COUNTRY_CODE]] = None
CountryOfOrigin: Optional[COUNTRY_CODE] = None
CreatorLanguageIsoCode: Optional[LANGUAGE_ISO_CODE] = None
DeActivationDateUtc: Optional[datetime] = None
DeprecationExplanation: Optional[str] = None
DocumentReference: Optional[str] = None
OwnedUri: Optional[str] = None
ReplacedObjectCodes: Optional[list[str]] = None
ReplacingObjectCodes: Optional[list[str]] = None
RevisionDateUtc: Optional[datetime] = None
RevisionNumber: Optional[int] = None
Status: Optional[CLASS_STATUS] = None
SubdivisionsOfUse: Optional[list[str]] = None
Uid: Optional[str] = None
VersionDateUtc: Optional[datetime] = None
VersionNumber: Optional[int] = None
VisualRepresentationUri: Optional[str] = None
ClassProperties: list[BsddClassProperty] = Field(default_factory=list)
ClassRelations: list[BsddClassRelation] = Field(default_factory=list)
_parent_ref: Optional[weakref.ReferenceType[BsddDictionary]] = PrivateAttr(default=None)
def _set_parent(self, parent: BsddDictionary) -> None:
self._parent_ref = weakref.ref(parent)
def parent(self) -> Optional[BsddDictionary]:
return self._parent_ref() if self._parent_ref is not None else None
def model_post_init(self, context):
for c in self.ClassProperties:
c._set_parent(self)
for cr in self.ClassRelations:
cr._set_parent(self)
class BsddClassRelation(CaseInsensitiveModel):
RelationType: CLASS_RELATION_TYPE
RelatedClassUri: str
RelatedClassName: Optional[str] = None
Fraction: Optional[float] = None
OwnedUri: Optional[str] = None
def _set_parent(self, parent: BsddClass) -> None:
self._parent_ref = weakref.ref(parent)
def parent(self) -> Optional[BsddClass]:
return self._parent_ref() if self._parent_ref is not None else None
class BsddAllowedValue(CaseInsensitiveModel):
Code: str
Value: str
Description: Optional[str] = None
Uri: Optional[str] = None
SortNumber: Optional[int] = None
OwnedUri: Optional[str] = None
class BsddClassProperty(CaseInsensitiveModel):
Code: str
PropertyCode: Optional[str] = None
PropertyUri: Optional[str] = None
Description: Optional[str] = None
PropertySet: Optional[str] = None
Unit: Optional[str] = None
PredefinedValue: Optional[str] = None
IsRequired: Optional[bool] = None
IsWritable: Optional[bool] = None
MaxExclusive: Optional[float] = None
MaxInclusive: Optional[float] = None
MinExclusive: Optional[float] = None
MinInclusive: Optional[float] = None
Pattern: Optional[str] = None
OwnedUri: Optional[str] = None
PropertyType: Optional[Literal["Property", "Dependency"]] = None
SortNumber: Optional[int] = None
Symbol: Optional[str] = None
AllowedValues: list[BsddAllowedValue] = Field(default_factory=list)
_parent_ref: Optional[weakref.ReferenceType[BsddClass]] = PrivateAttr(default=None)
def _set_parent(self, parent: BsddClass) -> None:
self._parent_ref = weakref.ref(parent)
def parent(self) -> Optional[BsddClass]:
return self._parent_ref() if self._parent_ref is not None else None
@model_validator(mode="after")
def _validate_property_code_or_uri(self):
"""
only one of PropertyCode or PropertyUri must be set (XOR)
"""
# normalize whitespace
code = self.PropertyCode.strip() if self.PropertyCode and isinstance(self.PropertyCode, str) else None
uri = self.PropertyUri.strip() if self.PropertyUri and isinstance(self.PropertyUri, str) else None
# XOR: exactly one must be provided
if bool(code) == bool(uri):
raise ValueError("Exactly one of PropertyCode or PropertyUri must be provided (not both, not neither)")
# assign normalized values back
object.__setattr__(self, "PropertyCode", code)
object.__setattr__(self, "PropertyUri", uri)
return self
class BsddProperty(CaseInsensitiveModel):
Code: str
Name: str
Definition: Optional[str] = None
Description: Optional[str] = None
DataType: Optional[DATATYPE_TYPE] = None
Units: Optional[list[UNITS_TYPE]] = None
Example: Optional[str] = None
ActivationDateUtc: Optional[datetime] = None
ConnectedPropertyCodes: Optional[list[str]] = None
CountriesOfUse: Optional[list[COUNTRY_CODE]] = None
CountryOfOrigin: Optional[COUNTRY_CODE] = None
CreatorLanguageIsoCode: Optional[LANGUAGE_ISO_CODE] = None
DeActivationDateUtc: Optional[datetime] = None
DeprecationExplanation: Optional[str] = None
Dimension: Optional[str] = None
DimensionLength: Optional[int] = None
DimensionMass: Optional[int] = None
DimensionTime: Optional[int] = None
DimensionElectricCurrent: Optional[int] = None
DimensionThermodynamicTemperature: Optional[int] = None
DimensionAmountOfSubstance: Optional[int] = None
DimensionLuminousIntensity: Optional[int] = None
DocumentReference: Optional[DOCUMENT_TYPE] = None
DynamicParameterPropertyCodes: Optional[list[str]] = None
IsDynamic: Optional[bool] = None
MaxExclusive: Optional[float] = None
MaxInclusive: Optional[float] = None
MinExclusive: Optional[float] = None
MinInclusive: Optional[float] = None
MethodOfMeasurement: Optional[str] = None
OwnedUri: Optional[str] = None
Pattern: Optional[str] = None
PhysicalQuantity: Optional[str] = None
PropertyValueKind: Optional[PROPERTY_VALUE_KIND_TYPE] = None
ReplacedObjectCodes: Optional[list[str]] = None
ReplacingObjectCodes: Optional[list[str]] = None
RevisionDateUtc: Optional[datetime] = None
RevisionNumber: Optional[int] = None
Status: Optional[PROPERTY_STATUS] = None
SubdivisionsOfUse: Optional[list[str]] = None
TextFormat: Optional[str] = None
Uid: Optional[str] = None
VersionDateUtc: Optional[datetime] = None
VersionNumber: Optional[int] = None
VisualRepresentationUri: Optional[str] = None
PropertyRelations: list[BsddPropertyRelation] = Field(default_factory=list)
AllowedValues: list[BsddAllowedValue] = Field(default_factory=list)
_parent_ref: Optional[weakref.ReferenceType[BsddDictionary]] = PrivateAttr(default=None)
def _set_parent(self, parent: BsddDictionary) -> None:
self._parent_ref = weakref.ref(parent)
def parent(self) -> Optional[BsddDictionary]:
return self._parent_ref() if self._parent_ref is not None else None
def model_post_init(self, context):
for pr in self.PropertyRelations:
pr._set_parent(self)
class BsddPropertyRelation(CaseInsensitiveModel):
RelatedPropertyName: Optional[str] = None
RelatedPropertyUri: str
RelationType: PROPERTY_RELATION_TYPE
OwnedUri: Optional[str] = None
def _set_parent(self, parent: BsddProperty) -> None:
self._parent_ref = weakref.ref(parent)
def parent(self) -> Optional[BsddProperty]:
return self._parent_ref() if self._parent_ref is not None else None