openepd 7.0.0__tar.gz → 7.0.1__tar.gz

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 (142) hide show
  1. {openepd-7.0.0 → openepd-7.0.1}/PKG-INFO +2 -2
  2. {openepd-7.0.0 → openepd-7.0.1}/pyproject.toml +3 -3
  3. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/__version__.py +1 -1
  4. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/category/sync_api.py +1 -1
  5. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/base.py +1 -1
  6. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/common.py +8 -5
  7. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/epd.py +4 -4
  8. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/generic_estimate.py +2 -2
  9. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/lcia.py +30 -24
  10. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/org.py +38 -26
  11. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/base.py +3 -2
  12. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/steel.py +3 -1
  13. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/validation/quantity.py +37 -32
  14. openepd-7.0.0/src/openepd/__init__.py +0 -21
  15. {openepd-7.0.0 → openepd-7.0.1}/LICENSE +0 -0
  16. {openepd-7.0.0 → openepd-7.0.1}/README.md +0 -0
  17. {openepd-7.0.0/src/openepd/api → openepd-7.0.1/src/openepd}/__init__.py +0 -0
  18. {openepd-7.0.0/src/openepd/api/average_dataset → openepd-7.0.1/src/openepd/api}/__init__.py +0 -0
  19. {openepd-7.0.0/src/openepd/api/category → openepd-7.0.1/src/openepd/api/average_dataset}/__init__.py +0 -0
  20. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/average_dataset/generic_estimate_sync_api.py +0 -0
  21. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/average_dataset/industry_epd_sync_api.py +0 -0
  22. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/base_sync_client.py +0 -0
  23. {openepd-7.0.0/src/openepd/api/dto → openepd-7.0.1/src/openepd/api/category}/__init__.py +0 -0
  24. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/category/dto.py +0 -0
  25. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/common.py +0 -0
  26. {openepd-7.0.0/src/openepd/api/epd → openepd-7.0.1/src/openepd/api/dto}/__init__.py +0 -0
  27. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/dto/base.py +0 -0
  28. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/dto/common.py +0 -0
  29. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/dto/meta.py +0 -0
  30. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/dto/mf.py +0 -0
  31. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/dto/params.py +0 -0
  32. {openepd-7.0.0/src/openepd/api/pcr → openepd-7.0.1/src/openepd/api/epd}/__init__.py +0 -0
  33. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/epd/dto.py +0 -0
  34. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/epd/sync_api.py +0 -0
  35. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/errors.py +0 -0
  36. {openepd-7.0.0/src/openepd/api/test → openepd-7.0.1/src/openepd/api/pcr}/__init__.py +0 -0
  37. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/pcr/sync_api.py +0 -0
  38. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/sync_client.py +0 -0
  39. {openepd-7.0.0/src/openepd/bundle → openepd-7.0.1/src/openepd/api/test}/__init__.py +0 -0
  40. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/api/utils.py +0 -0
  41. {openepd-7.0.0/src/openepd/model → openepd-7.0.1/src/openepd/bundle}/__init__.py +0 -0
  42. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/bundle/base.py +0 -0
  43. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/bundle/model.py +0 -0
  44. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/bundle/reader.py +0 -0
  45. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/bundle/writer.py +0 -0
  46. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/m49/__init__.py +0 -0
  47. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/m49/const.py +0 -0
  48. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/m49/utils.py +0 -0
  49. {openepd-7.0.0/src/openepd/model/specs/singular/mixins → openepd-7.0.1/src/openepd/model}/__init__.py +0 -0
  50. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/category.py +0 -0
  51. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/declaration.py +0 -0
  52. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/factory.py +0 -0
  53. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/geography.py +0 -0
  54. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/industry_epd.py +0 -0
  55. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/pcr.py +0 -0
  56. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/README.md +0 -0
  57. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/__init__.py +0 -0
  58. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/asphalt.py +0 -0
  59. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/concrete.py +0 -0
  60. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/enums.py +0 -0
  61. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/__init__.py +0 -0
  62. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/accessories.py +0 -0
  63. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/aggregates.py +0 -0
  64. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/aluminium.py +0 -0
  65. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/asphalt.py +0 -0
  66. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/bulk_materials.py +0 -0
  67. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/cast_decks_and_underlayment.py +0 -0
  68. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/cladding.py +0 -0
  69. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/cmu.py +0 -0
  70. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/concrete.py +0 -0
  71. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/conveying_equipment.py +0 -0
  72. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/electrical.py +0 -0
  73. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +0 -0
  74. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/electricity.py +0 -0
  75. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/finishes.py +0 -0
  76. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/fire_and_smoke_protection.py +0 -0
  77. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/furnishings.py +0 -0
  78. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/grouting.py +0 -0
  79. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/manufacturing_inputs.py +0 -0
  80. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/masonry.py +0 -0
  81. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/material_handling.py +0 -0
  82. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/mechanical.py +0 -0
  83. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/mechanical_insulation.py +0 -0
  84. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/network_infrastructure.py +0 -0
  85. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/openings.py +0 -0
  86. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/other_electrical_equipment.py +0 -0
  87. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/other_materials.py +0 -0
  88. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/plumbing.py +0 -0
  89. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/precast_concrete.py +0 -0
  90. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/sheathing.py +0 -0
  91. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/thermal_moisture_protection.py +0 -0
  92. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/utility_piping.py +0 -0
  93. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/wood.py +0 -0
  94. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/range/wood_joists.py +0 -0
  95. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/__init__.py +0 -0
  96. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/accessories.py +0 -0
  97. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/aggregates.py +0 -0
  98. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/aluminium.py +0 -0
  99. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/asphalt.py +0 -0
  100. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/bulk_materials.py +0 -0
  101. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/cast_decks_and_underlayment.py +0 -0
  102. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/cladding.py +0 -0
  103. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/cmu.py +0 -0
  104. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/common.py +0 -0
  105. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/concrete.py +0 -0
  106. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/conveying_equipment.py +0 -0
  107. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/deprecated/__init__.py +0 -0
  108. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/deprecated/concrete.py +0 -0
  109. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/deprecated/steel.py +0 -0
  110. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/electrical.py +0 -0
  111. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/electrical_transmission_and_distribution_equipment.py +0 -0
  112. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/electricity.py +0 -0
  113. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/finishes.py +0 -0
  114. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/fire_and_smoke_protection.py +0 -0
  115. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/furnishings.py +0 -0
  116. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/grouting.py +0 -0
  117. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/manufacturing_inputs.py +0 -0
  118. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/masonry.py +0 -0
  119. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/material_handling.py +0 -0
  120. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/mechanical.py +0 -0
  121. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/mechanical_insulation.py +0 -0
  122. {openepd-7.0.0/src/openepd/model/validation → openepd-7.0.1/src/openepd/model/specs/singular/mixins}/__init__.py +0 -0
  123. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/mixins/conduit_mixin.py +0 -0
  124. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/network_infrastructure.py +0 -0
  125. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/openings.py +0 -0
  126. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/other_electrical_equipment.py +0 -0
  127. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/other_materials.py +0 -0
  128. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/plumbing.py +0 -0
  129. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/precast_concrete.py +0 -0
  130. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/sheathing.py +0 -0
  131. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/steel.py +0 -0
  132. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/thermal_moisture_protection.py +0 -0
  133. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/utility_piping.py +0 -0
  134. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/wood.py +0 -0
  135. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/specs/singular/wood_joists.py +0 -0
  136. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/standard.py +0 -0
  137. /openepd-7.0.0/src/openepd/model/validation/numbers.py → /openepd-7.0.1/src/openepd/model/validation/__init__.py +0 -0
  138. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/validation/common.py +0 -0
  139. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/validation/enum.py +0 -0
  140. /openepd-7.0.0/src/openepd/patch_pydantic.py → /openepd-7.0.1/src/openepd/model/validation/numbers.py +0 -0
  141. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/model/versioning.py +0 -0
  142. {openepd-7.0.0 → openepd-7.0.1}/src/openepd/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 7.0.0
3
+ Version: 7.0.1
4
4
  Summary: Python library to work with OpenEPD format
5
5
  Home-page: https://github.com/cchangelabs/openepd
6
6
  License: Apache-2.0
@@ -21,7 +21,7 @@ Requires-Dist: email-validator (>=1.3.1)
21
21
  Requires-Dist: idna (>=3.7)
22
22
  Requires-Dist: open-xpd-uuid (>=0.2.1,<0.3.0)
23
23
  Requires-Dist: openlocationcode (>=1.0.1)
24
- Requires-Dist: pydantic (>=1.10,<3.0)
24
+ Requires-Dist: pydantic (>=2.0,<3)
25
25
  Requires-Dist: requests (>=2.0) ; extra == "api-client"
26
26
  Project-URL: Repository, https://github.com/cchangelabs/openepd
27
27
  Description-Content-Type: text/markdown
@@ -1,11 +1,11 @@
1
1
  [tool.ruff]
2
2
  line-length = 120
3
- target-version = "py312"
3
+ target-version = "py311"
4
4
  exclude = [".*pyi"]
5
5
 
6
6
  [tool.poetry]
7
7
  name = "openepd"
8
- version = "7.0.0"
8
+ version = "7.0.1"
9
9
  license = "Apache-2.0"
10
10
  description = "Python library to work with OpenEPD format"
11
11
  authors = ["C-Change Labs <support@c-change-labs.com>"]
@@ -26,7 +26,7 @@ exclude = ["**/test_*.py", "**/tests/**"]
26
26
 
27
27
  [tool.poetry.dependencies]
28
28
  python = "^3.11"
29
- pydantic = ">=1.10,<3.0"
29
+ pydantic = ">=2.0,<3"
30
30
  email-validator = ">=1.3.1"
31
31
  requests = { version = ">=2.0", optional = true }
32
32
  idna = ">=3.7"
@@ -13,4 +13,4 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- VERSION = "7.0.0"
16
+ VERSION = "7.0.1"
@@ -28,7 +28,7 @@ class CategoryApi(BaseApiMethodGroup):
28
28
  :return: categories tree wrapped in OpenEpdApiResponse
29
29
  """
30
30
  response = self._client.do_request("get", "/v2/categories/tree")
31
- return CategoryTreeResponse.model_validate(response.content)
31
+ return CategoryTreeResponse.model_validate_json(response.content)
32
32
 
33
33
  def get_tree(self) -> Category:
34
34
  """
@@ -234,7 +234,7 @@ class BaseDocumentFactory(Generic[TRootDocument]):
234
234
  for x, doc_cls in cls.VERSION_MAP.items():
235
235
  if x.major == version.major:
236
236
  if version.minor <= x.minor:
237
- return doc_cls(**data)
237
+ return doc_cls.model_validate(data)
238
238
  else:
239
239
  raise ValueError(
240
240
  f"Unsupported version: {version}. The highest supported version from branch {x.major}.x is {x}"
@@ -14,7 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  from enum import StrEnum
17
- from typing import Annotated, Any
17
+ from typing import Annotated, Any, Self
18
18
 
19
19
  import pydantic
20
20
  import pydantic_core
@@ -32,12 +32,15 @@ class Amount(BaseOpenEpdSchema):
32
32
  default=None,
33
33
  )
34
34
 
35
- @pydantic.model_validator(mode="before")
36
- def check_qty_or_unit(cls, values: dict[str, Any]):
35
+ model_config = pydantic.ConfigDict(from_attributes=True)
36
+
37
+ @pydantic.model_validator(mode="after")
38
+ def check_qty_or_unit(self) -> Self:
37
39
  """Ensure that qty or unit is provided."""
38
- if values.get("qty") is None and values.get("unit") is None:
40
+
41
+ if self.qty is None and self.unit is None:
39
42
  raise ValueError("Either qty or unit must be provided.")
40
- return values
43
+ return self
41
44
 
42
45
  def to_quantity_str(self):
43
46
  """Return a string representation of the amount."""
@@ -282,10 +282,10 @@ class EpdWithDepsV0(EpdV0, title="EPD (with Dependencies)"):
282
282
  some required fields in Org (like web_domain), and WithDeps would not.
283
283
  """
284
284
 
285
- manufacturer: Org | None = pydantic.Field(description=MANUFACTURER_DESCRIPTION)
286
- epd_developer: Org | None = pydantic.Field(description=DEVELOPER_DESCRIPTION, default=None)
287
- program_operator: Org | None = pydantic.Field(description=PROGRAM_OPERATOR_DESCRIPTION)
288
- third_party_verifier: Org | None = pydantic.Field(description=THIRD_PARTY_VERIFIER_DESCRIPTION)
285
+ manufacturer: Org | None = pydantic.Field(description=MANUFACTURER_DESCRIPTION, default=None) # type: ignore[assignment]
286
+ epd_developer: Org | None = pydantic.Field(description=DEVELOPER_DESCRIPTION, default=None) # type: ignore[assignment]
287
+ program_operator: Org | None = pydantic.Field(description=PROGRAM_OPERATOR_DESCRIPTION, default=None) # type: ignore[assignment]
288
+ third_party_verifier: Org | None = pydantic.Field(description=THIRD_PARTY_VERIFIER_DESCRIPTION, default=None) # type: ignore[assignment]
289
289
 
290
290
 
291
291
  EpdWithDeps = EpdWithDepsV0
@@ -110,8 +110,8 @@ class GenericEstimateWithDepsV0(GenericEstimateV0, title="Generic Estimate (with
110
110
  Contains related entities - orgs - with full fields, to support object matching in implementations.
111
111
  """
112
112
 
113
- publisher: Org | None = pydantic.Field(description="Organization that published the LCA results.")
114
- reviewer: Org | None = pydantic.Field(description="Org that performed a critical review of the LCA.")
113
+ publisher: Org | None = pydantic.Field(description="Organization that published the LCA results.", default=None) # type: ignore[assignment]
114
+ reviewer: Org | None = pydantic.Field(description="Org that performed a critical review of the LCA.", default=None) # type: ignore[assignment]
115
115
 
116
116
 
117
117
  GenericEstimateWithDeps = GenericEstimateWithDepsV0
@@ -18,6 +18,7 @@ from typing import Any, ClassVar
18
18
 
19
19
  import pydantic
20
20
  from pydantic import ConfigDict
21
+ from typing_extensions import Self
21
22
 
22
23
  from openepd.model.base import BaseOpenEpdSchema
23
24
  from openepd.model.common import Measurement
@@ -200,15 +201,18 @@ class ScopeSet(BaseOpenEpdSchema):
200
201
  description="Potential net benefits from reuse, recycling, and/or energy recovery beyond the system boundary.",
201
202
  )
202
203
 
203
- @pydantic.model_validator(mode="before")
204
- def _unit_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
204
+ model_config = pydantic.ConfigDict(from_attributes=True)
205
+
206
+ @pydantic.model_validator(mode="after")
207
+ def _unit_validator(self) -> Self:
205
208
  all_units = set()
206
209
 
207
- for k, v in values.items():
210
+ for k in self.model_fields:
211
+ v = getattr(self, k, None)
208
212
  if isinstance(v, Measurement):
209
213
  all_units.add(v.unit)
210
214
 
211
- if not cls.allowed_units:
215
+ if not self.allowed_units:
212
216
  # For unknown units - only units should be the same across all measurements (textually)
213
217
  if len(all_units) > 1:
214
218
  raise ValueError("All scopes and measurements should be expressed in the same unit.")
@@ -221,9 +225,9 @@ class ScopeSet(BaseOpenEpdSchema):
221
225
  ExternalValidationConfig.QUANTITY_VALIDATOR.validate_same_dimensionality(first, unit)
222
226
 
223
227
  # can correctly validate unit
224
- if cls.allowed_units is not None and len(all_units) == 1 and ExternalValidationConfig.QUANTITY_VALIDATOR:
228
+ if self.allowed_units is not None and len(all_units) == 1 and ExternalValidationConfig.QUANTITY_VALIDATOR:
225
229
  unit = next(iter(all_units))
226
- allowed_units = cls.allowed_units if isinstance(cls.allowed_units, tuple) else (cls.allowed_units,)
230
+ allowed_units = self.allowed_units if isinstance(self.allowed_units, tuple) else (self.allowed_units,)
227
231
 
228
232
  matched_unit = False
229
233
  for allowed_unit in allowed_units:
@@ -237,7 +241,7 @@ class ScopeSet(BaseOpenEpdSchema):
237
241
  f"'{', '.join(allowed_units)}' is only allowed unit for this scopeset. Provided '{unit}'"
238
242
  )
239
243
 
240
- return values
244
+ return self
241
245
 
242
246
 
243
247
  class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
@@ -279,7 +283,7 @@ class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
279
283
  def _extra_scopeset_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
280
284
  for f in values:
281
285
  # only interested in validating the extra fields
282
- if f in cls.__fields__:
286
+ if f in cls.model_fields:
283
287
  continue
284
288
 
285
289
  # extra impact of an unknown type - engage validation of ScopeSet
@@ -298,97 +302,97 @@ class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
298
302
  class ScopeSetGwp(ScopeSet):
299
303
  """ScopeSet measured in kgCO2e."""
300
304
 
301
- allowed_units = "kgCO2e"
305
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgCO2e"
302
306
 
303
307
 
304
308
  class ScopeSetOdp(ScopeSet):
305
309
  """ScopeSet measured in kgCFC11e."""
306
310
 
307
- allowed_units = "kgCFC11e"
311
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgCFC11e"
308
312
 
309
313
 
310
314
  class ScopeSetAp(ScopeSet):
311
315
  """ScopeSet measured in kgSO2e."""
312
316
 
313
- allowed_units = ("kgSO2e", "molHe")
317
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = ("kgSO2e", "molHe")
314
318
 
315
319
 
316
320
  class ScopeSetEpNe(ScopeSet):
317
321
  """ScopeSet measured in kgNe."""
318
322
 
319
- allowed_units = "kgNe"
323
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgNe"
320
324
 
321
325
 
322
326
  class ScopeSetPocp(ScopeSet):
323
327
  """ScopeSet measured in kgO3e."""
324
328
 
325
- allowed_units = ("kgO3e", "kgNMVOCe")
329
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = ("kgO3e", "kgNMVOCe")
326
330
 
327
331
 
328
332
  class ScopeSetEpFresh(ScopeSet):
329
333
  """ScopeSet measured in kgPO4e."""
330
334
 
331
- allowed_units = "kgPO4e"
335
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgPO4e"
332
336
 
333
337
 
334
338
  class ScopeSetEpTerr(ScopeSet):
335
339
  """ScopeSet measured in molNe."""
336
340
 
337
- allowed_units = "molNe"
341
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "molNe"
338
342
 
339
343
 
340
344
  class ScopeSetIrp(ScopeSet):
341
345
  """ScopeSet measured in kilo Becquerel equivalent of u235."""
342
346
 
343
- allowed_units = "kBqU235e"
347
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kBqU235e"
344
348
 
345
349
 
346
350
  class ScopeSetCTUh(ScopeSet):
347
351
  """ScopeSet measured in CTUh."""
348
352
 
349
- allowed_units = "CTUh"
353
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "CTUh"
350
354
 
351
355
 
352
356
  class ScopeSetM3Aware(ScopeSet):
353
357
  """ScopeSet measured in m3AWARE Water consumption by AWARE method."""
354
358
 
355
- allowed_units = "m3AWARE"
359
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "m3AWARE"
356
360
 
357
361
 
358
362
  class ScopeSetCTUe(ScopeSet):
359
363
  """ScopeSet measured in CTUe."""
360
364
 
361
- allowed_units = "CTUe"
365
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "CTUe"
362
366
 
363
367
 
364
368
  class ScopeSetDiseaseIncidence(ScopeSet):
365
369
  """ScopeSet measuring disease incidence measured in AnnualPerCapita (cases)."""
366
370
 
367
- allowed_units = "AnnualPerCapita"
371
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "AnnualPerCapita"
368
372
 
369
373
 
370
374
  class ScopeSetMass(ScopeSet):
371
375
  """ScopeSet measuring mass in kg."""
372
376
 
373
- allowed_units = "kg"
377
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kg"
374
378
 
375
379
 
376
380
  class ScopeSetVolume(ScopeSet):
377
381
  """ScopeSet measuring mass in kg."""
378
382
 
379
- allowed_units = "m3"
383
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "m3"
380
384
 
381
385
 
382
386
  class ScopeSetMassOrVolume(ScopeSet):
383
387
  """ScopeSet measuring mass in kg OR volume in m3, example: radioactive waste."""
384
388
 
385
- allowed_units = ("kg", "m3")
389
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = ("kg", "m3")
386
390
 
387
391
 
388
392
  class ScopeSetEnergy(ScopeSet):
389
393
  """ScopeSet measuring mass in kg."""
390
394
 
391
- allowed_units = "MJ"
395
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "MJ"
392
396
 
393
397
 
394
398
  class ImpactSet(ScopesetByNameBase):
@@ -484,6 +488,8 @@ class ImpactSet(ScopesetByNameBase):
484
488
  description="Land use related impacts / Soil quality, in potential soil quality parameters.",
485
489
  )
486
490
 
491
+ model_config = pydantic.ConfigDict(from_attributes=True)
492
+
487
493
 
488
494
  class LCIAMethod(StrEnum):
489
495
  """A list of available LCA methods."""
@@ -13,11 +13,11 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- from typing import Annotated, List, Optional
16
+ from typing import Any, Optional
17
17
 
18
18
  from openlocationcode import openlocationcode
19
19
  import pydantic
20
- from pydantic import ConfigDict, Field, StringConstraints
20
+ from pydantic import ConfigDict
21
21
 
22
22
  from openepd.model.base import BaseOpenEpdSchema
23
23
  from openepd.model.common import Location, WithAltIdsMixin, WithAttachmentsMixin
@@ -47,36 +47,46 @@ class OrgRef(BaseOpenEpdSchema):
47
47
  class Org(WithAttachmentsMixin, WithAltIdsMixin, OrgRef):
48
48
  """Represent an organization."""
49
49
 
50
- alt_names: (
51
- Annotated[
52
- list[str],
53
- Annotated[
54
- List[Annotated[str, StringConstraints(max_length=200)]],
55
- Field(max_length=255),
56
- ],
57
- ]
58
- | None
59
- ) = pydantic.Field(
50
+ alt_names: list[str] | None = pydantic.Field(
60
51
  description="List of other names for organization",
61
52
  examples=[["C-Change Labs", "C-Change Labs inc."]],
62
53
  default=None,
54
+ max_length=255,
63
55
  )
64
56
  # TODO: NEW field, not in the spec
65
57
 
58
+ @pydantic.field_validator("alt_names", mode="before")
59
+ def _validate_alt_names(cls, value: Any) -> list[str] | None:
60
+ if value is None:
61
+ return value
62
+
63
+ if not isinstance(value, list):
64
+ raise TypeError(f"Expected type list or None, got {type(value)}")
65
+
66
+ if any((len(item) > 200) for item in value):
67
+ raise ValueError("One or more alt_names are longer than 200 characters")
68
+
69
+ return value
70
+
66
71
  owner: Optional["OrgRef"] = pydantic.Field(description="Organization that controls this organization", default=None)
67
- subsidiaries: (
68
- Annotated[
69
- list["OrgRef"],
70
- Annotated[
71
- List[Annotated[str, StringConstraints(max_length=200)]],
72
- Field(max_length=255),
73
- ],
74
- ]
75
- | None
76
- ) = pydantic.Field(
77
- description="Organizations controlled by this organization",
78
- default=None,
72
+ subsidiaries: list["OrgRef"] | None = pydantic.Field(
73
+ description="Organizations controlled by this organization", default=None, max_length=255
79
74
  )
75
+
76
+ @pydantic.field_validator("subsidiaries", mode="before")
77
+ def _validate_subsidiaries(cls, value: Any) -> list[str] | None:
78
+ if value is None:
79
+ return value
80
+
81
+ if not isinstance(value, list):
82
+ raise TypeError(f"Expected type list or None, got {type(value)}")
83
+
84
+ for item in value:
85
+ if len(item.name) > 200:
86
+ raise ValueError("One or more subsidiaries name are longer than 200 characters")
87
+
88
+ return value
89
+
80
90
  hq_location: Location | None = pydantic.Field(
81
91
  default=None,
82
92
  description="Location of a place of business, preferably the corporate headquarters.",
@@ -111,8 +121,10 @@ class Plant(PlantRef, WithAttachmentsMixin, WithAltIdsMixin):
111
121
  pluscode: str | None = pydantic.Field(
112
122
  default=None,
113
123
  description="(deprecated) Plus code (aka Open Location Code) of plant's location",
114
- deprecated="Pluscode field is deprecated. If users need a pluscode they can obtain it from "
115
- "`id` like this: `id.spit('.', maxsplit=1)[0]`",
124
+ json_schema_extra={
125
+ "deprecated": "Pluscode field is deprecated. If users need a pluscode they can obtain it from "
126
+ "`id` like this: `id.spit('.', maxsplit=1)[0]`",
127
+ },
116
128
  )
117
129
  latitude: float | None = pydantic.Field(
118
130
  default=None,
@@ -20,10 +20,10 @@ from typing import Any
20
20
  import pydantic
21
21
  from pydantic import ConfigDict
22
22
 
23
- from openepd.model.base import BaseOpenEpdSchema, Version
23
+ from openepd.model.base import BaseOpenEpdSchema
24
24
  from openepd.model.validation.common import validate_version_compatibility, validate_version_format
25
25
  from openepd.model.validation.quantity import ExternalValidationConfig, QuantityValidator
26
- from openepd.model.versioning import WithExtVersionMixin
26
+ from openepd.model.versioning import Version, WithExtVersionMixin
27
27
 
28
28
 
29
29
  class BaseOpenEpdSpec(BaseOpenEpdSchema):
@@ -35,6 +35,7 @@ class BaseOpenEpdSpec(BaseOpenEpdSchema):
35
35
  class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
36
36
  """Base class for new specs (hierarchical, versioned)."""
37
37
 
38
+ # TODO: Refactor work with class-based and instance-based _EXT_VERSION
38
39
  def __init__(self, **data: Any) -> None:
39
40
  # ensure that all the concrete spec objects fail on creations if they dont have _EXT_VERSION declared to
40
41
  # something meaningful
@@ -35,6 +35,8 @@ __all__ = (
35
35
  "SteelRangeV1",
36
36
  )
37
37
 
38
+ from typing import ClassVar
39
+
38
40
  import pydantic
39
41
 
40
42
  from openepd.model.common import RangeFloat, RangeRatioFloat
@@ -313,7 +315,7 @@ class SteelRangeV1(BaseOpenEpdHierarchicalSpec):
313
315
  Range version.
314
316
  """
315
317
 
316
- _EXT_VERSION = "1.1"
318
+ _EXT_VERSION: ClassVar[str] = "1.1"
317
319
 
318
320
  yield_tensile_str: AmountRangePressureMpa | None = pydantic.Field(
319
321
  default=None,