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.
- {openepd-4.13.1 → openepd-5.1.0}/PKG-INFO +6 -1
- {openepd-4.13.1 → openepd-5.1.0}/README.md +5 -0
- {openepd-4.13.1 → openepd-5.1.0}/pyproject.toml +3 -2
- openepd-5.1.0/src/openepd/__version__.py +16 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/common.py +40 -1
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/declaration.py +7 -2
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/geography.py +1 -1
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/lcia.py +191 -19
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/pcr.py +2 -2
- openepd-5.1.0/src/openepd/model/specs/README.md +45 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/asphalt.py +2 -2
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/base.py +15 -5
- openepd-5.1.0/src/openepd/model/specs/generated/__init__.py +95 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/cladding.py +4 -4
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/concrete.py +8 -7
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/electrical.py +2 -2
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/finishes.py +10 -6
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/masonry.py +6 -2
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/network_infrastructure.py +7 -2
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/openings.py +10 -6
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/sheathing.py +8 -4
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/steel.py +10 -5
- openepd-5.1.0/src/openepd/model/specs/range/__init__.py +101 -0
- openepd-5.1.0/src/openepd/model/specs/range/accessories.py +97 -0
- openepd-5.1.0/src/openepd/model/specs/range/aggregates.py +57 -0
- openepd-5.1.0/src/openepd/model/specs/range/aluminium.py +92 -0
- openepd-5.1.0/src/openepd/model/specs/range/asphalt.py +61 -0
- openepd-5.1.0/src/openepd/model/specs/range/bulk_materials.py +31 -0
- openepd-5.1.0/src/openepd/model/specs/range/cast_decks_and_underlayment.py +34 -0
- openepd-5.1.0/src/openepd/model/specs/range/cladding.py +275 -0
- openepd-5.1.0/src/openepd/model/specs/range/cmu.py +44 -0
- openepd-5.1.0/src/openepd/model/specs/range/concrete.py +179 -0
- openepd-5.1.0/src/openepd/model/specs/range/conveying_equipment.py +86 -0
- openepd-5.1.0/src/openepd/model/specs/range/electrical.py +422 -0
- openepd-5.1.0/src/openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +96 -0
- openepd-5.1.0/src/openepd/model/specs/range/electricity.py +31 -0
- openepd-5.1.0/src/openepd/model/specs/range/finishes.py +585 -0
- openepd-5.1.0/src/openepd/model/specs/range/fire_and_smoke_protection.py +108 -0
- openepd-5.1.0/src/openepd/model/specs/range/furnishings.py +137 -0
- openepd-5.1.0/src/openepd/model/specs/range/grouting.py +34 -0
- openepd-5.1.0/src/openepd/model/specs/range/manufacturing_inputs.py +190 -0
- openepd-5.1.0/src/openepd/model/specs/range/masonry.py +87 -0
- openepd-5.1.0/src/openepd/model/specs/range/material_handling.py +50 -0
- openepd-5.1.0/src/openepd/model/specs/range/mechanical.py +307 -0
- openepd-5.1.0/src/openepd/model/specs/range/mechanical_insulation.py +42 -0
- openepd-5.1.0/src/openepd/model/specs/range/network_infrastructure.py +208 -0
- openepd-5.1.0/src/openepd/model/specs/range/openings.py +512 -0
- openepd-5.1.0/src/openepd/model/specs/range/other_electrical_equipment.py +31 -0
- openepd-5.1.0/src/openepd/model/specs/range/other_materials.py +194 -0
- openepd-5.1.0/src/openepd/model/specs/range/plumbing.py +200 -0
- openepd-5.1.0/src/openepd/model/specs/range/precast_concrete.py +115 -0
- openepd-5.1.0/src/openepd/model/specs/range/sheathing.py +86 -0
- openepd-5.1.0/src/openepd/model/specs/range/steel.py +332 -0
- openepd-5.1.0/src/openepd/model/specs/range/thermal_moisture_protection.py +336 -0
- openepd-5.1.0/src/openepd/model/specs/range/utility_piping.py +75 -0
- openepd-5.1.0/src/openepd/model/specs/range/wood.py +228 -0
- openepd-5.1.0/src/openepd/model/specs/range/wood_joists.py +44 -0
- openepd-5.1.0/src/openepd/model/validation/numbers.py +28 -0
- openepd-5.1.0/src/openepd/model/validation/quantity.py +726 -0
- openepd-4.13.1/src/openepd/__version__.py +0 -16
- openepd-4.13.1/src/openepd/model/specs/README.md +0 -19
- openepd-4.13.1/src/openepd/model/validation/__init__.py +0 -15
- openepd-4.13.1/src/openepd/model/validation/numbers.py +0 -22
- openepd-4.13.1/src/openepd/model/validation/quantity.py +0 -315
- {openepd-4.13.1 → openepd-5.1.0}/LICENSE +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/average_dataset/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/average_dataset/generic_estimate_sync_api.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/average_dataset/industry_epd_sync_api.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/base_sync_client.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/category/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/category/dto.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/category/sync_api.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/common.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/base.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/common.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/meta.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/mf.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/dto/params.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/epd/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/epd/dto.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/epd/sync_api.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/errors.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/pcr/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/pcr/sync_api.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/sync_client.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/test/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/api/utils.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/base.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/model.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/reader.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/bundle/writer.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/compat/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/compat/compat_functional_validators.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/compat/pydantic.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/base.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/category.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/epd.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/factory.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/generic_estimate.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/industry_epd.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/org.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/concrete.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/accessories.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/aggregates.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/aluminium.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/asphalt.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/bulk_materials.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/cast_decks_and_underlayment.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/cmu.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/common.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/conveying_equipment.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/electrical_transmission_and_distribution_equipment.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/electricity.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/enums.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/fire_and_smoke_protection.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/furnishings.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/grouting.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/manufacturing_inputs.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/material_handling.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/mechanical.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/mechanical_insulation.py +0 -0
- {openepd-4.13.1/src/openepd/model/specs/generated → openepd-5.1.0/src/openepd/model/specs/generated/mixins}/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/mixins/conduit_mixin.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/other_electrical_equipment.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/other_materials.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/plumbing.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/precast_concrete.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/thermal_moisture_protection.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/utility_piping.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/wood.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/specs/generated/wood_joists.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/standard.py +0 -0
- {openepd-4.13.1/src/openepd/model/specs/generated/mixins → openepd-5.1.0/src/openepd/model/validation}/__init__.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/validation/common.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/model/versioning.py +0 -0
- {openepd-4.13.1 → openepd-5.1.0}/src/openepd/patch_pydantic.py +0 -0
- {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:
|
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 = "
|
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 = ">=
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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:
|
236
|
-
ap:
|
237
|
-
ep:
|
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:
|
241
|
-
ep_marine:
|
242
|
-
|
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:
|
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:
|
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:
|
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:
|
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:
|
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.
|
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.
|
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,
|
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
|
-
|
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
|
-
|
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
|