openepd 2.0.0__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openepd/__init__.py +1 -1
- openepd/__version__.py +2 -2
- openepd/api/__init__.py +19 -0
- openepd/api/base_sync_client.py +550 -0
- openepd/api/category/__init__.py +19 -0
- openepd/api/category/dto.py +25 -0
- openepd/api/category/sync_api.py +44 -0
- openepd/api/common.py +239 -0
- openepd/api/dto/__init__.py +19 -0
- openepd/api/dto/base.py +41 -0
- openepd/api/dto/common.py +115 -0
- openepd/api/dto/meta.py +69 -0
- openepd/api/dto/mf.py +59 -0
- openepd/api/dto/params.py +19 -0
- openepd/api/epd/__init__.py +19 -0
- openepd/api/epd/dto.py +121 -0
- openepd/api/epd/sync_api.py +105 -0
- openepd/api/errors.py +86 -0
- openepd/api/pcr/__init__.py +19 -0
- openepd/api/pcr/dto.py +41 -0
- openepd/api/pcr/sync_api.py +49 -0
- openepd/api/sync_client.py +67 -0
- openepd/api/test/__init__.py +19 -0
- openepd/bundle/__init__.py +1 -1
- openepd/bundle/base.py +1 -1
- openepd/bundle/model.py +5 -6
- openepd/bundle/reader.py +5 -5
- openepd/bundle/writer.py +5 -4
- openepd/compat/__init__.py +19 -0
- openepd/compat/pydantic.py +29 -0
- openepd/model/__init__.py +1 -1
- openepd/model/base.py +114 -15
- openepd/model/category.py +39 -0
- openepd/model/common.py +33 -25
- openepd/model/epd.py +97 -78
- openepd/model/factory.py +48 -0
- openepd/model/lcia.py +24 -13
- openepd/model/org.py +28 -18
- openepd/model/pcr.py +42 -14
- openepd/model/specs/README.md +19 -0
- openepd/model/specs/__init__.py +20 -4
- openepd/model/specs/aluminium.py +67 -0
- openepd/model/specs/asphalt.py +87 -0
- openepd/model/specs/base.py +60 -0
- openepd/model/specs/concrete.py +453 -23
- openepd/model/specs/glass.py +404 -0
- openepd/model/specs/steel.py +193 -0
- openepd/model/specs/wood.py +130 -0
- openepd/model/standard.py +2 -3
- openepd/model/validation/__init__.py +19 -0
- openepd/model/validation/common.py +59 -0
- openepd/model/validation/numbers.py +26 -0
- openepd/model/validation/quantity.py +131 -0
- openepd/model/versioning.py +129 -0
- {openepd-2.0.0.dist-info → openepd-3.0.0.dist-info}/METADATA +36 -5
- openepd-3.0.0.dist-info/RECORD +59 -0
- openepd-2.0.0.dist-info/RECORD +0 -22
- {openepd-2.0.0.dist-info → openepd-3.0.0.dist-info}/LICENSE +0 -0
- {openepd-2.0.0.dist-info → openepd-3.0.0.dist-info}/WHEEL +0 -0
openepd/model/epd.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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:
|
93
|
-
default=None,
|
98
|
+
declaration_url: str | None = pyd.Field(
|
94
99
|
description="Link to data object on original registrar's site",
|
95
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
144
|
-
|
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.
|
149
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
192
|
+
example=10000,
|
201
193
|
)
|
202
194
|
applicable_in: list[Annotated[str, pyd.Field(min_length=2, max_length=2)]] | None = pyd.Field(
|
203
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
270
|
-
|
271
|
-
|
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}
|
openepd/model/factory.py
ADDED
@@ -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
|
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
|
-
|
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.
|
298
|
+
class Impacts(pyd.BaseModel):
|
300
299
|
"""List of environmental impacts, compiled per one of the standard Impact Assessment methods."""
|
301
300
|
|
302
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|
-
|
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
|
29
|
-
"""
|
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
|
-
|
42
|
+
example="https://buildingtransparency.org/ec3/epds/1u7zsed8",
|
43
|
+
description="Reference to this Org's JSON object",
|
39
44
|
)
|
40
|
-
|
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),
|
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
|
-
|
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
|