openepd 6.13.1__py3-none-any.whl → 7.0.0__py3-none-any.whl

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