openepd 6.13.2__py3-none-any.whl → 7.0.0__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 +4 -4
- 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 +60 -43
- openepd/model/category.py +13 -10
- openepd/model/common.py +100 -55
- 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 +132 -113
- openepd/model/org.py +54 -33
- openepd/model/pcr.py +38 -32
- openepd/model/specs/asphalt.py +31 -22
- openepd/model/specs/base.py +11 -9
- 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 +25 -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 +53 -25
- openepd/model/versioning.py +9 -6
- openepd/patch_pydantic.py +0 -93
- {openepd-6.13.2.dist-info → openepd-7.0.0.dist-info}/METADATA +1 -1
- openepd-7.0.0.dist-info/RECORD +142 -0
- openepd/compat/__init__.py +0 -15
- openepd/compat/compat_functional_validators.py +0 -25
- openepd/compat/pydantic.py +0 -30
- openepd-6.13.2.dist-info/RECORD +0 -145
- {openepd-6.13.2.dist-info → openepd-7.0.0.dist-info}/LICENSE +0 -0
- {openepd-6.13.2.dist-info → openepd-7.0.0.dist-info}/WHEEL +0 -0
openepd/model/common.py
CHANGED
@@ -16,18 +16,23 @@
|
|
16
16
|
from enum import StrEnum
|
17
17
|
from typing import Annotated, Any
|
18
18
|
|
19
|
-
|
19
|
+
import pydantic
|
20
|
+
import pydantic_core
|
21
|
+
|
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
|
-
@
|
35
|
+
@pydantic.model_validator(mode="before")
|
31
36
|
def check_qty_or_unit(cls, values: dict[str, Any]):
|
32
37
|
"""Ensure that qty or unit is provided."""
|
33
38
|
if values.get("qty") is None and values.get("unit") is None:
|
@@ -68,12 +73,13 @@ class Distribution(StrEnum):
|
|
68
73
|
class Measurement(BaseOpenEpdSchema):
|
69
74
|
"""A scientific value with units and uncertainty."""
|
70
75
|
|
71
|
-
mean: float =
|
72
|
-
unit: str | None =
|
73
|
-
rsd:
|
74
|
-
description="Relative standard deviation, i.e. standard_deviation/mean",
|
76
|
+
mean: float = pydantic.Field(description="Mean (expected) value of the measurement")
|
77
|
+
unit: str | None = pydantic.Field(description="Measurement unit")
|
78
|
+
rsd: pydantic.PositiveFloat | None = pydantic.Field(
|
79
|
+
description="Relative standard deviation, i.e. standard_deviation/mean",
|
80
|
+
default=None,
|
75
81
|
)
|
76
|
-
dist: Distribution | None =
|
82
|
+
dist: Distribution | None = pydantic.Field(
|
77
83
|
description="Statistical distribution of the measurement error.", default=None
|
78
84
|
)
|
79
85
|
|
@@ -98,26 +104,29 @@ class Ingredient(BaseOpenEpdSchema):
|
|
98
104
|
gwp_fraction, citation and evidence_type.
|
99
105
|
"""
|
100
106
|
|
101
|
-
qty: float | None =
|
102
|
-
description="Number of declared units of this consumed. Negative values indicate an outflow."
|
107
|
+
qty: float | None = pydantic.Field(
|
108
|
+
description="Number of declared units of this consumed. Negative values indicate an outflow.",
|
109
|
+
default=None,
|
103
110
|
)
|
104
|
-
link:
|
105
|
-
description="Link to this object's OpenEPD declaration. "
|
106
|
-
"An OpenIndustryEPD or OpenLCI link is also acceptable.",
|
111
|
+
link: pydantic.AnyUrl | None = pydantic.Field(
|
112
|
+
description="Link to this object's OpenEPD declaration. An OpenIndustryEPD or OpenLCI link is also acceptable.",
|
107
113
|
default=None,
|
108
114
|
)
|
109
115
|
|
110
|
-
gwp_fraction:
|
116
|
+
gwp_fraction: float | None = pydantic.Field(
|
111
117
|
default=None,
|
112
118
|
description="Fraction of product's A1-A3 GWP this flow represents. This value, along with the specificity of "
|
113
119
|
"the reference, are used to caclulate supply chain specificity.",
|
120
|
+
ge=0,
|
121
|
+
le=1,
|
114
122
|
)
|
115
|
-
evidence_type: IngredientEvidenceTypeEnum | None =
|
116
|
-
default=None,
|
123
|
+
evidence_type: IngredientEvidenceTypeEnum | None = pydantic.Field(
|
124
|
+
default=None,
|
125
|
+
description="Type of evidence used, which can be used to calculate degree of specificity",
|
117
126
|
)
|
118
|
-
citation: str | None =
|
127
|
+
citation: str | None = pydantic.Field(default=None, description="Text citation describing the data source ")
|
119
128
|
|
120
|
-
@
|
129
|
+
@pydantic.model_validator(mode="before")
|
121
130
|
def _validate_evidence(cls, values: dict[str, Any]) -> dict[str, Any]:
|
122
131
|
# gwp_fraction should be backed by some type of evidence (fraction coming from product EPD etc) to be accounted
|
123
132
|
# for in the calculation of uncertainty
|
@@ -133,19 +142,20 @@ class Ingredient(BaseOpenEpdSchema):
|
|
133
142
|
class LatLng(BaseOpenEpdSchema):
|
134
143
|
"""A latitude and longitude."""
|
135
144
|
|
136
|
-
lat: float =
|
137
|
-
lng: float =
|
145
|
+
lat: float = pydantic.Field(description="Latitude", examples=[47.6062])
|
146
|
+
lng: float = pydantic.Field(description="Longitude", examples=[-122.3321])
|
138
147
|
|
139
148
|
|
140
149
|
class Location(BaseOpenEpdSchema):
|
141
150
|
"""A location on the Earth's surface."""
|
142
151
|
|
143
|
-
pluscode: str | None =
|
144
|
-
latlng: LatLng | None =
|
145
|
-
address: str | None =
|
146
|
-
country: str | None =
|
147
|
-
jurisdiction: str | None =
|
148
|
-
default=None,
|
152
|
+
pluscode: str | None = pydantic.Field(default=None, description="Open Location code of this location")
|
153
|
+
latlng: LatLng | None = pydantic.Field(default=None, description="Latitude and longitude of this location")
|
154
|
+
address: str | None = pydantic.Field(default=None, description="Text address, preferably geocoded")
|
155
|
+
country: str | None = pydantic.Field(default=None, description="2-alpha country code")
|
156
|
+
jurisdiction: str | None = pydantic.Field(
|
157
|
+
default=None,
|
158
|
+
description="Province, State, or similar subdivision below the level of a country",
|
149
159
|
)
|
150
160
|
|
151
161
|
|
@@ -173,36 +183,69 @@ ATTACHMENT_KNOWN_KEYS: dict[str, str] = {
|
|
173
183
|
}
|
174
184
|
|
175
185
|
|
176
|
-
class AttachmentDict(dict[str,
|
186
|
+
class AttachmentDict(dict[str, pydantic.AnyUrl]):
|
177
187
|
"""Special form of dict for attachments."""
|
178
188
|
|
179
189
|
@classmethod
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
|
190
|
+
def __get_pydantic_core_schema__(
|
191
|
+
cls, source: type[Any], handler: pydantic.GetCoreSchemaHandler
|
192
|
+
) -> pydantic_core.core_schema.CoreSchema:
|
193
|
+
return pydantic_core.core_schema.no_info_after_validator_function(
|
194
|
+
cls._validate,
|
195
|
+
pydantic_core.core_schema.dict_schema(),
|
196
|
+
serialization=pydantic_core.core_schema.plain_serializer_function_ser_schema(
|
197
|
+
cls._serialize,
|
198
|
+
info_arg=False,
|
199
|
+
return_schema=pydantic_core.core_schema.dict_schema(),
|
200
|
+
),
|
201
|
+
)
|
202
|
+
|
203
|
+
@classmethod
|
204
|
+
def _validate(cls, value: Any) -> "AttachmentDict":
|
205
|
+
# Ensure the input is a dict.
|
206
|
+
if not isinstance(value, dict):
|
207
|
+
raise TypeError("AttachmentDict must be a dict")
|
208
|
+
|
209
|
+
return cls(value)
|
210
|
+
|
211
|
+
@classmethod
|
212
|
+
def _serialize(cls, value: "AttachmentDict") -> dict[str, Any]:
|
213
|
+
# Serialize each AnyUrl value to its string representation.
|
214
|
+
return {k: str(v) for k, v in value.items()}
|
215
|
+
|
216
|
+
@classmethod
|
217
|
+
def __get_pydantic_json_schema__(cls, core_schema: dict[str, Any], handler: Any) -> dict[str, Any]:
|
218
|
+
# Obtain the default schema using the handler.
|
219
|
+
schema = handler(core_schema)
|
220
|
+
|
221
|
+
# Get the description from the default schema (if any)
|
222
|
+
field_description = schema.get("description", "")
|
184
223
|
if field_description:
|
185
224
|
field_description = field_description.strip()
|
186
225
|
if not field_description.endswith("."):
|
187
226
|
field_description += "."
|
188
227
|
field_description += " "
|
189
228
|
|
190
|
-
|
191
|
-
|
229
|
+
# Update the schema with our custom description and properties.
|
230
|
+
schema["description"] = field_description + "Extra properties of string -> URL allowed."
|
231
|
+
schema["properties"] = {
|
192
232
|
k: {"type": "string", "format": "uri", "description": v} for k, v in ATTACHMENT_KNOWN_KEYS.items()
|
193
233
|
}
|
194
|
-
|
234
|
+
schema["additionalProperties"] = True
|
235
|
+
return schema
|
195
236
|
|
196
237
|
|
197
|
-
class WithAttachmentsMixin(
|
238
|
+
class WithAttachmentsMixin(pydantic.BaseModel):
|
198
239
|
"""Mixin for objects that can have attachments."""
|
199
240
|
|
200
|
-
attachments: AttachmentDict =
|
241
|
+
attachments: AttachmentDict | None = pydantic.Field(
|
201
242
|
description="Dict of URLs relevant to this entry",
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
243
|
+
examples=[
|
244
|
+
{
|
245
|
+
"Contact Us": "https://www.c-change-labs.com/en/contact-us/",
|
246
|
+
"LinkedIn": "https://www.linkedin.com/company/c-change-labs/",
|
247
|
+
}
|
248
|
+
],
|
206
249
|
default=None,
|
207
250
|
)
|
208
251
|
|
@@ -218,14 +261,16 @@ class WithAttachmentsMixin(pyd.BaseModel):
|
|
218
261
|
self.set_attachment(name, url)
|
219
262
|
|
220
263
|
|
221
|
-
class WithAltIdsMixin(
|
264
|
+
class WithAltIdsMixin(pydantic.BaseModel):
|
222
265
|
"""Mixin for objects that can have alt_ids."""
|
223
266
|
|
224
|
-
alt_ids: dict[Annotated[str,
|
267
|
+
alt_ids: dict[Annotated[str, pydantic.Field(max_length=200)], str] | None = pydantic.Field(
|
225
268
|
description="Dict identifiers for this entry.",
|
226
|
-
|
227
|
-
|
228
|
-
|
269
|
+
examples=[
|
270
|
+
{
|
271
|
+
"oekobau.dat": "bdda4364-451f-4df2-a68b-5912469ee4c9",
|
272
|
+
}
|
273
|
+
],
|
229
274
|
default=None,
|
230
275
|
)
|
231
276
|
|
@@ -262,7 +307,7 @@ class OpenEPDUnit(StrEnum):
|
|
262
307
|
class RangeBase(BaseOpenEpdSchema):
|
263
308
|
"""Base class for range types having min and max and order between them."""
|
264
309
|
|
265
|
-
@
|
310
|
+
@pydantic.model_validator(mode="before")
|
266
311
|
def _validate_range_bounds(cls, values: dict[str, Any]) -> dict[str, Any]:
|
267
312
|
min_boundary = values.get("min")
|
268
313
|
max_boundary = values.get("max")
|
@@ -274,28 +319,28 @@ class RangeBase(BaseOpenEpdSchema):
|
|
274
319
|
class RangeFloat(RangeBase):
|
275
320
|
"""Structure representing a range of floats."""
|
276
321
|
|
277
|
-
min: float | None =
|
278
|
-
max: float | None =
|
322
|
+
min: float | None = pydantic.Field(default=None, examples=[3.1])
|
323
|
+
max: float | None = pydantic.Field(default=None, examples=[5.8])
|
279
324
|
|
280
325
|
|
281
326
|
class RangeInt(RangeBase):
|
282
327
|
"""Structure representing a range of ints1."""
|
283
328
|
|
284
|
-
min: int | None =
|
285
|
-
max: int | None =
|
329
|
+
min: int | None = pydantic.Field(default=None, examples=[2])
|
330
|
+
max: int | None = pydantic.Field(default=None, examples=[3])
|
286
331
|
|
287
332
|
|
288
333
|
class RangeRatioFloat(RangeFloat):
|
289
334
|
"""Range of ratios (0-1 to 0-10)."""
|
290
335
|
|
291
|
-
min: float | None =
|
292
|
-
max: float | None =
|
336
|
+
min: float | None = pydantic.Field(default=None, examples=[0.2], ge=0, le=1)
|
337
|
+
max: float | None = pydantic.Field(default=None, examples=[0.65], ge=0, le=1)
|
293
338
|
|
294
339
|
|
295
340
|
class RangeAmount(RangeFloat):
|
296
341
|
"""Structure representing a range of quantities."""
|
297
342
|
|
298
|
-
unit: str | None =
|
343
|
+
unit: str | None = pydantic.Field(default=None, description="Unit of the range.")
|
299
344
|
|
300
345
|
|
301
346
|
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
|
|