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.
Files changed (99) hide show
  1. openepd/__init__.py +0 -6
  2. openepd/__version__.py +1 -1
  3. openepd/api/average_dataset/generic_estimate_sync_api.py +11 -10
  4. openepd/api/average_dataset/industry_epd_sync_api.py +9 -8
  5. openepd/api/base_sync_client.py +53 -9
  6. openepd/api/category/sync_api.py +1 -1
  7. openepd/api/dto/base.py +4 -4
  8. openepd/api/dto/common.py +24 -16
  9. openepd/api/dto/meta.py +15 -11
  10. openepd/api/dto/mf.py +9 -8
  11. openepd/api/epd/dto.py +43 -33
  12. openepd/api/epd/sync_api.py +9 -9
  13. openepd/api/pcr/sync_api.py +2 -2
  14. openepd/bundle/model.py +11 -10
  15. openepd/bundle/reader.py +12 -5
  16. openepd/bundle/writer.py +17 -6
  17. openepd/model/base.py +61 -44
  18. openepd/model/category.py +13 -10
  19. openepd/model/common.py +107 -59
  20. openepd/model/declaration.py +93 -64
  21. openepd/model/epd.py +51 -43
  22. openepd/model/generic_estimate.py +28 -13
  23. openepd/model/industry_epd.py +15 -9
  24. openepd/model/lcia.py +161 -136
  25. openepd/model/org.py +70 -37
  26. openepd/model/pcr.py +38 -32
  27. openepd/model/specs/asphalt.py +31 -22
  28. openepd/model/specs/base.py +14 -11
  29. openepd/model/specs/concrete.py +60 -39
  30. openepd/model/specs/range/aggregates.py +9 -9
  31. openepd/model/specs/range/aluminium.py +7 -7
  32. openepd/model/specs/range/asphalt.py +22 -19
  33. openepd/model/specs/range/cladding.py +16 -16
  34. openepd/model/specs/range/cmu.py +10 -9
  35. openepd/model/specs/range/concrete.py +36 -27
  36. openepd/model/specs/range/conveying_equipment.py +16 -15
  37. openepd/model/specs/range/electrical.py +24 -22
  38. openepd/model/specs/range/finishes.py +109 -104
  39. openepd/model/specs/range/fire_and_smoke_protection.py +7 -7
  40. openepd/model/specs/range/furnishings.py +16 -12
  41. openepd/model/specs/range/manufacturing_inputs.py +16 -16
  42. openepd/model/specs/range/masonry.py +16 -16
  43. openepd/model/specs/range/mechanical.py +47 -47
  44. openepd/model/specs/range/mechanical_insulation.py +7 -7
  45. openepd/model/specs/range/network_infrastructure.py +54 -46
  46. openepd/model/specs/range/openings.py +36 -31
  47. openepd/model/specs/range/plumbing.py +15 -13
  48. openepd/model/specs/range/precast_concrete.py +20 -16
  49. openepd/model/specs/range/sheathing.py +18 -18
  50. openepd/model/specs/range/steel.py +27 -25
  51. openepd/model/specs/range/thermal_moisture_protection.py +20 -20
  52. openepd/model/specs/range/utility_piping.py +9 -9
  53. openepd/model/specs/range/wood.py +19 -19
  54. openepd/model/specs/range/wood_joists.py +8 -8
  55. openepd/model/specs/singular/__init__.py +9 -5
  56. openepd/model/specs/singular/aggregates.py +22 -15
  57. openepd/model/specs/singular/aluminium.py +20 -5
  58. openepd/model/specs/singular/asphalt.py +44 -20
  59. openepd/model/specs/singular/cladding.py +38 -23
  60. openepd/model/specs/singular/cmu.py +26 -11
  61. openepd/model/specs/singular/common.py +3 -2
  62. openepd/model/specs/singular/concrete.py +85 -48
  63. openepd/model/specs/singular/conveying_equipment.py +30 -17
  64. openepd/model/specs/singular/deprecated/__init__.py +3 -2
  65. openepd/model/specs/singular/deprecated/concrete.py +68 -33
  66. openepd/model/specs/singular/deprecated/steel.py +28 -15
  67. openepd/model/specs/singular/electrical.py +69 -41
  68. openepd/model/specs/singular/finishes.py +250 -140
  69. openepd/model/specs/singular/fire_and_smoke_protection.py +9 -6
  70. openepd/model/specs/singular/furnishings.py +16 -14
  71. openepd/model/specs/singular/manufacturing_inputs.py +23 -14
  72. openepd/model/specs/singular/masonry.py +66 -21
  73. openepd/model/specs/singular/mechanical.py +48 -47
  74. openepd/model/specs/singular/mechanical_insulation.py +7 -6
  75. openepd/model/specs/singular/mixins/conduit_mixin.py +13 -10
  76. openepd/model/specs/singular/network_infrastructure.py +111 -52
  77. openepd/model/specs/singular/openings.py +127 -95
  78. openepd/model/specs/singular/plumbing.py +15 -12
  79. openepd/model/specs/singular/precast_concrete.py +68 -54
  80. openepd/model/specs/singular/sheathing.py +47 -27
  81. openepd/model/specs/singular/steel.py +69 -45
  82. openepd/model/specs/singular/thermal_moisture_protection.py +36 -20
  83. openepd/model/specs/singular/utility_piping.py +11 -8
  84. openepd/model/specs/singular/wood.py +48 -24
  85. openepd/model/specs/singular/wood_joists.py +19 -6
  86. openepd/model/standard.py +15 -8
  87. openepd/model/validation/common.py +9 -3
  88. openepd/model/validation/numbers.py +0 -13
  89. openepd/model/validation/quantity.py +88 -55
  90. openepd/model/versioning.py +9 -6
  91. {openepd-6.13.2.dist-info → openepd-7.0.1.dist-info}/METADATA +2 -2
  92. openepd-7.0.1.dist-info/RECORD +141 -0
  93. openepd/compat/__init__.py +0 -15
  94. openepd/compat/compat_functional_validators.py +0 -25
  95. openepd/compat/pydantic.py +0 -30
  96. openepd/patch_pydantic.py +0 -108
  97. openepd-6.13.2.dist-info/RECORD +0 -145
  98. {openepd-6.13.2.dist-info → openepd-7.0.1.dist-info}/LICENSE +0 -0
  99. {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 = pyd.Field(description="How much of this in the amount.", ge=0, default=None)
28
- unit: str | None = pyd.Field(description="Which unit. SI units are preferred.", example="kg", default=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
- @pyd.root_validator
31
- def check_qty_or_unit(cls, values: dict[str, Any]):
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
- if values.get("qty") is None and values.get("unit") is None:
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 values
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 = pyd.Field(description="Mean (expected) value of the measurement")
72
- unit: str | None = pyd.Field(description="Measurement unit")
73
- rsd: pyd.PositiveFloat | None = pyd.Field(
74
- description="Relative standard deviation, i.e. standard_deviation/mean", default=None
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 = pyd.Field(
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 = pyd.Field(
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: pyd.AnyUrl | None = pyd.Field(
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: RatioFloat | None = pyd.Field(
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 = pyd.Field(
116
- default=None, description="Type of evidence used, which can be used to calculate degree of specificity"
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 = pyd.Field(default=None, description="Text citation describing the data source ")
130
+ citation: str | None = pydantic.Field(default=None, description="Text citation describing the data source ")
119
131
 
120
- @pyd.root_validator(skip_on_failure=True)
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 = pyd.Field(description="Latitude", example=47.6062)
137
- lng: float = pyd.Field(description="Longitude", example=-122.3321)
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 = pyd.Field(default=None, description="Open Location code of this location")
144
- latlng: LatLng | None = pyd.Field(default=None, description="Latitude and longitude of this location")
145
- address: str | None = pyd.Field(default=None, description="Text address, preferably geocoded")
146
- country: str | None = pyd.Field(default=None, description="2-alpha country code")
147
- jurisdiction: str | None = pyd.Field(
148
- default=None, description="Province, State, or similar subdivision below the level of a country"
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, pyd.AnyUrl]):
189
+ class AttachmentDict(dict[str, pydantic.AnyUrl]):
177
190
  """Special form of dict for attachments."""
178
191
 
179
192
  @classmethod
180
- def __modify_schema__(cls, field_schema: dict[str, Any], field: pyd.fields.ModelField | None):
181
- # This may be generalized later to combine, for example, enum descriptions and field descriptions to provide
182
- # a better result.
183
- field_description = field.field_info.description if field else ""
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
- field_schema["description"] = field_description + "Extra properties of string -> URL allowed."
191
- field_schema["properties"] = {
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
- field_schema["additionalProperties"] = True
237
+ schema["additionalProperties"] = True
238
+ return schema
195
239
 
196
240
 
197
- class WithAttachmentsMixin(pyd.BaseModel):
241
+ class WithAttachmentsMixin(pydantic.BaseModel):
198
242
  """Mixin for objects that can have attachments."""
199
243
 
200
- attachments: AttachmentDict = pyd.Field(
244
+ attachments: AttachmentDict | None = pydantic.Field(
201
245
  description="Dict of URLs relevant to this entry",
202
- example={
203
- "Contact Us": "https://www.c-change-labs.com/en/contact-us/",
204
- "LinkedIn": "https://www.linkedin.com/company/c-change-labs/",
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(pyd.BaseModel):
267
+ class WithAltIdsMixin(pydantic.BaseModel):
222
268
  """Mixin for objects that can have alt_ids."""
223
269
 
224
- alt_ids: dict[Annotated[str, pyd.Field(max_length=200)], str] | None = pyd.Field(
270
+ alt_ids: dict[Annotated[str, pydantic.Field(max_length=200)], str] | None = pydantic.Field(
225
271
  description="Dict identifiers for this entry.",
226
- example={
227
- "oekobau.dat": "bdda4364-451f-4df2-a68b-5912469ee4c9",
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
- @pyd.root_validator
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 = pyd.Field(default=None, example=3.1)
278
- max: float | None = pyd.Field(default=None, example=5.8)
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 = pyd.Field(default=None, example=2)
285
- max: int | None = pyd.Field(default=None, example=3)
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 = pyd.Field(default=None, example=0.2, ge=0, le=1)
292
- max: float | None = pyd.Field(default=None, example=0.65, ge=0, le=1)
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 = pyd.Field(default=None, description="Unit of the range.")
346
+ unit: str | None = pydantic.Field(default=None, description="Unit of the range.")
299
347
 
300
348
 
301
349
  class EnumGroupingAware:
@@ -17,7 +17,8 @@ import abc
17
17
  import datetime
18
18
  from enum import StrEnum
19
19
 
20
- from openepd.compat.pydantic import pyd
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 = pyd.Field(
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
- example="1u7zsed8",
43
+ examples=["1u7zsed8"],
43
44
  default=None,
44
45
  )
45
- date_of_issue: datetime.datetime | None = pyd.Field(
46
- example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
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 = pyd.Field(
50
- example=datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.timezone.utc),
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: pyd.NonNegativeInt | None = pyd.Field(
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
- example=1,
60
+ examples=[1],
58
61
  default=None,
59
62
  )
60
63
 
61
- declared_unit: Amount | None = pyd.Field(
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 = pyd.Field(
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
- example=Amount(qty=12.5, unit="kg").to_serializable(exclude_unset=True),
73
+ examples=[Amount(qty=12.5, unit="kg").to_serializable(exclude_unset=True)],
70
74
  )
71
- compliance: list[Standard] = pyd.Field(
72
- description="Standard(s) to which this document is compliant.", default_factory=list
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]] = pyd.Field(
77
- description="List of classifications, including Masterformat and UNSPC", default_factory=dict
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 = pyd.Field(
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
- example="en",
90
+ examples=["en"],
91
+ default=None,
86
92
  )
87
- private: bool | None = pyd.Field(
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 = pyd.Field(
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 = pyd.Field(
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,, relevant PCRs, or program instructions and which do not
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
- example="""# Packaging
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: pyd.AnyUrl | None = pyd.Field(
132
- description="Pointer to image illustrating the product, which is no more than 200x200 pixels", default=None
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: pyd.AnyUrl | pyd.FileUrl | None = pyd.Field(
135
- description="pointer to image illustrating the product no more than 10MB", default=None
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 = pyd.Field(
148
+ declaration_url: str | None = pydantic.Field(
138
149
  description="Link to data object on original registrar's site",
139
- example="https://epd-online.com/EmbeddedEpdList/Download/6029",
150
+ examples=["https://epd-online.com/EmbeddedEpdList/Download/6029"],
151
+ default=None,
140
152
  )
141
- kg_C_per_declared_unit: AmountMass | None = pyd.Field(
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
- example=Amount(qty=8.76, unit="kgCO2e").to_serializable(exclude_unset=True),
158
+ examples=[Amount(qty=8.76, unit="kgCO2e").to_serializable(exclude_unset=True)],
147
159
  )
148
- kg_C_biogenic_per_declared_unit: AmountGWP | None = pyd.Field(
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
- example=Amount(qty=8.76, unit="kgCO2e").to_serializable(exclude_unset=True),
165
+ examples=[Amount(qty=8.76, unit="kgCO2e").to_serializable(exclude_unset=True)],
154
166
  )
155
- product_service_life_years: float | None = pyd.Field(
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
- example=50.0,
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(pyd.BaseModel, title="Average Dataset"):
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 = pyd.Field(
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 = pyd.Field(
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 = pyd.Field(
179
- default=None, description="Average dataset material performance specifications."
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(pyd.BaseModel):
206
+ class WithProgramOperatorMixin(pydantic.BaseModel):
184
207
  """Object which has a connection to ProgramOperator."""
185
208
 
186
- program_operator: Org | None = pyd.Field(description=PROGRAM_OPERATOR_DESCRIPTION)
187
- program_operator_doc_id: str | None = pyd.Field(
188
- max_length=200, description="Document identifier from Program Operator.", example="123-456.789/b"
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 = pyd.Field(
191
- max_length=200, description="Document version number from Program Operator.", example="4.3.0"
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(pyd.BaseModel):
221
+ class WithVerifierMixin(pydantic.BaseModel):
196
222
  """Set of fields related to verifier."""
197
223
 
198
- third_party_verifier: Org | None = pyd.Field(description=THIRD_PARTY_VERIFIER_DESCRIPTION)
199
- third_party_verification_url: pyd.AnyUrl | None = pyd.Field(
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
- example="https://we-verify-epds.com/en/letters/123-456.789b.pdf",
227
+ examples=["https://we-verify-epds.com/en/letters/123-456.789b.pdf"],
228
+ default=None,
202
229
  )
203
- third_party_verifier_email: pyd.EmailStr | None = pyd.Field(
204
- description="Email address of the third party verifier", example="john.doe@example.com", default=None
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(pyd.BaseModel):
237
+ class WithEpdDeveloperMixin(pydantic.BaseModel):
209
238
  """Set of fields related to EPD Developer."""
210
239
 
211
- epd_developer: Org | None = pyd.Field(
240
+ epd_developer: Org | None = pydantic.Field(
212
241
  description=DEVELOPER_DESCRIPTION,
213
242
  default=None,
214
243
  )
215
- epd_developer_email: pyd.EmailStr | None = pyd.Field(
244
+ epd_developer_email: pydantic.EmailStr | None = pydantic.Field(
216
245
  default=None,
217
- example="john.doe@we-do-lca.com",
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 = pyd.Field(
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
- example="1u7zsed8",
258
+ examples=["1u7zsed8"],
230
259
  default=None,
231
260
  )
232
261
 
233
- name: str | None = pyd.Field(max_length=200, description="Name of the object", default=None)
262
+ name: str | None = pydantic.Field(max_length=200, description="Name of the object", default=None)
234
263
 
235
- ref: ReferenceStr | None = pyd.Field(
264
+ ref: ReferenceStr | None = pydantic.Field(
236
265
  default=None,
237
- example="https://openepd.buildingtransparency.org/api/generic_estimates/EC300001",
266
+ examples=["https://openepd.buildingtransparency.org/api/generic_estimates/EC300001"],
238
267
  description="Reference to this JSON object",
239
268
  )
240
269