openepd 6.13.2__py3-none-any.whl → 7.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openepd/__init__.py +0 -6
- openepd/__version__.py +1 -1
- openepd/api/average_dataset/generic_estimate_sync_api.py +11 -10
- openepd/api/average_dataset/industry_epd_sync_api.py +9 -8
- openepd/api/base_sync_client.py +53 -9
- openepd/api/category/sync_api.py +1 -1
- openepd/api/dto/base.py +4 -4
- openepd/api/dto/common.py +24 -16
- openepd/api/dto/meta.py +15 -11
- openepd/api/dto/mf.py +9 -8
- openepd/api/epd/dto.py +43 -33
- openepd/api/epd/sync_api.py +9 -9
- openepd/api/pcr/sync_api.py +2 -2
- openepd/bundle/model.py +11 -10
- openepd/bundle/reader.py +12 -5
- openepd/bundle/writer.py +17 -6
- openepd/model/base.py +61 -44
- openepd/model/category.py +13 -10
- openepd/model/common.py +107 -59
- openepd/model/declaration.py +93 -64
- openepd/model/epd.py +51 -43
- openepd/model/generic_estimate.py +28 -13
- openepd/model/industry_epd.py +15 -9
- openepd/model/lcia.py +161 -136
- openepd/model/org.py +70 -37
- openepd/model/pcr.py +38 -32
- openepd/model/specs/asphalt.py +31 -22
- openepd/model/specs/base.py +14 -11
- openepd/model/specs/concrete.py +60 -39
- openepd/model/specs/range/aggregates.py +9 -9
- openepd/model/specs/range/aluminium.py +7 -7
- openepd/model/specs/range/asphalt.py +22 -19
- openepd/model/specs/range/cladding.py +16 -16
- openepd/model/specs/range/cmu.py +10 -9
- openepd/model/specs/range/concrete.py +36 -27
- openepd/model/specs/range/conveying_equipment.py +16 -15
- openepd/model/specs/range/electrical.py +24 -22
- openepd/model/specs/range/finishes.py +109 -104
- openepd/model/specs/range/fire_and_smoke_protection.py +7 -7
- openepd/model/specs/range/furnishings.py +16 -12
- openepd/model/specs/range/manufacturing_inputs.py +16 -16
- openepd/model/specs/range/masonry.py +16 -16
- openepd/model/specs/range/mechanical.py +47 -47
- openepd/model/specs/range/mechanical_insulation.py +7 -7
- openepd/model/specs/range/network_infrastructure.py +54 -46
- openepd/model/specs/range/openings.py +36 -31
- openepd/model/specs/range/plumbing.py +15 -13
- openepd/model/specs/range/precast_concrete.py +20 -16
- openepd/model/specs/range/sheathing.py +18 -18
- openepd/model/specs/range/steel.py +27 -25
- openepd/model/specs/range/thermal_moisture_protection.py +20 -20
- openepd/model/specs/range/utility_piping.py +9 -9
- openepd/model/specs/range/wood.py +19 -19
- openepd/model/specs/range/wood_joists.py +8 -8
- openepd/model/specs/singular/__init__.py +9 -5
- openepd/model/specs/singular/aggregates.py +22 -15
- openepd/model/specs/singular/aluminium.py +20 -5
- openepd/model/specs/singular/asphalt.py +44 -20
- openepd/model/specs/singular/cladding.py +38 -23
- openepd/model/specs/singular/cmu.py +26 -11
- openepd/model/specs/singular/common.py +3 -2
- openepd/model/specs/singular/concrete.py +85 -48
- openepd/model/specs/singular/conveying_equipment.py +30 -17
- openepd/model/specs/singular/deprecated/__init__.py +3 -2
- openepd/model/specs/singular/deprecated/concrete.py +68 -33
- openepd/model/specs/singular/deprecated/steel.py +28 -15
- openepd/model/specs/singular/electrical.py +69 -41
- openepd/model/specs/singular/finishes.py +250 -140
- openepd/model/specs/singular/fire_and_smoke_protection.py +9 -6
- openepd/model/specs/singular/furnishings.py +16 -14
- openepd/model/specs/singular/manufacturing_inputs.py +23 -14
- openepd/model/specs/singular/masonry.py +66 -21
- openepd/model/specs/singular/mechanical.py +48 -47
- openepd/model/specs/singular/mechanical_insulation.py +7 -6
- openepd/model/specs/singular/mixins/conduit_mixin.py +13 -10
- openepd/model/specs/singular/network_infrastructure.py +111 -52
- openepd/model/specs/singular/openings.py +127 -95
- openepd/model/specs/singular/plumbing.py +15 -12
- openepd/model/specs/singular/precast_concrete.py +68 -54
- openepd/model/specs/singular/sheathing.py +47 -27
- openepd/model/specs/singular/steel.py +69 -45
- openepd/model/specs/singular/thermal_moisture_protection.py +36 -20
- openepd/model/specs/singular/utility_piping.py +11 -8
- openepd/model/specs/singular/wood.py +48 -24
- openepd/model/specs/singular/wood_joists.py +19 -6
- openepd/model/standard.py +15 -8
- openepd/model/validation/common.py +9 -3
- openepd/model/validation/numbers.py +0 -13
- openepd/model/validation/quantity.py +88 -55
- openepd/model/versioning.py +9 -6
- {openepd-6.13.2.dist-info → openepd-7.0.1.dist-info}/METADATA +2 -2
- openepd-7.0.1.dist-info/RECORD +141 -0
- openepd/compat/__init__.py +0 -15
- openepd/compat/compat_functional_validators.py +0 -25
- openepd/compat/pydantic.py +0 -30
- openepd/patch_pydantic.py +0 -108
- openepd-6.13.2.dist-info/RECORD +0 -145
- {openepd-6.13.2.dist-info → openepd-7.0.1.dist-info}/LICENSE +0 -0
- {openepd-6.13.2.dist-info → openepd-7.0.1.dist-info}/WHEEL +0 -0
openepd/model/common.py
CHANGED
@@ -14,25 +14,33 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
from enum import StrEnum
|
17
|
-
from typing import Annotated, Any
|
17
|
+
from typing import Annotated, Any, Self
|
18
|
+
|
19
|
+
import pydantic
|
20
|
+
import pydantic_core
|
18
21
|
|
19
|
-
from openepd.compat.pydantic import pyd
|
20
22
|
from openepd.model.base import BaseOpenEpdSchema
|
21
|
-
from openepd.model.validation.numbers import RatioFloat
|
22
23
|
|
23
24
|
|
24
25
|
class Amount(BaseOpenEpdSchema):
|
25
26
|
"""A value-and-unit pairing for amounts that do not have an uncertainty."""
|
26
27
|
|
27
|
-
qty: float | None =
|
28
|
-
unit: str | None =
|
28
|
+
qty: float | None = pydantic.Field(description="How much of this in the amount.", ge=0, default=None)
|
29
|
+
unit: str | None = pydantic.Field(
|
30
|
+
description="Which unit. SI units are preferred.",
|
31
|
+
examples=["kg"],
|
32
|
+
default=None,
|
33
|
+
)
|
29
34
|
|
30
|
-
|
31
|
-
|
35
|
+
model_config = pydantic.ConfigDict(from_attributes=True)
|
36
|
+
|
37
|
+
@pydantic.model_validator(mode="after")
|
38
|
+
def check_qty_or_unit(self) -> Self:
|
32
39
|
"""Ensure that qty or unit is provided."""
|
33
|
-
|
40
|
+
|
41
|
+
if self.qty is None and self.unit is None:
|
34
42
|
raise ValueError("Either qty or unit must be provided.")
|
35
|
-
return
|
43
|
+
return self
|
36
44
|
|
37
45
|
def to_quantity_str(self):
|
38
46
|
"""Return a string representation of the amount."""
|
@@ -68,12 +76,13 @@ class Distribution(StrEnum):
|
|
68
76
|
class Measurement(BaseOpenEpdSchema):
|
69
77
|
"""A scientific value with units and uncertainty."""
|
70
78
|
|
71
|
-
mean: float =
|
72
|
-
unit: str | None =
|
73
|
-
rsd:
|
74
|
-
description="Relative standard deviation, i.e. standard_deviation/mean",
|
79
|
+
mean: float = pydantic.Field(description="Mean (expected) value of the measurement")
|
80
|
+
unit: str | None = pydantic.Field(description="Measurement unit")
|
81
|
+
rsd: pydantic.PositiveFloat | None = pydantic.Field(
|
82
|
+
description="Relative standard deviation, i.e. standard_deviation/mean",
|
83
|
+
default=None,
|
75
84
|
)
|
76
|
-
dist: Distribution | None =
|
85
|
+
dist: Distribution | None = pydantic.Field(
|
77
86
|
description="Statistical distribution of the measurement error.", default=None
|
78
87
|
)
|
79
88
|
|
@@ -98,26 +107,29 @@ class Ingredient(BaseOpenEpdSchema):
|
|
98
107
|
gwp_fraction, citation and evidence_type.
|
99
108
|
"""
|
100
109
|
|
101
|
-
qty: float | None =
|
102
|
-
description="Number of declared units of this consumed. Negative values indicate an outflow."
|
110
|
+
qty: float | None = pydantic.Field(
|
111
|
+
description="Number of declared units of this consumed. Negative values indicate an outflow.",
|
112
|
+
default=None,
|
103
113
|
)
|
104
|
-
link:
|
105
|
-
description="Link to this object's OpenEPD declaration. "
|
106
|
-
"An OpenIndustryEPD or OpenLCI link is also acceptable.",
|
114
|
+
link: pydantic.AnyUrl | None = pydantic.Field(
|
115
|
+
description="Link to this object's OpenEPD declaration. An OpenIndustryEPD or OpenLCI link is also acceptable.",
|
107
116
|
default=None,
|
108
117
|
)
|
109
118
|
|
110
|
-
gwp_fraction:
|
119
|
+
gwp_fraction: float | None = pydantic.Field(
|
111
120
|
default=None,
|
112
121
|
description="Fraction of product's A1-A3 GWP this flow represents. This value, along with the specificity of "
|
113
122
|
"the reference, are used to caclulate supply chain specificity.",
|
123
|
+
ge=0,
|
124
|
+
le=1,
|
114
125
|
)
|
115
|
-
evidence_type: IngredientEvidenceTypeEnum | None =
|
116
|
-
default=None,
|
126
|
+
evidence_type: IngredientEvidenceTypeEnum | None = pydantic.Field(
|
127
|
+
default=None,
|
128
|
+
description="Type of evidence used, which can be used to calculate degree of specificity",
|
117
129
|
)
|
118
|
-
citation: str | None =
|
130
|
+
citation: str | None = pydantic.Field(default=None, description="Text citation describing the data source ")
|
119
131
|
|
120
|
-
@
|
132
|
+
@pydantic.model_validator(mode="before")
|
121
133
|
def _validate_evidence(cls, values: dict[str, Any]) -> dict[str, Any]:
|
122
134
|
# gwp_fraction should be backed by some type of evidence (fraction coming from product EPD etc) to be accounted
|
123
135
|
# for in the calculation of uncertainty
|
@@ -133,19 +145,20 @@ class Ingredient(BaseOpenEpdSchema):
|
|
133
145
|
class LatLng(BaseOpenEpdSchema):
|
134
146
|
"""A latitude and longitude."""
|
135
147
|
|
136
|
-
lat: float =
|
137
|
-
lng: float =
|
148
|
+
lat: float = pydantic.Field(description="Latitude", examples=[47.6062])
|
149
|
+
lng: float = pydantic.Field(description="Longitude", examples=[-122.3321])
|
138
150
|
|
139
151
|
|
140
152
|
class Location(BaseOpenEpdSchema):
|
141
153
|
"""A location on the Earth's surface."""
|
142
154
|
|
143
|
-
pluscode: str | None =
|
144
|
-
latlng: LatLng | None =
|
145
|
-
address: str | None =
|
146
|
-
country: str | None =
|
147
|
-
jurisdiction: str | None =
|
148
|
-
default=None,
|
155
|
+
pluscode: str | None = pydantic.Field(default=None, description="Open Location code of this location")
|
156
|
+
latlng: LatLng | None = pydantic.Field(default=None, description="Latitude and longitude of this location")
|
157
|
+
address: str | None = pydantic.Field(default=None, description="Text address, preferably geocoded")
|
158
|
+
country: str | None = pydantic.Field(default=None, description="2-alpha country code")
|
159
|
+
jurisdiction: str | None = pydantic.Field(
|
160
|
+
default=None,
|
161
|
+
description="Province, State, or similar subdivision below the level of a country",
|
149
162
|
)
|
150
163
|
|
151
164
|
|
@@ -173,36 +186,69 @@ ATTACHMENT_KNOWN_KEYS: dict[str, str] = {
|
|
173
186
|
}
|
174
187
|
|
175
188
|
|
176
|
-
class AttachmentDict(dict[str,
|
189
|
+
class AttachmentDict(dict[str, pydantic.AnyUrl]):
|
177
190
|
"""Special form of dict for attachments."""
|
178
191
|
|
179
192
|
@classmethod
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
|
193
|
+
def __get_pydantic_core_schema__(
|
194
|
+
cls, source: type[Any], handler: pydantic.GetCoreSchemaHandler
|
195
|
+
) -> pydantic_core.core_schema.CoreSchema:
|
196
|
+
return pydantic_core.core_schema.no_info_after_validator_function(
|
197
|
+
cls._validate,
|
198
|
+
pydantic_core.core_schema.dict_schema(),
|
199
|
+
serialization=pydantic_core.core_schema.plain_serializer_function_ser_schema(
|
200
|
+
cls._serialize,
|
201
|
+
info_arg=False,
|
202
|
+
return_schema=pydantic_core.core_schema.dict_schema(),
|
203
|
+
),
|
204
|
+
)
|
205
|
+
|
206
|
+
@classmethod
|
207
|
+
def _validate(cls, value: Any) -> "AttachmentDict":
|
208
|
+
# Ensure the input is a dict.
|
209
|
+
if not isinstance(value, dict):
|
210
|
+
raise TypeError("AttachmentDict must be a dict")
|
211
|
+
|
212
|
+
return cls(value)
|
213
|
+
|
214
|
+
@classmethod
|
215
|
+
def _serialize(cls, value: "AttachmentDict") -> dict[str, Any]:
|
216
|
+
# Serialize each AnyUrl value to its string representation.
|
217
|
+
return {k: str(v) for k, v in value.items()}
|
218
|
+
|
219
|
+
@classmethod
|
220
|
+
def __get_pydantic_json_schema__(cls, core_schema: dict[str, Any], handler: Any) -> dict[str, Any]:
|
221
|
+
# Obtain the default schema using the handler.
|
222
|
+
schema = handler(core_schema)
|
223
|
+
|
224
|
+
# Get the description from the default schema (if any)
|
225
|
+
field_description = schema.get("description", "")
|
184
226
|
if field_description:
|
185
227
|
field_description = field_description.strip()
|
186
228
|
if not field_description.endswith("."):
|
187
229
|
field_description += "."
|
188
230
|
field_description += " "
|
189
231
|
|
190
|
-
|
191
|
-
|
232
|
+
# Update the schema with our custom description and properties.
|
233
|
+
schema["description"] = field_description + "Extra properties of string -> URL allowed."
|
234
|
+
schema["properties"] = {
|
192
235
|
k: {"type": "string", "format": "uri", "description": v} for k, v in ATTACHMENT_KNOWN_KEYS.items()
|
193
236
|
}
|
194
|
-
|
237
|
+
schema["additionalProperties"] = True
|
238
|
+
return schema
|
195
239
|
|
196
240
|
|
197
|
-
class WithAttachmentsMixin(
|
241
|
+
class WithAttachmentsMixin(pydantic.BaseModel):
|
198
242
|
"""Mixin for objects that can have attachments."""
|
199
243
|
|
200
|
-
attachments: AttachmentDict =
|
244
|
+
attachments: AttachmentDict | None = pydantic.Field(
|
201
245
|
description="Dict of URLs relevant to this entry",
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
246
|
+
examples=[
|
247
|
+
{
|
248
|
+
"Contact Us": "https://www.c-change-labs.com/en/contact-us/",
|
249
|
+
"LinkedIn": "https://www.linkedin.com/company/c-change-labs/",
|
250
|
+
}
|
251
|
+
],
|
206
252
|
default=None,
|
207
253
|
)
|
208
254
|
|
@@ -218,14 +264,16 @@ class WithAttachmentsMixin(pyd.BaseModel):
|
|
218
264
|
self.set_attachment(name, url)
|
219
265
|
|
220
266
|
|
221
|
-
class WithAltIdsMixin(
|
267
|
+
class WithAltIdsMixin(pydantic.BaseModel):
|
222
268
|
"""Mixin for objects that can have alt_ids."""
|
223
269
|
|
224
|
-
alt_ids: dict[Annotated[str,
|
270
|
+
alt_ids: dict[Annotated[str, pydantic.Field(max_length=200)], str] | None = pydantic.Field(
|
225
271
|
description="Dict identifiers for this entry.",
|
226
|
-
|
227
|
-
|
228
|
-
|
272
|
+
examples=[
|
273
|
+
{
|
274
|
+
"oekobau.dat": "bdda4364-451f-4df2-a68b-5912469ee4c9",
|
275
|
+
}
|
276
|
+
],
|
229
277
|
default=None,
|
230
278
|
)
|
231
279
|
|
@@ -262,7 +310,7 @@ class OpenEPDUnit(StrEnum):
|
|
262
310
|
class RangeBase(BaseOpenEpdSchema):
|
263
311
|
"""Base class for range types having min and max and order between them."""
|
264
312
|
|
265
|
-
@
|
313
|
+
@pydantic.model_validator(mode="before")
|
266
314
|
def _validate_range_bounds(cls, values: dict[str, Any]) -> dict[str, Any]:
|
267
315
|
min_boundary = values.get("min")
|
268
316
|
max_boundary = values.get("max")
|
@@ -274,28 +322,28 @@ class RangeBase(BaseOpenEpdSchema):
|
|
274
322
|
class RangeFloat(RangeBase):
|
275
323
|
"""Structure representing a range of floats."""
|
276
324
|
|
277
|
-
min: float | None =
|
278
|
-
max: float | None =
|
325
|
+
min: float | None = pydantic.Field(default=None, examples=[3.1])
|
326
|
+
max: float | None = pydantic.Field(default=None, examples=[5.8])
|
279
327
|
|
280
328
|
|
281
329
|
class RangeInt(RangeBase):
|
282
330
|
"""Structure representing a range of ints1."""
|
283
331
|
|
284
|
-
min: int | None =
|
285
|
-
max: int | None =
|
332
|
+
min: int | None = pydantic.Field(default=None, examples=[2])
|
333
|
+
max: int | None = pydantic.Field(default=None, examples=[3])
|
286
334
|
|
287
335
|
|
288
336
|
class RangeRatioFloat(RangeFloat):
|
289
337
|
"""Range of ratios (0-1 to 0-10)."""
|
290
338
|
|
291
|
-
min: float | None =
|
292
|
-
max: float | None =
|
339
|
+
min: float | None = pydantic.Field(default=None, examples=[0.2], ge=0, le=1)
|
340
|
+
max: float | None = pydantic.Field(default=None, examples=[0.65], ge=0, le=1)
|
293
341
|
|
294
342
|
|
295
343
|
class RangeAmount(RangeFloat):
|
296
344
|
"""Structure representing a range of quantities."""
|
297
345
|
|
298
|
-
unit: str | None =
|
346
|
+
unit: str | None = pydantic.Field(default=None, description="Unit of the range.")
|
299
347
|
|
300
348
|
|
301
349
|
class EnumGroupingAware:
|
openepd/model/declaration.py
CHANGED
@@ -17,7 +17,8 @@ import abc
|
|
17
17
|
import datetime
|
18
18
|
from enum import StrEnum
|
19
19
|
|
20
|
-
|
20
|
+
import pydantic
|
21
|
+
|
21
22
|
from openepd.model.base import BaseOpenEpdSchema, OpenXpdUUID, RootDocument
|
22
23
|
from openepd.model.common import Amount
|
23
24
|
from openepd.model.geography import Geography
|
@@ -36,55 +37,60 @@ THIRD_PARTY_VERIFIER_DESCRIPTION = "JSON object for Org that performed a critica
|
|
36
37
|
class BaseDeclaration(RootDocument, abc.ABC):
|
37
38
|
"""Base class for declaration-related documents (EPDs, Industry-wide EPDs, Generic Estimates)."""
|
38
39
|
|
39
|
-
id: OpenXpdUUID | None =
|
40
|
+
id: OpenXpdUUID | None = pydantic.Field(
|
40
41
|
description="The unique ID for this document. To ensure global uniqueness, should be registered at "
|
41
42
|
"open-xpd-uuid.cqd.io/register or a coordinating registry.",
|
42
|
-
|
43
|
+
examples=["1u7zsed8"],
|
43
44
|
default=None,
|
44
45
|
)
|
45
|
-
date_of_issue: datetime.datetime | None =
|
46
|
-
|
46
|
+
date_of_issue: datetime.datetime | None = pydantic.Field(
|
47
|
+
examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc)],
|
47
48
|
description="Date the document was issued. This should be the first day on which the document is valid.",
|
49
|
+
default=None,
|
48
50
|
)
|
49
|
-
valid_until: datetime.datetime | None =
|
50
|
-
|
51
|
+
valid_until: datetime.datetime | None = pydantic.Field(
|
52
|
+
examples=[datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.timezone.utc)],
|
51
53
|
description="Last date the document is valid on, including any extensions.",
|
54
|
+
default=None,
|
52
55
|
)
|
53
56
|
|
54
|
-
version:
|
57
|
+
version: pydantic.NonNegativeInt | None = pydantic.Field(
|
55
58
|
description="Version of this document. The document's issuer should increment it anytime even a single "
|
56
59
|
"character changes, as this value is used to determine the most recent version.",
|
57
|
-
|
60
|
+
examples=[1],
|
58
61
|
default=None,
|
59
62
|
)
|
60
63
|
|
61
|
-
declared_unit: Amount | None =
|
64
|
+
declared_unit: Amount | None = pydantic.Field(
|
62
65
|
description="SI declared unit for this document. If a functional unit is "
|
63
66
|
"utilized, the declared unit shall refer to the amount of "
|
64
|
-
"product associated with the A1-A3 life cycle stage."
|
67
|
+
"product associated with the A1-A3 life cycle stage.",
|
68
|
+
default=None,
|
65
69
|
)
|
66
|
-
kg_per_declared_unit: AmountMass | None =
|
70
|
+
kg_per_declared_unit: AmountMass | None = pydantic.Field(
|
67
71
|
default=None,
|
68
72
|
description="Mass of the product, in kilograms, per declared unit",
|
69
|
-
|
73
|
+
examples=[Amount(qty=12.5, unit="kg").to_serializable(exclude_unset=True)],
|
70
74
|
)
|
71
|
-
compliance: list[Standard] =
|
72
|
-
description="Standard(s) to which this document is compliant.",
|
75
|
+
compliance: list[Standard] = pydantic.Field(
|
76
|
+
description="Standard(s) to which this document is compliant.",
|
77
|
+
default_factory=list,
|
73
78
|
)
|
74
79
|
|
75
80
|
# TODO: add product_alt_names? E.g. ILCD has a list of synonymous names
|
76
|
-
product_classes: dict[str, str | list[str]] =
|
77
|
-
description="List of classifications, including Masterformat and UNSPC",
|
81
|
+
product_classes: dict[str, str | list[str]] = pydantic.Field(
|
82
|
+
description="List of classifications, including Masterformat and UNSPC",
|
83
|
+
default_factory=dict,
|
78
84
|
)
|
79
85
|
|
80
|
-
language: str | None =
|
86
|
+
language: str | None = pydantic.Field(
|
81
87
|
min_length=2,
|
82
88
|
max_length=2,
|
83
|
-
strip_whitespace=True,
|
84
89
|
description="Language this document is captured in, as an ISO 639-1 code",
|
85
|
-
|
90
|
+
examples=["en"],
|
91
|
+
default=None,
|
86
92
|
)
|
87
|
-
private: bool | None =
|
93
|
+
private: bool | None = pydantic.Field(
|
88
94
|
default=False,
|
89
95
|
description="This document's author does not wish the contents published. "
|
90
96
|
"Useful for draft, partial, or confidential declarations. "
|
@@ -94,19 +100,20 @@ class BaseDeclaration(RootDocument, abc.ABC):
|
|
94
100
|
"incomplete EPDs.",
|
95
101
|
)
|
96
102
|
|
97
|
-
pcr: Pcr | None =
|
103
|
+
pcr: Pcr | None = pydantic.Field(
|
98
104
|
description="JSON object for product category rules. Should point to the "
|
99
105
|
"most-specific PCR that applies; the PCR entry should point to any "
|
100
106
|
"parent PCR.",
|
101
107
|
default=None,
|
102
108
|
)
|
103
|
-
lca_discussion: str | None =
|
109
|
+
lca_discussion: str | None = pydantic.Field(
|
104
110
|
max_length=20000,
|
105
111
|
description="""A rich text description containing information for experts reviewing the document contents.
|
106
|
-
Text descriptions required by ISO 14025, ISO 21930, EN 15804
|
112
|
+
Text descriptions required by ISO 14025, ISO 21930, EN 15804, relevant PCRs, or program instructions and which do not
|
107
113
|
have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
|
108
114
|
separated by github flavored markdown formatting.""",
|
109
|
-
|
115
|
+
examples=[
|
116
|
+
"""# Packaging
|
110
117
|
|
111
118
|
Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
|
112
119
|
strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
|
@@ -125,96 +132,118 @@ class BaseDeclaration(RootDocument, abc.ABC):
|
|
125
132
|
Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
|
126
133
|
Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
|
127
134
|
In the absence of primary data, cleaning assumptions shall be documented.
|
128
|
-
"""
|
135
|
+
"""
|
136
|
+
],
|
137
|
+
default=None,
|
129
138
|
)
|
130
139
|
|
131
|
-
product_image_small:
|
132
|
-
description="Pointer to image illustrating the product, which is no more than 200x200 pixels",
|
140
|
+
product_image_small: pydantic.AnyUrl | None = pydantic.Field(
|
141
|
+
description="Pointer to image illustrating the product, which is no more than 200x200 pixels",
|
142
|
+
default=None,
|
133
143
|
)
|
134
|
-
product_image:
|
135
|
-
description="pointer to image illustrating the product no more than 10MB",
|
144
|
+
product_image: pydantic.AnyUrl | pydantic.FileUrl | None = pydantic.Field(
|
145
|
+
description="pointer to image illustrating the product no more than 10MB",
|
146
|
+
default=None,
|
136
147
|
)
|
137
|
-
declaration_url: str | None =
|
148
|
+
declaration_url: str | None = pydantic.Field(
|
138
149
|
description="Link to data object on original registrar's site",
|
139
|
-
|
150
|
+
examples=["https://epd-online.com/EmbeddedEpdList/Download/6029"],
|
151
|
+
default=None,
|
140
152
|
)
|
141
|
-
kg_C_per_declared_unit: AmountMass | None =
|
153
|
+
kg_C_per_declared_unit: AmountMass | None = pydantic.Field(
|
142
154
|
default=None,
|
143
155
|
description="Mass of elemental carbon, per declared unit, contained in the product itself at the manufacturing "
|
144
156
|
"facility gate. Used (among other things) to check a carbon balance or calculate incineration "
|
145
157
|
"emissions. The source of carbon (e.g. biogenic) is not relevant in this field.",
|
146
|
-
|
158
|
+
examples=[Amount(qty=8.76, unit="kgCO2e").to_serializable(exclude_unset=True)],
|
147
159
|
)
|
148
|
-
kg_C_biogenic_per_declared_unit: AmountGWP | None =
|
160
|
+
kg_C_biogenic_per_declared_unit: AmountGWP | None = pydantic.Field(
|
149
161
|
default=None,
|
150
162
|
description="Mass of elemental carbon from biogenic sources, per declared unit, contained in the product "
|
151
163
|
"itself at the manufacturing facility gate. It may be presumed that any biogenic carbon content "
|
152
164
|
"has been accounted for as -44/12 kgCO2e per kg C in stages A1-A3, per EN15804 and ISO 21930.",
|
153
|
-
|
165
|
+
examples=[Amount(qty=8.76, unit="kgCO2e").to_serializable(exclude_unset=True)],
|
154
166
|
)
|
155
|
-
product_service_life_years: float | None =
|
167
|
+
product_service_life_years: float | None = pydantic.Field(
|
156
168
|
gt=0.0009,
|
157
169
|
lt=101,
|
158
170
|
description="Reference service life of the product, in years. Serves as a maximum for replacement interval, "
|
159
171
|
"which may also be constrained by usage or the service life of what the product goes into "
|
160
172
|
"(e.g. a building).",
|
161
|
-
|
173
|
+
examples=[50.0],
|
174
|
+
default=None,
|
162
175
|
)
|
163
176
|
|
177
|
+
@pydantic.field_validator("language", mode="before")
|
178
|
+
def strip_language(cls, value: str | None) -> str | None:
|
179
|
+
"""Strip whitespace from language."""
|
180
|
+
if isinstance(value, str):
|
181
|
+
return value.strip()
|
182
|
+
return value
|
183
|
+
|
164
184
|
|
165
|
-
class AverageDatasetMixin(
|
185
|
+
class AverageDatasetMixin(pydantic.BaseModel, title="Average Dataset"):
|
166
186
|
"""Fields common for average dataset (Industry-wide EPDs, Generic Estimates)."""
|
167
187
|
|
168
|
-
description: str | None =
|
188
|
+
description: str | None = pydantic.Field(
|
169
189
|
max_length=2000,
|
170
190
|
description="1-paragraph description of the average dataset. Supports plain text or github flavored markdown.",
|
191
|
+
default=None,
|
171
192
|
)
|
172
193
|
|
173
|
-
geography: list[Geography] | None =
|
194
|
+
geography: list[Geography] | None = pydantic.Field(
|
174
195
|
description="Jurisdiction(s) in which the LCA result is applicable. An empty array, or absent properties, "
|
175
196
|
"implies global applicability.",
|
197
|
+
default=None,
|
176
198
|
)
|
177
199
|
|
178
|
-
specs: SpecsRange | None =
|
179
|
-
default=None,
|
200
|
+
specs: SpecsRange | None = pydantic.Field(
|
201
|
+
default=None,
|
202
|
+
description="Average dataset material performance specifications.",
|
180
203
|
)
|
181
204
|
|
182
205
|
|
183
|
-
class WithProgramOperatorMixin(
|
206
|
+
class WithProgramOperatorMixin(pydantic.BaseModel):
|
184
207
|
"""Object which has a connection to ProgramOperator."""
|
185
208
|
|
186
|
-
program_operator: Org | None =
|
187
|
-
program_operator_doc_id: str | None =
|
188
|
-
max_length=200,
|
209
|
+
program_operator: Org | None = pydantic.Field(description=PROGRAM_OPERATOR_DESCRIPTION, default=None)
|
210
|
+
program_operator_doc_id: str | None = pydantic.Field(
|
211
|
+
max_length=200,
|
212
|
+
description="Document identifier from Program Operator.",
|
213
|
+
examples=["123-456.789/b"],
|
214
|
+
default=None,
|
189
215
|
)
|
190
|
-
program_operator_version: str | None =
|
191
|
-
max_length=200, description="Document version number from Program Operator.",
|
216
|
+
program_operator_version: str | None = pydantic.Field(
|
217
|
+
max_length=200, description="Document version number from Program Operator.", examples=["4.3.0"], default=None
|
192
218
|
)
|
193
219
|
|
194
220
|
|
195
|
-
class WithVerifierMixin(
|
221
|
+
class WithVerifierMixin(pydantic.BaseModel):
|
196
222
|
"""Set of fields related to verifier."""
|
197
223
|
|
198
|
-
third_party_verifier: Org | None =
|
199
|
-
third_party_verification_url:
|
224
|
+
third_party_verifier: Org | None = pydantic.Field(description=THIRD_PARTY_VERIFIER_DESCRIPTION, default=None)
|
225
|
+
third_party_verification_url: pydantic.AnyUrl | None = pydantic.Field(
|
200
226
|
description="Optional link to a verification statement.",
|
201
|
-
|
227
|
+
examples=["https://we-verify-epds.com/en/letters/123-456.789b.pdf"],
|
228
|
+
default=None,
|
202
229
|
)
|
203
|
-
third_party_verifier_email:
|
204
|
-
description="Email address of the third party verifier",
|
230
|
+
third_party_verifier_email: pydantic.EmailStr | None = pydantic.Field(
|
231
|
+
description="Email address of the third party verifier",
|
232
|
+
examples=["john.doe@example.com"],
|
233
|
+
default=None,
|
205
234
|
)
|
206
235
|
|
207
236
|
|
208
|
-
class WithEpdDeveloperMixin(
|
237
|
+
class WithEpdDeveloperMixin(pydantic.BaseModel):
|
209
238
|
"""Set of fields related to EPD Developer."""
|
210
239
|
|
211
|
-
epd_developer: Org | None =
|
240
|
+
epd_developer: Org | None = pydantic.Field(
|
212
241
|
description=DEVELOPER_DESCRIPTION,
|
213
242
|
default=None,
|
214
243
|
)
|
215
|
-
epd_developer_email:
|
244
|
+
epd_developer_email: pydantic.EmailStr | None = pydantic.Field(
|
216
245
|
default=None,
|
217
|
-
|
246
|
+
examples=["john.doe@we-do-lca.com"],
|
218
247
|
description="Email contact for inquiries about development of this EPD. "
|
219
248
|
"This must be an email which can be publicly shared.",
|
220
249
|
)
|
@@ -223,18 +252,18 @@ class WithEpdDeveloperMixin(pyd.BaseModel):
|
|
223
252
|
class RefBase(BaseOpenEpdSchema, title="Ref Object"):
|
224
253
|
"""Base class for reference-style objects."""
|
225
254
|
|
226
|
-
id: OpenXpdUUID | None =
|
255
|
+
id: OpenXpdUUID | None = pydantic.Field(
|
227
256
|
description="The unique ID for this object. To ensure global uniqueness, should be registered at "
|
228
257
|
"open-xpd-uuid.cqd.io/register or a coordinating registry.",
|
229
|
-
|
258
|
+
examples=["1u7zsed8"],
|
230
259
|
default=None,
|
231
260
|
)
|
232
261
|
|
233
|
-
name: str | None =
|
262
|
+
name: str | None = pydantic.Field(max_length=200, description="Name of the object", default=None)
|
234
263
|
|
235
|
-
ref: ReferenceStr | None =
|
264
|
+
ref: ReferenceStr | None = pydantic.Field(
|
236
265
|
default=None,
|
237
|
-
|
266
|
+
examples=["https://openepd.buildingtransparency.org/api/generic_estimates/EC300001"],
|
238
267
|
description="Reference to this JSON object",
|
239
268
|
)
|
240
269
|
|