openepd 7.1.0__py3-none-any.whl → 7.3.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/__version__.py +1 -1
- openepd/api/average_dataset/generic_estimate_sync_api.py +2 -1
- openepd/api/average_dataset/industry_epd_sync_api.py +2 -1
- openepd/api/base_sync_client.py +10 -8
- openepd/api/common.py +17 -11
- openepd/api/dto/common.py +1 -1
- openepd/api/epd/sync_api.py +2 -1
- openepd/api/org/__init__.py +15 -0
- openepd/api/org/sync_api.py +79 -0
- openepd/api/pcr/sync_api.py +35 -0
- openepd/api/plant/__init__.py +15 -0
- openepd/api/plant/sync_api.py +79 -0
- openepd/api/standard/__init__.py +15 -0
- openepd/api/standard/sync_api.py +79 -0
- openepd/api/sync_client.py +27 -0
- openepd/bundle/base.py +47 -6
- openepd/bundle/model.py +1 -0
- openepd/bundle/reader.py +35 -10
- openepd/bundle/writer.py +21 -12
- openepd/m49/__init__.py +2 -0
- openepd/m49/const.py +1 -1
- openepd/m49/utils.py +16 -10
- openepd/model/base.py +20 -15
- openepd/model/common.py +10 -5
- openepd/model/declaration.py +2 -2
- openepd/model/epd.py +2 -1
- openepd/model/factory.py +5 -3
- openepd/model/generic_estimate.py +4 -0
- openepd/model/lcia.py +10 -10
- openepd/model/org.py +14 -7
- openepd/model/pcr.py +2 -2
- openepd/model/specs/__init__.py +37 -0
- openepd/model/specs/asphalt.py +3 -3
- openepd/model/specs/base.py +2 -1
- openepd/model/specs/enums.py +9 -1
- openepd/model/specs/range/__init__.py +5 -3
- openepd/model/specs/range/accessories.py +1 -1
- openepd/model/specs/range/aluminium.py +1 -1
- openepd/model/specs/range/cladding.py +10 -10
- openepd/model/specs/range/cmu.py +0 -2
- openepd/model/specs/range/concrete.py +25 -2
- openepd/model/specs/range/conveying_equipment.py +2 -2
- openepd/model/specs/range/electrical.py +18 -18
- openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +1 -1
- openepd/model/specs/range/exterior_improvements.py +47 -0
- openepd/model/specs/range/finishes.py +19 -40
- openepd/model/specs/range/fire_and_smoke_protection.py +3 -3
- openepd/model/specs/range/furnishings.py +7 -7
- openepd/model/specs/range/manufacturing_inputs.py +17 -5
- openepd/model/specs/range/masonry.py +1 -1
- openepd/model/specs/range/mechanical.py +6 -6
- openepd/model/specs/range/mixins/__init__.py +15 -0
- openepd/model/specs/range/mixins/access_flooring_mixin.py +43 -0
- openepd/model/specs/range/network_infrastructure.py +3 -3
- openepd/model/specs/range/openings.py +17 -17
- openepd/model/specs/range/other_materials.py +4 -4
- openepd/model/specs/range/plumbing.py +5 -5
- openepd/model/specs/range/precast_concrete.py +2 -2
- openepd/model/specs/range/steel.py +18 -9
- openepd/model/specs/range/thermal_moisture_protection.py +12 -12
- openepd/model/specs/range/wood.py +4 -6
- openepd/model/specs/singular/__init__.py +119 -2
- openepd/model/specs/singular/aluminium.py +2 -1
- openepd/model/specs/singular/concrete.py +25 -1
- openepd/model/specs/singular/deprecated/__init__.py +1 -1
- openepd/model/specs/singular/exterior_improvements.py +47 -0
- openepd/model/specs/singular/finishes.py +3 -59
- openepd/model/specs/singular/manufacturing_inputs.py +13 -1
- openepd/model/specs/singular/mixins/access_flooring_mixin.py +55 -0
- openepd/model/specs/singular/steel.py +10 -2
- openepd/model/validation/common.py +10 -6
- openepd/model/validation/enum.py +4 -2
- openepd/model/validation/quantity.py +13 -6
- openepd/model/versioning.py +8 -6
- {openepd-7.1.0.dist-info → openepd-7.3.0.dist-info}/METADATA +23 -13
- {openepd-7.1.0.dist-info → openepd-7.3.0.dist-info}/RECORD +78 -67
- {openepd-7.1.0.dist-info → openepd-7.3.0.dist-info}/WHEEL +1 -1
- {openepd-7.1.0.dist-info → openepd-7.3.0.dist-info}/LICENSE +0 -0
openepd/bundle/reader.py
CHANGED
@@ -13,10 +13,11 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
|
+
from collections.abc import Callable, Iterator, Sequence
|
16
17
|
import csv
|
17
18
|
import io
|
18
19
|
from os import PathLike
|
19
|
-
from typing import IO,
|
20
|
+
from typing import IO, cast
|
20
21
|
import zipfile
|
21
22
|
|
22
23
|
from openepd.bundle.base import AssetFilter, AssetRef, BaseBundleReader
|
@@ -60,8 +61,12 @@ class DefaultBundleReader(BaseBundleReader):
|
|
60
61
|
return False
|
61
62
|
if name is not None and a.name != name:
|
62
63
|
return False
|
63
|
-
if parent_ref is not None
|
64
|
-
|
64
|
+
if parent_ref is not None:
|
65
|
+
parent_ref_str = self._asset_ref_to_str(parent_ref)
|
66
|
+
# Get the actual list of related assets
|
67
|
+
rel_asset_list = self._get_rel_asset_list(a)
|
68
|
+
if parent_ref_str not in rel_asset_list:
|
69
|
+
return False
|
65
70
|
if ref_type is not None and a.rel_type != ref_type:
|
66
71
|
return False
|
67
72
|
if is_translated is not None and a.lang is not None and "translated" in a.lang:
|
@@ -84,6 +89,20 @@ class DefaultBundleReader(BaseBundleReader):
|
|
84
89
|
input_dict[x] = None
|
85
90
|
return input_dict
|
86
91
|
|
92
|
+
def _get_rel_asset_list(self, asset_info: AssetInfo) -> list[str]:
|
93
|
+
"""Get the list of related asset references from an AssetInfo object."""
|
94
|
+
if asset_info.rel_asset is None:
|
95
|
+
return []
|
96
|
+
|
97
|
+
# Deserialize from CSV format (semicolon-separated values)
|
98
|
+
deserialized = self._deserialize_rel_asset_from_csv(asset_info.rel_asset)
|
99
|
+
if isinstance(deserialized, list):
|
100
|
+
return deserialized
|
101
|
+
elif isinstance(deserialized, str):
|
102
|
+
return [deserialized]
|
103
|
+
else:
|
104
|
+
return []
|
105
|
+
|
87
106
|
def assets_iter(self) -> Iterator[AssetInfo]:
|
88
107
|
"""Iterate over all assets in the bundle."""
|
89
108
|
with self._bundle_archive.open("toc", "r") as toc_stream:
|
@@ -95,7 +114,8 @@ class DefaultBundleReader(BaseBundleReader):
|
|
95
114
|
with self._bundle_archive.open("toc", "r") as toc_stream:
|
96
115
|
toc_reader = csv.DictReader(io.TextIOWrapper(toc_stream, encoding="utf-8"), dialect="toc")
|
97
116
|
if not toc_reader.fieldnames or len(toc_reader.fieldnames) < len(self._TOC_FIELDS):
|
98
|
-
|
117
|
+
msg = "The bundle file is not valid. TOC reading error: wrong number of fields"
|
118
|
+
raise ValueError(msg)
|
99
119
|
|
100
120
|
def root_assets_iter(
|
101
121
|
self,
|
@@ -130,7 +150,8 @@ class DefaultBundleReader(BaseBundleReader):
|
|
130
150
|
rel_type = [rel_type]
|
131
151
|
asset_ref = self._asset_ref_to_str(asset)
|
132
152
|
for x in self.assets_iter():
|
133
|
-
|
153
|
+
rel_asset_list = self._get_rel_asset_list(x)
|
154
|
+
if asset_ref in rel_asset_list:
|
134
155
|
if rel_type is None or x.rel_type in rel_type:
|
135
156
|
yield x
|
136
157
|
|
@@ -148,17 +169,21 @@ class DefaultBundleReader(BaseBundleReader):
|
|
148
169
|
"""Read the blob asset."""
|
149
170
|
asset = self.get_asset_by_ref(asset_ref)
|
150
171
|
if asset is None:
|
151
|
-
|
172
|
+
msg = "Asset not found"
|
173
|
+
raise ValueError(msg)
|
152
174
|
return self._bundle_archive.open(asset.ref, "r")
|
153
175
|
|
154
|
-
def read_object_asset(self, obj_class:
|
176
|
+
def read_object_asset(self, obj_class: type[TOpenEpdObject], asset_ref: AssetRef) -> TOpenEpdObject:
|
155
177
|
"""Read the object asset."""
|
156
178
|
asset = self.get_asset_by_ref(asset_ref)
|
157
179
|
if asset is None:
|
158
|
-
|
180
|
+
msg = "Asset not found"
|
181
|
+
raise ValueError(msg)
|
159
182
|
if obj_class.get_asset_type() is None:
|
160
|
-
|
183
|
+
msg = f"Target object {obj_class.__name__} is not supported asset"
|
184
|
+
raise ValueError(msg)
|
161
185
|
if asset.type != obj_class.get_asset_type():
|
162
|
-
|
186
|
+
msg = f"Asset type mismatch. Expected {obj_class.get_asset_type()}, got {asset.type}"
|
187
|
+
raise ValueError(msg)
|
163
188
|
with self._bundle_archive.open(asset.ref, "r") as asset_stream:
|
164
189
|
return obj_class.model_validate_json(asset_stream.read())
|
openepd/bundle/writer.py
CHANGED
@@ -31,8 +31,9 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
31
31
|
"""Default bundle writer implementation. Writes the bundle to a ZIP file."""
|
32
32
|
|
33
33
|
def __init__(self, bundle_file: str | PathLike | IO[bytes], comment: str | None = None):
|
34
|
-
if isinstance(bundle_file,
|
35
|
-
|
34
|
+
if isinstance(bundle_file, PathLike | str) and Path(bundle_file).exists():
|
35
|
+
msg = "Amending existing files is not supported yet."
|
36
|
+
raise ValueError(msg)
|
36
37
|
self._bundle_archive = zipfile.ZipFile(bundle_file, mode="w")
|
37
38
|
self.__manifest = BundleManifest(
|
38
39
|
format="openEPD Bundle/1.0",
|
@@ -49,7 +50,7 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
49
50
|
self,
|
50
51
|
data: IO[bytes],
|
51
52
|
content_type: str | None = None,
|
52
|
-
rel_asset: AssetRef | None = None,
|
53
|
+
rel_asset: AssetRef | list[AssetRef] | None = None,
|
53
54
|
rel_type: str | None = None,
|
54
55
|
file_name: str | None = None,
|
55
56
|
name: str | None = None,
|
@@ -59,7 +60,9 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
59
60
|
custom_data: str | None = None,
|
60
61
|
) -> AssetInfo:
|
61
62
|
"""Write a blob asset to the bundle."""
|
62
|
-
|
63
|
+
# Convert multiple rel_asset to proper format and serialize for storage
|
64
|
+
rel_ref_converted = self._asset_refs_to_str(rel_asset)
|
65
|
+
rel_ref_serialized = self._serialize_rel_asset_for_csv(rel_ref_converted)
|
63
66
|
ref_str = self.__generate_entry_name(
|
64
67
|
AssetType.Blob,
|
65
68
|
self.__get_ext_for_content_type(content_type, "bin"),
|
@@ -71,7 +74,7 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
71
74
|
type=AssetType.Blob,
|
72
75
|
lang=lang,
|
73
76
|
rel_type=rel_type,
|
74
|
-
rel_asset=
|
77
|
+
rel_asset=rel_ref_serialized,
|
75
78
|
content_type=content_type,
|
76
79
|
comment=comment,
|
77
80
|
custom_type=custom_type,
|
@@ -84,7 +87,7 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
84
87
|
def write_object_asset(
|
85
88
|
self,
|
86
89
|
obj: TOpenEpdObject,
|
87
|
-
rel_asset: AssetRef | None = None,
|
90
|
+
rel_asset: list[AssetRef] | AssetRef | None = None,
|
88
91
|
rel_type: str | None = None,
|
89
92
|
file_name: str | None = None,
|
90
93
|
name: str | None = None,
|
@@ -96,9 +99,12 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
96
99
|
"""Write an object asset to the bundle. Object means subclass of BaseOpenEpdSchem."""
|
97
100
|
asset_type_str = obj.get_asset_type()
|
98
101
|
if asset_type_str is None:
|
99
|
-
|
102
|
+
msg = f"Object {obj} does not have a valid asset type and can't be written to a bundle."
|
103
|
+
raise ValueError(msg)
|
100
104
|
asset_type = AssetType(asset_type_str)
|
101
|
-
|
105
|
+
# Convert multiple rel_asset to proper format and serialize for storage
|
106
|
+
rel_ref_converted = self._asset_refs_to_str(rel_asset)
|
107
|
+
rel_ref_serialized = self._serialize_rel_asset_for_csv(rel_ref_converted)
|
102
108
|
ref_str = self.__generate_entry_name(
|
103
109
|
asset_type,
|
104
110
|
self.__get_ext_for_content_type("application/json", "json"),
|
@@ -109,7 +115,7 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
109
115
|
name=name,
|
110
116
|
type=asset_type,
|
111
117
|
lang=lang,
|
112
|
-
rel_asset=
|
118
|
+
rel_asset=rel_ref_serialized,
|
113
119
|
rel_type=rel_type,
|
114
120
|
content_type="application/json",
|
115
121
|
comment=comment,
|
@@ -139,14 +145,16 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
139
145
|
|
140
146
|
def __register_entry(self, asset_info: AssetInfo):
|
141
147
|
if asset_info.ref in self.__added_entries:
|
142
|
-
|
148
|
+
msg = f"Asset {asset_info.ref} already exists in the bundle."
|
149
|
+
raise ValueError(msg)
|
143
150
|
self._toc_writer.writerow(asset_info.model_dump(exclude_unset=True, exclude_none=True))
|
144
151
|
self.__added_entries.add(asset_info.ref)
|
145
152
|
type_counter = self.__manifest.assets.count_by_type.get(asset_info.type, 0) + 1
|
146
153
|
self.__manifest.assets.count_by_type[asset_info.type] = type_counter
|
147
154
|
self.__manifest.assets.total_count += 1
|
148
155
|
if asset_info.size is None:
|
149
|
-
|
156
|
+
msg = "Size of asset is not set."
|
157
|
+
raise ValueError(msg)
|
150
158
|
self.__manifest.assets.total_size += asset_info.size
|
151
159
|
|
152
160
|
def __generate_entry_name(
|
@@ -168,7 +176,8 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
168
176
|
info = self._bundle_archive.getinfo(f"{asset_type}/")
|
169
177
|
if info.is_dir():
|
170
178
|
return
|
171
|
-
|
179
|
+
msg = f"Object with name {asset_type} already exists in the bundle."
|
180
|
+
raise ValueError(msg)
|
172
181
|
except KeyError:
|
173
182
|
self._bundle_archive.mkdir(str(asset_type))
|
174
183
|
|
openepd/m49/__init__.py
CHANGED
openepd/m49/const.py
CHANGED
@@ -1169,7 +1169,7 @@ def is_m49_code(to_check: str) -> bool:
|
|
1169
1169
|
:param to_check: any string
|
1170
1170
|
:return: `True` if passed string is M49 code, `False` otherwise
|
1171
1171
|
"""
|
1172
|
-
warnings.warn("Use m49.utils.is_m49_code instead.", DeprecationWarning)
|
1172
|
+
warnings.warn("Use m49.utils.is_m49_code instead.", DeprecationWarning, stacklevel=2)
|
1173
1173
|
from . import utils
|
1174
1174
|
|
1175
1175
|
return utils.is_m49_code(to_check)
|
openepd/m49/utils.py
CHANGED
@@ -14,15 +14,15 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
__all__ = [
|
17
|
+
"is_m49_code",
|
17
18
|
"iso_to_m49",
|
18
19
|
"m49_to_iso",
|
19
|
-
"
|
20
|
+
"m49_to_openepd",
|
20
21
|
"m49_to_region_and_country_names",
|
21
22
|
"openepd_to_m49",
|
22
|
-
"
|
23
|
-
"is_m49_code",
|
23
|
+
"region_and_country_names_to_m49",
|
24
24
|
]
|
25
|
-
from
|
25
|
+
from collections.abc import Collection
|
26
26
|
|
27
27
|
from openepd.m49.const import (
|
28
28
|
COUNTRY_VERBOSE_NAME_TO_M49,
|
@@ -54,7 +54,8 @@ def iso_to_m49(regions: Collection[str]) -> set[str]:
|
|
54
54
|
if m49_code:
|
55
55
|
result.add(m49_code)
|
56
56
|
else:
|
57
|
-
|
57
|
+
msg = f"Country code '{code}' not found in M49 region codes."
|
58
|
+
raise ValueError(msg)
|
58
59
|
|
59
60
|
return result
|
60
61
|
|
@@ -77,7 +78,8 @@ def m49_to_iso(regions: Collection[str]) -> set[str]:
|
|
77
78
|
if iso_code:
|
78
79
|
result.add(iso_code)
|
79
80
|
else:
|
80
|
-
|
81
|
+
msg = f"Region code '{code}' not found in ISO3166."
|
82
|
+
raise ValueError(msg)
|
81
83
|
|
82
84
|
return result
|
83
85
|
|
@@ -99,7 +101,8 @@ def region_and_country_names_to_m49(regions: Collection[str]) -> set[str]:
|
|
99
101
|
for name in regions:
|
100
102
|
m49_code = REGION_VERBOSE_NAME_TO_M49.get(name.title()) or COUNTRY_VERBOSE_NAME_TO_M49.get(name.title())
|
101
103
|
if not m49_code:
|
102
|
-
|
104
|
+
msg = f"Region or country name '{name}' not found in M49 region codes."
|
105
|
+
raise ValueError(msg)
|
103
106
|
result.add(m49_code)
|
104
107
|
|
105
108
|
return result
|
@@ -120,7 +123,8 @@ def m49_to_region_and_country_names(regions: Collection[str]) -> set[str]:
|
|
120
123
|
result = set()
|
121
124
|
for code in regions:
|
122
125
|
if code not in M49_TO_REGION_VERBOSE_NAME and code not in M49_TO_COUNTRY_VERBOSE_NAME:
|
123
|
-
|
126
|
+
msg = f"Region code '{code}' not found in M49 region codes."
|
127
|
+
raise ValueError(msg)
|
124
128
|
|
125
129
|
name = M49_TO_REGION_VERBOSE_NAME.get(code) or M49_TO_COUNTRY_VERBOSE_NAME.get(code, code)
|
126
130
|
result.add(name)
|
@@ -153,7 +157,8 @@ def openepd_to_m49(regions: Collection[str]) -> set[str]:
|
|
153
157
|
elif is_m49_code(region):
|
154
158
|
result.add(region)
|
155
159
|
else:
|
156
|
-
|
160
|
+
msg = f"Region '{region}' not found in ISO3166 or OpenEPD special regions."
|
161
|
+
raise ValueError(msg)
|
157
162
|
return result
|
158
163
|
|
159
164
|
|
@@ -185,7 +190,8 @@ def m49_to_openepd(regions: list[str]) -> set[str]:
|
|
185
190
|
if iso_code:
|
186
191
|
result.add(iso_code)
|
187
192
|
else:
|
188
|
-
|
193
|
+
msg = f"Region code '{code}' not found in ISO3166 or OpenEPD special regions."
|
194
|
+
raise ValueError(msg)
|
189
195
|
|
190
196
|
return result
|
191
197
|
|
openepd/model/base.py
CHANGED
@@ -17,7 +17,7 @@ import abc
|
|
17
17
|
from collections.abc import Callable
|
18
18
|
from enum import StrEnum
|
19
19
|
import json
|
20
|
-
from typing import Any, ClassVar, Generic, Optional,
|
20
|
+
from typing import Any, ClassVar, Generic, Optional, TypeAlias, TypeVar
|
21
21
|
|
22
22
|
from cqd import open_xpd_uuid # type:ignore[import-untyped]
|
23
23
|
import pydantic
|
@@ -109,8 +109,8 @@ class BaseOpenEpdSchema(pydantic.BaseModel):
|
|
109
109
|
def get_typed_ext_field(
|
110
110
|
self,
|
111
111
|
key: str,
|
112
|
-
target_type:
|
113
|
-
default:
|
112
|
+
target_type: type[TAnySerializable],
|
113
|
+
default: TAnySerializable | None = None,
|
114
114
|
) -> TAnySerializable:
|
115
115
|
"""
|
116
116
|
Get an extension field from the model and convert it to the target type.
|
@@ -124,13 +124,14 @@ class BaseOpenEpdSchema(pydantic.BaseModel):
|
|
124
124
|
return target_type.model_validate(value) # type: ignore[return-value]
|
125
125
|
elif isinstance(value, target_type):
|
126
126
|
return value
|
127
|
-
|
127
|
+
msg = f"Cannot convert {value} to {target_type}"
|
128
|
+
raise ValueError(msg)
|
128
129
|
|
129
|
-
def get_ext(self, ext_type:
|
130
|
+
def get_ext(self, ext_type: type["TOpenEpdExtension"]) -> Optional["TOpenEpdExtension"]:
|
130
131
|
"""Get an extension field from the model or None if it doesn't exist."""
|
131
132
|
return self.get_typed_ext_field(ext_type.get_extension_name(), ext_type, None)
|
132
133
|
|
133
|
-
def get_ext_or_empty(self, ext_type:
|
134
|
+
def get_ext_or_empty(self, ext_type: type["TOpenEpdExtension"]) -> "TOpenEpdExtension":
|
134
135
|
"""Get an extension field from the model or an empty instance if it doesn't exist."""
|
135
136
|
return self.get_typed_ext_field(ext_type.get_extension_name(), ext_type, ext_type.model_construct(**{})) # type: ignore[return-value]
|
136
137
|
|
@@ -178,7 +179,7 @@ class OpenEpdExtension(BaseOpenEpdSchema, metaclass=abc.ABCMeta):
|
|
178
179
|
|
179
180
|
TOpenEpdExtension = TypeVar("TOpenEpdExtension", bound=OpenEpdExtension)
|
180
181
|
TOpenEpdObject = TypeVar("TOpenEpdObject", bound=BaseOpenEpdSchema)
|
181
|
-
TOpenEpdObjectClass = TypeVar("TOpenEpdObjectClass", bound=
|
182
|
+
TOpenEpdObjectClass = TypeVar("TOpenEpdObjectClass", bound=type[BaseOpenEpdSchema])
|
182
183
|
|
183
184
|
|
184
185
|
class RootDocument(abc.ABC, BaseOpenEpdSchema):
|
@@ -225,22 +226,24 @@ class BaseDocumentFactory(Generic[TRootDocument]):
|
|
225
226
|
"""Create a document from a dictionary."""
|
226
227
|
doctype: str | None = data.get("doctype")
|
227
228
|
if doctype is None:
|
228
|
-
|
229
|
+
msg = "Doctype not found in the data."
|
230
|
+
raise ValueError(msg)
|
229
231
|
if doctype.lower() != cls.DOCTYPE_CONSTRAINT.lower():
|
230
|
-
|
231
|
-
|
232
|
-
)
|
232
|
+
msg = f"Document type {doctype} not supported. This factory supports {cls.DOCTYPE_CONSTRAINT} only."
|
233
|
+
raise ValueError(msg)
|
233
234
|
version = Version.parse_version(data.get(OPENEPD_VERSION_FIELD, ""))
|
234
235
|
for x, doc_cls in cls.VERSION_MAP.items():
|
235
236
|
if x.major == version.major:
|
236
237
|
if version.minor <= x.minor:
|
237
238
|
return doc_cls.model_validate(data)
|
238
239
|
else:
|
239
|
-
|
240
|
+
msg = (
|
240
241
|
f"Unsupported version: {version}. The highest supported version from branch {x.major}.x is {x}"
|
241
242
|
)
|
243
|
+
raise ValueError(msg)
|
242
244
|
supported_versions = ", ".join(f"{v.major}.x" for v in cls.VERSION_MAP.keys())
|
243
|
-
|
245
|
+
msg = f"Version {version} is not supported. Supported versions are: {supported_versions}"
|
246
|
+
raise ValueError(msg)
|
244
247
|
|
245
248
|
|
246
249
|
class OpenXpdUUID(str):
|
@@ -253,13 +256,15 @@ class OpenXpdUUID(str):
|
|
253
256
|
@classmethod
|
254
257
|
def _validate_id(cls, value: Any, _: pydantic_core.core_schema.ValidatorFunctionWrapHandler) -> Any:
|
255
258
|
if not isinstance(value, str | None):
|
256
|
-
|
259
|
+
msg = f"Invalid value type: {type(value)}"
|
260
|
+
raise ValueError(msg)
|
257
261
|
|
258
262
|
try:
|
259
263
|
open_xpd_uuid.validate(open_xpd_uuid.sanitize(str(value)))
|
260
264
|
return value
|
261
265
|
except open_xpd_uuid.GuidValidationError as e:
|
262
|
-
|
266
|
+
msg = "Invalid format"
|
267
|
+
raise ValueError(msg) from e
|
263
268
|
|
264
269
|
@classmethod
|
265
270
|
def __get_pydantic_core_schema__(
|
openepd/model/common.py
CHANGED
@@ -39,7 +39,8 @@ class Amount(BaseOpenEpdSchema):
|
|
39
39
|
"""Ensure that qty or unit is provided."""
|
40
40
|
|
41
41
|
if self.qty is None and self.unit is None:
|
42
|
-
|
42
|
+
msg = "Either qty or unit must be provided."
|
43
|
+
raise ValueError(msg)
|
43
44
|
return self
|
44
45
|
|
45
46
|
def to_quantity_str(self):
|
@@ -135,9 +136,11 @@ class Ingredient(BaseOpenEpdSchema):
|
|
135
136
|
# for in the calculation of uncertainty
|
136
137
|
if values.get("gwp_fraction"):
|
137
138
|
if not values.get("evidence_type"):
|
138
|
-
|
139
|
+
msg = "evidence_type is required if gwp_fraction is provided"
|
140
|
+
raise ValueError(msg)
|
139
141
|
if not (values.get("citation") or values.get("link")):
|
140
|
-
|
142
|
+
msg = "link or citation is required if gwp_fraction is provided"
|
143
|
+
raise ValueError(msg)
|
141
144
|
|
142
145
|
return values
|
143
146
|
|
@@ -207,7 +210,8 @@ class AttachmentDict(dict[str, pydantic.AnyUrl]):
|
|
207
210
|
def _validate(cls, value: Any) -> "AttachmentDict":
|
208
211
|
# Ensure the input is a dict.
|
209
212
|
if not isinstance(value, dict):
|
210
|
-
|
213
|
+
msg = "AttachmentDict must be a dict"
|
214
|
+
raise TypeError(msg)
|
211
215
|
|
212
216
|
return cls(value)
|
213
217
|
|
@@ -315,7 +319,8 @@ class RangeBase(BaseOpenEpdSchema):
|
|
315
319
|
min_boundary = values.get("min")
|
316
320
|
max_boundary = values.get("max")
|
317
321
|
if min_boundary is not None and max_boundary is not None and min_boundary > max_boundary:
|
318
|
-
|
322
|
+
msg = "Max should be greater than min"
|
323
|
+
raise ValueError(msg)
|
319
324
|
return values
|
320
325
|
|
321
326
|
|
openepd/model/declaration.py
CHANGED
@@ -44,12 +44,12 @@ class BaseDeclaration(RootDocument, abc.ABC):
|
|
44
44
|
default=None,
|
45
45
|
)
|
46
46
|
date_of_issue: datetime.datetime | None = pydantic.Field(
|
47
|
-
examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.
|
47
|
+
examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.UTC)],
|
48
48
|
description="Date the document was issued. This should be the first day on which the document is valid.",
|
49
49
|
default=None,
|
50
50
|
)
|
51
51
|
valid_until: datetime.datetime | None = pydantic.Field(
|
52
|
-
examples=[datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.
|
52
|
+
examples=[datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.UTC)],
|
53
53
|
description="Last date the document is valid on, including any extensions.",
|
54
54
|
default=None,
|
55
55
|
)
|
openepd/model/epd.py
CHANGED
openepd/model/factory.py
CHANGED
@@ -38,7 +38,8 @@ class DocumentFactory:
|
|
38
38
|
:raise ValueError if doctype not supported or not found.
|
39
39
|
"""
|
40
40
|
if doctype is None:
|
41
|
-
|
41
|
+
msg = "The document type is not specified."
|
42
|
+
raise ValueError(msg)
|
42
43
|
factory = cls.DOCTYPE_TO_FACTORY.get(doctype)
|
43
44
|
if factory is None:
|
44
45
|
raise ValueError(
|
@@ -56,11 +57,12 @@ class DocumentFactory:
|
|
56
57
|
:raise ValueError: if the document type is not specified or not supported.
|
57
58
|
"""
|
58
59
|
doctype = data.get("doctype")
|
59
|
-
if doctype is None or not isinstance(doctype,
|
60
|
-
|
60
|
+
if doctype is None or not isinstance(doctype, str | OpenEpdDoctypes):
|
61
|
+
msg = (
|
61
62
|
f"The document type is not specified or not supported. "
|
62
63
|
f"Please specify it in `doctype` field. Supported are: {','.join(cls.DOCTYPE_TO_FACTORY)}"
|
63
64
|
)
|
65
|
+
raise ValueError(msg)
|
64
66
|
|
65
67
|
factory = cls.get_factory(OpenEpdDoctypes(doctype))
|
66
68
|
return factory.from_dict(data)
|
@@ -87,6 +87,10 @@ class GenericEstimatePreviewV0(
|
|
87
87
|
description="A link to the shared git repository containing the LCA model used for this estimate.",
|
88
88
|
)
|
89
89
|
|
90
|
+
model_config = pydantic.ConfigDict(
|
91
|
+
protected_namespaces=(),
|
92
|
+
)
|
93
|
+
|
90
94
|
|
91
95
|
GenericEstimatePreview = GenericEstimatePreviewV0
|
92
96
|
|
openepd/model/lcia.py
CHANGED
@@ -14,11 +14,10 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
from enum import StrEnum
|
17
|
-
from typing import Any, ClassVar
|
17
|
+
from typing import Any, ClassVar, Self
|
18
18
|
|
19
19
|
import pydantic
|
20
20
|
from pydantic import ConfigDict
|
21
|
-
from typing_extensions import Self
|
22
21
|
|
23
22
|
from openepd.model.base import BaseOpenEpdSchema
|
24
23
|
from openepd.model.common import Measurement
|
@@ -215,7 +214,8 @@ class ScopeSet(BaseOpenEpdSchema):
|
|
215
214
|
if not self.allowed_units:
|
216
215
|
# For unknown units - only units should be the same across all measurements (textually)
|
217
216
|
if len(all_units) > 1:
|
218
|
-
|
217
|
+
msg = "All scopes and measurements should be expressed in the same unit."
|
218
|
+
raise ValueError(msg)
|
219
219
|
else:
|
220
220
|
# might be multiple variations of the same unit (kgCFC-11e, kgCFC11e)
|
221
221
|
if len(all_units) > 1 and ExternalValidationConfig.QUANTITY_VALIDATOR:
|
@@ -237,9 +237,8 @@ class ScopeSet(BaseOpenEpdSchema):
|
|
237
237
|
except ValueError:
|
238
238
|
...
|
239
239
|
if not matched_unit:
|
240
|
-
|
241
|
-
|
242
|
-
)
|
240
|
+
msg = f"'{', '.join(allowed_units)}' is only allowed unit for this scopeset. Provided '{unit}'"
|
241
|
+
raise ValueError(msg)
|
243
242
|
|
244
243
|
return self
|
245
244
|
|
@@ -254,10 +253,10 @@ class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
|
|
254
253
|
:return: set of names, for example ['gwp', 'odp']
|
255
254
|
"""
|
256
255
|
result = []
|
257
|
-
for f in self.
|
256
|
+
for f in self.model_fields_set:
|
258
257
|
if f in ("ext",):
|
259
258
|
continue
|
260
|
-
field = self.
|
259
|
+
field = self.model_fields.get(f)
|
261
260
|
# field can be explicitly specified, or can be an unknown impact covered by extra='allow'
|
262
261
|
result.append(field.alias if field and field.alias else f)
|
263
262
|
|
@@ -271,7 +270,7 @@ class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
|
|
271
270
|
:return: A scopeset if found, None otherwise
|
272
271
|
"""
|
273
272
|
# check known impacts first
|
274
|
-
for f_name, f in self.
|
273
|
+
for f_name, f in self.model_fields.items():
|
275
274
|
if f.alias == name:
|
276
275
|
return getattr(self, f_name)
|
277
276
|
if f_name == name:
|
@@ -294,7 +293,8 @@ class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
|
|
294
293
|
case dict():
|
295
294
|
values[f] = ScopeSet(**extra_scopeset)
|
296
295
|
case _:
|
297
|
-
|
296
|
+
msg = f"{f} must be a ScopeSet schema"
|
297
|
+
raise ValueError(msg)
|
298
298
|
|
299
299
|
return values
|
300
300
|
|
openepd/model/org.py
CHANGED
@@ -61,10 +61,12 @@ class Org(WithAttachmentsMixin, WithAltIdsMixin, OrgRef):
|
|
61
61
|
return value
|
62
62
|
|
63
63
|
if not isinstance(value, list):
|
64
|
-
|
64
|
+
msg = f"Expected type list or None, got {type(value)}"
|
65
|
+
raise TypeError(msg)
|
65
66
|
|
66
67
|
if any((len(item) > 200) for item in value):
|
67
|
-
|
68
|
+
msg = "One or more alt_names are longer than 200 characters"
|
69
|
+
raise ValueError(msg)
|
68
70
|
|
69
71
|
return value
|
70
72
|
|
@@ -79,11 +81,13 @@ class Org(WithAttachmentsMixin, WithAltIdsMixin, OrgRef):
|
|
79
81
|
return value
|
80
82
|
|
81
83
|
if not isinstance(value, list):
|
82
|
-
|
84
|
+
msg = f"Expected type list or None, got {type(value)}"
|
85
|
+
raise TypeError(msg)
|
83
86
|
|
84
87
|
for item in value:
|
85
88
|
if len(item.name) > 200:
|
86
|
-
|
89
|
+
msg = "One or more subsidiaries name are longer than 200 characters"
|
90
|
+
raise ValueError(msg)
|
87
91
|
|
88
92
|
return value
|
89
93
|
|
@@ -158,13 +162,16 @@ class Plant(PlantRef, WithAttachmentsMixin, WithAltIdsMixin):
|
|
158
162
|
try:
|
159
163
|
pluscode, web_domain = v.split(".", maxsplit=1)
|
160
164
|
except ValueError as e:
|
161
|
-
|
165
|
+
msg = "Incorrectly formed id: should be pluscode.owner_web_domain"
|
166
|
+
raise ValueError(msg) from e
|
162
167
|
|
163
168
|
if not openlocationcode.isValid(pluscode):
|
164
|
-
|
169
|
+
msg = "Incorrect pluscode for plant"
|
170
|
+
raise ValueError(msg)
|
165
171
|
|
166
172
|
if not web_domain:
|
167
|
-
|
173
|
+
msg = "Incorrect web_domain for plant"
|
174
|
+
raise ValueError(msg)
|
168
175
|
return v
|
169
176
|
|
170
177
|
model_config = ConfigDict(populate_by_name=True)
|
openepd/model/pcr.py
CHANGED
@@ -93,12 +93,12 @@ class Pcr(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
|
93
93
|
default=None,
|
94
94
|
)
|
95
95
|
date_of_issue: datetime.datetime | None = pydantic.Field(
|
96
|
-
examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.
|
96
|
+
examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.UTC)],
|
97
97
|
default=None,
|
98
98
|
description="First day on which the document is valid",
|
99
99
|
)
|
100
100
|
valid_until: datetime.datetime | None = pydantic.Field(
|
101
|
-
examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.
|
101
|
+
examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.UTC)],
|
102
102
|
default=None,
|
103
103
|
description="Last day on which the document is valid",
|
104
104
|
)
|