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.
Files changed (78) hide show
  1. openepd/__version__.py +1 -1
  2. openepd/api/average_dataset/generic_estimate_sync_api.py +2 -1
  3. openepd/api/average_dataset/industry_epd_sync_api.py +2 -1
  4. openepd/api/base_sync_client.py +10 -8
  5. openepd/api/common.py +17 -11
  6. openepd/api/dto/common.py +1 -1
  7. openepd/api/epd/sync_api.py +2 -1
  8. openepd/api/org/__init__.py +15 -0
  9. openepd/api/org/sync_api.py +79 -0
  10. openepd/api/pcr/sync_api.py +35 -0
  11. openepd/api/plant/__init__.py +15 -0
  12. openepd/api/plant/sync_api.py +79 -0
  13. openepd/api/standard/__init__.py +15 -0
  14. openepd/api/standard/sync_api.py +79 -0
  15. openepd/api/sync_client.py +27 -0
  16. openepd/bundle/base.py +47 -6
  17. openepd/bundle/model.py +1 -0
  18. openepd/bundle/reader.py +35 -10
  19. openepd/bundle/writer.py +21 -12
  20. openepd/m49/__init__.py +2 -0
  21. openepd/m49/const.py +1 -1
  22. openepd/m49/utils.py +16 -10
  23. openepd/model/base.py +20 -15
  24. openepd/model/common.py +10 -5
  25. openepd/model/declaration.py +2 -2
  26. openepd/model/epd.py +2 -1
  27. openepd/model/factory.py +5 -3
  28. openepd/model/generic_estimate.py +4 -0
  29. openepd/model/lcia.py +10 -10
  30. openepd/model/org.py +14 -7
  31. openepd/model/pcr.py +2 -2
  32. openepd/model/specs/__init__.py +37 -0
  33. openepd/model/specs/asphalt.py +3 -3
  34. openepd/model/specs/base.py +2 -1
  35. openepd/model/specs/enums.py +9 -1
  36. openepd/model/specs/range/__init__.py +5 -3
  37. openepd/model/specs/range/accessories.py +1 -1
  38. openepd/model/specs/range/aluminium.py +1 -1
  39. openepd/model/specs/range/cladding.py +10 -10
  40. openepd/model/specs/range/cmu.py +0 -2
  41. openepd/model/specs/range/concrete.py +25 -2
  42. openepd/model/specs/range/conveying_equipment.py +2 -2
  43. openepd/model/specs/range/electrical.py +18 -18
  44. openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +1 -1
  45. openepd/model/specs/range/exterior_improvements.py +47 -0
  46. openepd/model/specs/range/finishes.py +19 -40
  47. openepd/model/specs/range/fire_and_smoke_protection.py +3 -3
  48. openepd/model/specs/range/furnishings.py +7 -7
  49. openepd/model/specs/range/manufacturing_inputs.py +17 -5
  50. openepd/model/specs/range/masonry.py +1 -1
  51. openepd/model/specs/range/mechanical.py +6 -6
  52. openepd/model/specs/range/mixins/__init__.py +15 -0
  53. openepd/model/specs/range/mixins/access_flooring_mixin.py +43 -0
  54. openepd/model/specs/range/network_infrastructure.py +3 -3
  55. openepd/model/specs/range/openings.py +17 -17
  56. openepd/model/specs/range/other_materials.py +4 -4
  57. openepd/model/specs/range/plumbing.py +5 -5
  58. openepd/model/specs/range/precast_concrete.py +2 -2
  59. openepd/model/specs/range/steel.py +18 -9
  60. openepd/model/specs/range/thermal_moisture_protection.py +12 -12
  61. openepd/model/specs/range/wood.py +4 -6
  62. openepd/model/specs/singular/__init__.py +119 -2
  63. openepd/model/specs/singular/aluminium.py +2 -1
  64. openepd/model/specs/singular/concrete.py +25 -1
  65. openepd/model/specs/singular/deprecated/__init__.py +1 -1
  66. openepd/model/specs/singular/exterior_improvements.py +47 -0
  67. openepd/model/specs/singular/finishes.py +3 -59
  68. openepd/model/specs/singular/manufacturing_inputs.py +13 -1
  69. openepd/model/specs/singular/mixins/access_flooring_mixin.py +55 -0
  70. openepd/model/specs/singular/steel.py +10 -2
  71. openepd/model/validation/common.py +10 -6
  72. openepd/model/validation/enum.py +4 -2
  73. openepd/model/validation/quantity.py +13 -6
  74. openepd/model/versioning.py +8 -6
  75. {openepd-7.1.0.dist-info → openepd-7.3.0.dist-info}/METADATA +23 -13
  76. {openepd-7.1.0.dist-info → openepd-7.3.0.dist-info}/RECORD +78 -67
  77. {openepd-7.1.0.dist-info → openepd-7.3.0.dist-info}/WHEEL +1 -1
  78. {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, Callable, Iterator, Sequence, Type, cast
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 and a.rel_asset != parent_ref:
64
- return False
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
- raise ValueError("The bundle file is not valid. TOC reading error: wrong number of fields")
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
- if x.rel_asset == asset_ref:
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
- raise ValueError("Asset not found")
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: Type[TOpenEpdObject], asset_ref: AssetRef) -> TOpenEpdObject:
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
- raise ValueError("Asset not found")
180
+ msg = "Asset not found"
181
+ raise ValueError(msg)
159
182
  if obj_class.get_asset_type() is None:
160
- raise ValueError(f"Target object {obj_class.__name__} is not supported asset")
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
- raise ValueError(f"Asset type mismatch. Expected {obj_class.get_asset_type()}, got {asset.type}")
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, (PathLike, str)) and Path(bundle_file).exists():
35
- raise ValueError("Amending existing files is not supported yet.")
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
- rel_ref_str = self._asset_ref_to_str(rel_asset) if rel_asset is not None else None
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=rel_ref_str,
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
- raise ValueError(f"Object {obj} does not have a valid asset type and can't be written to a bundle.")
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
- rel_ref_str = self._asset_ref_to_str(rel_asset) if rel_asset is not None else None
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=rel_ref_str,
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
- raise ValueError(f"Asset {asset_info.ref} already exists in the bundle.")
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
- raise ValueError("Size of asset is not set.")
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
- raise ValueError(f"Object with name {asset_type} already exists in the bundle.")
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
@@ -14,4 +14,6 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ __all__ = ["geo_converter"]
18
+
17
19
  from . import utils as geo_converter # for backwards compatibility
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
- "region_and_country_names_to_m49",
20
+ "m49_to_openepd",
20
21
  "m49_to_region_and_country_names",
21
22
  "openepd_to_m49",
22
- "m49_to_openepd",
23
- "is_m49_code",
23
+ "region_and_country_names_to_m49",
24
24
  ]
25
- from typing import Collection
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
- raise ValueError(f"Country code '{code}' not found in M49 region codes.")
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
- raise ValueError(f"Region code '{code}' not found in ISO3166.")
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
- raise ValueError(f"Region or country name '{name}' not found in M49 region codes.")
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
- raise ValueError(f"Region code '{code}' not found in M49 region codes.")
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
- raise ValueError(f"Region '{region}' not found in ISO3166 or OpenEPD special regions.")
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
- raise ValueError(f"Region code '{code}' not found in ISO3166 or OpenEPD special regions.")
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, Type, TypeAlias, TypeVar
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: Type[TAnySerializable],
113
- default: Optional[TAnySerializable] = None,
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
- raise ValueError(f"Cannot convert {value} to {target_type}")
127
+ msg = f"Cannot convert {value} to {target_type}"
128
+ raise ValueError(msg)
128
129
 
129
- def get_ext(self, ext_type: Type["TOpenEpdExtension"]) -> Optional["TOpenEpdExtension"]:
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: Type["TOpenEpdExtension"]) -> "TOpenEpdExtension":
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=Type[BaseOpenEpdSchema])
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
- raise ValueError("Doctype not found in the data.")
229
+ msg = "Doctype not found in the data."
230
+ raise ValueError(msg)
229
231
  if doctype.lower() != cls.DOCTYPE_CONSTRAINT.lower():
230
- raise ValueError(
231
- f"Document type {doctype} not supported. This factory supports {cls.DOCTYPE_CONSTRAINT} only."
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
- raise ValueError(
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
- raise ValueError(f"Version {version} is not supported. Supported versions are: {supported_versions}")
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
- raise ValueError(f"Invalid value type: {type(value)}")
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
- raise ValueError("Invalid format") from e
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
- raise ValueError("Either qty or unit must be provided.")
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
- raise ValueError("evidence_type is required if gwp_fraction is provided")
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
- raise ValueError("link or citation is required if gwp_fraction is provided")
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
- raise TypeError("AttachmentDict must be a dict")
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
- raise ValueError("Max should be greater than min")
322
+ msg = "Max should be greater than min"
323
+ raise ValueError(msg)
319
324
  return values
320
325
 
321
326
 
@@ -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.timezone.utc)],
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.timezone.utc)],
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
@@ -239,7 +239,8 @@ class EpdPreviewV0(
239
239
  """
240
240
  if not v or v.lower() == "openepd":
241
241
  return "openEPD"
242
- raise ValueError("Invalid doctype")
242
+ msg = "Invalid doctype"
243
+ raise ValueError(msg)
243
244
 
244
245
 
245
246
  EpdPreview = EpdPreviewV0
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
- raise ValueError("The document type is not specified.")
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, (str, OpenEpdDoctypes)):
60
- raise ValueError(
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
- raise ValueError("All scopes and measurements should be expressed in the same unit.")
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
- raise ValueError(
241
- f"'{', '.join(allowed_units)}' is only allowed unit for this scopeset. Provided '{unit}'"
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.__fields_set__:
256
+ for f in self.model_fields_set:
258
257
  if f in ("ext",):
259
258
  continue
260
- field = self.__fields__.get(f)
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.__fields__.items():
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
- raise ValueError(f"{f} must be a ScopeSet schema")
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
- raise TypeError(f"Expected type list or None, got {type(value)}")
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
- raise ValueError("One or more alt_names are longer than 200 characters")
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
- raise TypeError(f"Expected type list or None, got {type(value)}")
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
- raise ValueError("One or more subsidiaries name are longer than 200 characters")
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
- raise ValueError("Incorrectly formed id: should be pluscode.owner_web_domain") from e
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
- raise ValueError("Incorrect pluscode for plant")
169
+ msg = "Incorrect pluscode for plant"
170
+ raise ValueError(msg)
165
171
 
166
172
  if not web_domain:
167
- raise ValueError("Incorrect web_domain for plant")
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.timezone.utc)],
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.timezone.utc)],
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
  )