openepd 7.0.1__py3-none-any.whl → 7.2.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 +5 -4
- openepd/bundle/reader.py +13 -7
- openepd/bundle/writer.py +11 -6
- 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 +27 -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 +17 -17
- 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/furnishings.py +10 -10
- 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.0.1.dist-info → openepd-7.2.0.dist-info}/METADATA +5 -4
- {openepd-7.0.1.dist-info → openepd-7.2.0.dist-info}/RECORD +78 -67
- {openepd-7.0.1.dist-info → openepd-7.2.0.dist-info}/WHEEL +1 -1
- {openepd-7.0.1.dist-info → openepd-7.2.0.dist-info}/LICENSE +0 -0
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",
|
@@ -96,7 +97,8 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
96
97
|
"""Write an object asset to the bundle. Object means subclass of BaseOpenEpdSchem."""
|
97
98
|
asset_type_str = obj.get_asset_type()
|
98
99
|
if asset_type_str is None:
|
99
|
-
|
100
|
+
msg = f"Object {obj} does not have a valid asset type and can't be written to a bundle."
|
101
|
+
raise ValueError(msg)
|
100
102
|
asset_type = AssetType(asset_type_str)
|
101
103
|
rel_ref_str = self._asset_ref_to_str(rel_asset) if rel_asset is not None else None
|
102
104
|
ref_str = self.__generate_entry_name(
|
@@ -139,14 +141,16 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
139
141
|
|
140
142
|
def __register_entry(self, asset_info: AssetInfo):
|
141
143
|
if asset_info.ref in self.__added_entries:
|
142
|
-
|
144
|
+
msg = f"Asset {asset_info.ref} already exists in the bundle."
|
145
|
+
raise ValueError(msg)
|
143
146
|
self._toc_writer.writerow(asset_info.model_dump(exclude_unset=True, exclude_none=True))
|
144
147
|
self.__added_entries.add(asset_info.ref)
|
145
148
|
type_counter = self.__manifest.assets.count_by_type.get(asset_info.type, 0) + 1
|
146
149
|
self.__manifest.assets.count_by_type[asset_info.type] = type_counter
|
147
150
|
self.__manifest.assets.total_count += 1
|
148
151
|
if asset_info.size is None:
|
149
|
-
|
152
|
+
msg = "Size of asset is not set."
|
153
|
+
raise ValueError(msg)
|
150
154
|
self.__manifest.assets.total_size += asset_info.size
|
151
155
|
|
152
156
|
def __generate_entry_name(
|
@@ -168,7 +172,8 @@ class DefaultBundleWriter(BaseBundleWriter):
|
|
168
172
|
info = self._bundle_archive.getinfo(f"{asset_type}/")
|
169
173
|
if info.is_dir():
|
170
174
|
return
|
171
|
-
|
175
|
+
msg = f"Object with name {asset_type} already exists in the bundle."
|
176
|
+
raise ValueError(msg)
|
172
177
|
except KeyError:
|
173
178
|
self._bundle_archive.mkdir(str(asset_type))
|
174
179
|
|
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
|
|
@@ -365,6 +365,12 @@ class ScopeSetCTUe(ScopeSet):
|
|
365
365
|
allowed_units: ClassVar[str | tuple[str, ...] | None] = "CTUe"
|
366
366
|
|
367
367
|
|
368
|
+
class ScopeSetKgSbe(ScopeSet):
|
369
|
+
"""ScopeSet measured in kgSbe."""
|
370
|
+
|
371
|
+
allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgSbe"
|
372
|
+
|
373
|
+
|
368
374
|
class ScopeSetDiseaseIncidence(ScopeSet):
|
369
375
|
"""ScopeSet measuring disease incidence measured in AnnualPerCapita (cases)."""
|
370
376
|
|
@@ -487,6 +493,17 @@ class ImpactSet(ScopesetByNameBase):
|
|
487
493
|
default=None,
|
488
494
|
description="Land use related impacts / Soil quality, in potential soil quality parameters.",
|
489
495
|
)
|
496
|
+
ADP_mineral: ScopeSetKgSbe | None = pydantic.Field(
|
497
|
+
alias="ADP-mineral",
|
498
|
+
default=None,
|
499
|
+
description='Abiotic depletion potential for non-fossil resources. EN15804 calls this "ADP - minerals and metals".',
|
500
|
+
)
|
501
|
+
|
502
|
+
ADP_fossil: ScopeSetEnergy | None = pydantic.Field(
|
503
|
+
alias="ADP-fossil",
|
504
|
+
default=None,
|
505
|
+
description="Abiotic depletion potential for fossil resources",
|
506
|
+
)
|
490
507
|
|
491
508
|
model_config = pydantic.ConfigDict(from_attributes=True)
|
492
509
|
|
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
|
)
|
openepd/model/specs/__init__.py
CHANGED
@@ -14,6 +14,43 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
|
17
|
+
__all__ = [
|
18
|
+
"CMUV1",
|
19
|
+
"AccessoriesV1",
|
20
|
+
"AggregatesV1",
|
21
|
+
"AluminiumV1",
|
22
|
+
"AsphaltV1",
|
23
|
+
"BulkMaterialsV1",
|
24
|
+
"CastDecksAndUnderlaymentV1",
|
25
|
+
"CladdingV1",
|
26
|
+
"ConcreteV1",
|
27
|
+
"ConveyingEquipmentV1",
|
28
|
+
"ElectricalTransmissionAndDistributionEquipmentV1",
|
29
|
+
"ElectricalV1",
|
30
|
+
"ElectricityV1",
|
31
|
+
"FinishesV1",
|
32
|
+
"FireAndSmokeProtectionV1",
|
33
|
+
"FurnishingsV1",
|
34
|
+
"GroutingV1",
|
35
|
+
"ManufacturingInputsV1",
|
36
|
+
"MasonryV1",
|
37
|
+
"MaterialHandlingV1",
|
38
|
+
"MechanicalInsulationV1",
|
39
|
+
"MechanicalV1",
|
40
|
+
"NetworkInfrastructureV1",
|
41
|
+
"OpeningsV1",
|
42
|
+
"OtherElectricalEquipmentV1",
|
43
|
+
"OtherMaterialsV1",
|
44
|
+
"PlumbingV1",
|
45
|
+
"PrecastConcreteV1",
|
46
|
+
"SheathingV1",
|
47
|
+
"SteelV1",
|
48
|
+
"ThermalMoistureProtectionV1",
|
49
|
+
"UtilityPipingV1",
|
50
|
+
"WoodJoistsV1",
|
51
|
+
"WoodV1",
|
52
|
+
]
|
53
|
+
|
17
54
|
from openepd.model.specs.singular.accessories import AccessoriesV1
|
18
55
|
from openepd.model.specs.singular.aggregates import AggregatesV1
|
19
56
|
from openepd.model.specs.singular.aluminium import AluminiumV1
|
openepd/model/specs/asphalt.py
CHANGED
@@ -51,19 +51,19 @@ class AsphaltV1(BaseOpenEpdHierarchicalSpec):
|
|
51
51
|
|
52
52
|
asphalt_rap: float | None = pydantic.Field(
|
53
53
|
default=None,
|
54
|
-
description="Percent of mixture that has been replaced by recycled
|
54
|
+
description="Percent of mixture that has been replaced by recycled asphalt pavement (RAP).",
|
55
55
|
ge=0,
|
56
56
|
le=1,
|
57
57
|
)
|
58
58
|
asphalt_ras: float | None = pydantic.Field(
|
59
59
|
default=None,
|
60
|
-
description="Percent of mixture that has been replaced by recycled
|
60
|
+
description="Percent of mixture that has been replaced by recycled asphalt shingles (RAS).",
|
61
61
|
ge=0,
|
62
62
|
le=1,
|
63
63
|
)
|
64
64
|
asphalt_ground_tire_rubber: float | None = pydantic.Field(
|
65
65
|
default=None,
|
66
|
-
description="Percent of mixture that has been replaced
|
66
|
+
description="Percent of mixture that has been replaced by ground tire rubber (GTR).",
|
67
67
|
ge=0,
|
68
68
|
le=1,
|
69
69
|
)
|
openepd/model/specs/base.py
CHANGED
@@ -40,7 +40,8 @@ class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
|
|
40
40
|
# ensure that all the concrete spec objects fail on creations if they dont have _EXT_VERSION declared to
|
41
41
|
# something meaningful
|
42
42
|
if not hasattr(self, "_EXT_VERSION") or self._EXT_VERSION is None:
|
43
|
-
|
43
|
+
msg = f"Class {self.__class__} must declare an extension version"
|
44
|
+
raise ValueError(msg)
|
44
45
|
Version.parse_version(self._EXT_VERSION) # validate format correctness
|
45
46
|
super().__init__(**{"ext_version": self._EXT_VERSION, **data})
|
46
47
|
|
openepd/model/specs/enums.py
CHANGED
@@ -2017,7 +2017,8 @@ class InsulatingMaterial(StrEnum):
|
|
2017
2017
|
- Polyisocyanurate: Polyisocyanurate
|
2018
2018
|
- Expanded Polyethylene: Expanded Polyethylene
|
2019
2019
|
- EPS: EPS
|
2020
|
-
-
|
2020
|
+
- Wood Fiber = Wood Fiber
|
2021
|
+
- Other: Other
|
2021
2022
|
|
2022
2023
|
"""
|
2023
2024
|
|
@@ -2029,6 +2030,8 @@ class InsulatingMaterial(StrEnum):
|
|
2029
2030
|
POLYISOCYANURATE = "Polyisocyanurate"
|
2030
2031
|
EXPANDED_POLYETHYLENE = "Expanded Polyethylene"
|
2031
2032
|
EPS = "EPS"
|
2033
|
+
WOOD_FIBER = "Wood Fiber"
|
2034
|
+
POLYURETHANE = "Polyurethane"
|
2032
2035
|
OTHER = "Other"
|
2033
2036
|
|
2034
2037
|
|
@@ -2439,13 +2442,18 @@ class CementBoardThickness(StrEnum):
|
|
2439
2442
|
INCH_3_8 = '3/8"'
|
2440
2443
|
INCH_1_2 = '1/2"'
|
2441
2444
|
INCH_5_8 = '5/8"'
|
2445
|
+
MM_4 = "4 mm"
|
2446
|
+
MM_6 = "6 mm"
|
2442
2447
|
MM_6_5 = "6.5 mm"
|
2443
2448
|
MM_8_0 = "8.0 mm"
|
2444
2449
|
MM_9_0 = "9.0 mm"
|
2445
2450
|
MM_9_5 = "9.5 mm"
|
2451
|
+
MM_10 = "10 mm"
|
2452
|
+
MM_12 = "12 mm"
|
2446
2453
|
MM_12_5 = "12.5 mm"
|
2447
2454
|
MM_13 = "13 mm"
|
2448
2455
|
MM_15 = "15 mm"
|
2456
|
+
MM_16 = "16 mm"
|
2449
2457
|
MM_18 = "18 mm"
|
2450
2458
|
|
2451
2459
|
|
@@ -13,9 +13,9 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
|
-
__all__ =
|
17
|
-
|
18
|
-
|
16
|
+
__all__ = [
|
17
|
+
"SpecsRange",
|
18
|
+
]
|
19
19
|
|
20
20
|
|
21
21
|
from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
|
@@ -33,6 +33,7 @@ from .conveying_equipment import ConveyingEquipmentRangeV1
|
|
33
33
|
from .electrical import ElectricalRangeV1
|
34
34
|
from .electrical_transmission_and_distribution_equipment import ElectricalTransmissionAndDistributionEquipmentRangeV1
|
35
35
|
from .electricity import ElectricityRangeV1
|
36
|
+
from .exterior_improvements import ExteriorImprovementsRangeV1
|
36
37
|
from .finishes import FinishesRangeV1
|
37
38
|
from .fire_and_smoke_protection import FireAndSmokeProtectionRangeV1
|
38
39
|
from .furnishings import FurnishingsRangeV1
|
@@ -99,3 +100,4 @@ class SpecsRange(BaseOpenEpdHierarchicalSpec):
|
|
99
100
|
MechanicalInsulation: MechanicalInsulationRangeV1 | None = None
|
100
101
|
OtherElectricalEquipment: OtherElectricalEquipmentRangeV1 | None = None
|
101
102
|
WoodJoists: WoodJoistsRangeV1 | None = None
|
103
|
+
ExteriorImprovements: ExteriorImprovementsRangeV1 | None = None
|