mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-18 03:19:53 +00:00
282 lines
10 KiB
Python
282 lines
10 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from pydantic import BaseModel
|
||
|
|
from .type_hints import *
|
||
|
|
from typing import List, Optional, Literal
|
||
|
|
from datetime import datetime
|
||
|
|
from pydantic import BaseModel, Field, PrivateAttr, model_validator, ConfigDict
|
||
|
|
import json
|
||
|
|
import weakref
|
||
|
|
import logging
|
||
|
|
|
||
|
|
|
||
|
|
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)
|
||
|
|
|
||
|
|
def _apply_code_side_effects(self, code: str) -> None:
|
||
|
|
from bsdd_json.utils import class_utils as cl_utils
|
||
|
|
|
||
|
|
if not code.strip():
|
||
|
|
logging.info("Empty Code is not allowed")
|
||
|
|
raise ValueError("Empty Code is not allowed")
|
||
|
|
|
||
|
|
parent = self._parent_ref() if self._parent_ref else None
|
||
|
|
if parent is not None and code in cl_utils.get_all_class_codes(parent):
|
||
|
|
logging.info(f"Code '{code}' exists already")
|
||
|
|
raise ValueError(f"Code '{code}' exists already")
|
||
|
|
|
||
|
|
# propagate to children
|
||
|
|
for child in cl_utils.get_children(self):
|
||
|
|
child.ParentClassCode = code
|
||
|
|
|
||
|
|
|
||
|
|
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
|