openepd 4.5.1__py3-none-any.whl → 4.6.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/__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 = "4.5.1"
16
+ VERSION = "4.6.0"
@@ -0,0 +1,113 @@
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
+ import abc
17
+ import datetime
18
+
19
+ from openepd.compat.pydantic import pyd
20
+ from openepd.model.base import RootDocument
21
+ from openepd.model.common import Amount
22
+ from openepd.model.pcr import Pcr
23
+ from openepd.model.standard import Standard
24
+
25
+
26
+ class BaseDeclaration(RootDocument, abc.ABC):
27
+ """Base class for declaration-related documents (EPDs, Industry-wide EPDs, Generic Estimates)."""
28
+
29
+ # TODO: Add validator for open-xpd-uuid on this field
30
+ id: str | None = pyd.Field(
31
+ description="The unique ID for this document. To ensure global uniqueness, should be registered at "
32
+ "open-xpd-uuid.cqd.io/register or a coordinating registry.",
33
+ example="1u7zsed8",
34
+ default=None,
35
+ )
36
+ date_of_issue: datetime.datetime | None = pyd.Field(
37
+ example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
38
+ description="Date the document was issued. This should be the first day on which the document is valid.",
39
+ )
40
+ valid_until: datetime.datetime | None = pyd.Field(
41
+ example=datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.timezone.utc),
42
+ description="Last date the document is valid on, including any extensions.",
43
+ )
44
+
45
+ declared_unit: Amount | None = pyd.Field(
46
+ description="SI declared unit for this document. If a functional unit is "
47
+ "utilized, the declared unit shall refer to the amount of "
48
+ "product associated with the A1-A3 life cycle stage."
49
+ )
50
+ kg_per_declared_unit: Amount | None = pyd.Field(
51
+ default=None,
52
+ description="Mass of the product, in kilograms, per declared unit",
53
+ example=Amount(qty=12.5, unit="kg"),
54
+ )
55
+ compliance: list[Standard] = pyd.Field(
56
+ description="Standard(s) to which this document is compliant.", default_factory=list
57
+ )
58
+
59
+ # TODO: add product_alt_names? E.g. ILCD has a list of synonymous names
60
+ product_classes: dict[str, str | list[str]] = pyd.Field(
61
+ description="List of classifications, including Masterformat and UNSPC", default_factory=dict
62
+ )
63
+
64
+ language: str | None = pyd.Field(
65
+ min_length=2,
66
+ max_length=2,
67
+ strip_whitespace=True,
68
+ description="Language this document is captured in, as an ISO 639-1 code",
69
+ example="en",
70
+ )
71
+ private: bool = pyd.Field(
72
+ default=False,
73
+ description="This document's author does not wish the contents published. "
74
+ "Useful for draft, partial, or confidential declarations. "
75
+ "How (or whether) privacy is implemented is up to the receiving system. "
76
+ "Null is treated as false (public). Private (draft) entries have a reduced "
77
+ "number of required fields, to allow for multiple systems to coordinate "
78
+ "incomplete EPDs.",
79
+ )
80
+
81
+ pcr: Pcr | None = pyd.Field(
82
+ description="JSON object for product category rules. Should point to the "
83
+ "most-specific PCR that applies; the PCR entry should point to any "
84
+ "parent PCR.",
85
+ default=None,
86
+ )
87
+ lca_discussion: str | None = pyd.Field(
88
+ max_length=20000,
89
+ description="""A rich text description containing information for experts reviewing the document contents.
90
+ Text descriptions required by ISO 14025, ISO 21930, EN 15804,, relevant PCRs, or program instructions and which do not
91
+ have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
92
+ separated by github flavored markdown formatting.""",
93
+ example="""# Packaging
94
+
95
+ Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
96
+ strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
97
+ scenario assumptions, including disposition pathways for each packaging material by reuse, recycling, or landfill
98
+ disposal based on packaging type.*
99
+
100
+ # Product Installation
101
+
102
+ A description of the type of processing, machinery, tools, dust extraction equipment, auxiliary materials, etc.
103
+ to be used during installation shall be included. Information on industrial and environmental protection may be
104
+ included in this section. Any waste treatment included within the system boundary of installation waste should be
105
+ specified.
106
+
107
+ # Use Conditions
108
+
109
+ Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
110
+ Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
111
+ In the absence of primary data, cleaning assumptions shall be documented.
112
+ """,
113
+ )
openepd/model/epd.py CHANGED
@@ -13,129 +13,33 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- import datetime
17
16
  from typing import Annotated
18
17
 
19
18
  from openepd.compat.pydantic import pyd
20
- from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes, RootDocument
19
+ from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes
21
20
  from openepd.model.common import Amount, Ingredient, WithAltIdsMixin, WithAttachmentsMixin
22
- from openepd.model.lcia import Impacts, OutputFlowSet, ResourceUseSet
23
- from openepd.model.org import Org, Plant
24
- from openepd.model.pcr import Pcr
21
+ from openepd.model.declaration import BaseDeclaration
22
+ from openepd.model.lcia import WithLciaMixin
23
+ from openepd.model.org import Org, OrgRef, Plant
25
24
  from openepd.model.specs import Specs
26
- from openepd.model.standard import Standard
27
25
  from openepd.model.versioning import OpenEpdVersions, Version
28
26
 
27
+ MANUFACTURER_DESCRIPTION = (
28
+ 'JSON object for declaring Org. Sometimes called the "Declaration Holder" or "Declaration Owner".'
29
+ )
30
+ DEVELOPER_DESCRIPTION = "The organization responsible for the underlying LCA (and subsequent summarization as EPD)."
31
+ PROGRAM_OPERATOR_DESCRIPTION = "JSON object for program operator Org"
32
+ THIRD_PARTY_VERIFIER_DESCRIPTION = "JSON object for Org that performed a critical review of the EPD data"
29
33
 
30
- class BaseDeclaration(RootDocument):
31
- """
32
- Base class for EPD documents.
33
34
 
34
- This class should not be used directly. Use Epd or EpdVx instead.
35
+ class EpdPreviewV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration, title="EPD (Preview)"):
35
36
  """
37
+ EPD preview, used in API list responses and where there is no need for a full object.
36
38
 
37
- id: str | None = pyd.Field(
38
- description="The unique ID for this EPD. To ensure global uniqueness, should be registered at "
39
- "open-xpd-uuid.cqd.io/register or a coordinating registry.",
40
- example="1u7zsed8",
41
- default=None,
42
- )
43
- date_of_issue: datetime.datetime | None = pyd.Field(
44
- example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
45
- description="Date the EPD was issued. This should be the first day on which the EPD is valid.",
46
- )
47
- valid_until: datetime.datetime | None = pyd.Field(
48
- example=datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.timezone.utc),
49
- description="Last date the EPD is valid on, including any extensions.",
50
- )
51
-
52
- declared_unit: Amount | None = pyd.Field(
53
- description="SI declared unit for this EPD. If a functional unit is "
54
- "utilized, the declared unit shall refer to the amount of "
55
- "product associated with the A1-A3 life cycle stage."
56
- )
57
- kg_per_declared_unit: Amount | None = pyd.Field(
58
- default=None,
59
- description="Mass of the product, in kilograms, per declared unit",
60
- example=Amount(qty=12.5, unit="kg"),
61
- )
62
- compliance: list[Standard] = pyd.Field(
63
- description="Standard(s) to which this declaration is compliant.", default_factory=list
64
- )
65
-
66
- # TODO: add product_alt_names? E.g. ILCD has a list of synonymous names
67
- product_classes: dict[str, str | list[str]] = pyd.Field(
68
- description="List of classifications, including Masterformat and UNSPC", default_factory=dict
69
- )
70
-
71
- language: str | None = pyd.Field(
72
- min_length=2,
73
- max_length=2,
74
- strip_whitespace=True,
75
- description="Language this EPD is captured in, as an ISO 639-1 code",
76
- example="en",
77
- )
78
- private: bool = pyd.Field(
79
- default=False,
80
- description="This document's author does not wish the contents published. "
81
- "Useful for draft, partial, or confidential declarations. "
82
- "How (or whether) privacy is implemented is up to the receiving system. "
83
- "Null is treated as false (public). Private (draft) entries have a reduced "
84
- "number of required fields, to allow for multiple systems to coordinate "
85
- "incomplete EPDs.",
86
- )
87
- impacts: Impacts | None = pyd.Field(
88
- description="List of environmental impacts, compiled per one of the standard Impact Assessment methods"
89
- )
90
- resource_uses: ResourceUseSet | None = pyd.Field(
91
- description="Set of Resource Use Indicators, over various LCA scopes"
92
- )
93
- output_flows: OutputFlowSet | None = pyd.Field(
94
- description="Set of Waste and Output Flow indicators which describe the waste categories "
95
- "and other material output flows derived from the LCI."
96
- )
97
-
98
- pcr: Pcr | None = pyd.Field(
99
- description="JSON object for product category rules. Should point to the "
100
- "most-specific PCR that applies; the PCR entry should point to any "
101
- "parent PCR.",
102
- default=None,
103
- )
104
- lca_discussion: str | None = pyd.Field(
105
- max_length=20000,
106
- description="""A rich text description containing information for experts reviewing the EPD contents.
107
- Text descriptions required by ISO 14025, ISO 21930, EN 15804,, relevant PCRs, or program instructions and which do not
108
- have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
109
- separated by github flavored markdown formatting.""",
110
- example="""# Packaging
111
-
112
- Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
113
- strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
114
- scenario assumptions, including disposition pathways for each packaging material by reuse, recycling, or landfill
115
- disposal based on packaging type.*
116
-
117
- # Product Installation
118
-
119
- A description of the type of processing, machinery, tools, dust extraction equipment, auxiliary materials, etc.
120
- to be used during installation shall be included. Information on industrial and environmental protection may be
121
- included in this section. Any waste treatment included within the system boundary of installation waste should be
122
- specified.
123
-
124
- # Use Conditions
39
+ Excludes LCIA data.
125
40
 
126
- Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
127
- Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
128
- In the absence of primary data, cleaning assumptions shall be documented.
129
- """,
130
- )
131
-
132
-
133
- class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
134
- """Represent an EPD."""
135
-
136
- _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
41
+ """
137
42
 
138
- # TODO: Add validator for open-xpd-uuid on this field
139
43
  product_name: str | None = pyd.Field(
140
44
  max_length=200, description="The name of the product described by this EPD", example="Mix 12345AC", default=None
141
45
  )
@@ -162,12 +66,9 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
162
66
  description="Link to data object on original registrar's site",
163
67
  example="https://epd-online.com/EmbeddedEpdList/Download/6029",
164
68
  )
165
- manufacturer: Org | None = pyd.Field(
166
- description="JSON object for declaring Org. Sometimes called the "
167
- '"Declaration Holder" or "Declaration Owner".'
168
- )
169
- epd_developer: Org | None = pyd.Field(
170
- description="The organization responsible for the underlying LCA (and subsequent summarization as EPD).",
69
+ manufacturer: OrgRef | None = pyd.Field(description=MANUFACTURER_DESCRIPTION)
70
+ epd_developer: OrgRef | None = pyd.Field(
71
+ description=DEVELOPER_DESCRIPTION,
171
72
  default=None,
172
73
  )
173
74
  epd_developer_email: pyd.EmailStr | None = pyd.Field(
@@ -181,16 +82,14 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
181
82
  description="List of object(s) for one or more plant(s) that this declaration applies to.",
182
83
  default_factory=list,
183
84
  )
184
- program_operator: Org | None = pyd.Field(description="JSON object for program operator Org")
85
+ program_operator: OrgRef | None = pyd.Field(description=PROGRAM_OPERATOR_DESCRIPTION)
185
86
  program_operator_doc_id: str | None = pyd.Field(
186
87
  max_length=200, description="Document identifier from Program Operator.", example="123-456.789/b"
187
88
  )
188
89
  program_operator_version: str | None = pyd.Field(
189
90
  max_length=200, description="Document version number from Program Operator.", example="4.3.0"
190
91
  )
191
- third_party_verifier: Org | None = pyd.Field(
192
- description="JSON object for Org that performed a critical review of the EPD data"
193
- )
92
+ third_party_verifier: OrgRef | None = pyd.Field(description=THIRD_PARTY_VERIFIER_DESCRIPTION)
194
93
  third_party_verification_url: pyd.AnyUrl | None = pyd.Field(
195
94
  description="Optional link to a verification statement.",
196
95
  example="https://we-verify-epds.com/en/letters/123-456.789b.pdf",
@@ -268,6 +167,15 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
268
167
  default_factory=list,
269
168
  )
270
169
 
170
+
171
+ EpdPreview = EpdPreviewV0
172
+
173
+
174
+ class EpdV0(WithLciaMixin, EpdPreviewV0, title="EPD (Full)"):
175
+ """Represent an EPD."""
176
+
177
+ _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
178
+
271
179
  @classmethod
272
180
  def get_asset_type(cls) -> str | None:
273
181
  """Return the asset type of this class (see BaseOpenEpdSchema.get_asset_type for details)."""
@@ -291,6 +199,22 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
291
199
  Epd = EpdV0
292
200
 
293
201
 
202
+ class EpdWithDepsV0(EpdV0, title="EPD (with Dependencies)"):
203
+ """
204
+ Expanded version of the EPD.
205
+
206
+ Contains related entities - orgs - with full fields, to support object matching in implementations.
207
+ """
208
+
209
+ manufacturer: Org | None = pyd.Field(description=MANUFACTURER_DESCRIPTION)
210
+ epd_developer: Org | None = pyd.Field(description=DEVELOPER_DESCRIPTION, default=None)
211
+ program_operator: Org | None = pyd.Field(description=PROGRAM_OPERATOR_DESCRIPTION)
212
+ third_party_verifier: Org | None = pyd.Field(description=THIRD_PARTY_VERIFIER_DESCRIPTION)
213
+
214
+
215
+ EpdWithDeps = EpdWithDepsV0
216
+
217
+
294
218
  class EpdFactory(BaseDocumentFactory[BaseDeclaration]):
295
219
  """Factory for EPD objects."""
296
220
 
@@ -18,9 +18,10 @@ from enum import StrEnum
18
18
  from openepd.compat.pydantic import pyd
19
19
  from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes
20
20
  from openepd.model.common import WithAltIdsMixin, WithAttachmentsMixin
21
- from openepd.model.epd import BaseDeclaration
21
+ from openepd.model.declaration import BaseDeclaration
22
22
  from openepd.model.geography import Geography
23
- from openepd.model.org import Org
23
+ from openepd.model.lcia import WithLciaMixin
24
+ from openepd.model.org import Org, OrgRef
24
25
  from openepd.model.versioning import OpenEpdVersions, Version
25
26
 
26
27
 
@@ -45,27 +46,30 @@ class LicenseTerms(StrEnum):
45
46
  """
46
47
 
47
48
 
48
- class GenericEstimateV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
49
- """Represent a Generic Estimate."""
49
+ class GenericEstimatePreviewV0(WithAttachmentsMixin, BaseDeclaration, title="Generic Estimate (preview)"):
50
+ """
51
+ Generic Estimate preview, used in API list responses and where there is no need for a full object.
50
52
 
51
- _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
53
+ Excludes LCIA data.
54
+ """
52
55
 
53
56
  doctype: str = pyd.Field(
54
57
  description='Describes the type and schema of the document. Must always be "openGenericEstimate"',
55
58
  default="openGenericEstimate",
56
59
  )
57
60
 
58
- name: str | None = pyd.Field(max_length=200, description="", default=None)
61
+ name: str | None = pyd.Field(max_length=200, description="Name of the generic estimate", default=None)
62
+
59
63
  description: str | None = pyd.Field(
60
64
  max_length=2000,
61
65
  description="1-paragraph description of the Generic Estimate. Supports plain text or github flavored markdown.",
62
66
  )
63
67
 
64
- publisher: Org | None = pyd.Field(description="Organization that published the LCA results.")
68
+ publisher: OrgRef | None = pyd.Field(description="Organization that published the LCA results.")
65
69
  reviewer_email: pyd.EmailStr | None = pyd.Field(
66
70
  description="Email address of the third party verifier", example="john.doe@example.com", default=None
67
71
  )
68
- reviewer: Org | None = pyd.Field(description="Org that performed a critical review of the LCA.")
72
+ reviewer: OrgRef | None = pyd.Field(description="Org that performed a critical review of the LCA.")
69
73
  license_terms: LicenseTerms | None = pyd.Field(description="The license terms for use of the data.")
70
74
  geography: list[Geography] | None = pyd.Field(
71
75
  "Jurisdiction(s) in which the LCA result is applicable. An empty array, or absent properties, implies global applicability."
@@ -75,9 +79,38 @@ class GenericEstimateV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
75
79
  )
76
80
 
77
81
 
82
+ GenericEstimatePreview = GenericEstimatePreviewV0
83
+
84
+
85
+ class GenericEstimateV0(GenericEstimatePreviewV0, WithLciaMixin, WithAltIdsMixin, title="Generic Estimate (Full)"):
86
+ """
87
+ Represent a full Generic Estimate object.
88
+
89
+ This is considered the most complete valid openEPD object for GenericEstimate. In addition to it, several related
90
+ models are defined, either with fewer fields (to be used in APIs for list requests) or with more relaxed structure
91
+ to support related entities matching.
92
+ """
93
+
94
+ _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
95
+
96
+
78
97
  GenericEstimate = GenericEstimateV0
79
98
 
80
99
 
100
+ class GenericEstimateWithDepsV0(GenericEstimateV0, title="Generic Estimate (with Dependencies)"):
101
+ """
102
+ Expanded version of the GenericEstimate.
103
+
104
+ Contains related entities - orgs - with full fields, to support object matching in implementations.
105
+ """
106
+
107
+ publisher: Org | None = pyd.Field(description="Organization that published the LCA results.")
108
+ reviewer: Org | None = pyd.Field(description="Org that performed a critical review of the LCA.")
109
+
110
+
111
+ GenericEstimateWithDeps = GenericEstimateWithDepsV0
112
+
113
+
81
114
  class GenericEstimateFactory(BaseDocumentFactory[GenericEstimate]):
82
115
  """Factory for EPD objects."""
83
116