openepd 6.2.0__py3-none-any.whl → 6.3.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.
openepd/__version__.py CHANGED
@@ -13,4 +13,4 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- VERSION = "6.2.0"
16
+ VERSION = "6.3.1"
@@ -15,6 +15,7 @@
15
15
  #
16
16
  import abc
17
17
  import datetime
18
+ from enum import StrEnum
18
19
 
19
20
  from openepd.compat.pydantic import pyd
20
21
  from openepd.model.base import BaseOpenEpdSchema, OpenXpdUUID, RootDocument
@@ -50,7 +51,7 @@ class BaseDeclaration(RootDocument, abc.ABC):
50
51
  description="Last date the document is valid on, including any extensions.",
51
52
  )
52
53
 
53
- version: pyd.PositiveInt | None = pyd.Field(
54
+ version: pyd.NonNegativeInt | None = pyd.Field(
54
55
  description="Version of this document. The document's issuer should increment it anytime even a single "
55
56
  "character changes, as this value is used to determine the most recent version.",
56
57
  example=1,
@@ -83,7 +84,7 @@ class BaseDeclaration(RootDocument, abc.ABC):
83
84
  description="Language this document is captured in, as an ISO 639-1 code",
84
85
  example="en",
85
86
  )
86
- private: bool = pyd.Field(
87
+ private: bool | None = pyd.Field(
87
88
  default=False,
88
89
  description="This document's author does not wish the contents published. "
89
90
  "Useful for draft, partial, or confidential declarations. "
@@ -137,7 +138,7 @@ class BaseDeclaration(RootDocument, abc.ABC):
137
138
  description="Link to data object on original registrar's site",
138
139
  example="https://epd-online.com/EmbeddedEpdList/Download/6029",
139
140
  )
140
- kg_C_per_declared_unit: AmountGWP | None = pyd.Field(
141
+ kg_C_per_declared_unit: AmountMass | None = pyd.Field(
141
142
  default=None,
142
143
  description="Mass of elemental carbon, per declared unit, contained in the product itself at the manufacturing "
143
144
  "facility gate. Used (among other things) to check a carbon balance or calculate incineration "
@@ -236,3 +237,75 @@ class RefBase(BaseOpenEpdSchema, title="Ref Object"):
236
237
  example="https://openepd.buildingtransparency.org/api/generic_estimates/EC300001",
237
238
  description="Reference to this JSON object",
238
239
  )
240
+
241
+
242
+ class OriginalDataFormat(StrEnum):
243
+ """
244
+ Original data format for this EPD.
245
+
246
+ A system receiving an EPD via openEPD should preserve this field if it exists. Otherwise, it is up to the
247
+ receiving system to infer the original data format based on the source.
248
+ """
249
+
250
+ OPENEPD_1_0 = "openEPDv1.0"
251
+ """
252
+ Data was generated in an openEPD-compliant system, such as by manual entry in EC3 or generation in an openEPD
253
+ compliant EPD generator.
254
+ """
255
+
256
+ CUSTOM_API = "custom_api"
257
+ """
258
+ Data was gathered from a non-standardized API offered by a data provider. Numerical and text data is very likely
259
+ to be correct and up to date, but important fields may be absent and identification of organizations, plants, and
260
+ addresses may be ambiguous.
261
+ """
262
+
263
+ ILCD_EPD_v1_2 = "ILCD+EPDv1_2"
264
+ """
265
+ Data was generated and published in ILCD+EPD format, version 1.2 Numerical and text data is very likely to be
266
+ correct and up to date, but important fields may be absent and identification of organizations, plants, and
267
+ addresses may be ambiguous.
268
+ """
269
+
270
+ ILCD_EPD_v1_1 = "ILCD+EPDv1_1"
271
+ """
272
+ Data was generated and published in ILCD+EPD format, version 1.1 Numerical and text data is very likely to be
273
+ correct and up to date, but important fields may be absent and identification of organizations, plants, and
274
+ addresses may be ambiguous.
275
+ """
276
+
277
+ ILCD_EPD_v1_0 = "ILCD+EPDv1_0"
278
+ """
279
+ Data was generated and published in ILCD+EPD format, version 1.0 Numerical and text data is very likely to be
280
+ correct and up to date, but important fields may be absent and identification of organizations, plants, and
281
+ addresses may be ambiguous.
282
+ """
283
+
284
+ WEB = "web"
285
+ """
286
+ Data was gathered through comprehension of a viewable HTML page, typically offered by a program operator.
287
+ This is more reliable than PDF, but less reliable than API or a standardized XML like ILCD+EPD.
288
+ """
289
+
290
+ PDF_MANUAL = "pdf_manual"
291
+ """
292
+ Data was entered by hand based on PDF and/or validated by a qualified professional.
293
+ """
294
+
295
+ PDF_MIXED = "pdf_mixed"
296
+ """
297
+ Data is a mix of structured data from an information system, but with significant missing data filled in by
298
+ extraction from a PDF.
299
+ """
300
+
301
+ PDF = "pdf"
302
+ """
303
+ Data was extracted by analyzing or comprehending a PDF document. Includes documents where up to 4 fields (e.g.
304
+ program operator) were inferred from sources outside the PDF. This method is prone to errors and omissions due
305
+ to the many different formats and terms used in these relatively unstructured documents.
306
+ """
307
+
308
+ OTHER = "other"
309
+ """
310
+ Data source is not of a type listed here.
311
+ """
openepd/model/epd.py CHANGED
@@ -13,20 +13,22 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- from typing import Annotated
17
16
 
18
17
  from openepd.compat.pydantic import pyd
19
- from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes
18
+ from openepd.model.base import BaseDocumentFactory, BaseOpenEpdSchema, OpenEpdDoctypes
20
19
  from openepd.model.common import Ingredient, WithAltIdsMixin, WithAttachmentsMixin
21
20
  from openepd.model.declaration import (
22
21
  DEVELOPER_DESCRIPTION,
23
22
  PROGRAM_OPERATOR_DESCRIPTION,
24
23
  THIRD_PARTY_VERIFIER_DESCRIPTION,
25
24
  BaseDeclaration,
25
+ OriginalDataFormat,
26
+ RefBase,
26
27
  WithEpdDeveloperMixin,
27
28
  WithProgramOperatorMixin,
28
29
  WithVerifierMixin,
29
30
  )
31
+ from openepd.model.geography import Geography
30
32
  from openepd.model.lcia import WithLciaMixin
31
33
  from openepd.model.org import Org, Plant
32
34
  from openepd.model.specs.singular import Specs
@@ -37,6 +39,94 @@ MANUFACTURER_DESCRIPTION = (
37
39
  )
38
40
 
39
41
 
42
+ #
43
+ # Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
44
+ #
45
+ # Licensed under the Apache License, Version 2.0 (the "License");
46
+ # you may not use this file except in compliance with the License.
47
+ # You may obtain a copy of the License at
48
+ #
49
+ # http://www.apache.org/licenses/LICENSE-2.0
50
+ #
51
+ # Unless required by applicable law or agreed to in writing, software
52
+ # distributed under the License is distributed on an "AS IS" BASIS,
53
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
54
+ # See the License for the specific language governing permissions and
55
+ # limitations under the License.
56
+ #
57
+ # This software was developed with support from the Skanska USA,
58
+ # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
59
+ # Find out more at www.BuildingTransparency.org
60
+ #
61
+
62
+
63
+ class Ec3EpdExtension(BaseOpenEpdSchema):
64
+ """Extension for EC3 specific fields on openEPD."""
65
+
66
+ # While the extensions should be stored under the 'ext' key - extension point of the BaseOpenepdModel - the EC3
67
+ # extension was started before the introduction of extension management, and so is located at the root of the EPD
68
+ # object.
69
+
70
+ uaGWP_a1a2a3_traci21: float | None = pyd.Field(
71
+ default=None,
72
+ description="""The A1A2A3 uncertainty-adjusted GWP, in kgCO2e per declared unit, calculated for the TRACI 2.1
73
+ LCIA method. This is the value that should be used to compare EPDs against each other, or against an
74
+ uncertainty-adjusted limit/benchmark, once both have been converted to the same declared unit. This is a
75
+ materialized value provided for the convenience of integrators, and can be regenerated at any time from the
76
+ specificity and EC3 category. The value is provided per comparison_unit.""",
77
+ example=22.5,
78
+ )
79
+ uaGWP_a1a2a3_ar5: float | None = pyd.Field(
80
+ default=None,
81
+ description="""The A1A2A3 uncertainty-adjusted GWP, calculated for the IPCC AR5 LCIA method. This is the value
82
+ that should be used to compare EPDs against each other, or against an uncertainty-adjusted limit/benchmark,
83
+ once both have been converted to the same declared unit. This is a materialized value, and can be regenerated
84
+ at any time from the specificity and EC3 category.""",
85
+ example=22.5,
86
+ )
87
+ category: str | None = pyd.Field(
88
+ default=None,
89
+ description="The category of the EPD in EC3 notation. Same as EC3 category from root EPD's product_classes for "
90
+ "EC3.",
91
+ )
92
+ manufacturer_specific: bool | None = pyd.Field(
93
+ default=None,
94
+ description="""An EPD is Manufacturer Specific if it is based on data from a single manufacturer, as opposed
95
+ to an industry group, sector, or generic process. This field should always be true for openEPD documents
96
+ (as opposed to openIndustryEPDs).""",
97
+ )
98
+ plant_specific: bool | None = pyd.Field(
99
+ default=None,
100
+ description="""An EPD is Product Specific if it is based on data regarding the specific product being
101
+ delivered, as opposed to a range of products whose GWP per unit may vary by more than 10%.""",
102
+ )
103
+ product_specific: bool | None = pyd.Field(
104
+ default=None,
105
+ description="""An EPD is Product Specific if it is based on data regarding the specific product being
106
+ delivered, as opposed to a range of products whose GWP per unit may vary by more than 10%.""",
107
+ )
108
+ batch_specific: bool | None = pyd.Field(
109
+ default=None,
110
+ description="""An EPD is Product Specific if it is created with production data for a single production run of
111
+ no more than 90 days. Typically these must be generated on a just-in-time or on-demand basis.""",
112
+ )
113
+ supply_chain_specificity: float | None = pyd.Field(
114
+ default=None,
115
+ description="""An EPD is Supply Chain Specific to the extent that impacts of process inputs are based on
116
+ product-specific, facility-specific EPDs or third-party verified LCA for those inputs. For example, a concrete
117
+ where the cement impacts are based on a plant-specific, product-specific EPD for the actual cement used would
118
+ have around 85% supply chain specificity.""",
119
+ )
120
+
121
+ original_data_format: OriginalDataFormat | None = pyd.Field(default=None)
122
+
123
+
124
+ class EpdRef(RefBase, title="EPD (Ref)"):
125
+ """Reference (short) version of EPD object."""
126
+
127
+ pass
128
+
129
+
40
130
  class EpdPreviewV0(
41
131
  WithAttachmentsMixin,
42
132
  WithProgramOperatorMixin,
@@ -85,7 +175,7 @@ class EpdPreviewV0(
85
175
  "product volume is 123,456 m3, a value of 120,000, 100,000 or even 87,654 would be acceptable.",
86
176
  example=10000,
87
177
  )
88
- applicable_in: list[Annotated[str, pyd.Field(min_length=2, max_length=2)]] | None = pyd.Field(
178
+ applicable_in: list[Geography] | None = pyd.Field(
89
179
  max_items=100,
90
180
  default=None,
91
181
  description="Jurisdiction(s) in which EPD is applicable. An empty array, or absent properties, "
@@ -122,6 +212,7 @@ class EpdPreviewV0(
122
212
  "Each one should be an EPD or digitized LCI process.",
123
213
  default_factory=list,
124
214
  )
215
+ ec3: Ec3EpdExtension | None = pyd.Field(default=None, description="EC3-specific EPD extension.")
125
216
 
126
217
  @pyd.validator("doctype")
127
218
  def validate_doctype(cls, v: str | None) -> str:
openepd/model/lcia.py CHANGED
@@ -104,7 +104,7 @@ class ScopeSet(BaseOpenEpdSchema):
104
104
  )
105
105
  B1_years: float | None = pyd.Field(
106
106
  gt=0,
107
- lt=11,
107
+ lt=100,
108
108
  description="Timeframe over which B1 is evaluated, in years. "
109
109
  "For example, an impact of 1.23 kgCO2e per year for ten years could be B1=1.23kgCO2e, "
110
110
  "B1_years=1.0 or B1=12.3kgCO2e, B1_years=10.0",
@@ -116,7 +116,7 @@ class ScopeSet(BaseOpenEpdSchema):
116
116
  )
117
117
  B2_years: float | None = pyd.Field(
118
118
  gt=0,
119
- lt=11,
119
+ lt=100,
120
120
  description="Predicted Maintenance Impacts over Reference Service Life",
121
121
  default=None,
122
122
  )
@@ -126,7 +126,7 @@ class ScopeSet(BaseOpenEpdSchema):
126
126
  )
127
127
  B3_years: float | None = pyd.Field(
128
128
  gt=0,
129
- lt=11,
129
+ lt=100,
130
130
  description="Timeframe over which B3 is evaluated, in years",
131
131
  default=None,
132
132
  )
@@ -137,7 +137,7 @@ class ScopeSet(BaseOpenEpdSchema):
137
137
  )
138
138
  B4_years: float | None = pyd.Field(
139
139
  gt=0,
140
- lt=11,
140
+ lt=100,
141
141
  description="Timeframe over which B4 is evaluated, in years",
142
142
  default=None,
143
143
  )
@@ -148,7 +148,7 @@ class ScopeSet(BaseOpenEpdSchema):
148
148
  )
149
149
  B5_years: float | None = pyd.Field(
150
150
  gt=0,
151
- lt=11,
151
+ lt=100,
152
152
  description="Timeframe over which B5 is evaluated, in years",
153
153
  default=None,
154
154
  )
@@ -158,7 +158,7 @@ class ScopeSet(BaseOpenEpdSchema):
158
158
  )
159
159
  B6_years: float | None = pyd.Field(
160
160
  gt=0,
161
- lt=11,
161
+ lt=100,
162
162
  description="Timeframe over which B6 is evaluated, in years",
163
163
  default=None,
164
164
  )
@@ -168,7 +168,7 @@ class ScopeSet(BaseOpenEpdSchema):
168
168
  )
169
169
  B7_years: float | None = pyd.Field(
170
170
  gt=0,
171
- lt=11,
171
+ lt=100,
172
172
  description="Timeframe over which B7 is evaluated, in years",
173
173
  default=None,
174
174
  )
openepd/model/org.py CHANGED
@@ -15,6 +15,8 @@
15
15
  #
16
16
  from typing import Annotated, Optional
17
17
 
18
+ from openlocationcode import openlocationcode
19
+
18
20
  from openepd.compat.pydantic import pyd
19
21
  from openepd.model.base import BaseOpenEpdSchema
20
22
  from openepd.model.common import Location, WithAltIdsMixin, WithAttachmentsMixin
@@ -71,6 +73,12 @@ class Plant(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
71
73
  example="865P2W3V+3W.interface.com",
72
74
  default=None,
73
75
  )
76
+ pluscode: str | None = pyd.Field(
77
+ default=None,
78
+ description="(deprecated) Plus code (aka Open Location Code) of plant's location",
79
+ deprecated="Pluscode field is deprecated. If users need a pluscode they can obtain it from "
80
+ "`id` like this: `id.spit('.', maxsplit=1)[0]`",
81
+ )
74
82
  owner: Org | None = pyd.Field(description="Organization that owns the plant", default=None)
75
83
  name: str | None = pyd.Field(
76
84
  max_length=200,
@@ -93,5 +101,19 @@ class Plant(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
93
101
  """Return the asset type of this class (see BaseOpenEpdSchema.get_asset_type for details)."""
94
102
  return "org"
95
103
 
104
+ @pyd.validator("id")
105
+ def _validate_id(cls, v: str) -> str:
106
+ try:
107
+ pluscode, web_domain = v.split(".", maxsplit=1)
108
+ except ValueError as e:
109
+ raise ValueError("Incorrectly formed id: should be pluscode.owner_web_domain") from e
110
+
111
+ if not openlocationcode.isValid(pluscode):
112
+ raise ValueError("Incorrect pluscode for plant")
113
+
114
+ if not web_domain:
115
+ raise ValueError("Incorrect web_domain for plant")
116
+ return v
117
+
96
118
  class Config(BaseOpenEpdSchema.Config):
97
119
  allow_population_by_field_name = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 6.2.0
3
+ Version: 6.3.1
4
4
  Summary: Python library to work with OpenEPD format
5
5
  Home-page: https://github.com/cchangelabs/openepd
6
6
  License: Apache-2.0
@@ -20,6 +20,7 @@ Provides-Extra: api-client
20
20
  Requires-Dist: email-validator (>=1.3.1)
21
21
  Requires-Dist: idna (>=3.7)
22
22
  Requires-Dist: open-xpd-uuid (>=0.2.1,<0.3.0)
23
+ Requires-Dist: openlocationcode (>=1.0.1)
23
24
  Requires-Dist: pydantic (>=1.10,<3.0)
24
25
  Requires-Dist: requests (>=2.0) ; extra == "api-client"
25
26
  Project-URL: Repository, https://github.com/cchangelabs/openepd
@@ -1,5 +1,5 @@
1
1
  openepd/__init__.py,sha256=Shkfh0Kun0YRhmRDw7LkUj2eQL3X-HnP55u2THOEALw,794
2
- openepd/__version__.py,sha256=jAQO98uPb9JBudq_etYXIE_CAsYMQVe2UwsQNueY3Pw,638
2
+ openepd/__version__.py,sha256=9-O6i7qcYZLc-Ty16U1UcqrtWq9-iQfVCVLwVjya99s,638
3
3
  openepd/api/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
4
4
  openepd/api/average_dataset/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
5
5
  openepd/api/average_dataset/generic_estimate_sync_api.py,sha256=mxWwDokEGMe87Px8C_aHvIdVKZVHrEAuVtaSA1zJchU,7953
@@ -36,14 +36,14 @@ openepd/model/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
36
36
  openepd/model/base.py,sha256=OEYNFUTL4BivBNAt_LGowTlDuUjvKMHgf5U5ZBncZwQ,9805
37
37
  openepd/model/category.py,sha256=IQXNGQFQmFZ_H9PRONloX_UOSf1sTMDq1rM1yz8JR0Y,1639
38
38
  openepd/model/common.py,sha256=yx3m9Tqb7WgqW3SaHB3WftQaShD80dxTys2r63rswy8,9700
39
- openepd/model/declaration.py,sha256=Wh5Ni6AG903lmPTV5sS627lNjSazH7km3OIr2lkKKJE,11072
40
- openepd/model/epd.py,sha256=tKT3gCPZXasVd_mCCXLVabZMbuhM9scBEgn7caS_WJM,7488
39
+ openepd/model/declaration.py,sha256=oBJ_v_ESoQhybQIH5S5tmYVkgkoX3gwe3nvFyPqb4uk,13832
40
+ openepd/model/epd.py,sha256=AAksfqbpxBiipwsphPil5MvQouCBDMVcAzvIYkeq5OQ,12070
41
41
  openepd/model/factory.py,sha256=XP7eeQNW5tqwX_4hfuEb3lK6BFQDb4KB0fSN0r8-lCU,2656
42
42
  openepd/model/generic_estimate.py,sha256=bbU0cR4izSqjZcfxUHNbdO4pllqqd8OaUFikrEgCFoA,3992
43
43
  openepd/model/geography.py,sha256=G3Oz3QBw5n-RiSCAv-vAGxrOZBhwIT5rASnPlo9dkcs,42095
44
44
  openepd/model/industry_epd.py,sha256=rgXhCUDAgzZ9eGio7ExqE3ymP3zTXnrrwcIDvg5YP1A,3285
45
- openepd/model/lcia.py,sha256=6aJPmza5BN2YKzSl5IRGVK7lPlBdBFqOJU3977583LY,24383
46
- openepd/model/org.py,sha256=6nIL6XPHnLdw65YynXtQmZUGfJo2UC9jd1j6iPS0pe8,3734
45
+ openepd/model/lcia.py,sha256=EChQO_zcc9ZV0cYHCsjjO24pn5oi8d3WMeUxB1Cj3Ww,24390
46
+ openepd/model/org.py,sha256=xEYeqcqtmyd9fr322iCYwjhUlpRLREfzDe7gdBL7f04,4588
47
47
  openepd/model/pcr.py,sha256=QknLtTn6Y14JORWKQ1qBqGgKnZpbKgqNiYF3Axl4U4c,5494
48
48
  openepd/model/specs/README.md,sha256=UGhSiFJ9hOxT1mZl-5ZrhkOrPKf1W_gcu5CI9hzV7LU,2430
49
49
  openepd/model/specs/__init__.py,sha256=IAevXqqYrCWlTH4z4Fy9o77vaOLinX56G05iIJJfm0M,3094
@@ -133,7 +133,7 @@ openepd/model/validation/quantity.py,sha256=UP6x2nUj1nP7G8e2rpi-HigHx9rwKeCuIDh_
133
133
  openepd/model/versioning.py,sha256=R_zm6rCrgF3vlJQYbpyWhirdS_Oek16cv_mvZmpuE8I,4473
134
134
  openepd/patch_pydantic.py,sha256=xrkzblatmU9HBzukWkp1cPq9ZSuohoz1p0pQqVKSlKs,4122
135
135
  openepd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
- openepd-6.2.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
137
- openepd-6.2.0.dist-info/METADATA,sha256=f6kiHW5bMVEqEfQFVfPhLlKJDSKQcRMmkuIqZ1GiaKY,8996
138
- openepd-6.2.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
139
- openepd-6.2.0.dist-info/RECORD,,
136
+ openepd-6.3.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
137
+ openepd-6.3.1.dist-info/METADATA,sha256=e71TcIfypVycIAtmh7z2JSBoGJSbaP5-RCK_-xEFn2c,9038
138
+ openepd-6.3.1.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
139
+ openepd-6.3.1.dist-info/RECORD,,