openepd 0.10.0__tar.gz → 0.11.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.
- {openepd-0.10.0 → openepd-0.11.1}/PKG-INFO +2 -2
- {openepd-0.10.0 → openepd-0.11.1}/pyproject.toml +4 -4
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/__version__.py +1 -1
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/base.py +12 -1
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/common.py +19 -0
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/epd.py +21 -6
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/lcia.py +258 -71
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/org.py +25 -21
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/pcr.py +9 -3
- {openepd-0.10.0 → openepd-0.11.1}/LICENSE +0 -0
- {openepd-0.10.0 → openepd-0.11.1}/README.md +0 -0
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/__init__.py +0 -0
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/__init__.py +0 -0
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/specs/__init__.py +0 -0
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/specs/concrete.py +0 -0
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/model/standard.py +0 -0
- {openepd-0.10.0 → openepd-0.11.1}/src/openepd/py.typed +0 -0
@@ -1,13 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: openepd
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.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
|
7
7
|
Author: C-Change Labs
|
8
8
|
Author-email: support@c-change-labs.com
|
9
9
|
Maintainer: C-Change Labs
|
10
|
-
Maintainer-email:
|
10
|
+
Maintainer-email: open-source@c-change-labs.com
|
11
11
|
Requires-Python: >=3.11,<4.0
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
13
13
|
Classifier: Intended Audience :: Developers
|
@@ -1,10 +1,10 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "openepd"
|
3
|
-
version = "0.
|
3
|
+
version = "0.11.1"
|
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>"]
|
7
|
-
maintainers = ["C-Change Labs <
|
7
|
+
maintainers = ["C-Change Labs <open-source@c-change-labs.com>"]
|
8
8
|
repository = "https://github.com/cchangelabs/openepd"
|
9
9
|
keywords = []
|
10
10
|
classifiers = [
|
@@ -34,9 +34,9 @@ pytest = "~=7.2"
|
|
34
34
|
pytest-subtests = "~=0.4"
|
35
35
|
pytest-cov = "~=4.0"
|
36
36
|
teamcity-messages = ">=1.31"
|
37
|
+
wheel = "~=0.40.0"
|
37
38
|
|
38
39
|
# Dev tools
|
39
|
-
#virtualenv = "<=20.23.0" # to avoid conflict with flake plugins
|
40
40
|
black = "~=22.3"
|
41
41
|
licenseheaders = "~=0.8"
|
42
42
|
flake8 = "~=4.0"
|
@@ -56,7 +56,7 @@ types-deprecated = ">=1.2.9"
|
|
56
56
|
|
57
57
|
|
58
58
|
[tool.commitizen]
|
59
|
-
version = "0.
|
59
|
+
version = "0.11.1"
|
60
60
|
bump_version = "bump: version $current_version → $new_version"
|
61
61
|
update_changelog_on_bump = true
|
62
62
|
pre_bump_hooks = []
|
@@ -18,7 +18,8 @@
|
|
18
18
|
# Find out more at www.BuildingTransparency.org
|
19
19
|
#
|
20
20
|
import abc
|
21
|
-
|
21
|
+
import json
|
22
|
+
from typing import Any, Optional, Type, TypeVar
|
22
23
|
|
23
24
|
import pydantic
|
24
25
|
from pydantic.generics import GenericModel
|
@@ -38,6 +39,14 @@ class BaseOpenEpdSchema(pydantic.BaseModel):
|
|
38
39
|
allow_population_by_field_name = True
|
39
40
|
use_enum_values = True
|
40
41
|
|
42
|
+
def to_serializable(self, *args, **kwargs) -> dict[str, Any]:
|
43
|
+
"""
|
44
|
+
Return a serializable dict representation of the DTO.
|
45
|
+
|
46
|
+
It expects the same arguments as the pydantic.BaseModel.json() method.
|
47
|
+
"""
|
48
|
+
return json.loads(self.json(*args, **kwargs))
|
49
|
+
|
41
50
|
def has_values(self) -> bool:
|
42
51
|
"""Return True if the model has any values."""
|
43
52
|
return len(self.dict(exclude_unset=True, exclude_none=True)) > 0
|
@@ -72,6 +81,8 @@ class BaseOpenEpdSchema(pydantic.BaseModel):
|
|
72
81
|
:raise ValueError: if the value cannot be converted to the target type.
|
73
82
|
"""
|
74
83
|
value = self.get_ext_field(key, default)
|
84
|
+
if value is None:
|
85
|
+
return None # type: ignore
|
75
86
|
if issubclass(target_type, pydantic.BaseModel) and isinstance(value, dict):
|
76
87
|
return target_type.construct(**value) # type: ignore
|
77
88
|
elif isinstance(value, target_type):
|
@@ -73,6 +73,25 @@ class Ingredient(BaseOpenEpdSchema):
|
|
73
73
|
)
|
74
74
|
|
75
75
|
|
76
|
+
class LatLng(BaseOpenEpdSchema):
|
77
|
+
"""A latitude and longitude."""
|
78
|
+
|
79
|
+
lat: float = pyd.Field(description="Latitude", example=47.6062)
|
80
|
+
lng: float = pyd.Field(description="Longitude", example=-122.3321)
|
81
|
+
|
82
|
+
|
83
|
+
class Location(BaseOpenEpdSchema):
|
84
|
+
"""A location on the Earth's surface."""
|
85
|
+
|
86
|
+
pluscode: str | None = pyd.Field(default=None, description="Open Location code of this location")
|
87
|
+
latlng: LatLng | None = pyd.Field(default=None, description="Latitude and longitude of this location")
|
88
|
+
address: str | None = pyd.Field(default=None, description="Text address, preferably geocoded")
|
89
|
+
country: str | None = pyd.Field(default=None, description="2-alpha country code")
|
90
|
+
jurisdiction: str | None = pyd.Field(
|
91
|
+
default=None, description="Province, State, or similar subdivision below the level of a country"
|
92
|
+
)
|
93
|
+
|
94
|
+
|
76
95
|
class WithAttachmentsMixin(BaseModel):
|
77
96
|
"""Mixin for objects that can have attachments."""
|
78
97
|
|
@@ -18,7 +18,7 @@
|
|
18
18
|
# Find out more at www.BuildingTransparency.org
|
19
19
|
#
|
20
20
|
import datetime
|
21
|
-
from typing import Annotated
|
21
|
+
from typing import Annotated
|
22
22
|
|
23
23
|
import pydantic as pyd
|
24
24
|
|
@@ -35,16 +35,17 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
|
35
35
|
"""Represent an EPD."""
|
36
36
|
|
37
37
|
# TODO: Add validator for open-xpd-uuid on this field
|
38
|
-
id: str = pyd.Field(
|
38
|
+
id: str | None = pyd.Field(
|
39
39
|
description="The unique ID for this EPD. To ensure global uniqueness, should be registered at "
|
40
40
|
"open-xpd-uuid.cqd.io/register or a coordinating registry.",
|
41
41
|
example="1u7zsed8",
|
42
|
+
default=None,
|
42
43
|
)
|
43
|
-
doctype:
|
44
|
+
doctype: str = pyd.Field(
|
44
45
|
description='Describes the type and schema of the document. Must always always read "openEPD".',
|
45
46
|
default="OpenEPD",
|
46
47
|
)
|
47
|
-
product_name: str = pyd.Field(
|
48
|
+
product_name: str | None = pyd.Field(
|
48
49
|
max_length=200, description="The name of the product described by this EPD", example="Mix 12345AC"
|
49
50
|
)
|
50
51
|
product_sku: str | None = pyd.Field(
|
@@ -64,10 +65,11 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
|
64
65
|
product_image: pyd.AnyUrl | None = pyd.Field(
|
65
66
|
description="pointer to image illustrating the product no more than 10MB", default=None
|
66
67
|
)
|
67
|
-
version: pyd.PositiveInt = pyd.Field(
|
68
|
+
version: pyd.PositiveInt | None = pyd.Field(
|
68
69
|
description="Version of this document. The document's issuer should increment it anytime even a single "
|
69
70
|
"character changes, as this value is used to determine the most recent version.",
|
70
71
|
example=1,
|
72
|
+
default=None,
|
71
73
|
)
|
72
74
|
language: str | None = pyd.Field(
|
73
75
|
min_length=2,
|
@@ -89,11 +91,20 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
|
89
91
|
description="Link to data object on original registrar's site",
|
90
92
|
example="https://epd-online.com/EmbeddedEpdList/Download/6029",
|
91
93
|
)
|
92
|
-
# ilcd_uuid: str | None = pyd.Field(description="An optional UUID (for use with ILCD and similar systems)")
|
93
94
|
manufacturer: Org | None = pyd.Field(
|
94
95
|
description="JSON object for declaring Org. Sometimes called the "
|
95
96
|
'"Declaration Holder" or "Declaration Owner".'
|
96
97
|
)
|
98
|
+
epd_developer: Org | None = pyd.Field(
|
99
|
+
description="The organization responsible for the underlying LCA (and subsequent summarization as EPD).",
|
100
|
+
default=None,
|
101
|
+
)
|
102
|
+
epd_developer_email: pyd.EmailStr | None = pyd.Field(
|
103
|
+
default=None,
|
104
|
+
example="john.doe@we-do-lca.com",
|
105
|
+
description="Email contact for inquiries about development of this EPD. "
|
106
|
+
"This must be an email which can be publicly shared.",
|
107
|
+
)
|
97
108
|
plants: list[Plant] = pyd.Field(
|
98
109
|
max_items=32,
|
99
110
|
description="List of object(s) for one or more plant(s) that this declaration applies to.",
|
@@ -113,6 +124,9 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
|
113
124
|
description="Optional link to a verification statement.",
|
114
125
|
example="https://we-verify-epds.com/en/letters/123-456.789b.pdf",
|
115
126
|
)
|
127
|
+
third_party_verifier_email: pyd.EmailStr | None = pyd.Field(
|
128
|
+
description="Email address of the third party verifier", example="john.doe@example.com", default=None
|
129
|
+
)
|
116
130
|
date_of_issue: datetime.date | None = pyd.Field(
|
117
131
|
example=datetime.date(day=11, month=9, year=2019),
|
118
132
|
description="Date the EPD was issued. This should be the first day on which the EPD is valid.",
|
@@ -212,6 +226,7 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
|
212
226
|
max_items=255,
|
213
227
|
description="List of JSON objects pointing to product components. "
|
214
228
|
"Each one should be an EPD or digitized LCI process.",
|
229
|
+
default_factory=list,
|
215
230
|
)
|
216
231
|
lca_discussion: str | None = pyd.Field(
|
217
232
|
max_length=20000,
|
@@ -42,14 +42,28 @@ class EolScenario(BaseOpenEpdSchema):
|
|
42
42
|
description="The weigting of this scenario used in the C1 .. C4 dataset. For example, the overall C1 shoudl be "
|
43
43
|
"equal to weighted sum of C1 from all the scenarios, weighted by likelihood.",
|
44
44
|
example=0.33,
|
45
|
+
default=None,
|
46
|
+
)
|
47
|
+
C1: Measurement | None = pyd.Field(
|
48
|
+
description="Deconstruction and Demolition under this scenario",
|
49
|
+
default=None,
|
50
|
+
)
|
51
|
+
C2: Measurement | None = pyd.Field(
|
52
|
+
description="Transport to waste processing or disposal under this scenario.",
|
53
|
+
default=None,
|
54
|
+
)
|
55
|
+
C3: Measurement | None = pyd.Field(
|
56
|
+
description="Waste Processing under this scenario",
|
57
|
+
default=None,
|
58
|
+
)
|
59
|
+
C4: Measurement | None = pyd.Field(
|
60
|
+
description="Disposal under this scenario",
|
61
|
+
default=None,
|
45
62
|
)
|
46
|
-
C1: Measurement | None = pyd.Field(description="Deconstruction and Demolition under this scenario")
|
47
|
-
C2: Measurement | None = pyd.Field(description="Transport to waste processing or disposal under this scenario.")
|
48
|
-
C3: Measurement | None = pyd.Field(description="Waste Processing under this scenario")
|
49
|
-
C4: Measurement | None = pyd.Field(description="Disposal under this scenario")
|
50
63
|
D: Measurement | None = pyd.Field(
|
51
64
|
description="Potential net benefits from reuse, recycling, and/or energy recovery beyond "
|
52
|
-
"the system boundary under this scenario"
|
65
|
+
"the system boundary under this scenario",
|
66
|
+
default=None,
|
53
67
|
)
|
54
68
|
|
55
69
|
|
@@ -61,50 +75,128 @@ class ScopeSet(BaseOpenEpdSchema):
|
|
61
75
|
The 'unit' field must be consistent across all scopes in a single scopeset.
|
62
76
|
"""
|
63
77
|
|
64
|
-
A1A2A3: Measurement | None = pyd.Field(
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
78
|
+
A1A2A3: Measurement | None = pyd.Field(
|
79
|
+
description="Sum of A1..A3",
|
80
|
+
default=None,
|
81
|
+
)
|
82
|
+
A1: Measurement | None = pyd.Field(
|
83
|
+
description="Raw Material Supply",
|
84
|
+
default=None,
|
85
|
+
)
|
86
|
+
A2: Measurement | None = pyd.Field(
|
87
|
+
description="Transport to Manufacturing",
|
88
|
+
default=None,
|
89
|
+
)
|
90
|
+
A3: Measurement | None = pyd.Field(
|
91
|
+
description="Manufacturing",
|
92
|
+
default=None,
|
93
|
+
)
|
94
|
+
A4: Measurement | None = pyd.Field(
|
95
|
+
description="Transport to Construction",
|
96
|
+
default=None,
|
97
|
+
)
|
98
|
+
A5: Measurement | None = pyd.Field(
|
99
|
+
description="Construction",
|
100
|
+
default=None,
|
101
|
+
)
|
102
|
+
B1: Measurement | None = pyd.Field(
|
103
|
+
description="Use impacts over Reference Service Life (Predicted)",
|
104
|
+
default=None,
|
105
|
+
)
|
71
106
|
B1_years: float | None = pyd.Field(
|
72
107
|
gt=0,
|
73
108
|
lt=11,
|
74
109
|
description="Timeframe over which B1 is evaluated, in years. "
|
75
110
|
"For example, an impact of 1.23 kgCO2e per year for ten years could be B1=1.23kgCO2e, "
|
76
111
|
"B1_years=1.0 or B1=12.3kgCO2e, B1_years=10.0",
|
112
|
+
default=None,
|
113
|
+
)
|
114
|
+
B2: Measurement | None = pyd.Field(
|
115
|
+
description="Predicted Maintenance Impacts over Reference Service Life",
|
116
|
+
default=None,
|
77
117
|
)
|
78
|
-
B2: Measurement | None = pyd.Field(description="Predicted Maintenance Impacts over Reference Service Life")
|
79
118
|
B2_years: float | None = pyd.Field(
|
80
|
-
gt=0,
|
119
|
+
gt=0,
|
120
|
+
lt=11,
|
121
|
+
description="Predicted Maintenance Impacts over Reference Service Life",
|
122
|
+
default=None,
|
123
|
+
)
|
124
|
+
B3: Measurement | None = pyd.Field(
|
125
|
+
description="Predicted Repair impacts over Reference Service Life",
|
126
|
+
default=None,
|
127
|
+
)
|
128
|
+
B3_years: float | None = pyd.Field(
|
129
|
+
gt=0,
|
130
|
+
lt=11,
|
131
|
+
description="Timeframe over which B3 is evaluated, in years",
|
132
|
+
default=None,
|
81
133
|
)
|
82
|
-
B3: Measurement | None = pyd.Field(description="Predicted Repair impacts over Reference Service Life")
|
83
|
-
B3_years: float | None = pyd.Field(gt=0, lt=11, description="Timeframe over which B3 is evaluated, in years")
|
84
134
|
B4: Measurement | None = pyd.Field(
|
85
135
|
description="Predicted Replacement Impacts over the Building lifetime "
|
86
|
-
"('Estimated Construction Works lifespan') specified in the PCR."
|
136
|
+
"('Estimated Construction Works lifespan') specified in the PCR.",
|
137
|
+
default=None,
|
138
|
+
)
|
139
|
+
B4_years: float | None = pyd.Field(
|
140
|
+
gt=0,
|
141
|
+
lt=11,
|
142
|
+
description="Timeframe over which B4 is evaluated, in years",
|
143
|
+
default=None,
|
87
144
|
)
|
88
|
-
B4_years: float | None = pyd.Field(gt=0, lt=11, description="Timeframe over which B4 is evaluated, in years")
|
89
145
|
B5: Measurement | None = pyd.Field(
|
90
146
|
description="Predicted Refurbishment Impacts over the Building lifetime "
|
91
|
-
"('Estimated Construction Works lifespan') specified in the PCR."
|
147
|
+
"('Estimated Construction Works lifespan') specified in the PCR.",
|
148
|
+
default=None,
|
149
|
+
)
|
150
|
+
B5_years: float | None = pyd.Field(
|
151
|
+
gt=0,
|
152
|
+
lt=11,
|
153
|
+
description="Timeframe over which B5 is evaluated, in years",
|
154
|
+
default=None,
|
155
|
+
)
|
156
|
+
B6: Measurement | None = pyd.Field(
|
157
|
+
description="Predicted Impacts related to Operational Energy Use",
|
158
|
+
default=None,
|
159
|
+
)
|
160
|
+
B6_years: float | None = pyd.Field(
|
161
|
+
gt=0,
|
162
|
+
lt=11,
|
163
|
+
description="Timeframe over which B6 is evaluated, in years",
|
164
|
+
default=None,
|
165
|
+
)
|
166
|
+
B7: Measurement | None = pyd.Field(
|
167
|
+
description="Predicted Impacts related to Operational Water Use",
|
168
|
+
default=None,
|
169
|
+
)
|
170
|
+
B7_years: float | None = pyd.Field(
|
171
|
+
gt=0,
|
172
|
+
lt=11,
|
173
|
+
description="Timeframe over which B7 is evaluated, in years",
|
174
|
+
default=None,
|
92
175
|
)
|
93
|
-
B5_years: float | None = pyd.Field(gt=0, lt=11, description="Timeframe over which B5 is evaluated, in years")
|
94
|
-
B6: Measurement | None = pyd.Field(description="Predicted Impacts related to Operational Energy Use")
|
95
|
-
B6_years: float | None = pyd.Field(gt=0, lt=11, description="Timeframe over which B6 is evaluated, in years")
|
96
|
-
B7: Measurement | None = pyd.Field(description="Predicted Impacts related to Operational Water Use")
|
97
|
-
B7_years: float | None = pyd.Field(gt=0, lt=11, description="Timeframe over which B7 is evaluated, in years")
|
98
176
|
C_scenarios: list[EolScenario] | None = pyd.Field(
|
99
177
|
description="A list of possible end-of-life scenarios, "
|
100
|
-
"for use in analyses where the end-of-life can be predicted."
|
178
|
+
"for use in analyses where the end-of-life can be predicted.",
|
179
|
+
default=None,
|
180
|
+
)
|
181
|
+
C1: Measurement | None = pyd.Field(
|
182
|
+
description="Deconstruction and Demolition",
|
183
|
+
default=None,
|
184
|
+
)
|
185
|
+
C2: Measurement | None = pyd.Field(
|
186
|
+
description="Transport to waste processing or disposal.",
|
187
|
+
default=None,
|
188
|
+
)
|
189
|
+
C3: Measurement | None = pyd.Field(
|
190
|
+
description="Waste Processing",
|
191
|
+
default=None,
|
192
|
+
)
|
193
|
+
C4: Measurement | None = pyd.Field(
|
194
|
+
description="Disposal",
|
195
|
+
default=None,
|
101
196
|
)
|
102
|
-
C1: Measurement | None = pyd.Field(description="Deconstruction and Demolition")
|
103
|
-
C2: Measurement | None = pyd.Field(description="Transport to waste processing or disposal.")
|
104
|
-
C3: Measurement | None = pyd.Field(description="Waste Processing")
|
105
|
-
C4: Measurement | None = pyd.Field(description="Disposal")
|
106
197
|
D: Measurement | None = pyd.Field(
|
107
|
-
|
198
|
+
default=None,
|
199
|
+
description="Potential net benefits from reuse, recycling, and/or energy recovery beyond the system boundary.",
|
108
200
|
)
|
109
201
|
|
110
202
|
|
@@ -112,26 +204,28 @@ class ImpactSet(BaseOpenEpdSchema):
|
|
112
204
|
"""A set of impacts, such as GWP, ODP, AP, EP, POCP, EP-marine, EP-terrestrial, EP-freshwater, etc."""
|
113
205
|
|
114
206
|
gwp: ScopeSet | None = pyd.Field(
|
207
|
+
default=None,
|
115
208
|
description="GWP100, calculated per IPCC guidelines. If any CO2 removals are "
|
116
209
|
"part of this figure, the gwp-fossil, gwp-bioganic, gwp-luluc, an "
|
117
210
|
"gwp-nonCO2 fields are required, as is "
|
118
|
-
"kg_C_biogenic_per_declared_unit."
|
211
|
+
"kg_C_biogenic_per_declared_unit.",
|
119
212
|
)
|
120
|
-
odp: ScopeSet | None = pyd.Field(description="Ozone Depletion Potential")
|
121
|
-
ap: ScopeSet | None = pyd.Field(description="Acidification Potential")
|
213
|
+
odp: ScopeSet | None = pyd.Field(default=None, description="Ozone Depletion Potential")
|
214
|
+
ap: ScopeSet | None = pyd.Field(default=None, description="Acidification Potential")
|
122
215
|
ep: ScopeSet | None = pyd.Field(
|
123
|
-
description="Eutrophication Potential in Marine Ecosystems. Has the same meaning as ep-marine."
|
216
|
+
default=None, description="Eutrophication Potential in Marine Ecosystems. Has the same meaning as ep-marine."
|
124
217
|
)
|
125
|
-
pocp: ScopeSet | None = pyd.Field(description="Photochemical Smog (Ozone) creation potential")
|
126
|
-
ep_marine: ScopeSet | None = pyd.Field(alias="ep-marine", description="Has the same meaning as 'ep'")
|
218
|
+
pocp: ScopeSet | None = pyd.Field(default=None, description="Photochemical Smog (Ozone) creation potential")
|
219
|
+
ep_marine: ScopeSet | None = pyd.Field(alias="ep-marine", default=None, description="Has the same meaning as 'ep'")
|
127
220
|
ep_fresh: ScopeSet | None = pyd.Field(
|
128
|
-
alias="ep-fresh", description="Eutrophication Potential in Freshwater Ecosystems"
|
221
|
+
alias="ep-fresh", default=None, description="Eutrophication Potential in Freshwater Ecosystems"
|
129
222
|
)
|
130
223
|
ep_terr: ScopeSet | None = pyd.Field(
|
131
|
-
alias="ep-terr", description="Eutrophication Potential in Terrestrial Ecosystems"
|
224
|
+
alias="ep-terr", default=None, description="Eutrophication Potential in Terrestrial Ecosystems"
|
132
225
|
)
|
133
226
|
gwp_biogenic: ScopeSet | None = pyd.Field(
|
134
227
|
alias="gwp-biogenic",
|
228
|
+
default=None,
|
135
229
|
description="Net GWP from removals of atmospheric CO2 into biomass and emissions of CO2 from biomass sources. "
|
136
230
|
"To be counted as negative, CO2 removals must be closely linked to production in "
|
137
231
|
"operational control (i.e. no purchased offsets), time (within three years of production) and "
|
@@ -140,6 +234,7 @@ class ImpactSet(BaseOpenEpdSchema):
|
|
140
234
|
)
|
141
235
|
gwp_luluc: ScopeSet | None = pyd.Field(
|
142
236
|
alias="gwp-luluc",
|
237
|
+
default=None,
|
143
238
|
description="Climate change effects related to land use and land use change, for example biogenic carbon "
|
144
239
|
"exchanges resulting from deforestation or other soil activities (including soil carbon "
|
145
240
|
"emissions). All related emissions for native forests are included under this category. "
|
@@ -147,10 +242,12 @@ class ImpactSet(BaseOpenEpdSchema):
|
|
147
242
|
)
|
148
243
|
gwp_nonCO2: ScopeSet | None = pyd.Field(
|
149
244
|
alias="gwp-nonCO2",
|
245
|
+
default=None,
|
150
246
|
description="GWP from non-CO2, non-fossil sources, such as livestock-sourced CH4 and agricultural N2O.",
|
151
247
|
)
|
152
248
|
gwp_fossil: ScopeSet | None = pyd.Field(
|
153
249
|
alias="gwp-fossil",
|
250
|
+
default=None,
|
154
251
|
description="Climate change effects due to greenhouse gas emissions originating from the oxidation or "
|
155
252
|
"reduction of fossil fuels or materials containing fossil carbon. [Source: EN15804]",
|
156
253
|
)
|
@@ -199,12 +296,14 @@ class LCIAMethod(StrEnum):
|
|
199
296
|
return cls.UNKNOWN
|
200
297
|
|
201
298
|
|
202
|
-
class Impacts(
|
299
|
+
class Impacts(pyd.BaseModel):
|
203
300
|
"""List of environmental impacts, compiled per one of the standard Impact Assessment methods."""
|
204
301
|
|
302
|
+
__root__: dict[LCIAMethod, ImpactSet]
|
303
|
+
|
205
304
|
def set_unknown_lcia(self, impact_set: ImpactSet):
|
206
305
|
"""Set the impact set as an unknown LCIA method."""
|
207
|
-
self[LCIAMethod.UNKNOWN] = impact_set
|
306
|
+
self.__root__[LCIAMethod.UNKNOWN] = impact_set
|
208
307
|
|
209
308
|
def set_impact_set(self, lcia_method: LCIAMethod | str | None, impact_set: ImpactSet):
|
210
309
|
"""
|
@@ -217,11 +316,25 @@ class Impacts(dict[LCIAMethod, ImpactSet]):
|
|
217
316
|
else:
|
218
317
|
if isinstance(lcia_method, str):
|
219
318
|
lcia_method = LCIAMethod.get_by_name(lcia_method)
|
220
|
-
self[lcia_method] = impact_set
|
319
|
+
self.__root__[lcia_method] = impact_set
|
320
|
+
|
321
|
+
def get_impact_set(
|
322
|
+
self, lcia_method: LCIAMethod | str | None, default_val: ImpactSet | None = None
|
323
|
+
) -> ImpactSet | None:
|
324
|
+
"""Return the impact set for the given LCIA method."""
|
325
|
+
if lcia_method is None:
|
326
|
+
return self.__root__.get(LCIAMethod.UNKNOWN, default_val)
|
327
|
+
if isinstance(lcia_method, str):
|
328
|
+
lcia_method = LCIAMethod.get_by_name(lcia_method)
|
329
|
+
return self.__root__.get(lcia_method, default_val)
|
221
330
|
|
222
331
|
def available_methods(self) -> set[LCIAMethod]:
|
223
332
|
"""Return a list of available LCIA methods."""
|
224
|
-
return set(self.keys())
|
333
|
+
return set(self.__root__.keys())
|
334
|
+
|
335
|
+
def as_dict(self) -> dict[LCIAMethod, ImpactSet]:
|
336
|
+
"""Return the impacts as a dictionary."""
|
337
|
+
return self.__root__
|
225
338
|
|
226
339
|
|
227
340
|
class ResourceUseSet(BaseOpenEpdSchema):
|
@@ -230,47 +343,121 @@ class ResourceUseSet(BaseOpenEpdSchema):
|
|
230
343
|
RPRec: ScopeSet | None = pyd.Field(
|
231
344
|
description="Renewable primary resources used as energy carrier (fuel). "
|
232
345
|
"First use bio-based materials used as an energy source. Hydropower, solar and wind power used "
|
233
|
-
"in the technosphere are also included in this indicator"
|
346
|
+
"in the technosphere are also included in this indicator",
|
347
|
+
default=None,
|
234
348
|
)
|
235
349
|
RPRm: ScopeSet | None = pyd.Field(
|
236
350
|
description="Renewable primary resources with energy content used as material. "
|
237
|
-
"First use biobased materials used as materials (e.g. wood, hemp, etc.)."
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
351
|
+
"First use biobased materials used as materials (e.g. wood, hemp, etc.).",
|
352
|
+
default=None,
|
353
|
+
)
|
354
|
+
rpre: ScopeSet | None = pyd.Field(
|
355
|
+
description="Renewable primary energy resources as energy",
|
356
|
+
default=None,
|
357
|
+
)
|
358
|
+
nrpre: ScopeSet | None = pyd.Field(
|
359
|
+
description="Non-renewable primary resources as energy (fuel)",
|
360
|
+
default=None,
|
361
|
+
)
|
362
|
+
nrprm: ScopeSet | None = pyd.Field(
|
363
|
+
description="Non-renewable primary resources as material",
|
364
|
+
default=None,
|
365
|
+
)
|
366
|
+
fw: ScopeSet | None = pyd.Field(
|
367
|
+
description="Use of net fresh water",
|
368
|
+
default=None,
|
369
|
+
)
|
370
|
+
sm: ScopeSet | None = pyd.Field(
|
371
|
+
description="Use of secondary materials",
|
372
|
+
default=None,
|
373
|
+
)
|
374
|
+
rsf: ScopeSet | None = pyd.Field(
|
375
|
+
description="Use of renewable secondary materials",
|
376
|
+
default=None,
|
377
|
+
)
|
378
|
+
nrsf: ScopeSet | None = pyd.Field(
|
379
|
+
description="Use of non-renewable secondary fuels",
|
380
|
+
default=None,
|
381
|
+
)
|
382
|
+
re: ScopeSet | None = pyd.Field(
|
383
|
+
description="Renewable energy resources",
|
384
|
+
default=None,
|
385
|
+
)
|
247
386
|
pere: ScopeSet | None = pyd.Field(
|
248
|
-
description="Use of renewable primary energy excluding renewable primary energy resources used as raw materials"
|
387
|
+
description="Use of renewable primary energy excluding renewable primary energy resources used as raw materials",
|
388
|
+
default=None,
|
389
|
+
)
|
390
|
+
perm: ScopeSet | None = pyd.Field(
|
391
|
+
description="Use of renewable primary energy resources used as raw materials",
|
392
|
+
default=None,
|
393
|
+
)
|
394
|
+
pert: ScopeSet | None = pyd.Field(
|
395
|
+
description="Total use of renewable primary energy resources",
|
396
|
+
default=None,
|
249
397
|
)
|
250
|
-
perm: ScopeSet | None = pyd.Field(description="Use of renewable primary energy resources used as raw materials")
|
251
|
-
pert: ScopeSet | None = pyd.Field(description="Total use of renewable primary energy resources")
|
252
398
|
penre: ScopeSet | None = pyd.Field(
|
253
399
|
description="Use of non-renewable primary energy excluding "
|
254
|
-
"non-renewable primary energy resources used as raw materials"
|
400
|
+
"non-renewable primary energy resources used as raw materials",
|
401
|
+
default=None,
|
255
402
|
)
|
256
403
|
penrm: ScopeSet | None = pyd.Field(
|
257
|
-
description="Use of non-renewable primary energy resources used as raw materials"
|
404
|
+
description="Use of non-renewable primary energy resources used as raw materials",
|
405
|
+
default=None,
|
406
|
+
)
|
407
|
+
penrt: ScopeSet | None = pyd.Field(
|
408
|
+
description="Total use of non-renewable primary energy resources",
|
409
|
+
default=None,
|
258
410
|
)
|
259
|
-
penrt: ScopeSet | None = pyd.Field(description="Total use of non-renewable primary energy resources")
|
260
411
|
|
261
412
|
|
262
413
|
class OutputFlowSet(BaseOpenEpdSchema):
|
263
414
|
"""A set of output flows, such as waste, emissions, etc."""
|
264
415
|
|
265
|
-
twd: ScopeSet | None = pyd.Field(
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
416
|
+
twd: ScopeSet | None = pyd.Field(
|
417
|
+
description="Total waste disposed",
|
418
|
+
default=None,
|
419
|
+
)
|
420
|
+
hwd: ScopeSet | None = pyd.Field(
|
421
|
+
description="Hazardous waste disposed",
|
422
|
+
default=None,
|
423
|
+
)
|
424
|
+
nhwd: ScopeSet | None = pyd.Field(
|
425
|
+
description="Non-hazardous waste disposed",
|
426
|
+
default=None,
|
427
|
+
)
|
428
|
+
rwd: ScopeSet | None = pyd.Field(
|
429
|
+
description="Radioactive waste disposed",
|
430
|
+
default=None,
|
431
|
+
)
|
432
|
+
hlrw: ScopeSet | None = pyd.Field(
|
433
|
+
description="High level radioactive waste disposed",
|
434
|
+
default=None,
|
435
|
+
)
|
436
|
+
illrw: ScopeSet | None = pyd.Field(
|
437
|
+
description="Intermediate level radioactive waste disposed",
|
438
|
+
default=None,
|
439
|
+
)
|
440
|
+
cru: ScopeSet | None = pyd.Field(
|
441
|
+
description="Components for re-use",
|
442
|
+
default=None,
|
443
|
+
)
|
444
|
+
mr: ScopeSet | None = pyd.Field(
|
445
|
+
description="Recycled materials",
|
446
|
+
default=None,
|
447
|
+
)
|
448
|
+
mfr: ScopeSet | None = pyd.Field(
|
449
|
+
description="Materials for recycling",
|
450
|
+
default=None,
|
451
|
+
)
|
452
|
+
mer: ScopeSet | None = pyd.Field(
|
453
|
+
description="Materials for energy recovery",
|
454
|
+
default=None,
|
455
|
+
)
|
456
|
+
ee: ScopeSet | None = pyd.Field(
|
457
|
+
description="Exported energy",
|
458
|
+
default=None,
|
459
|
+
)
|
460
|
+
eh: ScopeSet | None = pyd.Field(
|
461
|
+
description="Exported heat",
|
462
|
+
default=None,
|
463
|
+
)
|
@@ -22,62 +22,66 @@ from typing import Annotated, Optional
|
|
22
22
|
import pydantic as pyd
|
23
23
|
|
24
24
|
from openepd.model.base import BaseOpenEpdSchema
|
25
|
-
from openepd.model.common import WithAltIdsMixin, WithAttachmentsMixin
|
26
|
-
|
27
|
-
|
28
|
-
class Contact(BaseOpenEpdSchema): # TODO: NEW Object, not in the spec
|
29
|
-
"""Contact information of a person or organization."""
|
30
|
-
|
31
|
-
email: pyd.EmailStr | None = pyd.Field(description="Email", example="contact@c-change-labs.com", default=None)
|
32
|
-
phone: str | None = pyd.Field(description="Phone number", example="+15263327352", default=None)
|
33
|
-
website: pyd.AnyUrl | None = pyd.Field(
|
34
|
-
description="Url of the website", example="http://buildingtransparency.org", default=None
|
35
|
-
)
|
36
|
-
address: str | None = pyd.Field(description="Address", example="123 Main St, San Francisco, CA 94105", default=None)
|
25
|
+
from openepd.model.common import Location, WithAltIdsMixin, WithAttachmentsMixin
|
37
26
|
|
38
27
|
|
39
28
|
class Org(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
40
29
|
"""Represent an organization."""
|
41
30
|
|
42
|
-
web_domain: str = pyd.Field(
|
43
|
-
description="A web domain owned by organization. Typically is the org's home website address",
|
31
|
+
web_domain: str | None = pyd.Field(
|
32
|
+
description="A web domain owned by organization. Typically is the org's home website address", default=None
|
33
|
+
)
|
34
|
+
name: str | None = pyd.Field(
|
35
|
+
max_length=200,
|
36
|
+
description="Common name for organization",
|
37
|
+
example="C Change Labs",
|
38
|
+
default=None,
|
44
39
|
)
|
45
|
-
name: str = pyd.Field(max_length=200, description="Common name for organization", example="C Change Labs")
|
46
40
|
alt_names: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
|
47
41
|
description="List of other names for organization",
|
48
42
|
example=["C-Change Labs", "C-Change Labs inc."],
|
49
43
|
default=None,
|
50
44
|
)
|
51
45
|
# TODO: NEW field, not in the spec
|
52
|
-
contacts: Contact | None = pyd.Field(description="Contact information of the organization", default=None)
|
53
46
|
owner: Optional["Org"] = pyd.Field(description="Organization that controls this organization", default=None)
|
54
47
|
subsidiaries: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
|
55
48
|
description="Organizations controlled by this organization",
|
56
49
|
example=["cqd.io", "supplychaincarbonpricing.org"],
|
57
50
|
default=None,
|
58
51
|
)
|
52
|
+
hq_location: Location | None = pyd.Field(
|
53
|
+
default=None,
|
54
|
+
description="Location of a place of business, prefereably the corporate headquarters.",
|
55
|
+
)
|
59
56
|
|
60
57
|
|
61
58
|
class Plant(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
62
59
|
"""Represent a manufacturing plant."""
|
63
60
|
|
64
61
|
# TODO: Add proper validator
|
65
|
-
id: str = pyd.Field(
|
62
|
+
id: str | None = pyd.Field(
|
66
63
|
description="Plus code (aka Open Location Code) of plant's location and "
|
67
64
|
"owner's web domain joined with `.`(dot).",
|
68
65
|
example="865P2W3V+3W.interface.com",
|
69
66
|
alias="pluscode",
|
67
|
+
default=None,
|
70
68
|
)
|
71
69
|
owner: Org | None = pyd.Field(description="Organization that owns the plant", default=None)
|
72
|
-
name: str = pyd.Field(
|
73
|
-
max_length=200,
|
70
|
+
name: str | None = pyd.Field(
|
71
|
+
max_length=200,
|
72
|
+
description="Manufacturer's name for plant. Recommended < 40 chars",
|
73
|
+
example="Dalton, GA",
|
74
|
+
default=None,
|
74
75
|
)
|
75
|
-
address: str = pyd.Field(
|
76
|
+
address: str | None = pyd.Field(
|
76
77
|
max_length=200,
|
78
|
+
default=None,
|
77
79
|
description="Text address, preferably geocoded",
|
78
80
|
example="1503 Orchard Hill Rd, LaGrange, GA 30240, United States",
|
79
81
|
)
|
80
|
-
contact_email: pyd.EmailStr | None = pyd.Field(
|
82
|
+
contact_email: pyd.EmailStr | None = pyd.Field(
|
83
|
+
description="Email contact", example="info@interface.com", default=None
|
84
|
+
)
|
81
85
|
|
82
86
|
class Config(BaseOpenEpdSchema.Config):
|
83
87
|
allow_population_by_field_name = True
|
@@ -30,44 +30,50 @@ from openepd.model.org import Org
|
|
30
30
|
class Pcr(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
|
31
31
|
"""Represent a PCR (Product Category Rules)."""
|
32
32
|
|
33
|
-
id: str = pyd.Field(
|
33
|
+
id: str | None = pyd.Field(
|
34
34
|
description="The unique ID for this PCR. To ensure global uniqueness, should be registered "
|
35
35
|
"at open-xpd-uuid.cqd.io/register or a coordinating registry.",
|
36
36
|
example="ec3xpgq2",
|
37
|
+
default=None,
|
37
38
|
)
|
38
39
|
issuer: Org | None = pyd.Field(description="Organization issuing this PCR", default=None)
|
39
40
|
issuer_doc_id: str | None = pyd.Field(
|
40
41
|
max_length=40,
|
42
|
+
default=None,
|
41
43
|
description="Document ID or code created by issuer",
|
42
44
|
example="c-PCR-003",
|
43
45
|
)
|
44
46
|
name: str | None = pyd.Field(
|
45
47
|
max_length=200,
|
48
|
+
default=None,
|
46
49
|
description="Full document name as listed in source document",
|
47
50
|
example="c-PCR-003 Concrete and concrete elements (EN 16757)",
|
48
51
|
)
|
49
52
|
short_name: str | None = pyd.Field(
|
50
53
|
max_length=40,
|
54
|
+
default=None,
|
51
55
|
description="A shortened name without boilerplate text.",
|
52
56
|
example="Concrete and Concrete Elements",
|
53
57
|
)
|
54
|
-
version:
|
58
|
+
version: str | None = pyd.Field(
|
55
59
|
description="Document version, as expressed in document.",
|
56
60
|
example="1.0.2",
|
61
|
+
default=None,
|
57
62
|
)
|
58
63
|
date_of_issue: datetime.date | None = pyd.Field(
|
59
64
|
example=datetime.date(day=11, month=2, year=2022),
|
65
|
+
default=None,
|
60
66
|
description="First day on which the document is valid",
|
61
67
|
)
|
62
68
|
valid_until: datetime.date | None = pyd.Field(
|
63
69
|
example=datetime.date(day=11, month=2, year=2024),
|
70
|
+
default=None,
|
64
71
|
description="Last day on which the document is valid",
|
65
72
|
)
|
66
73
|
parent: Optional["Pcr"] = pyd.Field(
|
67
74
|
description="The parent PCR, base PCR, `Part A` PCR",
|
68
75
|
default=None,
|
69
76
|
)
|
70
|
-
# TODO: why plural?
|
71
77
|
product_classes: dict[str, str | list[str]] = pyd.Field(
|
72
78
|
description="List of classifications, including Masterformat and UNSPC", default_factory=dict
|
73
79
|
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|