openepd 6.13.2__py3-none-any.whl → 7.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openepd/__init__.py +4 -4
- openepd/__version__.py +1 -1
- openepd/api/average_dataset/generic_estimate_sync_api.py +11 -10
- openepd/api/average_dataset/industry_epd_sync_api.py +9 -8
- openepd/api/base_sync_client.py +53 -9
- openepd/api/category/sync_api.py +1 -1
- openepd/api/dto/base.py +4 -4
- openepd/api/dto/common.py +24 -16
- openepd/api/dto/meta.py +15 -11
- openepd/api/dto/mf.py +9 -8
- openepd/api/epd/dto.py +43 -33
- openepd/api/epd/sync_api.py +9 -9
- openepd/api/pcr/sync_api.py +2 -2
- openepd/bundle/model.py +11 -10
- openepd/bundle/reader.py +12 -5
- openepd/bundle/writer.py +17 -6
- openepd/model/base.py +60 -43
- openepd/model/category.py +13 -10
- openepd/model/common.py +100 -55
- openepd/model/declaration.py +93 -64
- openepd/model/epd.py +51 -43
- openepd/model/generic_estimate.py +28 -13
- openepd/model/industry_epd.py +15 -9
- openepd/model/lcia.py +132 -113
- openepd/model/org.py +54 -33
- openepd/model/pcr.py +38 -32
- openepd/model/specs/asphalt.py +31 -22
- openepd/model/specs/base.py +11 -9
- openepd/model/specs/concrete.py +60 -39
- openepd/model/specs/range/aggregates.py +9 -9
- openepd/model/specs/range/aluminium.py +7 -7
- openepd/model/specs/range/asphalt.py +22 -19
- openepd/model/specs/range/cladding.py +16 -16
- openepd/model/specs/range/cmu.py +10 -9
- openepd/model/specs/range/concrete.py +36 -27
- openepd/model/specs/range/conveying_equipment.py +16 -15
- openepd/model/specs/range/electrical.py +24 -22
- openepd/model/specs/range/finishes.py +109 -104
- openepd/model/specs/range/fire_and_smoke_protection.py +7 -7
- openepd/model/specs/range/furnishings.py +16 -12
- openepd/model/specs/range/manufacturing_inputs.py +16 -16
- openepd/model/specs/range/masonry.py +16 -16
- openepd/model/specs/range/mechanical.py +47 -47
- openepd/model/specs/range/mechanical_insulation.py +7 -7
- openepd/model/specs/range/network_infrastructure.py +54 -46
- openepd/model/specs/range/openings.py +36 -31
- openepd/model/specs/range/plumbing.py +15 -13
- openepd/model/specs/range/precast_concrete.py +20 -16
- openepd/model/specs/range/sheathing.py +18 -18
- openepd/model/specs/range/steel.py +25 -25
- openepd/model/specs/range/thermal_moisture_protection.py +20 -20
- openepd/model/specs/range/utility_piping.py +9 -9
- openepd/model/specs/range/wood.py +19 -19
- openepd/model/specs/range/wood_joists.py +8 -8
- openepd/model/specs/singular/__init__.py +9 -5
- openepd/model/specs/singular/aggregates.py +22 -15
- openepd/model/specs/singular/aluminium.py +20 -5
- openepd/model/specs/singular/asphalt.py +44 -20
- openepd/model/specs/singular/cladding.py +38 -23
- openepd/model/specs/singular/cmu.py +26 -11
- openepd/model/specs/singular/common.py +3 -2
- openepd/model/specs/singular/concrete.py +85 -48
- openepd/model/specs/singular/conveying_equipment.py +30 -17
- openepd/model/specs/singular/deprecated/__init__.py +3 -2
- openepd/model/specs/singular/deprecated/concrete.py +68 -33
- openepd/model/specs/singular/deprecated/steel.py +28 -15
- openepd/model/specs/singular/electrical.py +69 -41
- openepd/model/specs/singular/finishes.py +250 -140
- openepd/model/specs/singular/fire_and_smoke_protection.py +9 -6
- openepd/model/specs/singular/furnishings.py +16 -14
- openepd/model/specs/singular/manufacturing_inputs.py +23 -14
- openepd/model/specs/singular/masonry.py +66 -21
- openepd/model/specs/singular/mechanical.py +48 -47
- openepd/model/specs/singular/mechanical_insulation.py +7 -6
- openepd/model/specs/singular/mixins/conduit_mixin.py +13 -10
- openepd/model/specs/singular/network_infrastructure.py +111 -52
- openepd/model/specs/singular/openings.py +127 -95
- openepd/model/specs/singular/plumbing.py +15 -12
- openepd/model/specs/singular/precast_concrete.py +68 -54
- openepd/model/specs/singular/sheathing.py +47 -27
- openepd/model/specs/singular/steel.py +69 -45
- openepd/model/specs/singular/thermal_moisture_protection.py +36 -20
- openepd/model/specs/singular/utility_piping.py +11 -8
- openepd/model/specs/singular/wood.py +48 -24
- openepd/model/specs/singular/wood_joists.py +19 -6
- openepd/model/standard.py +15 -8
- openepd/model/validation/common.py +9 -3
- openepd/model/validation/numbers.py +0 -13
- openepd/model/validation/quantity.py +53 -25
- openepd/model/versioning.py +9 -6
- openepd/patch_pydantic.py +0 -93
- {openepd-6.13.2.dist-info → openepd-7.0.0.dist-info}/METADATA +1 -1
- openepd-7.0.0.dist-info/RECORD +142 -0
- openepd/compat/__init__.py +0 -15
- openepd/compat/compat_functional_validators.py +0 -25
- openepd/compat/pydantic.py +0 -30
- openepd-6.13.2.dist-info/RECORD +0 -145
- {openepd-6.13.2.dist-info → openepd-7.0.0.dist-info}/LICENSE +0 -0
- {openepd-6.13.2.dist-info → openepd-7.0.0.dist-info}/WHEEL +0 -0
openepd/api/pcr/sync_api.py
CHANGED
@@ -30,7 +30,7 @@ class PcrApi(BaseApiMethodGroup):
|
|
30
30
|
:raise ValidationError: if openxpd_uuid is invalid
|
31
31
|
"""
|
32
32
|
content = self._client.do_request("get", f"/pcrs/{uuid}").json()
|
33
|
-
return Pcr.
|
33
|
+
return Pcr.model_validate(content)
|
34
34
|
|
35
35
|
def create(self, pcr: Pcr) -> PcrRef:
|
36
36
|
"""
|
@@ -41,4 +41,4 @@ class PcrApi(BaseApiMethodGroup):
|
|
41
41
|
:raise ValidationError: if given object PCR is invalid
|
42
42
|
"""
|
43
43
|
pcr_ref_obj = self._client.do_request("post", "/pcrs", json=pcr.to_serializable()).json()
|
44
|
-
return PcrRef.
|
44
|
+
return PcrRef.model_validate(pcr_ref_obj)
|
openepd/bundle/model.py
CHANGED
@@ -16,7 +16,8 @@
|
|
16
16
|
from datetime import datetime
|
17
17
|
from enum import StrEnum
|
18
18
|
|
19
|
-
|
19
|
+
import pydantic
|
20
|
+
|
20
21
|
from openepd.model.base import BaseOpenEpdSchema
|
21
22
|
|
22
23
|
|
@@ -29,7 +30,7 @@ class BundleManifestAssetsStats(BaseOpenEpdSchema):
|
|
29
30
|
total_size: int = 0
|
30
31
|
"""The total size of assets in bytes."""
|
31
32
|
|
32
|
-
count_by_type: dict[str, int] =
|
33
|
+
count_by_type: dict[str, int] = pydantic.Field(default_factory=dict)
|
33
34
|
"""The number of assets by type."""
|
34
35
|
|
35
36
|
|
@@ -60,9 +61,9 @@ class BundleManifest(BaseOpenEpdSchema):
|
|
60
61
|
"""The format of the bundle."""
|
61
62
|
generator: str
|
62
63
|
"""The generator of the bundle."""
|
63
|
-
assets: BundleManifestAssetsStats =
|
64
|
-
comment: str | None =
|
65
|
-
created_at: datetime =
|
64
|
+
assets: BundleManifestAssetsStats = pydantic.Field(default_factory=BundleManifestAssetsStats)
|
65
|
+
comment: str | None = pydantic.Field(default=None)
|
66
|
+
created_at: datetime = pydantic.Field(default_factory=datetime.utcnow)
|
66
67
|
"""The date and time when the bundle was generated."""
|
67
68
|
|
68
69
|
|
@@ -79,8 +80,8 @@ class AssetInfo(BaseOpenEpdSchema):
|
|
79
80
|
"""The language of the asset."""
|
80
81
|
rel_type: str | None
|
81
82
|
rel_asset: str | None
|
82
|
-
comment: str | None =
|
83
|
-
content_type: str | None =
|
84
|
-
size: int | None =
|
85
|
-
custom_type: str | None =
|
86
|
-
custom_data: str | None =
|
83
|
+
comment: str | None = pydantic.Field(default=None)
|
84
|
+
content_type: str | None = pydantic.Field(default=None)
|
85
|
+
size: int | None = pydantic.Field(default=None)
|
86
|
+
custom_type: str | None = pydantic.Field(default=None)
|
87
|
+
custom_data: str | None = pydantic.Field(default=None)
|
openepd/bundle/reader.py
CHANGED
@@ -31,7 +31,7 @@ class DefaultBundleReader(BaseBundleReader):
|
|
31
31
|
self._bundle_archive = zipfile.ZipFile(bundle_file, mode="r")
|
32
32
|
try:
|
33
33
|
with self._bundle_archive.open("manifest", "r") as manifest_stream:
|
34
|
-
self.__manifest = BundleManifest.
|
34
|
+
self.__manifest = BundleManifest.model_validate_json(manifest_stream.read())
|
35
35
|
except Exception as e:
|
36
36
|
raise ValueError("The bundle file is not valid. Manifest reading error: " + str(e)) from e
|
37
37
|
try:
|
@@ -45,7 +45,7 @@ class DefaultBundleReader(BaseBundleReader):
|
|
45
45
|
|
46
46
|
def get_manifest(self) -> BundleManifest:
|
47
47
|
"""Get the manifest of the bundle. Manifest object is immutable."""
|
48
|
-
return self.__manifest.
|
48
|
+
return self.__manifest.model_copy(deep=True)
|
49
49
|
|
50
50
|
def __create_asset_filter(
|
51
51
|
self,
|
@@ -71,7 +71,14 @@ class DefaultBundleReader(BaseBundleReader):
|
|
71
71
|
return _filter
|
72
72
|
|
73
73
|
def __preprocess_csv_dict(self, input_dict: dict[str, str | None]) -> dict[str, str | None]:
|
74
|
-
default_to_none_fields = (
|
74
|
+
default_to_none_fields = (
|
75
|
+
"rel_type",
|
76
|
+
"rel_asset",
|
77
|
+
"lang",
|
78
|
+
"content_type",
|
79
|
+
"custom_type",
|
80
|
+
"custom_data",
|
81
|
+
)
|
75
82
|
for x in default_to_none_fields:
|
76
83
|
if input_dict[x] == "":
|
77
84
|
input_dict[x] = None
|
@@ -82,7 +89,7 @@ class DefaultBundleReader(BaseBundleReader):
|
|
82
89
|
with self._bundle_archive.open("toc", "r") as toc_stream:
|
83
90
|
toc_reader = csv.DictReader(io.TextIOWrapper(toc_stream, encoding="utf-8"), dialect="toc")
|
84
91
|
for x in toc_reader:
|
85
|
-
yield AssetInfo.
|
92
|
+
yield AssetInfo.model_validate(self.__preprocess_csv_dict(x))
|
86
93
|
|
87
94
|
def __check_toc(self):
|
88
95
|
with self._bundle_archive.open("toc", "r") as toc_stream:
|
@@ -154,4 +161,4 @@ class DefaultBundleReader(BaseBundleReader):
|
|
154
161
|
if asset.type != obj_class.get_asset_type():
|
155
162
|
raise ValueError(f"Asset type mismatch. Expected {obj_class.get_asset_type()}, got {asset.type}")
|
156
163
|
with self._bundle_archive.open(asset.ref, "r") as asset_stream:
|
157
|
-
return obj_class.
|
164
|
+
return obj_class.model_validate_json(asset_stream.read())
|
openepd/bundle/writer.py
CHANGED
@@ -61,7 +61,9 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
61
61
|
"""Write a blob asset to the bundle."""
|
62
62
|
rel_ref_str = self._asset_ref_to_str(rel_asset) if rel_asset is not None else None
|
63
63
|
ref_str = self.__generate_entry_name(
|
64
|
-
AssetType.Blob,
|
64
|
+
AssetType.Blob,
|
65
|
+
self.__get_ext_for_content_type(content_type, "bin"),
|
66
|
+
file_name,
|
65
67
|
)
|
66
68
|
asset_info = AssetInfo(
|
67
69
|
ref=ref_str,
|
@@ -98,7 +100,9 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
98
100
|
asset_type = AssetType(asset_type_str)
|
99
101
|
rel_ref_str = self._asset_ref_to_str(rel_asset) if rel_asset is not None else None
|
100
102
|
ref_str = self.__generate_entry_name(
|
101
|
-
asset_type,
|
103
|
+
asset_type,
|
104
|
+
self.__get_ext_for_content_type("application/json", "json"),
|
105
|
+
file_name,
|
102
106
|
)
|
103
107
|
asset_info = AssetInfo(
|
104
108
|
ref=ref_str,
|
@@ -114,7 +118,9 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
114
118
|
)
|
115
119
|
self.__write_data_stream(
|
116
120
|
asset_info,
|
117
|
-
BytesIO(
|
121
|
+
BytesIO(
|
122
|
+
obj.model_dump_json(indent=2, exclude_unset=True, exclude_none=True, by_alias=True).encode("utf-8")
|
123
|
+
),
|
118
124
|
)
|
119
125
|
self.__register_entry(asset_info)
|
120
126
|
return asset_info
|
@@ -122,7 +128,7 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
122
128
|
def commit(self):
|
123
129
|
"""Write the manifest and TOC to the bundle. This will be called automatically when the bundle is closed."""
|
124
130
|
with self._bundle_archive.open("manifest", "w") as manifest_stream:
|
125
|
-
manifest_stream.write(self.__manifest.
|
131
|
+
manifest_stream.write(self.__manifest.model_dump_json(indent=2, exclude_none=True).encode("utf-8"))
|
126
132
|
with self._bundle_archive.open("toc", "w") as toc_stream:
|
127
133
|
toc_stream.write(self.__toc_buffer.getvalue().encode("utf-8"))
|
128
134
|
|
@@ -134,7 +140,7 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
134
140
|
def __register_entry(self, asset_info: AssetInfo):
|
135
141
|
if asset_info.ref in self.__added_entries:
|
136
142
|
raise ValueError(f"Asset {asset_info.ref} already exists in the bundle.")
|
137
|
-
self._toc_writer.writerow(asset_info.
|
143
|
+
self._toc_writer.writerow(asset_info.model_dump(exclude_unset=True, exclude_none=True))
|
138
144
|
self.__added_entries.add(asset_info.ref)
|
139
145
|
type_counter = self.__manifest.assets.count_by_type.get(asset_info.type, 0) + 1
|
140
146
|
self.__manifest.assets.count_by_type[asset_info.type] = type_counter
|
@@ -143,7 +149,12 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
143
149
|
raise ValueError("Size of asset is not set.")
|
144
150
|
self.__manifest.assets.total_size += asset_info.size
|
145
151
|
|
146
|
-
def __generate_entry_name(
|
152
|
+
def __generate_entry_name(
|
153
|
+
self,
|
154
|
+
asset_type: str,
|
155
|
+
extension: str | None = None,
|
156
|
+
file_name: str | None = None,
|
157
|
+
) -> str:
|
147
158
|
current_counter = self.__manifest.assets.count_by_type.get(asset_type, 0)
|
148
159
|
current_counter += 1
|
149
160
|
if file_name is None:
|
openepd/model/base.py
CHANGED
@@ -14,17 +14,20 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
import abc
|
17
|
+
from collections.abc import Callable
|
17
18
|
from enum import StrEnum
|
18
19
|
import json
|
19
|
-
from typing import Any,
|
20
|
+
from typing import Any, ClassVar, Generic, Optional, Type, TypeAlias, TypeVar
|
20
21
|
|
21
|
-
from cqd import open_xpd_uuid # type:ignore[import-untyped
|
22
|
+
from cqd import open_xpd_uuid # type:ignore[import-untyped]
|
23
|
+
import pydantic
|
24
|
+
from pydantic import ConfigDict
|
25
|
+
import pydantic_core
|
22
26
|
|
23
|
-
from openepd.compat.pydantic import pyd, pyd_generics
|
24
27
|
from openepd.model.validation.common import validate_version_compatibility, validate_version_format
|
25
28
|
from openepd.model.versioning import OpenEpdVersions, Version
|
26
29
|
|
27
|
-
AnySerializable: TypeAlias = int | str | bool | float | list | dict |
|
30
|
+
AnySerializable: TypeAlias = int | str | bool | float | list | dict | pydantic.BaseModel | None
|
28
31
|
TAnySerializable = TypeVar("TAnySerializable", bound=AnySerializable)
|
29
32
|
|
30
33
|
OPENEPD_VERSION_FIELD = "openepd_version"
|
@@ -60,29 +63,27 @@ def modify_pydantic_schema(schema_dict: dict, cls: type) -> dict:
|
|
60
63
|
return schema_dict
|
61
64
|
|
62
65
|
|
63
|
-
class BaseOpenEpdSchema(
|
66
|
+
class BaseOpenEpdSchema(pydantic.BaseModel):
|
64
67
|
"""Base class for all OpenEPD models."""
|
65
68
|
|
66
|
-
ext: dict[str, AnySerializable] | None =
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
use_enum_values = True
|
73
|
-
schema_extra: Callable | dict = modify_pydantic_schema
|
69
|
+
ext: dict[str, AnySerializable] | None = pydantic.Field(alias="ext", default=None)
|
70
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(
|
71
|
+
validate_assignment=False,
|
72
|
+
populate_by_name=True,
|
73
|
+
use_enum_values=True,
|
74
|
+
)
|
74
75
|
|
75
76
|
def to_serializable(self, *args, **kwargs) -> dict[str, Any]:
|
76
77
|
"""
|
77
78
|
Return a serializable dict representation of the DTO.
|
78
79
|
|
79
|
-
It expects the same arguments as the
|
80
|
+
It expects the same arguments as the pydantic.BaseModel.model_dump_json() method.
|
80
81
|
"""
|
81
|
-
return json.loads(self.
|
82
|
+
return json.loads(self.model_dump_json(*args, **kwargs))
|
82
83
|
|
83
84
|
def has_values(self) -> bool:
|
84
85
|
"""Return True if the model has any values."""
|
85
|
-
return len(self.
|
86
|
+
return len(self.model_dump(exclude_unset=True, exclude_none=True)) > 0
|
86
87
|
|
87
88
|
def set_ext(self, ext: "OpenEpdExtension") -> None:
|
88
89
|
"""Set the extension field."""
|
@@ -106,7 +107,10 @@ class BaseOpenEpdSchema(pyd.BaseModel):
|
|
106
107
|
return self.ext.get(key, default)
|
107
108
|
|
108
109
|
def get_typed_ext_field(
|
109
|
-
self,
|
110
|
+
self,
|
111
|
+
key: str,
|
112
|
+
target_type: Type[TAnySerializable],
|
113
|
+
default: Optional[TAnySerializable] = None,
|
110
114
|
) -> TAnySerializable:
|
111
115
|
"""
|
112
116
|
Get an extension field from the model and convert it to the target type.
|
@@ -116,8 +120,8 @@ class BaseOpenEpdSchema(pyd.BaseModel):
|
|
116
120
|
value = self.get_ext_field(key, default)
|
117
121
|
if value is None:
|
118
122
|
return None # type: ignore
|
119
|
-
if issubclass(target_type,
|
120
|
-
return target_type.
|
123
|
+
if issubclass(target_type, pydantic.BaseModel) and isinstance(value, dict):
|
124
|
+
return target_type.model_validate(value) # type: ignore[return-value]
|
121
125
|
elif isinstance(value, target_type):
|
122
126
|
return value
|
123
127
|
raise ValueError(f"Cannot convert {value} to {target_type}")
|
@@ -128,7 +132,7 @@ class BaseOpenEpdSchema(pyd.BaseModel):
|
|
128
132
|
|
129
133
|
def get_ext_or_empty(self, ext_type: Type["TOpenEpdExtension"]) -> "TOpenEpdExtension":
|
130
134
|
"""Get an extension field from the model or an empty instance if it doesn't exist."""
|
131
|
-
return self.get_typed_ext_field(ext_type.get_extension_name(), ext_type, ext_type.
|
135
|
+
return self.get_typed_ext_field(ext_type.get_extension_name(), ext_type, ext_type.model_construct(**{})) # type: ignore[return-value]
|
132
136
|
|
133
137
|
@classmethod
|
134
138
|
def is_allowed_field_name(cls, field_name: str) -> bool:
|
@@ -137,10 +141,10 @@ class BaseOpenEpdSchema(pyd.BaseModel):
|
|
137
141
|
|
138
142
|
Both property name and aliases are checked.
|
139
143
|
"""
|
140
|
-
if field_name in cls.
|
144
|
+
if field_name in cls.model_fields:
|
141
145
|
return True
|
142
146
|
else:
|
143
|
-
for x in cls.
|
147
|
+
for x in cls.model_fields.values():
|
144
148
|
if x.alias == field_name:
|
145
149
|
return True
|
146
150
|
return False
|
@@ -156,7 +160,7 @@ class BaseOpenEpdSchema(pyd.BaseModel):
|
|
156
160
|
return None
|
157
161
|
|
158
162
|
|
159
|
-
class BaseOpenEpdGenericSchema(
|
163
|
+
class BaseOpenEpdGenericSchema(BaseOpenEpdSchema):
|
160
164
|
"""Base class for all OpenEPD generic models."""
|
161
165
|
|
162
166
|
pass
|
@@ -183,21 +187,24 @@ class RootDocument(abc.ABC, BaseOpenEpdSchema):
|
|
183
187
|
_FORMAT_VERSION: ClassVar[str]
|
184
188
|
"""Version of this document format. Must be defined in the concrete class."""
|
185
189
|
|
186
|
-
doctype: str =
|
190
|
+
doctype: str = pydantic.Field(
|
187
191
|
description='Describes the type and schema of the document. Must always always read "openEPD".',
|
188
192
|
default="openEPD",
|
189
193
|
)
|
190
|
-
openepd_version: str =
|
194
|
+
openepd_version: str = pydantic.Field(
|
191
195
|
description="Version of the document format, related to /doctype",
|
192
196
|
default=OpenEpdVersions.get_current().as_str(),
|
193
197
|
)
|
194
198
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
)
|
199
|
+
@pydantic.field_validator(OPENEPD_VERSION_FIELD)
|
200
|
+
def version_format_validator(cls, value: str) -> str:
|
201
|
+
"""Validate the correctness of version format."""
|
202
|
+
return validate_version_format(value)
|
203
|
+
|
204
|
+
@pydantic.field_validator(OPENEPD_VERSION_FIELD)
|
205
|
+
def version_major_match_validator(cls, value: str) -> str:
|
206
|
+
"""Validate that the version major matches the class version major."""
|
207
|
+
return validate_version_compatibility("_FORMAT_VERSION")(cls, value) # type: ignore[arg-type]
|
201
208
|
|
202
209
|
|
203
210
|
TRootDocument = TypeVar("TRootDocument", bound=RootDocument)
|
@@ -230,8 +237,7 @@ class BaseDocumentFactory(Generic[TRootDocument]):
|
|
230
237
|
return doc_cls(**data)
|
231
238
|
else:
|
232
239
|
raise ValueError(
|
233
|
-
f"Unsupported version: {version}. "
|
234
|
-
f"The highest supported version from branch {x.major}.x is {x}"
|
240
|
+
f"Unsupported version: {version}. The highest supported version from branch {x.major}.x is {x}"
|
235
241
|
)
|
236
242
|
supported_versions = ", ".join(f"{v.major}.x" for v in cls.VERSION_MAP.keys())
|
237
243
|
raise ValueError(f"Version {version} is not supported. Supported versions are: {supported_versions}")
|
@@ -244,22 +250,33 @@ class OpenXpdUUID(str):
|
|
244
250
|
See https://github.com/cchangelabs/open-xpd-uuid-lib for details.
|
245
251
|
"""
|
246
252
|
|
247
|
-
|
248
|
-
|
249
|
-
|
253
|
+
@classmethod
|
254
|
+
def _validate_id(cls, value: Any, _: pydantic_core.core_schema.ValidatorFunctionWrapHandler) -> Any:
|
255
|
+
if not isinstance(value, str | None):
|
256
|
+
raise ValueError(f"Invalid value type: {type(value)}")
|
250
257
|
|
251
258
|
try:
|
252
|
-
open_xpd_uuid.validate(open_xpd_uuid.sanitize(str(
|
253
|
-
return
|
259
|
+
open_xpd_uuid.validate(open_xpd_uuid.sanitize(str(value)))
|
260
|
+
return value
|
254
261
|
except open_xpd_uuid.GuidValidationError as e:
|
255
262
|
raise ValueError("Invalid format") from e
|
256
263
|
|
257
264
|
@classmethod
|
258
|
-
def
|
259
|
-
|
265
|
+
def __get_pydantic_core_schema__(
|
266
|
+
cls,
|
267
|
+
source_type: Any,
|
268
|
+
handler: Callable[[Any], pydantic_core.core_schema.CoreSchema],
|
269
|
+
) -> pydantic_core.core_schema.CoreSchema:
|
270
|
+
# In this example we assume that the underlying type to validate is a string.
|
271
|
+
# If it's different, adjust the call to handler() accordingly.
|
272
|
+
underlying_schema = handler(str)
|
273
|
+
# Wrap the underlying schema with your custom validator.
|
274
|
+
return pydantic_core.core_schema.no_info_wrap_validator_function(cls._validate_id, underlying_schema)
|
260
275
|
|
261
276
|
@classmethod
|
262
|
-
def
|
263
|
-
|
264
|
-
|
277
|
+
def __get_pydantic_json_schema__(cls, core_schema, handler):
|
278
|
+
json_schema = handler(core_schema)
|
279
|
+
json_schema.update(
|
280
|
+
examples=["XC300001"],
|
265
281
|
)
|
282
|
+
return json_schema
|
openepd/model/category.py
CHANGED
@@ -13,7 +13,8 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
|
-
|
16
|
+
import pydantic
|
17
|
+
|
17
18
|
from openepd.model.base import BaseOpenEpdSchema
|
18
19
|
from openepd.model.common import Amount
|
19
20
|
|
@@ -21,15 +22,17 @@ from openepd.model.common import Amount
|
|
21
22
|
class Category(BaseOpenEpdSchema):
|
22
23
|
"""DTO for Category model, recursive."""
|
23
24
|
|
24
|
-
id: str =
|
25
|
-
name: str =
|
26
|
-
short_name: str =
|
27
|
-
openepd_hierarchical_name: str =
|
25
|
+
id: str = pydantic.Field(description="Category short ID (readable unique string)")
|
26
|
+
name: str = pydantic.Field(description="Category display name (user-friendly)")
|
27
|
+
short_name: str = pydantic.Field(description="Category short user-friendly name")
|
28
|
+
openepd_hierarchical_name: str = pydantic.Field(
|
28
29
|
"Special form of hierarchical category ID where the >> is hierarchy separator"
|
29
30
|
)
|
30
|
-
masterformat: str | None =
|
31
|
-
description: str | None =
|
32
|
-
declared_unit: Amount | None =
|
33
|
-
|
34
|
-
|
31
|
+
masterformat: str | None = pydantic.Field(description="Default category code in Masterformat", default=None)
|
32
|
+
description: str | None = pydantic.Field(description="Category verbose description", default=None)
|
33
|
+
declared_unit: Amount | None = pydantic.Field(
|
34
|
+
description="Declared unit of category, for example 1 kg", default=None
|
35
|
+
)
|
36
|
+
subcategories: list["Category"] = pydantic.Field(
|
37
|
+
description="List of subcategories. This makes categories tree-like structure", default_factory=list
|
35
38
|
)
|