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/lcia.py CHANGED
@@ -16,7 +16,10 @@
16
16
  from enum import StrEnum
17
17
  from typing import Any, ClassVar
18
18
 
19
- from openepd.compat.pydantic import pyd
19
+ import pydantic
20
+ from pydantic import ConfigDict
21
+ from typing_extensions import Self
22
+
20
23
  from openepd.model.base import BaseOpenEpdSchema
21
24
  from openepd.model.common import Measurement
22
25
  from openepd.model.validation.quantity import ExternalValidationConfig
@@ -30,34 +33,34 @@ class EolScenario(BaseOpenEpdSchema):
30
33
  otherwise the outcomes can be statistically combined.
31
34
  """
32
35
 
33
- name: str = pyd.Field(
36
+ name: str = pydantic.Field(
34
37
  max_length=40,
35
- example="Landfill",
38
+ examples=["Landfill"],
36
39
  description="A brief text description of the scenario, preferably from list eol_scenario_names",
37
40
  )
38
- likelihood: float | None = pyd.Field(
41
+ likelihood: float | None = pydantic.Field(
39
42
  description="The weigting of this scenario used in the C1 .. C4 dataset. For example, the overall C1 shoudl be "
40
43
  "equal to weighted sum of C1 from all the scenarios, weighted by likelihood.",
41
- example=0.33,
44
+ examples=[0.33],
42
45
  default=None,
43
46
  )
44
- C1: Measurement | None = pyd.Field(
47
+ C1: Measurement | None = pydantic.Field(
45
48
  description="Deconstruction and Demolition under this scenario",
46
49
  default=None,
47
50
  )
48
- C2: Measurement | None = pyd.Field(
51
+ C2: Measurement | None = pydantic.Field(
49
52
  description="Transport to waste processing or disposal under this scenario.",
50
53
  default=None,
51
54
  )
52
- C3: Measurement | None = pyd.Field(
55
+ C3: Measurement | None = pydantic.Field(
53
56
  description="Waste Processing under this scenario",
54
57
  default=None,
55
58
  )
56
- C4: Measurement | None = pyd.Field(
59
+ C4: Measurement | None = pydantic.Field(
57
60
  description="Disposal under this scenario",
58
61
  default=None,
59
62
  )
60
- D: Measurement | None = pyd.Field(
63
+ D: Measurement | None = pydantic.Field(
61
64
  description="Potential net benefits from reuse, recycling, and/or energy recovery beyond "
62
65
  "the system boundary under this scenario",
63
66
  default=None,
@@ -74,35 +77,35 @@ class ScopeSet(BaseOpenEpdSchema):
74
77
 
75
78
  allowed_units: ClassVar[str | tuple[str, ...] | None] = None
76
79
 
77
- A1A2A3: Measurement | None = pyd.Field(
80
+ A1A2A3: Measurement | None = pydantic.Field(
78
81
  description="Sum of A1..A3",
79
82
  default=None,
80
83
  )
81
- A1: Measurement | None = pyd.Field(
84
+ A1: Measurement | None = pydantic.Field(
82
85
  description="Raw Material Supply",
83
86
  default=None,
84
87
  )
85
- A2: Measurement | None = pyd.Field(
88
+ A2: Measurement | None = pydantic.Field(
86
89
  description="Transport to Manufacturing",
87
90
  default=None,
88
91
  )
89
- A3: Measurement | None = pyd.Field(
92
+ A3: Measurement | None = pydantic.Field(
90
93
  description="Manufacturing",
91
94
  default=None,
92
95
  )
93
- A4: Measurement | None = pyd.Field(
96
+ A4: Measurement | None = pydantic.Field(
94
97
  description="Transport to Construction",
95
98
  default=None,
96
99
  )
97
- A5: Measurement | None = pyd.Field(
100
+ A5: Measurement | None = pydantic.Field(
98
101
  description="Construction",
99
102
  default=None,
100
103
  )
101
- B1: Measurement | None = pyd.Field(
104
+ B1: Measurement | None = pydantic.Field(
102
105
  description="Use impacts over Reference Service Life (Predicted)",
103
106
  default=None,
104
107
  )
105
- B1_years: float | None = pyd.Field(
108
+ B1_years: float | None = pydantic.Field(
106
109
  gt=0,
107
110
  lt=100,
108
111
  description="Timeframe over which B1 is evaluated, in years. "
@@ -110,103 +113,106 @@ class ScopeSet(BaseOpenEpdSchema):
110
113
  "B1_years=1.0 or B1=12.3kgCO2e, B1_years=10.0",
111
114
  default=None,
112
115
  )
113
- B2: Measurement | None = pyd.Field(
116
+ B2: Measurement | None = pydantic.Field(
114
117
  description="Predicted Maintenance Impacts over Reference Service Life",
115
118
  default=None,
116
119
  )
117
- B2_years: float | None = pyd.Field(
120
+ B2_years: float | None = pydantic.Field(
118
121
  gt=0,
119
122
  lt=100,
120
123
  description="Predicted Maintenance Impacts over Reference Service Life",
121
124
  default=None,
122
125
  )
123
- B3: Measurement | None = pyd.Field(
126
+ B3: Measurement | None = pydantic.Field(
124
127
  description="Predicted Repair impacts over Reference Service Life",
125
128
  default=None,
126
129
  )
127
- B3_years: float | None = pyd.Field(
130
+ B3_years: float | None = pydantic.Field(
128
131
  gt=0,
129
132
  lt=100,
130
133
  description="Timeframe over which B3 is evaluated, in years",
131
134
  default=None,
132
135
  )
133
- B4: Measurement | None = pyd.Field(
136
+ B4: Measurement | None = pydantic.Field(
134
137
  description="Predicted Replacement Impacts over the Building lifetime "
135
138
  "('Estimated Construction Works lifespan') specified in the PCR.",
136
139
  default=None,
137
140
  )
138
- B4_years: float | None = pyd.Field(
141
+ B4_years: float | None = pydantic.Field(
139
142
  gt=0,
140
143
  lt=100,
141
144
  description="Timeframe over which B4 is evaluated, in years",
142
145
  default=None,
143
146
  )
144
- B5: Measurement | None = pyd.Field(
147
+ B5: Measurement | None = pydantic.Field(
145
148
  description="Predicted Refurbishment Impacts over the Building lifetime "
146
149
  "('Estimated Construction Works lifespan') specified in the PCR.",
147
150
  default=None,
148
151
  )
149
- B5_years: float | None = pyd.Field(
152
+ B5_years: float | None = pydantic.Field(
150
153
  gt=0,
151
154
  lt=100,
152
155
  description="Timeframe over which B5 is evaluated, in years",
153
156
  default=None,
154
157
  )
155
- B6: Measurement | None = pyd.Field(
158
+ B6: Measurement | None = pydantic.Field(
156
159
  description="Predicted Impacts related to Operational Energy Use",
157
160
  default=None,
158
161
  )
159
- B6_years: float | None = pyd.Field(
162
+ B6_years: float | None = pydantic.Field(
160
163
  gt=0,
161
164
  lt=100,
162
165
  description="Timeframe over which B6 is evaluated, in years",
163
166
  default=None,
164
167
  )
165
- B7: Measurement | None = pyd.Field(
168
+ B7: Measurement | None = pydantic.Field(
166
169
  description="Predicted Impacts related to Operational Water Use",
167
170
  default=None,
168
171
  )
169
- B7_years: float | None = pyd.Field(
172
+ B7_years: float | None = pydantic.Field(
170
173
  gt=0,
171
174
  lt=100,
172
175
  description="Timeframe over which B7 is evaluated, in years",
173
176
  default=None,
174
177
  )
175
- C_scenarios: list[EolScenario] | None = pyd.Field(
178
+ C_scenarios: list[EolScenario] | None = pydantic.Field(
176
179
  description="A list of possible end-of-life scenarios, "
177
180
  "for use in analyses where the end-of-life can be predicted.",
178
181
  default=None,
179
182
  )
180
- C1: Measurement | None = pyd.Field(
183
+ C1: Measurement | None = pydantic.Field(
181
184
  description="Deconstruction and Demolition",
182
185
  default=None,
183
186
  )
184
- C2: Measurement | None = pyd.Field(
187
+ C2: Measurement | None = pydantic.Field(
185
188
  description="Transport to waste processing or disposal.",
186
189
  default=None,
187
190
  )
188
- C3: Measurement | None = pyd.Field(
191
+ C3: Measurement | None = pydantic.Field(
189
192
  description="Waste Processing",
190
193
  default=None,
191
194
  )
192
- C4: Measurement | None = pyd.Field(
195
+ C4: Measurement | None = pydantic.Field(
193
196
  description="Disposal",
194
197
  default=None,
195
198
  )
196
- D: Measurement | None = pyd.Field(
199
+ D: Measurement | None = pydantic.Field(
197
200
  default=None,
198
201
  description="Potential net benefits from reuse, recycling, and/or energy recovery beyond the system boundary.",
199
202
  )
200
203
 
201
- @pyd.root_validator
202
- def _unit_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
204
+ model_config = pydantic.ConfigDict(from_attributes=True)
205
+
206
+ @pydantic.model_validator(mode="after")
207
+ def _unit_validator(self) -> Self:
203
208
  all_units = set()
204
209
 
205
- for k, v in values.items():
210
+ for k in self.model_fields:
211
+ v = getattr(self, k, None)
206
212
  if isinstance(v, Measurement):
207
213
  all_units.add(v.unit)
208
214
 
209
- if not cls.allowed_units:
215
+ if not self.allowed_units:
210
216
  # For unknown units - only units should be the same across all measurements (textually)
211
217
  if len(all_units) > 1:
212
218
  raise ValueError("All scopes and measurements should be expressed in the same unit.")
@@ -219,9 +225,9 @@ class ScopeSet(BaseOpenEpdSchema):
219
225
  ExternalValidationConfig.QUANTITY_VALIDATOR.validate_same_dimensionality(first, unit)
220
226
 
221
227
  # can correctly validate unit
222
- if cls.allowed_units is not None and len(all_units) == 1 and ExternalValidationConfig.QUANTITY_VALIDATOR:
228
+ if self.allowed_units is not None and len(all_units) == 1 and ExternalValidationConfig.QUANTITY_VALIDATOR:
223
229
  unit = next(iter(all_units))
224
- allowed_units = cls.allowed_units if isinstance(cls.allowed_units, tuple) else (cls.allowed_units,)
230
+ allowed_units = self.allowed_units if isinstance(self.allowed_units, tuple) else (self.allowed_units,)
225
231
 
226
232
  matched_unit = False
227
233
  for allowed_unit in allowed_units:
@@ -235,7 +241,7 @@ class ScopeSet(BaseOpenEpdSchema):
235
241
  f"'{', '.join(allowed_units)}' is only allowed unit for this scopeset. Provided '{unit}'"
236
242
  )
237
243
 
238
- return values
244
+ return self
239
245
 
240
246
 
241
247
  class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
@@ -273,11 +279,11 @@ class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
273
279
  # probably unknown impact, coming from 'extra' fields
274
280
  return getattr(self, name, None)
275
281
 
276
- @pyd.root_validator(skip_on_failure=True)
282
+ @pydantic.model_validator(mode="before")
277
283
  def _extra_scopeset_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
278
284
  for f in values:
279
285
  # only interested in validating the extra fields
280
- if f in cls.__fields__:
286
+ if f in cls.model_fields:
281
287
  continue
282
288
 
283
289
  # extra impact of an unknown type - engage validation of ScopeSet
@@ -296,125 +302,132 @@ class ScopesetByNameBase(BaseOpenEpdSchema, extra="allow"):
296
302
  class ScopeSetGwp(ScopeSet):
297
303
  """ScopeSet measured in kgCO2e."""
298
304
 
299
- allowed_units = "kgCO2e"
305
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgCO2e"
300
306
 
301
307
 
302
308
  class ScopeSetOdp(ScopeSet):
303
309
  """ScopeSet measured in kgCFC11e."""
304
310
 
305
- allowed_units = "kgCFC11e"
311
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgCFC11e"
306
312
 
307
313
 
308
314
  class ScopeSetAp(ScopeSet):
309
315
  """ScopeSet measured in kgSO2e."""
310
316
 
311
- allowed_units = ("kgSO2e", "molHe")
317
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = ("kgSO2e", "molHe")
312
318
 
313
319
 
314
320
  class ScopeSetEpNe(ScopeSet):
315
321
  """ScopeSet measured in kgNe."""
316
322
 
317
- allowed_units = "kgNe"
323
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgNe"
318
324
 
319
325
 
320
326
  class ScopeSetPocp(ScopeSet):
321
327
  """ScopeSet measured in kgO3e."""
322
328
 
323
- allowed_units = ("kgO3e", "kgNMVOCe")
329
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = ("kgO3e", "kgNMVOCe")
324
330
 
325
331
 
326
332
  class ScopeSetEpFresh(ScopeSet):
327
333
  """ScopeSet measured in kgPO4e."""
328
334
 
329
- allowed_units = "kgPO4e"
335
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kgPO4e"
330
336
 
331
337
 
332
338
  class ScopeSetEpTerr(ScopeSet):
333
339
  """ScopeSet measured in molNe."""
334
340
 
335
- allowed_units = "molNe"
341
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "molNe"
336
342
 
337
343
 
338
344
  class ScopeSetIrp(ScopeSet):
339
345
  """ScopeSet measured in kilo Becquerel equivalent of u235."""
340
346
 
341
- allowed_units = "kBqU235e"
347
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kBqU235e"
342
348
 
343
349
 
344
350
  class ScopeSetCTUh(ScopeSet):
345
351
  """ScopeSet measured in CTUh."""
346
352
 
347
- allowed_units = "CTUh"
353
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "CTUh"
348
354
 
349
355
 
350
356
  class ScopeSetM3Aware(ScopeSet):
351
357
  """ScopeSet measured in m3AWARE Water consumption by AWARE method."""
352
358
 
353
- allowed_units = "m3AWARE"
359
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "m3AWARE"
354
360
 
355
361
 
356
362
  class ScopeSetCTUe(ScopeSet):
357
363
  """ScopeSet measured in CTUe."""
358
364
 
359
- allowed_units = "CTUe"
365
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "CTUe"
360
366
 
361
367
 
362
368
  class ScopeSetDiseaseIncidence(ScopeSet):
363
369
  """ScopeSet measuring disease incidence measured in AnnualPerCapita (cases)."""
364
370
 
365
- allowed_units = "AnnualPerCapita"
371
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "AnnualPerCapita"
366
372
 
367
373
 
368
374
  class ScopeSetMass(ScopeSet):
369
375
  """ScopeSet measuring mass in kg."""
370
376
 
371
- allowed_units = "kg"
377
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "kg"
372
378
 
373
379
 
374
380
  class ScopeSetVolume(ScopeSet):
375
381
  """ScopeSet measuring mass in kg."""
376
382
 
377
- allowed_units = "m3"
383
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "m3"
378
384
 
379
385
 
380
386
  class ScopeSetMassOrVolume(ScopeSet):
381
387
  """ScopeSet measuring mass in kg OR volume in m3, example: radioactive waste."""
382
388
 
383
- allowed_units = ("kg", "m3")
389
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = ("kg", "m3")
384
390
 
385
391
 
386
392
  class ScopeSetEnergy(ScopeSet):
387
393
  """ScopeSet measuring mass in kg."""
388
394
 
389
- allowed_units = "MJ"
395
+ allowed_units: ClassVar[str | tuple[str, ...] | None] = "MJ"
390
396
 
391
397
 
392
398
  class ImpactSet(ScopesetByNameBase):
393
399
  """A set of impacts, such as GWP, ODP, AP, EP, POCP, EP-marine, EP-terrestrial, EP-freshwater, etc."""
394
400
 
395
- gwp: ScopeSetGwp | None = pyd.Field(
401
+ gwp: ScopeSetGwp | None = pydantic.Field(
396
402
  default=None,
397
403
  description="GWP100, calculated per IPCC guidelines. If any CO2 removals are "
398
404
  "part of this figure, the gwp-fossil, gwp-bioganic, gwp-luluc, an "
399
405
  "gwp-nonCO2 fields are required, as is "
400
406
  "kg_C_biogenic_per_declared_unit.",
401
407
  )
402
- odp: ScopeSetOdp | None = pyd.Field(default=None, description="Ozone Depletion Potential")
403
- ap: ScopeSetAp | None = pyd.Field(default=None, description="Acidification Potential")
404
- ep: ScopeSetEpNe | None = pyd.Field(
405
- default=None, description="Eutrophication Potential in Marine Ecosystems. Has the same meaning as ep-marine."
408
+ odp: ScopeSetOdp | None = pydantic.Field(default=None, description="Ozone Depletion Potential")
409
+ ap: ScopeSetAp | None = pydantic.Field(default=None, description="Acidification Potential")
410
+ ep: ScopeSetEpNe | None = pydantic.Field(
411
+ default=None,
412
+ description="Eutrophication Potential in Marine Ecosystems. Has the same meaning as ep-marine.",
413
+ )
414
+ pocp: ScopeSetPocp | None = pydantic.Field(
415
+ default=None, description="Photochemical Smog (Ozone) creation potential"
406
416
  )
407
- pocp: ScopeSetPocp | None = pyd.Field(default=None, description="Photochemical Smog (Ozone) creation potential")
408
- ep_marine: ScopeSetEpNe | None = pyd.Field(
417
+ ep_marine: ScopeSetEpNe | None = pydantic.Field(
409
418
  alias="ep-marine", default=None, description="Has the same meaning as 'ep'"
410
419
  )
411
- ep_fresh: ScopeSetEpFresh | None = pyd.Field(
412
- alias="ep-fresh", default=None, description="Eutrophication Potential in Freshwater Ecosystems"
420
+ ep_fresh: ScopeSetEpFresh | None = pydantic.Field(
421
+ alias="ep-fresh",
422
+ default=None,
423
+ description="Eutrophication Potential in Freshwater Ecosystems",
413
424
  )
414
- ep_terr: ScopeSetEpTerr | None = pyd.Field(
415
- alias="ep-terr", default=None, description="Eutrophication Potential in Terrestrial Ecosystems"
425
+ ep_terr: ScopeSetEpTerr | None = pydantic.Field(
426
+ alias="ep-terr",
427
+ default=None,
428
+ description="Eutrophication Potential in Terrestrial Ecosystems",
416
429
  )
417
- gwp_biogenic: ScopeSetGwp | None = pyd.Field(
430
+ gwp_biogenic: ScopeSetGwp | None = pydantic.Field(
418
431
  alias="gwp-biogenic",
419
432
  default=None,
420
433
  description="Net GWP from removals of atmospheric CO2 into biomass and emissions of CO2 from biomass sources. "
@@ -423,7 +436,7 @@ class ImpactSet(ScopesetByNameBase):
423
436
  "space (similar biome). They must not have been sold, committed, or credited to any other "
424
437
  "product. Harvesting from native forests is handled under GWP_luluc for EN15804.",
425
438
  )
426
- gwp_luluc: ScopeSetGwp | None = pyd.Field(
439
+ gwp_luluc: ScopeSetGwp | None = pydantic.Field(
427
440
  alias="gwp-luluc",
428
441
  default=None,
429
442
  description="Climate change effects related to land use and land use change, for example biogenic carbon "
@@ -431,50 +444,52 @@ class ImpactSet(ScopesetByNameBase):
431
444
  "emissions). All related emissions for native forests are included under this category. "
432
445
  "Uptake for native forests is set to 0 kgCO2 for EN15804.",
433
446
  )
434
- gwp_nonCO2: ScopeSetGwp | None = pyd.Field(
447
+ gwp_nonCO2: ScopeSetGwp | None = pydantic.Field(
435
448
  alias="gwp-nonCO2",
436
449
  default=None,
437
450
  description="GWP from non-CO2, non-fossil sources, such as livestock-sourced CH4 and agricultural N2O.",
438
451
  )
439
- gwp_fossil: ScopeSetGwp | None = pyd.Field(
452
+ gwp_fossil: ScopeSetGwp | None = pydantic.Field(
440
453
  alias="gwp-fossil",
441
454
  default=None,
442
455
  description="Climate change effects due to greenhouse gas emissions originating from the oxidation or "
443
456
  "reduction of fossil fuels or materials containing fossil carbon. [Source: EN15804]",
444
457
  )
445
- WDP: ScopeSetM3Aware | None = pyd.Field(
458
+ WDP: ScopeSetM3Aware | None = pydantic.Field(
446
459
  default=None,
447
460
  description="Deprivation-weighted water consumption, calculated by the AWARE method "
448
461
  "(https://wulca-waterlca.org/aware/what-is-aware)",
449
462
  )
450
- PM: ScopeSetDiseaseIncidence | None = pyd.Field(
463
+ PM: ScopeSetDiseaseIncidence | None = pydantic.Field(
451
464
  default=None,
452
465
  description="Potential incidence of disease due to particulate matter emissions.",
453
466
  )
454
- IRP: ScopeSetIrp | None = pyd.Field(
467
+ IRP: ScopeSetIrp | None = pydantic.Field(
455
468
  default=None,
456
469
  description="Potential ionizing radiation effect on human health, relative to U235.",
457
470
  )
458
- ETP_fw: ScopeSetCTUe | None = pyd.Field(
471
+ ETP_fw: ScopeSetCTUe | None = pydantic.Field(
459
472
  alias="ETP-fw",
460
473
  default=None,
461
474
  description="Ecotoxicity in freshwater, in potential Comparative Toxic Unit for ecosystems.",
462
475
  )
463
- HTP_c: ScopeSetCTUh | None = pyd.Field(
476
+ HTP_c: ScopeSetCTUh | None = pydantic.Field(
464
477
  alias="HTP-c",
465
478
  default=None,
466
479
  description="Human toxicity, cancer effects in potential Comparative Toxic Units for humans.",
467
480
  )
468
- HTP_nc: ScopeSetCTUh | None = pyd.Field(
481
+ HTP_nc: ScopeSetCTUh | None = pydantic.Field(
469
482
  alias="HTP-nc",
470
483
  default=None,
471
484
  description="Human toxicity, noncancer effects in potential Comparative Toxic Units for humans.",
472
485
  )
473
- SQP: ScopeSet | None = pyd.Field(
486
+ SQP: ScopeSet | None = pydantic.Field(
474
487
  default=None,
475
488
  description="Land use related impacts / Soil quality, in potential soil quality parameters.",
476
489
  )
477
490
 
491
+ model_config = pydantic.ConfigDict(from_attributes=True)
492
+
478
493
 
479
494
  class LCIAMethod(StrEnum):
480
495
  """A list of available LCA methods."""
@@ -520,24 +535,31 @@ class LCIAMethod(StrEnum):
520
535
  return cls.UNKNOWN
521
536
 
522
537
 
523
- class Impacts(pyd.BaseModel):
538
+ class Impacts(pydantic.RootModel[dict[LCIAMethod, ImpactSet]]):
524
539
  """List of environmental impacts, compiled per one of the standard Impact Assessment methods."""
525
540
 
526
- class Config:
527
- # pydantic schema generator gets lost in this structure, so we need to re-establish it manually for openapi
528
- schema_extra = {
529
- "properties": {
530
- str(lm): {"description": str(lm), "allOf": [{"$ref": "#/components/schemas/ImpactSet"}]}
531
- for lm in LCIAMethod
532
- },
533
- "additionalProperties": None,
534
- }
535
-
536
- __root__: dict[LCIAMethod, ImpactSet]
541
+ @staticmethod
542
+ def _update_schema_extra(schema, model):
543
+ schema.update(
544
+ {
545
+ "properties": {
546
+ str(lm): {
547
+ "description": str(lm),
548
+ # This is an internal representation of the reference which exists in Pydantic during
549
+ # generation process
550
+ "allOf": [{"$ref": "#/components/schemas/openepd__model__lcia__ImpactSet-Input__1"}],
551
+ }
552
+ for lm in LCIAMethod
553
+ },
554
+ "additionalProperties": None,
555
+ }
556
+ )
557
+
558
+ model_config: ClassVar[ConfigDict] = ConfigDict(json_schema_extra=_update_schema_extra)
537
559
 
538
560
  def set_unknown_lcia(self, impact_set: ImpactSet):
539
561
  """Set the impact set as an unknown LCIA method."""
540
- self.__root__[LCIAMethod.UNKNOWN] = impact_set
562
+ self.root[LCIAMethod.UNKNOWN] = impact_set
541
563
 
542
564
  def set_impact_set(self, lcia_method: LCIAMethod | str | None, impact_set: ImpactSet):
543
565
  """
@@ -550,7 +572,7 @@ class Impacts(pyd.BaseModel):
550
572
  else:
551
573
  if isinstance(lcia_method, str):
552
574
  lcia_method = LCIAMethod.get_by_name(lcia_method)
553
- self.__root__[lcia_method] = impact_set
575
+ self.root[lcia_method] = impact_set
554
576
 
555
577
  def replace_lcia_method(self, lcia_method: LCIAMethod, new_lcia_method: LCIAMethod) -> None:
556
578
  """
@@ -562,95 +584,95 @@ class Impacts(pyd.BaseModel):
562
584
  if impact_set is None:
563
585
  return None
564
586
  self.set_impact_set(new_lcia_method, impact_set)
565
- del self.__root__[lcia_method]
587
+ del self.root[lcia_method]
566
588
 
567
589
  def get_impact_set(
568
590
  self, lcia_method: LCIAMethod | str | None, default_val: ImpactSet | None = None
569
591
  ) -> ImpactSet | None:
570
592
  """Return the impact set for the given LCIA method."""
571
593
  if lcia_method is None:
572
- return self.__root__.get(LCIAMethod.UNKNOWN, default_val)
594
+ return self.root.get(LCIAMethod.UNKNOWN, default_val)
573
595
  if isinstance(lcia_method, str):
574
596
  lcia_method = LCIAMethod.get_by_name(lcia_method)
575
- return self.__root__.get(lcia_method, default_val)
597
+ return self.root.get(lcia_method, default_val)
576
598
 
577
599
  def available_methods(self) -> set[LCIAMethod]:
578
600
  """Return a list of available LCIA methods."""
579
- return set(self.__root__.keys())
601
+ return set(self.root.keys())
580
602
 
581
603
  def as_dict(self) -> dict[LCIAMethod, ImpactSet]:
582
604
  """Return the impacts as a dictionary."""
583
- return self.__root__
605
+ return self.root
584
606
 
585
607
 
586
608
  class ResourceUseSet(ScopesetByNameBase):
587
609
  """A set of resource use indicators, such as RPRec, RPRm, etc."""
588
610
 
589
- RPRec: ScopeSetEnergy | None = pyd.Field(
611
+ RPRec: ScopeSetEnergy | None = pydantic.Field(
590
612
  description="Renewable primary resources used as energy carrier (fuel). "
591
613
  "First use bio-based materials used as an energy source. Hydropower, solar and wind power used "
592
614
  "in the technosphere are also included in this indicator",
593
615
  default=None,
594
616
  )
595
- RPRm: ScopeSetEnergy | None = pyd.Field(
617
+ RPRm: ScopeSetEnergy | None = pydantic.Field(
596
618
  description="Renewable primary resources with energy content used as material. "
597
619
  "First use biobased materials used as materials (e.g. wood, hemp, etc.).",
598
620
  default=None,
599
621
  )
600
- rpre: ScopeSetEnergy | None = pyd.Field(
622
+ rpre: ScopeSetEnergy | None = pydantic.Field(
601
623
  description="Renewable primary energy resources as energy",
602
624
  default=None,
603
625
  )
604
- nrpre: ScopeSetEnergy | None = pyd.Field(
626
+ nrpre: ScopeSetEnergy | None = pydantic.Field(
605
627
  description="Non-renewable primary resources as energy (fuel)",
606
628
  default=None,
607
629
  )
608
- nrprm: ScopeSetEnergy | None = pyd.Field(
630
+ nrprm: ScopeSetEnergy | None = pydantic.Field(
609
631
  description="Non-renewable primary resources as material",
610
632
  default=None,
611
633
  )
612
- fw: ScopeSetVolume | None = pyd.Field(
634
+ fw: ScopeSetVolume | None = pydantic.Field(
613
635
  description="Use of net fresh water",
614
636
  default=None,
615
637
  )
616
- sm: ScopeSetMass | None = pyd.Field(
638
+ sm: ScopeSetMass | None = pydantic.Field(
617
639
  description="Use of secondary materials",
618
640
  default=None,
619
641
  )
620
- rsf: ScopeSetEnergy | None = pyd.Field(
642
+ rsf: ScopeSetEnergy | None = pydantic.Field(
621
643
  description="Use of renewable secondary materials",
622
644
  default=None,
623
645
  )
624
- nrsf: ScopeSetEnergy | None = pyd.Field(
646
+ nrsf: ScopeSetEnergy | None = pydantic.Field(
625
647
  description="Use of non-renewable secondary fuels",
626
648
  default=None,
627
649
  )
628
- re: ScopeSetEnergy | None = pyd.Field(
650
+ re: ScopeSetEnergy | None = pydantic.Field(
629
651
  description="Renewable energy resources",
630
652
  default=None,
631
653
  )
632
- pere: ScopeSetEnergy | None = pyd.Field(
654
+ pere: ScopeSetEnergy | None = pydantic.Field(
633
655
  description="Use of renewable primary energy excluding renewable primary energy resources used as raw materials",
634
656
  default=None,
635
657
  )
636
- perm: ScopeSetEnergy | None = pyd.Field(
658
+ perm: ScopeSetEnergy | None = pydantic.Field(
637
659
  description="Use of renewable primary energy resources used as raw materials",
638
660
  default=None,
639
661
  )
640
- pert: ScopeSetEnergy | None = pyd.Field(
662
+ pert: ScopeSetEnergy | None = pydantic.Field(
641
663
  description="Total use of renewable primary energy resources",
642
664
  default=None,
643
665
  )
644
- penre: ScopeSetEnergy | None = pyd.Field(
666
+ penre: ScopeSetEnergy | None = pydantic.Field(
645
667
  description="Use of non-renewable primary energy excluding "
646
668
  "non-renewable primary energy resources used as raw materials",
647
669
  default=None,
648
670
  )
649
- penrm: ScopeSetEnergy | None = pyd.Field(
671
+ penrm: ScopeSetEnergy | None = pydantic.Field(
650
672
  description="Use of non-renewable primary energy resources used as raw materials",
651
673
  default=None,
652
674
  )
653
- penrt: ScopeSetEnergy | None = pyd.Field(
675
+ penrt: ScopeSetEnergy | None = pydantic.Field(
654
676
  description="Total use of non-renewable primary energy resources",
655
677
  default=None,
656
678
  )
@@ -659,51 +681,51 @@ class ResourceUseSet(ScopesetByNameBase):
659
681
  class OutputFlowSet(ScopesetByNameBase):
660
682
  """A set of output flows, such as waste, emissions, etc."""
661
683
 
662
- twd: ScopeSetMass | None = pyd.Field(
684
+ twd: ScopeSetMass | None = pydantic.Field(
663
685
  description="Total waste disposed",
664
686
  default=None,
665
687
  )
666
- hwd: ScopeSetMass | None = pyd.Field(
688
+ hwd: ScopeSetMass | None = pydantic.Field(
667
689
  description="Hazardous waste disposed",
668
690
  default=None,
669
691
  )
670
- nhwd: ScopeSetMass | None = pyd.Field(
692
+ nhwd: ScopeSetMass | None = pydantic.Field(
671
693
  description="Non-hazardous waste disposed",
672
694
  default=None,
673
695
  )
674
- rwd: ScopeSetMass | None = pyd.Field(
696
+ rwd: ScopeSetMass | None = pydantic.Field(
675
697
  description="Radioactive waste disposed",
676
698
  default=None,
677
699
  )
678
- hlrw: ScopeSetMassOrVolume | None = pyd.Field(
700
+ hlrw: ScopeSetMassOrVolume | None = pydantic.Field(
679
701
  description="High level radioactive waste disposed",
680
702
  default=None,
681
703
  )
682
- illrw: ScopeSetMassOrVolume | None = pyd.Field(
704
+ illrw: ScopeSetMassOrVolume | None = pydantic.Field(
683
705
  description="Intermediate level radioactive waste disposed",
684
706
  default=None,
685
707
  )
686
- cru: ScopeSetMass | None = pyd.Field(
708
+ cru: ScopeSetMass | None = pydantic.Field(
687
709
  description="Components for re-use",
688
710
  default=None,
689
711
  )
690
- mr: ScopeSetMass | None = pyd.Field(
712
+ mr: ScopeSetMass | None = pydantic.Field(
691
713
  description="Recycled materials",
692
714
  default=None,
693
715
  )
694
- mfr: ScopeSetMass | None = pyd.Field(
716
+ mfr: ScopeSetMass | None = pydantic.Field(
695
717
  description="Materials for recycling",
696
718
  default=None,
697
719
  )
698
- mer: ScopeSetMass | None = pyd.Field(
720
+ mer: ScopeSetMass | None = pydantic.Field(
699
721
  description="Materials for energy recovery",
700
722
  default=None,
701
723
  )
702
- ee: ScopeSetEnergy | None = pyd.Field(
724
+ ee: ScopeSetEnergy | None = pydantic.Field(
703
725
  description="Exported energy",
704
726
  default=None,
705
727
  )
706
- eh: ScopeSetEnergy | None = pyd.Field(
728
+ eh: ScopeSetEnergy | None = pydantic.Field(
707
729
  description="Exported heat",
708
730
  default=None,
709
731
  )
@@ -712,16 +734,19 @@ class OutputFlowSet(ScopesetByNameBase):
712
734
  class WithLciaMixin(BaseOpenEpdSchema):
713
735
  """Mixin for LCIA data."""
714
736
 
715
- impacts: Impacts | None = pyd.Field(
737
+ impacts: Impacts | None = pydantic.Field(
716
738
  description="List of environmental impacts, compiled per one of the standard Impact Assessment methods",
717
- example={"TRACI 2.1": {"gwp": {"A1A2A3": {"mean": 22.4, "unit": "kgCO2e"}}}},
739
+ examples=[{"TRACI 2.1": {"gwp": {"A1A2A3": {"mean": 22.4, "unit": "kgCO2e"}}}}],
740
+ default=None,
718
741
  )
719
- resource_uses: ResourceUseSet | None = pyd.Field(
742
+ resource_uses: ResourceUseSet | None = pydantic.Field(
720
743
  description="Set of Resource Use Indicators, over various LCA scopes",
721
- example={"RPRe": {"A1A2A3": {"mean": 12, "unit": "MJ", "rsd": 0.12}}},
744
+ examples=[{"RPRe": {"A1A2A3": {"mean": 12, "unit": "MJ", "rsd": 0.12}}}],
745
+ default=None,
722
746
  )
723
- output_flows: OutputFlowSet | None = pyd.Field(
747
+ output_flows: OutputFlowSet | None = pydantic.Field(
724
748
  description="Set of Waste and Output Flow indicators which describe the waste categories "
725
749
  "and other material output flows derived from the LCI.",
726
- example={"hwd": {"A1A2A3": {"mean": 2300, "unit": "kg", "rsd": 0.22}}},
750
+ examples=[{"hwd": {"A1A2A3": {"mean": 2300, "unit": "kg", "rsd": 0.22}}}],
751
+ default=None,
727
752
  )