openepd 2.0.0__py3-none-any.whl → 3.1.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.
Files changed (95) hide show
  1. openepd/__init__.py +1 -1
  2. openepd/__version__.py +2 -2
  3. openepd/api/__init__.py +19 -0
  4. openepd/api/base_sync_client.py +550 -0
  5. openepd/api/category/__init__.py +19 -0
  6. openepd/api/category/dto.py +25 -0
  7. openepd/api/category/sync_api.py +44 -0
  8. openepd/api/common.py +239 -0
  9. openepd/api/dto/__init__.py +19 -0
  10. openepd/api/dto/base.py +41 -0
  11. openepd/api/dto/common.py +115 -0
  12. openepd/api/dto/meta.py +69 -0
  13. openepd/api/dto/mf.py +59 -0
  14. openepd/api/dto/params.py +19 -0
  15. openepd/api/epd/__init__.py +19 -0
  16. openepd/api/epd/dto.py +121 -0
  17. openepd/api/epd/sync_api.py +105 -0
  18. openepd/api/errors.py +86 -0
  19. openepd/api/pcr/__init__.py +19 -0
  20. openepd/api/pcr/dto.py +41 -0
  21. openepd/api/pcr/sync_api.py +49 -0
  22. openepd/api/sync_client.py +67 -0
  23. openepd/api/test/__init__.py +19 -0
  24. openepd/bundle/__init__.py +1 -1
  25. openepd/bundle/base.py +1 -1
  26. openepd/bundle/model.py +5 -6
  27. openepd/bundle/reader.py +5 -5
  28. openepd/bundle/writer.py +5 -4
  29. openepd/compat/__init__.py +19 -0
  30. openepd/compat/pydantic.py +29 -0
  31. openepd/model/__init__.py +1 -1
  32. openepd/model/base.py +114 -15
  33. openepd/model/category.py +39 -0
  34. openepd/model/common.py +33 -25
  35. openepd/model/epd.py +97 -78
  36. openepd/model/factory.py +48 -0
  37. openepd/model/lcia.py +24 -13
  38. openepd/model/org.py +28 -18
  39. openepd/model/pcr.py +42 -14
  40. openepd/model/specs/README.md +19 -0
  41. openepd/model/specs/__init__.py +72 -4
  42. openepd/model/specs/aluminium.py +67 -0
  43. openepd/model/specs/asphalt.py +87 -0
  44. openepd/model/specs/base.py +60 -0
  45. openepd/model/specs/concrete.py +288 -24
  46. openepd/model/specs/generated/accessories.py +63 -0
  47. openepd/model/specs/generated/aggregates.py +71 -0
  48. openepd/model/specs/generated/aluminium.py +66 -0
  49. openepd/model/specs/generated/asphalt.py +86 -0
  50. openepd/model/specs/generated/bulk_materials.py +26 -0
  51. openepd/model/specs/generated/cast_decks_and_underlayment.py +26 -0
  52. openepd/model/specs/generated/cladding.py +214 -0
  53. openepd/model/specs/generated/cmu.py +46 -0
  54. openepd/model/specs/generated/common.py +27 -0
  55. openepd/model/specs/generated/concrete.py +151 -0
  56. openepd/model/specs/generated/conveying_equipment.py +57 -0
  57. openepd/model/specs/generated/electrical.py +297 -0
  58. openepd/model/specs/generated/electrical_transmission_and_distribution_equipment.py +63 -0
  59. openepd/model/specs/generated/electricity.py +26 -0
  60. openepd/model/specs/generated/enums.py +2420 -0
  61. openepd/model/specs/generated/finishes.py +519 -0
  62. openepd/model/specs/generated/fire_and_smoke_protection.py +79 -0
  63. openepd/model/specs/generated/furnishings.py +95 -0
  64. openepd/model/specs/generated/grouting.py +26 -0
  65. openepd/model/specs/generated/manufacturing_inputs.py +131 -0
  66. openepd/model/specs/generated/masonry.py +77 -0
  67. openepd/model/specs/generated/material_handling.py +35 -0
  68. openepd/model/specs/generated/mechanical.py +271 -0
  69. openepd/model/specs/generated/mechanical_insulation.py +41 -0
  70. openepd/model/specs/generated/network_infrastructure.py +181 -0
  71. openepd/model/specs/generated/openings.py +423 -0
  72. openepd/model/specs/generated/other_electrical_equipment.py +26 -0
  73. openepd/model/specs/generated/other_materials.py +123 -0
  74. openepd/model/specs/generated/plumbing.py +153 -0
  75. openepd/model/specs/generated/precast_concrete.py +68 -0
  76. openepd/model/specs/generated/sheathing.py +74 -0
  77. openepd/model/specs/generated/steel.py +224 -0
  78. openepd/model/specs/generated/thermal_moisture_protection.py +233 -0
  79. openepd/model/specs/generated/utility_piping.py +65 -0
  80. openepd/model/specs/generated/wood.py +167 -0
  81. openepd/model/specs/generated/wood_joists.py +38 -0
  82. openepd/model/specs/glass.py +360 -0
  83. openepd/model/specs/steel.py +184 -0
  84. openepd/model/specs/wood.py +130 -0
  85. openepd/model/standard.py +2 -3
  86. openepd/model/validation/__init__.py +19 -0
  87. openepd/model/validation/common.py +59 -0
  88. openepd/model/validation/numbers.py +26 -0
  89. openepd/model/validation/quantity.py +132 -0
  90. openepd/model/versioning.py +129 -0
  91. {openepd-2.0.0.dist-info → openepd-3.1.0.dist-info}/METADATA +36 -5
  92. openepd-3.1.0.dist-info/RECORD +95 -0
  93. openepd-2.0.0.dist-info/RECORD +0 -22
  94. {openepd-2.0.0.dist-info → openepd-3.1.0.dist-info}/LICENSE +0 -0
  95. {openepd-2.0.0.dist-info → openepd-3.1.0.dist-info}/WHEEL +0 -0
openepd/model/epd.py CHANGED
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2023 by C Change Labs Inc. www.c-change-labs.com
2
+ # Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -20,42 +20,48 @@
20
20
  import datetime
21
21
  from typing import Annotated
22
22
 
23
- import pydantic as pyd
24
-
25
- from openepd.model.base import BaseOpenEpdSchema
23
+ from openepd.compat.pydantic import pyd
24
+ from openepd.model.base import BaseDocumentFactory, RootDocument
26
25
  from openepd.model.common import Amount, Ingredient, WithAltIdsMixin, WithAttachmentsMixin
27
26
  from openepd.model.lcia import Impacts, OutputFlowSet, ResourceUseSet
28
27
  from openepd.model.org import Org, Plant
29
28
  from openepd.model.pcr import Pcr
30
29
  from openepd.model.specs import Specs
31
30
  from openepd.model.standard import Standard
31
+ from openepd.model.versioning import OpenEpdVersions, Version
32
+
33
+
34
+ class BaseEpd(RootDocument):
35
+ """
36
+ Base class for EPD documents.
32
37
 
38
+ This class should not be used directly. Use Epd or EpdVx instead.
39
+ """
40
+
41
+ pass
33
42
 
34
- class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
43
+
44
+ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseEpd):
35
45
  """Represent an EPD."""
36
46
 
47
+ _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
48
+
37
49
  # TODO: Add validator for open-xpd-uuid on this field
38
50
  id: str | None = pyd.Field(
39
51
  description="The unique ID for this EPD. To ensure global uniqueness, should be registered at "
40
52
  "open-xpd-uuid.cqd.io/register or a coordinating registry.",
53
+ example="1u7zsed8",
41
54
  default=None,
42
- json_schema_extra=dict(example="1u7zsed8"),
43
- )
44
- doctype: str = pyd.Field(
45
- description='Describes the type and schema of the document. Must always always read "openEPD".',
46
- default="OpenEPD",
47
55
  )
48
56
  product_name: str | None = pyd.Field(
49
- max_length=200,
50
- description="The name of the product described by this EPD",
51
- json_schema_extra=dict(example="Mix 12345AC"),
57
+ max_length=200, description="The name of the product described by this EPD", example="Mix 12345AC", default=None
52
58
  )
53
59
  product_sku: str | None = pyd.Field(
54
- default=None, max_length=200, description="Unique stock keeping identifier assigned by manufacturer"
60
+ max_length=200, description="Unique stock keeping identifier assigned by manufacturer"
55
61
  )
56
62
  product_description: str | None = pyd.Field(
57
63
  max_length=2000,
58
- description="1-paragraph description of product. " "Supports plain text or github flavored markdown.",
64
+ description="1-paragraph description of product. Supports plain text or github flavored markdown.",
59
65
  )
60
66
  # TODO: add product_alt_names? E.g. ILCD has a list of synonymous names
61
67
  product_classes: dict[str, str | list[str]] = pyd.Field(
@@ -64,21 +70,21 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
64
70
  product_image_small: pyd.AnyUrl | None = pyd.Field(
65
71
  description="Pointer to image illustrating the product, which is no more than 200x200 pixels", default=None
66
72
  )
67
- product_image: pyd.AnyUrl | None = pyd.Field(
73
+ product_image: pyd.AnyUrl | pyd.FileUrl | None = pyd.Field(
68
74
  description="pointer to image illustrating the product no more than 10MB", default=None
69
75
  )
70
76
  version: pyd.PositiveInt | None = pyd.Field(
71
77
  description="Version of this document. The document's issuer should increment it anytime even a single "
72
78
  "character changes, as this value is used to determine the most recent version.",
79
+ example=1,
73
80
  default=None,
74
- json_schema_extra=dict(example=1),
75
81
  )
76
82
  language: str | None = pyd.Field(
77
- default=None,
78
83
  min_length=2,
79
84
  max_length=2,
85
+ strip_whitespace=True,
80
86
  description="Language this EPD is captured in, as an ISO 639-1 code",
81
- json_schema_extra=dict(example="en"),
87
+ example="en",
82
88
  )
83
89
  private: bool = pyd.Field(
84
90
  default=False,
@@ -89,66 +95,53 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
89
95
  "number of required fields, to allow for multiple systems to coordinate "
90
96
  "incomplete EPDs.",
91
97
  )
92
- declaration_url: pyd.AnyUrl | None = pyd.Field(
93
- default=None,
98
+ declaration_url: str | None = pyd.Field(
94
99
  description="Link to data object on original registrar's site",
95
- json_schema_extra=dict(example="https://epd-online.com/EmbeddedEpdList/Download/6029"),
100
+ example="https://epd-online.com/EmbeddedEpdList/Download/6029",
96
101
  )
97
102
  manufacturer: Org | None = pyd.Field(
98
- default=None,
99
103
  description="JSON object for declaring Org. Sometimes called the "
100
- '"Declaration Holder" or "Declaration Owner".',
104
+ '"Declaration Holder" or "Declaration Owner".'
101
105
  )
102
106
  epd_developer: Org | None = pyd.Field(
103
- default=None,
104
107
  description="The organization responsible for the underlying LCA (and subsequent summarization as EPD).",
108
+ default=None,
105
109
  )
106
110
  epd_developer_email: pyd.EmailStr | None = pyd.Field(
107
111
  default=None,
112
+ example="john.doe@we-do-lca.com",
108
113
  description="Email contact for inquiries about development of this EPD. "
109
114
  "This must be an email which can be publicly shared.",
110
- json_schema_extra=dict(example="john.doe@we-do-lca.com"),
111
115
  )
112
116
  plants: list[Plant] = pyd.Field(
113
- max_length=32,
117
+ max_items=32,
114
118
  description="List of object(s) for one or more plant(s) that this declaration applies to.",
115
119
  default_factory=list,
116
120
  )
117
121
  program_operator: Org | None = pyd.Field(description="JSON object for program operator Org")
118
122
  program_operator_doc_id: str | None = pyd.Field(
119
- default=None,
120
- max_length=200,
121
- description="Document identifier from Program Operator.",
122
- json_schema_extra=dict(example="123-456.789/b"),
123
+ max_length=200, description="Document identifier from Program Operator.", example="123-456.789/b"
123
124
  )
124
125
  program_operator_version: str | None = pyd.Field(
125
- default=None,
126
- max_length=200,
127
- description="Document version number from Program Operator.",
128
- json_schema_extra=dict(example="4.3.0"),
126
+ max_length=200, description="Document version number from Program Operator.", example="4.3.0"
129
127
  )
130
128
  third_party_verifier: Org | None = pyd.Field(
131
- default=None, description="JSON object for Org that performed a critical review of the EPD data"
129
+ description="JSON object for Org that performed a critical review of the EPD data"
132
130
  )
133
131
  third_party_verification_url: pyd.AnyUrl | None = pyd.Field(
134
- default=None,
135
132
  description="Optional link to a verification statement.",
136
- json_schema_extra=dict(example="https://we-verify-epds.com/en/letters/123-456.789b.pdf"),
133
+ example="https://we-verify-epds.com/en/letters/123-456.789b.pdf",
137
134
  )
138
135
  third_party_verifier_email: pyd.EmailStr | None = pyd.Field(
139
- description="Email address of the third party verifier",
140
- default=None,
141
- json_schema_extra=dict(example="john.doe@example.com"),
136
+ description="Email address of the third party verifier", example="john.doe@example.com", default=None
142
137
  )
143
- date_of_issue: datetime.date | None = pyd.Field(
144
- default=None,
138
+ date_of_issue: datetime.datetime | None = pyd.Field(
139
+ example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
145
140
  description="Date the EPD was issued. This should be the first day on which the EPD is valid.",
146
- json_schema_extra=dict(example=datetime.date(day=11, month=9, year=2019)),
147
141
  )
148
- valid_until: datetime.date | None = pyd.Field(
149
- default=None,
142
+ valid_until: datetime.datetime | None = pyd.Field(
143
+ example=datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.timezone.utc),
150
144
  description="Last date the EPD is valid on, including any extensions.",
151
- json_schema_extra=dict(example=datetime.date(day=11, month=9, year=2028)),
152
145
  )
153
146
  pcr: Pcr | None = pyd.Field(
154
147
  description="JSON object for product category rules. Should point to the "
@@ -164,30 +157,29 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
164
157
  kg_per_declared_unit: Amount | None = pyd.Field(
165
158
  default=None,
166
159
  description="Mass of the product, in kilograms, per declared unit",
167
- json_schema_extra=dict(example=Amount(qty=12.5, unit="kg")),
160
+ example=Amount(qty=12.5, unit="kg"),
168
161
  )
169
162
  kg_C_per_declared_unit: Amount | None = pyd.Field(
170
163
  default=None,
171
164
  description="Mass of elemental carbon, per declared unit, contained in the product itself at the manufacturing "
172
165
  "facility gate. Used (among other things) to check a carbon balance or calculate incineration "
173
166
  "emissions. The source of carbon (e.g. biogenic) is not relevant in this field.",
174
- json_schema_extra=dict(example=Amount(qty=8.76, unit="kg")),
167
+ example=Amount(qty=8.76, unit="kg"),
175
168
  )
176
169
  kg_C_biogenic_per_declared_unit: Amount | None = pyd.Field(
177
170
  default=None,
178
171
  description="Mass of elemental carbon from biogenic sources, per declared unit, contained in the product "
179
172
  "itself at the manufacturing facility gate. It may be presumed that any biogenic carbon content "
180
173
  "has been accounted for as -44/12 kgCO2e per kg C in stages A1-A3, per EN15804 and ISO 21930.",
181
- json_schema_extra=dict(example=Amount(qty=8.76, unit="kg")),
174
+ example=Amount(qty=8.76, unit="kg"),
182
175
  )
183
176
  product_service_life_years: float | None = pyd.Field(
184
- default=None,
185
177
  gt=0.0009,
186
178
  lt=101,
187
179
  description="Reference service life of the product, in years. Serves as a maximum for replacement interval, "
188
180
  "which may also be constrained by usage or the service life of what the product goes into "
189
181
  "(e.g. a building).",
190
- json_schema_extra=dict(example=50.0),
182
+ example=50.0,
191
183
  )
192
184
  annual_production: float | None = pyd.Field(
193
185
  gt=0,
@@ -197,14 +189,18 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
197
189
  "Providing this data is optional, and it is acceptable to round or obfuscate it downwards "
198
190
  "(but not upwards) by any amount desired to protect confidentiality. For example, if the "
199
191
  "product volume is 123,456 m3, a value of 120,000, 100,000 or even 87,654 would be acceptable.",
200
- json_schema_extra=dict(example=10000),
192
+ example=10000,
201
193
  )
202
194
  applicable_in: list[Annotated[str, pyd.Field(min_length=2, max_length=2)]] | None = pyd.Field(
203
- max_length=100,
195
+ max_items=100,
204
196
  default=None,
205
197
  description="Jurisdiction(s) in which EPD is applicable. An empty array, or absent properties, "
206
- "implies global applicability.",
207
- json_schema_extra=dict(example=["US", "CA", "MX"]),
198
+ "implies global applicability. Accepts "
199
+ "[2-letter country codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2), "
200
+ "[M49 region codes](https://unstats.un.org/unsd/methodology/m49/), "
201
+ 'or the alias "EU27" for the 27 members of the Euro bloc, or the alias "NAFTA" '
202
+ "for the members of North American Free Trade Agreement",
203
+ example=["US", "CA", "MX", "EU27", "NAFTA"],
208
204
  )
209
205
  product_usage_description: str | None = pyd.Field(
210
206
  default=None,
@@ -239,41 +235,64 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
239
235
  description="Data structure(s) describing performance specs of product. Unique for each material type.",
240
236
  )
241
237
  includes: list[Ingredient] = pyd.Field(
242
- max_length=255,
238
+ max_items=255,
243
239
  description="List of JSON objects pointing to product components. "
244
240
  "Each one should be an EPD or digitized LCI process.",
245
241
  default_factory=list,
246
242
  )
247
243
  lca_discussion: str | None = pyd.Field(
248
244
  max_length=20000,
249
- description="""A rich text description containing information for experts reviewing the EPD contents.
250
- Text descriptions required by ISO 14025, ISO 21930, EN 15804,, relevant PCRs, or program instructions and which do not
251
- have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
252
- separated by github flavored markdown formatting.""",
253
- json_schema_extra=dict(
254
- example="""# Packaging
255
- Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
256
- strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
257
- scenario assumptions, including disposition pathways for each packaging material by reuse, recycling, or landfill
258
- disposal based on packaging type.*
245
+ description="""A rich text description containing information for experts reviewing the EPD contents.
246
+ Text descriptions required by ISO 14025, ISO 21930, EN 15804,, relevant PCRs, or program instructions and which do not
247
+ have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
248
+ separated by github flavored markdown formatting.""",
249
+ example="""# Packaging
259
250
 
260
- # Product Installation
251
+ Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
252
+ strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
253
+ scenario assumptions, including disposition pathways for each packaging material by reuse, recycling, or landfill
254
+ disposal based on packaging type.*
261
255
 
262
- A description of the type of processing, machinery, tools, dust extraction equipment, auxiliary materials, etc.
263
- to be used during installation shall be included. Information on industrial and environmental protection may be
264
- included in this section. Any waste treatment included within the system boundary of installation waste should be
265
- specified.
256
+ # Product Installation
266
257
 
267
- # Use Conditions
258
+ A description of the type of processing, machinery, tools, dust extraction equipment, auxiliary materials, etc.
259
+ to be used during installation shall be included. Information on industrial and environmental protection may be
260
+ included in this section. Any waste treatment included within the system boundary of installation waste should be
261
+ specified.
268
262
 
269
- Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
270
- Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
271
- In the absence of primary data, cleaning assumptions shall be documented.
272
- """
273
- ),
263
+ # Use Conditions
264
+
265
+ Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
266
+ Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
267
+ In the absence of primary data, cleaning assumptions shall be documented.
268
+ """,
274
269
  )
275
270
 
276
271
  @classmethod
277
272
  def get_asset_type(cls) -> str | None:
278
273
  """Return the asset type of this class (see BaseOpenEpdSchema.get_asset_type for details)."""
279
274
  return "epd"
275
+
276
+ @pyd.validator("compliance", always=True, pre=True)
277
+ def validate_compliance(cls, v: list | None):
278
+ """Handle correctly None values for compliance field."""
279
+ if v is None:
280
+ return []
281
+ return v
282
+
283
+ @pyd.validator("includes", always=True, pre=True)
284
+ def validate_includes(cls, v: list | None):
285
+ """Handle correctly None values for includes field."""
286
+ if v is None:
287
+ return []
288
+ return v
289
+
290
+
291
+ Epd = EpdV0
292
+
293
+
294
+ class EpdFactory(BaseDocumentFactory[BaseEpd]):
295
+ """Factory for EPD objects."""
296
+
297
+ DOCTYPE_CONSTRAINT = "openEPD"
298
+ VERSION_MAP: dict[Version, type[BaseEpd]] = {OpenEpdVersions.Version0: EpdV0}
@@ -0,0 +1,48 @@
1
+ #
2
+ # Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # This software was developed with support from the Skanska USA,
17
+ # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
18
+ # Find out more at www.BuildingTransparency.org
19
+ #
20
+ from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes, RootDocument
21
+ from openepd.model.epd import EpdFactory
22
+
23
+
24
+ class DocumentFactory:
25
+ """A factory for creating documents regardless of the type."""
26
+
27
+ DOCTYPE_TO_FACTORY: dict[str, type[BaseDocumentFactory]] = {
28
+ OpenEpdDoctypes.Epd: EpdFactory,
29
+ }
30
+
31
+ @classmethod
32
+ def from_dict(cls, data: dict) -> RootDocument:
33
+ """
34
+ Create a document from the dictionary.
35
+
36
+ Type of the document will be recognized from the `doctype` field.
37
+ :raise ValueError: if the document type is not specified or not supported.
38
+ """
39
+ doctype = data.get("doctype")
40
+ if doctype is None:
41
+ raise ValueError("The document type is not specified.")
42
+ factory = cls.DOCTYPE_TO_FACTORY.get(doctype)
43
+ if factory is None:
44
+ raise ValueError(
45
+ f"The document of type `{doctype}` is not supported. Supported documents are: "
46
+ + ", ".join(cls.DOCTYPE_TO_FACTORY.keys())
47
+ )
48
+ return factory.from_dict(data)
openepd/model/lcia.py CHANGED
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2023 by C Change Labs Inc. www.c-change-labs.com
2
+ # Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -19,8 +19,7 @@
19
19
  #
20
20
  from enum import StrEnum
21
21
 
22
- import pydantic as pyd
23
-
22
+ from openepd.compat.pydantic import pyd
24
23
  from openepd.model.base import BaseOpenEpdSchema
25
24
  from openepd.model.common import Measurement
26
25
 
@@ -35,14 +34,14 @@ class EolScenario(BaseOpenEpdSchema):
35
34
 
36
35
  name: str = pyd.Field(
37
36
  max_length=40,
37
+ example="Landfill",
38
38
  description="A brief text description of the scenario, preferably from list eol_scenario_names",
39
- json_schema_extra=dict(example="Landfill"),
40
39
  )
41
40
  likelihood: float | None = pyd.Field(
42
41
  description="The weigting of this scenario used in the C1 .. C4 dataset. For example, the overall C1 shoudl be "
43
42
  "equal to weighted sum of C1 from all the scenarios, weighted by likelihood.",
43
+ example=0.33,
44
44
  default=None,
45
- json_schema_extra=dict(example=0.33),
46
45
  )
47
46
  C1: Measurement | None = pyd.Field(
48
47
  description="Deconstruction and Demolition under this scenario",
@@ -296,14 +295,14 @@ class LCIAMethod(StrEnum):
296
295
  return cls.UNKNOWN
297
296
 
298
297
 
299
- class Impacts(pyd.RootModel):
298
+ class Impacts(pyd.BaseModel):
300
299
  """List of environmental impacts, compiled per one of the standard Impact Assessment methods."""
301
300
 
302
- root: dict[LCIAMethod, ImpactSet]
301
+ __root__: dict[LCIAMethod, ImpactSet]
303
302
 
304
303
  def set_unknown_lcia(self, impact_set: ImpactSet):
305
304
  """Set the impact set as an unknown LCIA method."""
306
- self.root[LCIAMethod.UNKNOWN] = impact_set
305
+ self.__root__[LCIAMethod.UNKNOWN] = impact_set
307
306
 
308
307
  def set_impact_set(self, lcia_method: LCIAMethod | str | None, impact_set: ImpactSet):
309
308
  """
@@ -316,25 +315,37 @@ class Impacts(pyd.RootModel):
316
315
  else:
317
316
  if isinstance(lcia_method, str):
318
317
  lcia_method = LCIAMethod.get_by_name(lcia_method)
319
- self.root[lcia_method] = impact_set
318
+ self.__root__[lcia_method] = impact_set
319
+
320
+ def replace_lcia_method(self, lcia_method: LCIAMethod, new_lcia_method: LCIAMethod) -> None:
321
+ """
322
+ Replace the LCIA method.
323
+
324
+ If the there is no impact set for the given LCIA method, do nothing.
325
+ """
326
+ impact_set = self.get_impact_set(lcia_method)
327
+ if impact_set is None:
328
+ return None
329
+ self.set_impact_set(new_lcia_method, impact_set)
330
+ del self.__root__[lcia_method]
320
331
 
321
332
  def get_impact_set(
322
333
  self, lcia_method: LCIAMethod | str | None, default_val: ImpactSet | None = None
323
334
  ) -> ImpactSet | None:
324
335
  """Return the impact set for the given LCIA method."""
325
336
  if lcia_method is None:
326
- return self.root.get(LCIAMethod.UNKNOWN, default_val)
337
+ return self.__root__.get(LCIAMethod.UNKNOWN, default_val)
327
338
  if isinstance(lcia_method, str):
328
339
  lcia_method = LCIAMethod.get_by_name(lcia_method)
329
- return self.root.get(lcia_method, default_val)
340
+ return self.__root__.get(lcia_method, default_val)
330
341
 
331
342
  def available_methods(self) -> set[LCIAMethod]:
332
343
  """Return a list of available LCIA methods."""
333
- return set(self.root.keys())
344
+ return set(self.__root__.keys())
334
345
 
335
346
  def as_dict(self) -> dict[LCIAMethod, ImpactSet]:
336
347
  """Return the impacts as a dictionary."""
337
- return self.root
348
+ return self.__root__
338
349
 
339
350
 
340
351
  class ResourceUseSet(BaseOpenEpdSchema):
openepd/model/org.py CHANGED
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2023 by C Change Labs Inc. www.c-change-labs.com
2
+ # Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -19,14 +19,14 @@
19
19
  #
20
20
  from typing import Annotated, Optional
21
21
 
22
- import pydantic as pyd
23
-
22
+ from openepd.compat.pydantic import pyd
24
23
  from openepd.model.base import BaseOpenEpdSchema
25
24
  from openepd.model.common import Location, WithAltIdsMixin, WithAttachmentsMixin
25
+ from openepd.model.validation.common import ReferenceStr
26
26
 
27
27
 
28
- class Org(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
29
- """Represent an organization."""
28
+ class OrgRef(BaseOpenEpdSchema):
29
+ """Represents Organisation with minimal data."""
30
30
 
31
31
  web_domain: str | None = pyd.Field(
32
32
  description="A web domain owned by organization. Typically is the org's home website address", default=None
@@ -34,20 +34,31 @@ class Org(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
34
34
  name: str | None = pyd.Field(
35
35
  max_length=200,
36
36
  description="Common name for organization",
37
+ example="C Change Labs",
38
+ default=None,
39
+ )
40
+ ref: ReferenceStr | None = pyd.Field(
37
41
  default=None,
38
- json_schema_extra=dict(example="C Change Labs"),
42
+ example="https://buildingtransparency.org/ec3/epds/1u7zsed8",
43
+ description="Reference to this Org's JSON object",
39
44
  )
40
- alt_names: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_length=255)] | None = pyd.Field(
45
+
46
+
47
+ class Org(WithAttachmentsMixin, WithAltIdsMixin, OrgRef):
48
+ """Represent an organization."""
49
+
50
+ alt_names: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
41
51
  description="List of other names for organization",
52
+ example=["C-Change Labs", "C-Change Labs inc."],
42
53
  default=None,
43
- json_schema_extra=dict(example=["C-Change Labs", "C-Change Labs inc."]),
44
54
  )
45
55
  # TODO: NEW field, not in the spec
56
+
46
57
  owner: Optional["Org"] = pyd.Field(description="Organization that controls this organization", default=None)
47
- subsidiaries: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_length=255)] | None = pyd.Field(
58
+ subsidiaries: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
48
59
  description="Organizations controlled by this organization",
60
+ example=["cqd.io", "supplychaincarbonpricing.org"],
49
61
  default=None,
50
- json_schema_extra=dict(example=["cqd.io", "supplychaincarbonpricing.org"]),
51
62
  )
52
63
  hq_location: Location | None = pyd.Field(
53
64
  default=None,
@@ -58,36 +69,35 @@ class Org(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
58
69
  class Plant(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
59
70
  """Represent a manufacturing plant."""
60
71
 
61
- model_config = pyd.ConfigDict(populate_by_name=True)
62
-
63
72
  # TODO: Add proper validator
64
73
  id: str | None = pyd.Field(
65
74
  description="Plus code (aka Open Location Code) of plant's location and "
66
75
  "owner's web domain joined with `.`(dot).",
76
+ example="865P2W3V+3W.interface.com",
67
77
  alias="pluscode",
68
78
  default=None,
69
- json_schema_extra=dict(example="865P2W3V+3W.interface.com"),
70
79
  )
71
80
  owner: Org | None = pyd.Field(description="Organization that owns the plant", default=None)
72
81
  name: str | None = pyd.Field(
73
82
  max_length=200,
74
83
  description="Manufacturer's name for plant. Recommended < 40 chars",
84
+ example="Dalton, GA",
75
85
  default=None,
76
- json_schema_extra=dict(example="Dalton, GA"),
77
86
  )
78
87
  address: str | None = pyd.Field(
79
88
  max_length=200,
80
89
  default=None,
81
90
  description="Text address, preferably geocoded",
82
- json_schema_extra=dict(example="1503 Orchard Hill Rd, LaGrange, GA 30240, United States"),
91
+ example="1503 Orchard Hill Rd, LaGrange, GA 30240, United States",
83
92
  )
84
93
  contact_email: pyd.EmailStr | None = pyd.Field(
85
- description="Email contact",
86
- default=None,
87
- json_schema_extra=dict(example="info@interface.com"),
94
+ description="Email contact", example="info@interface.com", default=None
88
95
  )
89
96
 
90
97
  @classmethod
91
98
  def get_asset_type(cls) -> str | None:
92
99
  """Return the asset type of this class (see BaseOpenEpdSchema.get_asset_type for details)."""
93
100
  return "org"
101
+
102
+ class Config(BaseOpenEpdSchema.Config):
103
+ allow_population_by_field_name = True