openepd 4.13.1__tar.gz → 5.1.0__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 (143) hide show
  1. {openepd-4.13.1 → openepd-5.1.0}/PKG-INFO +6 -1
  2. {openepd-4.13.1 → openepd-5.1.0}/README.md +5 -0
  3. {openepd-4.13.1 → openepd-5.1.0}/pyproject.toml +3 -2
  4. openepd-5.1.0/src/openepd/__version__.py +16 -0
  5. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/common.py +40 -1
  6. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/declaration.py +7 -2
  7. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/geography.py +1 -1
  8. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/lcia.py +191 -19
  9. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/pcr.py +2 -2
  10. openepd-5.1.0/src/openepd/model/specs/README.md +45 -0
  11. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/asphalt.py +2 -2
  12. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/base.py +15 -5
  13. openepd-5.1.0/src/openepd/model/specs/generated/__init__.py +95 -0
  14. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/cladding.py +4 -4
  15. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/concrete.py +8 -7
  16. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/electrical.py +2 -2
  17. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/finishes.py +10 -6
  18. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/masonry.py +6 -2
  19. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/network_infrastructure.py +7 -2
  20. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/openings.py +10 -6
  21. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/sheathing.py +8 -4
  22. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/steel.py +10 -5
  23. openepd-5.1.0/src/openepd/model/specs/range/__init__.py +101 -0
  24. openepd-5.1.0/src/openepd/model/specs/range/accessories.py +97 -0
  25. openepd-5.1.0/src/openepd/model/specs/range/aggregates.py +57 -0
  26. openepd-5.1.0/src/openepd/model/specs/range/aluminium.py +92 -0
  27. openepd-5.1.0/src/openepd/model/specs/range/asphalt.py +61 -0
  28. openepd-5.1.0/src/openepd/model/specs/range/bulk_materials.py +31 -0
  29. openepd-5.1.0/src/openepd/model/specs/range/cast_decks_and_underlayment.py +34 -0
  30. openepd-5.1.0/src/openepd/model/specs/range/cladding.py +275 -0
  31. openepd-5.1.0/src/openepd/model/specs/range/cmu.py +44 -0
  32. openepd-5.1.0/src/openepd/model/specs/range/concrete.py +179 -0
  33. openepd-5.1.0/src/openepd/model/specs/range/conveying_equipment.py +86 -0
  34. openepd-5.1.0/src/openepd/model/specs/range/electrical.py +422 -0
  35. openepd-5.1.0/src/openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +96 -0
  36. openepd-5.1.0/src/openepd/model/specs/range/electricity.py +31 -0
  37. openepd-5.1.0/src/openepd/model/specs/range/finishes.py +585 -0
  38. openepd-5.1.0/src/openepd/model/specs/range/fire_and_smoke_protection.py +108 -0
  39. openepd-5.1.0/src/openepd/model/specs/range/furnishings.py +137 -0
  40. openepd-5.1.0/src/openepd/model/specs/range/grouting.py +34 -0
  41. openepd-5.1.0/src/openepd/model/specs/range/manufacturing_inputs.py +190 -0
  42. openepd-5.1.0/src/openepd/model/specs/range/masonry.py +87 -0
  43. openepd-5.1.0/src/openepd/model/specs/range/material_handling.py +50 -0
  44. openepd-5.1.0/src/openepd/model/specs/range/mechanical.py +307 -0
  45. openepd-5.1.0/src/openepd/model/specs/range/mechanical_insulation.py +42 -0
  46. openepd-5.1.0/src/openepd/model/specs/range/network_infrastructure.py +208 -0
  47. openepd-5.1.0/src/openepd/model/specs/range/openings.py +512 -0
  48. openepd-5.1.0/src/openepd/model/specs/range/other_electrical_equipment.py +31 -0
  49. openepd-5.1.0/src/openepd/model/specs/range/other_materials.py +194 -0
  50. openepd-5.1.0/src/openepd/model/specs/range/plumbing.py +200 -0
  51. openepd-5.1.0/src/openepd/model/specs/range/precast_concrete.py +115 -0
  52. openepd-5.1.0/src/openepd/model/specs/range/sheathing.py +86 -0
  53. openepd-5.1.0/src/openepd/model/specs/range/steel.py +332 -0
  54. openepd-5.1.0/src/openepd/model/specs/range/thermal_moisture_protection.py +336 -0
  55. openepd-5.1.0/src/openepd/model/specs/range/utility_piping.py +75 -0
  56. openepd-5.1.0/src/openepd/model/specs/range/wood.py +228 -0
  57. openepd-5.1.0/src/openepd/model/specs/range/wood_joists.py +44 -0
  58. openepd-5.1.0/src/openepd/model/validation/numbers.py +28 -0
  59. openepd-5.1.0/src/openepd/model/validation/quantity.py +726 -0
  60. openepd-4.13.1/src/openepd/__version__.py +0 -16
  61. openepd-4.13.1/src/openepd/model/specs/README.md +0 -19
  62. openepd-4.13.1/src/openepd/model/validation/__init__.py +0 -15
  63. openepd-4.13.1/src/openepd/model/validation/numbers.py +0 -22
  64. openepd-4.13.1/src/openepd/model/validation/quantity.py +0 -315
  65. {openepd-4.13.1 → openepd-5.1.0}/LICENSE +0 -0
  66. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/__init__.py +0 -0
  67. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/__init__.py +0 -0
  68. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/average_dataset/__init__.py +0 -0
  69. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/average_dataset/generic_estimate_sync_api.py +0 -0
  70. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/average_dataset/industry_epd_sync_api.py +0 -0
  71. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/base_sync_client.py +0 -0
  72. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/category/__init__.py +0 -0
  73. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/category/dto.py +0 -0
  74. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/category/sync_api.py +0 -0
  75. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/common.py +0 -0
  76. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/__init__.py +0 -0
  77. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/base.py +0 -0
  78. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/common.py +0 -0
  79. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/meta.py +0 -0
  80. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/mf.py +0 -0
  81. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/params.py +0 -0
  82. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/epd/__init__.py +0 -0
  83. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/epd/dto.py +0 -0
  84. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/epd/sync_api.py +0 -0
  85. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/errors.py +0 -0
  86. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/pcr/__init__.py +0 -0
  87. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/pcr/sync_api.py +0 -0
  88. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/sync_client.py +0 -0
  89. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/test/__init__.py +0 -0
  90. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/utils.py +0 -0
  91. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/__init__.py +0 -0
  92. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/base.py +0 -0
  93. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/model.py +0 -0
  94. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/reader.py +0 -0
  95. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/writer.py +0 -0
  96. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/compat/__init__.py +0 -0
  97. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/compat/compat_functional_validators.py +0 -0
  98. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/compat/pydantic.py +0 -0
  99. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/__init__.py +0 -0
  100. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/base.py +0 -0
  101. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/category.py +0 -0
  102. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/epd.py +0 -0
  103. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/factory.py +0 -0
  104. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/generic_estimate.py +0 -0
  105. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/industry_epd.py +0 -0
  106. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/org.py +0 -0
  107. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/__init__.py +0 -0
  108. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/concrete.py +0 -0
  109. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/accessories.py +0 -0
  110. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/aggregates.py +0 -0
  111. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/aluminium.py +0 -0
  112. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/asphalt.py +0 -0
  113. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/bulk_materials.py +0 -0
  114. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/cast_decks_and_underlayment.py +0 -0
  115. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/cmu.py +0 -0
  116. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/common.py +0 -0
  117. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/conveying_equipment.py +0 -0
  118. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/electrical_transmission_and_distribution_equipment.py +0 -0
  119. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/electricity.py +0 -0
  120. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/enums.py +0 -0
  121. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/fire_and_smoke_protection.py +0 -0
  122. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/furnishings.py +0 -0
  123. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/grouting.py +0 -0
  124. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/manufacturing_inputs.py +0 -0
  125. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/material_handling.py +0 -0
  126. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/mechanical.py +0 -0
  127. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/mechanical_insulation.py +0 -0
  128. {openepd-4.13.1/src/openepd/model/specs/generated → openepd-5.1.0/src/openepd/model/specs/generated/mixins}/__init__.py +0 -0
  129. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/mixins/conduit_mixin.py +0 -0
  130. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/other_electrical_equipment.py +0 -0
  131. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/other_materials.py +0 -0
  132. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/plumbing.py +0 -0
  133. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/precast_concrete.py +0 -0
  134. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/thermal_moisture_protection.py +0 -0
  135. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/utility_piping.py +0 -0
  136. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/wood.py +0 -0
  137. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/wood_joists.py +0 -0
  138. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/standard.py +0 -0
  139. {openepd-4.13.1/src/openepd/model/specs/generated/mixins → openepd-5.1.0/src/openepd/model/validation}/__init__.py +0 -0
  140. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/validation/common.py +0 -0
  141. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/versioning.py +0 -0
  142. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/patch_pydantic.py +0 -0
  143. {openepd-4.13.1 → openepd-5.1.0}/src/openepd/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 4.13.1
3
+ Version: 5.1.0
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
@@ -170,6 +170,11 @@ codes, UN m49 codification, and special regions. To update the enums, first upda
170
170
  Windows is not supported for development. You can use WSL2 with Ubuntu 20.04 or higher.
171
171
  Instructions are the same as for regular GNU/Linux installation.
172
172
 
173
+ ### Commit messages
174
+
175
+ Commit messages should follow [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/#specification)
176
+ specification as we use automatic version with [commitizen](https://commitizen-tools.github.io/commitizen/).
177
+
173
178
  # Credits
174
179
 
175
180
  This library has been written and maintained by [C-Change Labs](https://c-change-labs.com/).
@@ -143,6 +143,11 @@ codes, UN m49 codification, and special regions. To update the enums, first upda
143
143
  Windows is not supported for development. You can use WSL2 with Ubuntu 20.04 or higher.
144
144
  Instructions are the same as for regular GNU/Linux installation.
145
145
 
146
+ ### Commit messages
147
+
148
+ Commit messages should follow [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/#specification)
149
+ specification as we use automatic version with [commitizen](https://commitizen-tools.github.io/commitizen/).
150
+
146
151
  # Credits
147
152
 
148
153
  This library has been written and maintained by [C-Change Labs](https://c-change-labs.com/).
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openepd"
3
- version = "4.13.1"
3
+ version = "5.1.0"
4
4
  license = "Apache-2.0"
5
5
  description = "Python library to work with OpenEPD format"
6
6
  authors = ["C-Change Labs <support@c-change-labs.com>"]
@@ -38,6 +38,7 @@ pytest-subtests = "~=0.4"
38
38
  pytest-cov = "~=4.0"
39
39
  teamcity-messages = ">=1.31"
40
40
  wheel = "~=0.40.0"
41
+ click = "~=8.1.7"
41
42
 
42
43
  # Dev tools
43
44
  black = "~=24.3"
@@ -58,7 +59,7 @@ types-requests = ">=2.0"
58
59
  # Code generation
59
60
  # For list of countries
60
61
  pycountry = ">=24.6.1"
61
- jinja2 = ">=2.10.3"
62
+ jinja2 = ">=3.1.4"
62
63
 
63
64
 
64
65
  [tool.poetry.extras]
@@ -0,0 +1,16 @@
1
+ #
2
+ # Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ VERSION = "5.1.0"
@@ -42,7 +42,7 @@ class Measurement(BaseOpenEpdSchema):
42
42
  """A scientific value with units and uncertainty."""
43
43
 
44
44
  mean: float = pyd.Field(description="Mean (expected) value of the measurement")
45
- unit: str = pyd.Field(description="Measurement unit")
45
+ unit: str | None = pyd.Field(description="Measurement unit")
46
46
  rsd: pyd.PositiveFloat | None = pyd.Field(
47
47
  description="Relative standard deviation, i.e. standard_deviation/mean", default=None
48
48
  )
@@ -150,3 +150,42 @@ class OpenEPDUnit(StrEnum):
150
150
  degree_c = "°C"
151
151
  kg_co2 = "kgCO2e"
152
152
  hour = "hour"
153
+
154
+
155
+ class RangeBase(BaseOpenEpdSchema):
156
+ """Base class for range types having min and max and order between them."""
157
+
158
+ @pyd.root_validator
159
+ def _validate_range_bounds(cls, values: dict[str, Any]) -> dict[str, Any]:
160
+ min_boundary = values.get("min")
161
+ max_boundary = values.get("max")
162
+ if min_boundary is not None and max_boundary is not None and min_boundary > max_boundary:
163
+ raise ValueError("Max should be greater than min")
164
+ return values
165
+
166
+
167
+ class RangeFloat(RangeBase):
168
+ """Structure representing a range of floats."""
169
+
170
+ min: float | None = pyd.Field(default=None, example=3.1)
171
+ max: float | None = pyd.Field(default=None, example=5.8)
172
+
173
+
174
+ class RangeInt(RangeBase):
175
+ """Structure representing a range of ints1."""
176
+
177
+ min: int | None = pyd.Field(default=None, example=2)
178
+ max: int | None = pyd.Field(default=None, example=3)
179
+
180
+
181
+ class RangeRatioFloat(RangeFloat):
182
+ """Range of ratios (0-1 to 0-10)."""
183
+
184
+ min: float | None = pyd.Field(default=None, example=0.2, ge=0, le=1)
185
+ max: float | None = pyd.Field(default=None, example=0.65, ge=0, le=1)
186
+
187
+
188
+ class RangeAmount(RangeFloat):
189
+ """Structure representing a range of quantities."""
190
+
191
+ unit: str | None = pyd.Field(default=None, description="Unit of the range.")
@@ -22,6 +22,7 @@ from openepd.model.common import Amount
22
22
  from openepd.model.geography import Geography
23
23
  from openepd.model.org import Org
24
24
  from openepd.model.pcr import Pcr
25
+ from openepd.model.specs.range import SpecsRange
25
26
  from openepd.model.standard import Standard
26
27
  from openepd.model.validation.common import ReferenceStr
27
28
  from openepd.model.validation.quantity import AmountGWP, AmountMass
@@ -141,14 +142,14 @@ class BaseDeclaration(RootDocument, abc.ABC):
141
142
  description="Mass of elemental carbon, per declared unit, contained in the product itself at the manufacturing "
142
143
  "facility gate. Used (among other things) to check a carbon balance or calculate incineration "
143
144
  "emissions. The source of carbon (e.g. biogenic) is not relevant in this field.",
144
- example=Amount(qty=8.76, unit="kgCO2e"),
145
+ example=Amount(qty=8.76, unit="kgCO2e").to_serializable(exclude_unset=True),
145
146
  )
146
147
  kg_C_biogenic_per_declared_unit: AmountGWP | None = pyd.Field(
147
148
  default=None,
148
149
  description="Mass of elemental carbon from biogenic sources, per declared unit, contained in the product "
149
150
  "itself at the manufacturing facility gate. It may be presumed that any biogenic carbon content "
150
151
  "has been accounted for as -44/12 kgCO2e per kg C in stages A1-A3, per EN15804 and ISO 21930.",
151
- example=Amount(qty=8.76, unit="kgCO2e"),
152
+ example=Amount(qty=8.76, unit="kgCO2e").to_serializable(exclude_unset=True),
152
153
  )
153
154
  product_service_life_years: float | None = pyd.Field(
154
155
  gt=0.0009,
@@ -173,6 +174,10 @@ class AverageDatasetMixin(pyd.BaseModel, title="Average Dataset"):
173
174
  "implies global applicability.",
174
175
  )
175
176
 
177
+ specs: SpecsRange | None = pyd.Field(
178
+ default=None, description="Average dataset material performance specifications."
179
+ )
180
+
176
181
 
177
182
  class WithProgramOperatorMixin(pyd.BaseModel):
178
183
  """Object which has a connection to ProgramOperator."""
@@ -279,7 +279,7 @@ class Geography(StrEnum):
279
279
  * ZM: Zambia
280
280
  * ZW: Zimbabwe
281
281
 
282
- USA and Canada subdivisions, see https://en.wikipedia.org/wiki/ISO_3166-1:
282
+ USA states and Canada provinces, see https://en.wikipedia.org/wiki/ISO_3166-1:
283
283
 
284
284
  * CA-AB: Alberta, Canada
285
285
  * CA-BC: British Columbia, Canada
@@ -14,10 +14,12 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  from enum import StrEnum
17
+ from typing import Any, ClassVar
17
18
 
18
19
  from openepd.compat.pydantic import pyd
19
20
  from openepd.model.base import BaseOpenEpdSchema
20
21
  from openepd.model.common import Measurement
22
+ from openepd.model.validation.quantity import ExternalValidationConfig
21
23
 
22
24
 
23
25
  class EolScenario(BaseOpenEpdSchema):
@@ -70,6 +72,8 @@ class ScopeSet(BaseOpenEpdSchema):
70
72
  The 'unit' field must be consistent across all scopes in a single scopeset.
71
73
  """
72
74
 
75
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = None
76
+
73
77
  A1A2A3: Measurement | None = pyd.Field(
74
78
  description="Sum of A1..A3",
75
79
  default=None,
@@ -194,17 +198,56 @@ class ScopeSet(BaseOpenEpdSchema):
194
198
  description="Potential net benefits from reuse, recycling, and/or energy recovery beyond the system boundary.",
195
199
  )
196
200
 
201
+ @pyd.root_validator
202
+ def _unit_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
203
+ all_units = set()
204
+
205
+ for k, v in values.items():
206
+ if isinstance(v, Measurement):
207
+ all_units.add(v.unit)
208
+
209
+ # units should be the same across all measurements (textually)
210
+ if len(all_units) > 1:
211
+ raise ValueError("All scopes and measurements should be expressed in the same unit.")
212
+
213
+ # can correctly validate unit
214
+ if cls.allowed_units is not None and len(all_units) == 1 and ExternalValidationConfig.QUANTITY_VALIDATOR:
215
+ unit = next(iter(all_units))
216
+ allowed_units = cls.allowed_units if isinstance(cls.allowed_units, tuple) else (cls.allowed_units,)
197
217
 
198
- class ScopesetByNameBase(BaseOpenEpdSchema):
218
+ matched_unit = False
219
+ for allowed_unit in allowed_units:
220
+ try:
221
+ ExternalValidationConfig.QUANTITY_VALIDATOR.validate_same_dimensionality(unit, allowed_unit)
222
+ matched_unit = True
223
+ except ValueError:
224
+ ...
225
+ if not matched_unit:
226
+ raise ValueError(
227
+ f"'{', '.join(allowed_units)}' is only allowed unit for this scopeset. Provided '{unit}'"
228
+ )
229
+
230
+ return values
231
+
232
+
233
+ class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
199
234
  """Base class for the data structures presented as typed name:scopeset mapping ."""
200
235
 
201
236
  def get_scopeset_names(self) -> list[str]:
202
237
  """
203
238
  Get the names of scopesets which have been set by model (not defaults).
204
239
 
205
- :return: set of names, for example ['gwp', 'odp]
240
+ :return: set of names, for example ['gwp', 'odp']
206
241
  """
207
- return [self.__fields__[f].alias or f for f in self.__fields_set__ if f not in ("ext",)]
242
+ result = []
243
+ for f in self.__fields_set__:
244
+ if f in ("ext",):
245
+ continue
246
+ field = self.__fields__.get(f)
247
+ # field can be explicitly specified, or can be an unknown impact covered by extra='allow'
248
+ result.append(field.alias if field and field.alias else f)
249
+
250
+ return result
208
251
 
209
252
  def get_scopeset_by_name(self, name: str) -> ScopeSet | None:
210
253
  """
@@ -213,39 +256,133 @@ class ScopesetByNameBase(BaseOpenEpdSchema):
213
256
  :param name: The name of the scopeset.
214
257
  :return: A scopeset if found, None otherwise
215
258
  """
259
+ # check known impacts first
216
260
  for f_name, f in self.__fields__.items():
217
261
  if f.alias == name:
218
262
  return getattr(self, f_name)
219
263
  if f_name == name:
220
264
  return getattr(self, f_name)
265
+ # probably unknown impact, coming from 'extra' fields
266
+ return getattr(self, name, None)
267
+
268
+ @pyd.root_validator(skip_on_failure=True)
269
+ def _extra_scopeset_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
270
+ for f in values:
271
+ # only interested in validating the extra fields
272
+ if f in cls.__fields__:
273
+ continue
274
+
275
+ # extra impact of an unknown type - engage validation of ScopeSet
276
+ extra_scopeset = values.get(f)
277
+ match extra_scopeset:
278
+ case ScopeSet():
279
+ continue
280
+ case dict():
281
+ values[f] = ScopeSet(**extra_scopeset)
282
+ case _:
283
+ raise ValueError(f"{f} must be a ScopeSet schema")
284
+
285
+ return values
286
+
287
+
288
+ class ScopeSetGwp(ScopeSet):
289
+ """ScopeSet measured in kgCO2e."""
290
+
291
+ allowed_units = "kgCO2e"
292
+
293
+
294
+ class ScopeSetOdp(ScopeSet):
295
+ """ScopeSet measured in kgCFC11e."""
296
+
297
+ allowed_units = "kgCFC11e"
298
+
299
+
300
+ class ScopeSetAp(ScopeSet):
301
+ """ScopeSet measured in kgSO2e."""
302
+
303
+ allowed_units = ("kgSO2e", "molHe")
304
+
305
+
306
+ class ScopeSetEpNe(ScopeSet):
307
+ """ScopeSet measured in kgNe."""
308
+
309
+ allowed_units = "kgNe"
310
+
311
+
312
+ class ScopeSetPocp(ScopeSet):
313
+ """ScopeSet measured in kgO3e."""
314
+
315
+ allowed_units = ("kgO3e", "kgNMVOCe")
316
+
317
+
318
+ class ScopeSetEpFresh(ScopeSet):
319
+ """ScopeSet measured in kgPO4e."""
320
+
321
+ allowed_units = "kgPO4e"
322
+
323
+
324
+ class ScopeSetEpTerr(ScopeSet):
325
+ """ScopeSet measured in molNe."""
221
326
 
222
- return None
327
+ allowed_units = "molNe"
328
+
329
+
330
+ class ScopeSetIrp(ScopeSet):
331
+ """ScopeSet measured in kilo Becquerel equivalent of u235."""
332
+
333
+ allowed_units = "kBqU235e"
334
+
335
+
336
+ class ScopeSetCTUh(ScopeSet):
337
+ """ScopeSet measured in CTUh."""
338
+
339
+ allowed_units = "CTUh"
340
+
341
+
342
+ class ScopeSetM3Aware(ScopeSet):
343
+ """ScopeSet measured in m3AWARE Water consumption by AWARE method."""
344
+
345
+ allowed_units = "m3AWARE"
346
+
347
+
348
+ class ScopeSetCTUe(ScopeSet):
349
+ """ScopeSet measured in CTUe."""
350
+
351
+ allowed_units = "CTUe"
352
+
353
+
354
+ class ScopeSetDiseaseIncidence(ScopeSet):
355
+ """ScopeSet measuring disease incidence measured in AnnualPerCapita (cases)."""
356
+
357
+ allowed_units = "AnnualPerCapita"
223
358
 
224
359
 
225
360
  class ImpactSet(ScopesetByNameBase):
226
361
  """A set of impacts, such as GWP, ODP, AP, EP, POCP, EP-marine, EP-terrestrial, EP-freshwater, etc."""
227
362
 
228
- gwp: ScopeSet | None = pyd.Field(
363
+ gwp: ScopeSetGwp | None = pyd.Field(
229
364
  default=None,
230
365
  description="GWP100, calculated per IPCC guidelines. If any CO2 removals are "
231
366
  "part of this figure, the gwp-fossil, gwp-bioganic, gwp-luluc, an "
232
367
  "gwp-nonCO2 fields are required, as is "
233
368
  "kg_C_biogenic_per_declared_unit.",
234
369
  )
235
- odp: ScopeSet | None = pyd.Field(default=None, description="Ozone Depletion Potential")
236
- ap: ScopeSet | None = pyd.Field(default=None, description="Acidification Potential")
237
- ep: ScopeSet | None = pyd.Field(
370
+ odp: ScopeSetOdp | None = pyd.Field(default=None, description="Ozone Depletion Potential")
371
+ ap: ScopeSetAp | None = pyd.Field(default=None, description="Acidification Potential")
372
+ ep: ScopeSetEpNe | None = pyd.Field(
238
373
  default=None, description="Eutrophication Potential in Marine Ecosystems. Has the same meaning as ep-marine."
239
374
  )
240
- pocp: ScopeSet | None = pyd.Field(default=None, description="Photochemical Smog (Ozone) creation potential")
241
- ep_marine: ScopeSet | None = pyd.Field(alias="ep-marine", default=None, description="Has the same meaning as 'ep'")
242
- ep_fresh: ScopeSet | None = pyd.Field(
375
+ pocp: ScopeSetPocp | None = pyd.Field(default=None, description="Photochemical Smog (Ozone) creation potential")
376
+ ep_marine: ScopeSetEpNe | None = pyd.Field(
377
+ alias="ep-marine", default=None, description="Has the same meaning as 'ep'"
378
+ )
379
+ ep_fresh: ScopeSetEpFresh | None = pyd.Field(
243
380
  alias="ep-fresh", default=None, description="Eutrophication Potential in Freshwater Ecosystems"
244
381
  )
245
- ep_terr: ScopeSet | None = pyd.Field(
382
+ ep_terr: ScopeSetEpTerr | None = pyd.Field(
246
383
  alias="ep-terr", default=None, description="Eutrophication Potential in Terrestrial Ecosystems"
247
384
  )
248
- gwp_biogenic: ScopeSet | None = pyd.Field(
385
+ gwp_biogenic: ScopeSetGwp | None = pyd.Field(
249
386
  alias="gwp-biogenic",
250
387
  default=None,
251
388
  description="Net GWP from removals of atmospheric CO2 into biomass and emissions of CO2 from biomass sources. "
@@ -254,7 +391,7 @@ class ImpactSet(ScopesetByNameBase):
254
391
  "space (similar biome). They must not have been sold, committed, or credited to any other "
255
392
  "product. Harvesting from native forests is handled under GWP_luluc for EN15804.",
256
393
  )
257
- gwp_luluc: ScopeSet | None = pyd.Field(
394
+ gwp_luluc: ScopeSetGwp | None = pyd.Field(
258
395
  alias="gwp-luluc",
259
396
  default=None,
260
397
  description="Climate change effects related to land use and land use change, for example biogenic carbon "
@@ -262,17 +399,49 @@ class ImpactSet(ScopesetByNameBase):
262
399
  "emissions). All related emissions for native forests are included under this category. "
263
400
  "Uptake for native forests is set to 0 kgCO2 for EN15804.",
264
401
  )
265
- gwp_nonCO2: ScopeSet | None = pyd.Field(
402
+ gwp_nonCO2: ScopeSetGwp | None = pyd.Field(
266
403
  alias="gwp-nonCO2",
267
404
  default=None,
268
405
  description="GWP from non-CO2, non-fossil sources, such as livestock-sourced CH4 and agricultural N2O.",
269
406
  )
270
- gwp_fossil: ScopeSet | None = pyd.Field(
407
+ gwp_fossil: ScopeSetGwp | None = pyd.Field(
271
408
  alias="gwp-fossil",
272
409
  default=None,
273
410
  description="Climate change effects due to greenhouse gas emissions originating from the oxidation or "
274
411
  "reduction of fossil fuels or materials containing fossil carbon. [Source: EN15804]",
275
412
  )
413
+ WDP: ScopeSetM3Aware | None = pyd.Field(
414
+ default=None,
415
+ description="Deprivation-weighted water consumption, calculated by the AWARE method "
416
+ "(https://wulca-waterlca.org/aware/what-is-aware)",
417
+ )
418
+ PM: ScopeSetDiseaseIncidence | None = pyd.Field(
419
+ default=None,
420
+ description="Potential incidence of disease due to particulate matter emissions.",
421
+ )
422
+ IRP: ScopeSetIrp | None = pyd.Field(
423
+ default=None,
424
+ description="Potential ionizing radiation effect on human health, relative to U235.",
425
+ )
426
+ ETP_fw: ScopeSetCTUe | None = pyd.Field(
427
+ alias="ETP-fw",
428
+ default=None,
429
+ description="Ecotoxicity in freshwater, in potential Comparative Toxic Unit for ecosystems.",
430
+ )
431
+ HTP_c: ScopeSetCTUh | None = pyd.Field(
432
+ alias="HTP-c",
433
+ default=None,
434
+ description="Human toxicity, cancer effects in potential Comparative Toxic Units for humans.",
435
+ )
436
+ HTP_nc: ScopeSetCTUh | None = pyd.Field(
437
+ alias="HTP-nc",
438
+ default=None,
439
+ description="Human toxicity, noncancer effects in potential Comparative Toxic Units for humans.",
440
+ )
441
+ SQP: ScopeSet | None = pyd.Field(
442
+ default=None,
443
+ description="Land use related impacts / Soil quality, in potential soil quality parameters.",
444
+ )
276
445
 
277
446
 
278
447
  class LCIAMethod(StrEnum):
@@ -512,12 +681,15 @@ class WithLciaMixin(BaseOpenEpdSchema):
512
681
  """Mixin for LCIA data."""
513
682
 
514
683
  impacts: Impacts | None = pyd.Field(
515
- description="List of environmental impacts, compiled per one of the standard Impact Assessment methods"
684
+ description="List of environmental impacts, compiled per one of the standard Impact Assessment methods",
685
+ example={"TRACI 2.1": {"gwp": {"A1A2A3": {"mean": 22.4, "unit": "kgCO2e"}}}},
516
686
  )
517
687
  resource_uses: ResourceUseSet | None = pyd.Field(
518
- description="Set of Resource Use Indicators, over various LCA scopes"
688
+ description="Set of Resource Use Indicators, over various LCA scopes",
689
+ example={"RPRe": {"A1A2A3": {"mean": 12, "unit": "MJ", "rsd": 0.12}}},
519
690
  )
520
691
  output_flows: OutputFlowSet | None = pyd.Field(
521
692
  description="Set of Waste and Output Flow indicators which describe the waste categories "
522
- "and other material output flows derived from the LCI."
693
+ "and other material output flows derived from the LCI.",
694
+ example={"hwd": {"A1A2A3": {"mean": 2300, "unit": "kg", "rsd": 0.22}}},
523
695
  )
@@ -91,12 +91,12 @@ class Pcr(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
91
91
  default=None,
92
92
  )
93
93
  date_of_issue: datetime.datetime | None = pyd.Field(
94
- example=datetime.date(day=11, month=2, year=2022),
94
+ example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
95
95
  default=None,
96
96
  description="First day on which the document is valid",
97
97
  )
98
98
  valid_until: datetime.datetime | None = pyd.Field(
99
- example=datetime.date(day=11, month=2, year=2024),
99
+ example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
100
100
  default=None,
101
101
  description="Last day on which the document is valid",
102
102
  )
@@ -0,0 +1,45 @@
1
+ # Material Extensions
2
+
3
+ This package contains openEPD material extensions. They are used to represent the material properties of the openEPD
4
+ materials, and are a more dynamic, frequently changing part of the standard.
5
+
6
+ ## Versioning
7
+
8
+ Extensions are versioned separately from the openEPD standard or openEPD API.
9
+
10
+ Each material extension is named after the corresponding EC3 product class, and is located in the relevant place
11
+ in the specs tree. For example, `RebarSteel` is nested under `Steel`.
12
+
13
+ Extensions are versioned as Major.Minor, for example "2.4".
14
+
15
+ Rules:
16
+
17
+ 1. Minor versions for the same major version should be backwards compatible.
18
+ 2. Major versions are not compatible between themselves.
19
+ 3. Pydantic models representing versions are named in a pattern SpecNameV1, where 1 is major version and SpecName is
20
+ the name of the material extension.
21
+
22
+ ## Range specs
23
+
24
+ Normal EPDs get singular specs (e.g. `SteelV1`). Single specs can express performance parameters of one concrete
25
+ material/EPD. However, the IndustryEDPs and Generic Estimates often cover a specific segment of the market, and
26
+ include a range of products under the hood, thus demanding for ranges. For example, a single EPD has one `strength_28d`
27
+ of `4000 psi`, but an industry EPD can be applicable to certain concretes in the range of `4000 psi` to `5000 psi`.
28
+
29
+ Range specs are used to express that. Range specs are located in `specs.range` package, and are auto-generated from the
30
+ single specs, please see `make codegen` command and `tools/openepd/codegen/generate_range_spec_models.py`
31
+
32
+ Range specs are created by following general rules:
33
+
34
+ 1. A QuantityStr (such as `QuantityMassKg`) becomes an `AmountRange` of certain type - `AmountRangeMass`
35
+ 2. Float -> RangeFloat, Ratio -> RatioRange, int -> IntRange
36
+ 3. Enums become lists of enums, for example: `cable_trays_material: enums.CableTrayMaterial` in normal spec becomes a
37
+ `cable_trays_material: list[enums.CabeTrayMaterial]` in the range spec.
38
+ 4. Complex objects, strings remain unchanged.
39
+
40
+ This is, however, not always desired. For example, `recarbonation: float` and `recarbonation_z: float` property of CMU
41
+ should not be converted to ranges as these do not make sense.
42
+
43
+ The default rule-base behaviour can be overridden with the
44
+ `CodeGenSpec` class annotation like this: `recarbonation: Annotated[float, CodeGenSpec(override_type=float)]` in single
45
+ spec to make RangeSpec have normal `float` type.
@@ -19,7 +19,7 @@ from openepd.compat.pydantic import pyd
19
19
  from openepd.model.common import OpenEPDUnit
20
20
  from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
21
21
  from openepd.model.validation.numbers import RatioFloat
22
- from openepd.model.validation.quantity import LengthMmStr, TemperatureCStr, validate_unit_factory
22
+ from openepd.model.validation.quantity import LengthMmStr, TemperatureCStr, validate_quantity_unit_factory
23
23
 
24
24
 
25
25
  class AsphaltMixType(StrEnum):
@@ -79,5 +79,5 @@ class AsphaltV1(BaseOpenEpdHierarchicalSpec):
79
79
  asphalt_pmb: bool | None = pyd.Field(default=None, description="Polymer modified bitumen (PMB)")
80
80
 
81
81
  _aggregate_size_max_validator = pyd.validator("asphalt_aggregate_size_max", allow_reuse=True)(
82
- validate_unit_factory(OpenEPDUnit.m)
82
+ validate_quantity_unit_factory(OpenEPDUnit.m)
83
83
  )
@@ -13,12 +13,13 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
+ import dataclasses
16
17
  from typing import Any
17
18
 
18
19
  from openepd.compat.pydantic import pyd
19
20
  from openepd.model.base import BaseOpenEpdSchema, Version
20
21
  from openepd.model.validation.common import validate_version_compatibility, validate_version_format
21
- from openepd.model.validation.quantity import QuantityValidator
22
+ from openepd.model.validation.quantity import ExternalValidationConfig, QuantityValidator
22
23
  from openepd.model.versioning import WithExtVersionMixin
23
24
 
24
25
 
@@ -32,9 +33,6 @@ class BaseOpenEpdSpec(BaseOpenEpdSchema):
32
33
  class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
33
34
  """Base class for new specs (hierarchical, versioned)."""
34
35
 
35
- # external validator for quantities (e.g. length, mass, etc.) which should be setup by the user of the library.
36
- _QUANTITY_VALIDATOR: QuantityValidator | None = None
37
-
38
36
  def __init__(self, **data: Any) -> None:
39
37
  # ensure that all the concrete spec objects fail on creations if they dont have _EXT_VERSION declared to
40
38
  # something meaningful
@@ -53,4 +51,16 @@ class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
53
51
 
54
52
  def setup_external_validators(quantity_validator: QuantityValidator):
55
53
  """Set the implementation unit validator for specs."""
56
- BaseOpenEpdHierarchicalSpec._QUANTITY_VALIDATOR = quantity_validator
54
+ ExternalValidationConfig.QUANTITY_VALIDATOR = quantity_validator
55
+
56
+
57
+ @dataclasses.dataclass(kw_only=True)
58
+ class CodegenSpec:
59
+ """
60
+ Specification for codegen when generating RangeSpecs from normal specs.
61
+
62
+ See openepd.mode.specs.README.md for details.
63
+ """
64
+
65
+ exclude_from_codegen: bool = False
66
+ override_type: type