openepd 1.4.0__py3-none-any.whl → 1.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
@@ -17,4 +17,4 @@
17
17
  # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
- VERSION = "1.4.0"
20
+ VERSION = "1.6.0"
openepd/model/base.py CHANGED
@@ -18,32 +18,62 @@
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
20
  import abc
21
+ from enum import StrEnum
21
22
  import json
22
- from typing import Any, Optional, Type, TypeVar
23
+ from typing import Any, Callable, Generic, Optional, Type, TypeVar
23
24
 
24
- import pydantic
25
+ import pydantic as pyd
25
26
  from pydantic.generics import GenericModel
26
27
 
27
- AnySerializable = int | str | bool | float | list | dict | pydantic.BaseModel | None
28
+ from openepd.model.validation.common import validate_version_compatibility, validate_version_format
29
+ from openepd.model.versioning import OpenEpdVersions, Version
30
+
31
+ AnySerializable = int | str | bool | float | list | dict | pyd.BaseModel | None
28
32
  TAnySerializable = TypeVar("TAnySerializable", bound=AnySerializable)
29
33
 
34
+ OPENEPD_VERSION_FIELD = "openepd_version"
35
+ """Field name for the openEPD format version."""
36
+
37
+
38
+ class OpenEpdDoctypes(StrEnum):
39
+ """Enum of supported openEPD document types."""
40
+
41
+ Epd = "openEPD"
42
+
43
+
44
+ def modify_pydantic_schema(schema_dict: dict, cls: type) -> dict:
45
+ """
46
+ Modify the schema dictionary to add the required fields.
47
+
48
+ :param schema_dict: schema dictionary
49
+ :param cls: class for which the schema was generated
50
+ :return: modified schema dictionary
51
+ """
52
+ ext = schema_dict.get("properties", {}).get("ext")
53
+ # move to bottom
54
+ if ext is not None:
55
+ del schema_dict["properties"]["ext"]
56
+ schema_dict["properties"]["ext"] = ext
57
+ return schema_dict
30
58
 
31
- class BaseOpenEpdSchema(pydantic.BaseModel):
59
+
60
+ class BaseOpenEpdSchema(pyd.BaseModel):
32
61
  """Base class for all OpenEPD models."""
33
62
 
34
- ext: dict[str, AnySerializable] | None = pydantic.Field(alias="ext", default=None)
63
+ ext: dict[str, AnySerializable] | None = pyd.Field(alias="ext", default=None)
35
64
 
36
65
  class Config:
37
66
  allow_mutation = True
38
67
  validate_assignment = False
39
68
  allow_population_by_field_name = True
40
69
  use_enum_values = True
70
+ schema_extra: Callable | dict = modify_pydantic_schema
41
71
 
42
72
  def to_serializable(self, *args, **kwargs) -> dict[str, Any]:
43
73
  """
44
74
  Return a serializable dict representation of the DTO.
45
75
 
46
- It expects the same arguments as the pydantic.BaseModel.json() method.
76
+ It expects the same arguments as the pyd.BaseModel.json() method.
47
77
  """
48
78
  return json.loads(self.json(*args, **kwargs))
49
79
 
@@ -83,7 +113,7 @@ class BaseOpenEpdSchema(pydantic.BaseModel):
83
113
  value = self.get_ext_field(key, default)
84
114
  if value is None:
85
115
  return None # type: ignore
86
- if issubclass(target_type, pydantic.BaseModel) and isinstance(value, dict):
116
+ if issubclass(target_type, pyd.BaseModel) and isinstance(value, dict):
87
117
  return target_type.construct(**value) # type: ignore
88
118
  elif isinstance(value, target_type):
89
119
  return value
@@ -129,12 +159,6 @@ class BaseOpenEpdGenericSchema(GenericModel, BaseOpenEpdSchema):
129
159
  pass
130
160
 
131
161
 
132
- class BaseOpenEpdSpec(BaseOpenEpdSchema):
133
- """Base class for all OpenEPD specs."""
134
-
135
- pass
136
-
137
-
138
162
  class OpenEpdExtension(BaseOpenEpdSchema, metaclass=abc.ABCMeta):
139
163
  """Base class for OpenEPD extension models."""
140
164
 
@@ -148,3 +172,63 @@ class OpenEpdExtension(BaseOpenEpdSchema, metaclass=abc.ABCMeta):
148
172
  TOpenEpdExtension = TypeVar("TOpenEpdExtension", bound=OpenEpdExtension)
149
173
  TOpenEpdObject = TypeVar("TOpenEpdObject", bound=BaseOpenEpdSchema)
150
174
  TOpenEpdObjectClass = TypeVar("TOpenEpdObjectClass", bound=Type[BaseOpenEpdSchema])
175
+
176
+
177
+ class RootDocument(abc.ABC, BaseOpenEpdSchema):
178
+ """Base class for all objects representing openEPD root element. E.g. Epd, IndustryEpd, GenericEstimate, etc."""
179
+
180
+ _FORMAT_VERSION: str
181
+ """Version of this document format. Must be defined in the concrete class."""
182
+
183
+ doctype: str = pyd.Field(
184
+ description='Describes the type and schema of the document. Must always always read "openEPD".',
185
+ default="OpenEPD",
186
+ )
187
+ openepd_version: str = pyd.Field(
188
+ description="Version of the document format, related to /doctype",
189
+ default=OpenEpdVersions.get_current().as_str(),
190
+ )
191
+
192
+ _version_format_validator = pyd.validator(OPENEPD_VERSION_FIELD, allow_reuse=True, check_fields=False)(
193
+ validate_version_format
194
+ )
195
+ _version_major_match_validator = pyd.validator(OPENEPD_VERSION_FIELD, allow_reuse=True, check_fields=False)(
196
+ validate_version_compatibility("_FORMAT_VERSION")
197
+ )
198
+
199
+
200
+ TRootDocument = TypeVar("TRootDocument", bound=RootDocument)
201
+
202
+
203
+ class BaseDocumentFactory(Generic[TRootDocument]):
204
+ """
205
+ Base class for document factories.
206
+
207
+ Extend it to create a factory for a specific document type e.g. for industry epd, epd, etc.
208
+ """
209
+
210
+ DOCTYPE_CONSTRAINT: str = ""
211
+ VERSION_MAP: dict[Version, type[TRootDocument]] = {}
212
+
213
+ @classmethod
214
+ def from_dict(cls, data: dict) -> TRootDocument:
215
+ """Create a document from a dictionary."""
216
+ doctype: str | None = data.get("doctype")
217
+ if doctype is None:
218
+ raise ValueError("Doctype not found in the data.")
219
+ if doctype.lower() != cls.DOCTYPE_CONSTRAINT.lower():
220
+ raise ValueError(
221
+ f"Document type {doctype} not supported. This factory supports {cls.DOCTYPE_CONSTRAINT} only."
222
+ )
223
+ version = Version.parse_version(data.get(OPENEPD_VERSION_FIELD, ""))
224
+ for x, doc_cls in cls.VERSION_MAP.items():
225
+ if x.major == version.major:
226
+ if version.minor <= x.minor:
227
+ return doc_cls(**data)
228
+ else:
229
+ raise ValueError(
230
+ f"Unsupported version: {version}. "
231
+ f"The highest supported version from branch {x.major}.x is {x}"
232
+ )
233
+ supported_versions = ", ".join(f"{v.major}.x" for v in cls.VERSION_MAP.keys())
234
+ raise ValueError(f"Version {version} is not supported. Supported versions are: {supported_versions}")
openepd/model/common.py CHANGED
@@ -17,6 +17,7 @@
17
17
  # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
+ from enum import StrEnum
20
21
  from typing import Annotated, Any
21
22
 
22
23
  import pydantic as pyd
@@ -137,3 +138,18 @@ class WithAltIdsMixin(BaseModel):
137
138
  """Set an alt_id if value is not None."""
138
139
  if value is not None:
139
140
  self.set_alt_id(domain_name, value)
141
+
142
+
143
+ class OpenEPDUnit(StrEnum):
144
+ """OpenEPD allowed units."""
145
+
146
+ kg = "kg"
147
+ m2 = "m2"
148
+ m = "m"
149
+ M2_RSI = "m2 * RSI"
150
+ MJ = "MJ"
151
+ t_km = "t * km"
152
+ MPa = "MPa"
153
+ item = "item"
154
+ W = "W"
155
+ use = "use"
openepd/model/epd.py CHANGED
@@ -22,18 +22,31 @@ from typing import Annotated
22
22
 
23
23
  import pydantic as pyd
24
24
 
25
- from openepd.model.base import BaseOpenEpdSchema
25
+ from openepd.model.base import BaseDocumentFactory, RootDocument
26
26
  from openepd.model.common import Amount, Ingredient, WithAltIdsMixin, WithAttachmentsMixin
27
27
  from openepd.model.lcia import Impacts, OutputFlowSet, ResourceUseSet
28
28
  from openepd.model.org import Org, Plant
29
29
  from openepd.model.pcr import Pcr
30
30
  from openepd.model.specs import Specs
31
31
  from openepd.model.standard import Standard
32
+ from openepd.model.versioning import OpenEpdVersions, Version
32
33
 
33
34
 
34
- class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
35
+ class BaseEpd(RootDocument):
36
+ """
37
+ Base class for EPD documents.
38
+
39
+ This class should not be used directly. Use Epd or EpdVx instead.
40
+ """
41
+
42
+ pass
43
+
44
+
45
+ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseEpd):
35
46
  """Represent an EPD."""
36
47
 
48
+ _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
49
+
37
50
  # TODO: Add validator for open-xpd-uuid on this field
38
51
  id: str | None = pyd.Field(
39
52
  description="The unique ID for this EPD. To ensure global uniqueness, should be registered at "
@@ -41,10 +54,6 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
41
54
  example="1u7zsed8",
42
55
  default=None,
43
56
  )
44
- doctype: str = pyd.Field(
45
- description='Describes the type and schema of the document. Must always always read "openEPD".',
46
- default="OpenEPD",
47
- )
48
57
  product_name: str | None = pyd.Field(
49
58
  max_length=200, description="The name of the product described by this EPD", example="Mix 12345AC"
50
59
  )
@@ -53,7 +62,7 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
53
62
  )
54
63
  product_description: str | None = pyd.Field(
55
64
  max_length=2000,
56
- description="1-paragraph description of product. " "Supports plain text or github flavored markdown.",
65
+ description="1-paragraph description of product. Supports plain text or github flavored markdown.",
57
66
  )
58
67
  # TODO: add product_alt_names? E.g. ILCD has a list of synonymous names
59
68
  product_classes: dict[str, str | list[str]] = pyd.Field(
@@ -234,28 +243,28 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
234
243
  )
235
244
  lca_discussion: str | None = pyd.Field(
236
245
  max_length=20000,
237
- description="""A rich text description containing information for experts reviewing the EPD contents.
238
- Text descriptions required by ISO 14025, ISO 21930, EN 15804,, relevant PCRs, or program instructions and which do not
239
- have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
246
+ description="""A rich text description containing information for experts reviewing the EPD contents.
247
+ Text descriptions required by ISO 14025, ISO 21930, EN 15804,, relevant PCRs, or program instructions and which do not
248
+ have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
240
249
  separated by github flavored markdown formatting.""",
241
250
  example="""# Packaging
242
251
 
243
- Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
244
- strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
245
- scenario assumptions, including disposition pathways for each packaging material by reuse, recycling, or landfill
252
+ Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
253
+ strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
254
+ scenario assumptions, including disposition pathways for each packaging material by reuse, recycling, or landfill
246
255
  disposal based on packaging type.*
247
256
 
248
257
  # Product Installation
249
258
 
250
- A description of the type of processing, machinery, tools, dust extraction equipment, auxiliary materials, etc.
251
- to be used during installation shall be included. Information on industrial and environmental protection may be
252
- included in this section. Any waste treatment included within the system boundary of installation waste should be
259
+ A description of the type of processing, machinery, tools, dust extraction equipment, auxiliary materials, etc.
260
+ to be used during installation shall be included. Information on industrial and environmental protection may be
261
+ included in this section. Any waste treatment included within the system boundary of installation waste should be
253
262
  specified.
254
263
 
255
264
  # Use Conditions
256
265
 
257
- Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
258
- Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
266
+ Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
267
+ Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
259
268
  In the absence of primary data, cleaning assumptions shall be documented.
260
269
  """,
261
270
  )
@@ -278,3 +287,13 @@ class Epd(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
278
287
  if v is None:
279
288
  return []
280
289
  return v
290
+
291
+
292
+ Epd = EpdV0
293
+
294
+
295
+ class EpdFactory(BaseDocumentFactory[BaseEpd]):
296
+ """Factory for EPD objects."""
297
+
298
+ DOCTYPE_CONSTRAINT = "openEPD"
299
+ 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/org.py CHANGED
@@ -25,8 +25,8 @@ from openepd.model.base import BaseOpenEpdSchema
25
25
  from openepd.model.common import Location, WithAltIdsMixin, WithAttachmentsMixin
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
@@ -37,12 +37,18 @@ class Org(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
37
37
  example="C Change Labs",
38
38
  default=None,
39
39
  )
40
+
41
+
42
+ class Org(WithAttachmentsMixin, WithAltIdsMixin, OrgRef):
43
+ """Represent an organization."""
44
+
40
45
  alt_names: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
41
46
  description="List of other names for organization",
42
47
  example=["C-Change Labs", "C-Change Labs inc."],
43
48
  default=None,
44
49
  )
45
50
  # TODO: NEW field, not in the spec
51
+
46
52
  owner: Optional["Org"] = pyd.Field(description="Organization that controls this organization", default=None)
47
53
  subsidiaries: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
48
54
  description="Organizations controlled by this organization",
@@ -0,0 +1,19 @@
1
+ # Material Extensions
2
+
3
+ This package contains openEPD material extensions. They are used to represent the material properties of the openEPD
4
+ materials, and are a more dynamic, frequently changing part of the standard.
5
+
6
+ ## Versioning
7
+
8
+ Extensions are versioned separately from the openEPD standard or openEPD API.
9
+
10
+ Each material extension is named after the corresponding EC3 product class, and is located in the relevant place
11
+ in the specs tree. For example, `RebarSteel` is nested under `Steel`.
12
+
13
+ Extensions are versioned as Major.Minor, for example "2.4".
14
+
15
+ Rules:
16
+ 1. Minor versions for the same major version should be backwards compatible.
17
+ 2. Major versions are not compatible between themselves.
18
+ 3. Pydantic models representing versions are named in a pattern SpecNameV1, where 1 is major version and SpecName is
19
+ the name of the material extension.
@@ -17,13 +17,19 @@
17
17
  # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
+
20
21
  import pydantic as pyd
21
22
 
22
23
  from openepd.model.base import BaseOpenEpdSchema
23
- from openepd.model.specs.concrete import CmuSpec
24
+ from openepd.model.specs import concrete, steel
24
25
 
25
26
 
26
27
  class Specs(BaseOpenEpdSchema):
27
28
  """Material specific specs."""
28
29
 
29
- cmu: CmuSpec | None = pyd.Field(default=None, description="Concrete Masonry Unit-specific (CMU) specs")
30
+ cmu: concrete.CmuSpec | None = pyd.Field(default=None, description="Concrete Masonry Unit-specific (CMU) specs")
31
+ CMU: concrete.CmuSpec | None = pyd.Field(default=None, description="Concrete Masonry Unit-specific (CMU) specs")
32
+ Steel: steel.SteelV1 | None = pyd.Field(default=None, title="SteelV1", description="Steel-specific specs")
33
+ Concrete: concrete.ConcreteV1 | None = pyd.Field(
34
+ default=None, title="ConcreteV1", description="Concrete-specific specs"
35
+ )
@@ -0,0 +1,60 @@
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 typing import Any
21
+
22
+ import pydantic as pyd
23
+
24
+ from openepd.model.base import BaseOpenEpdSchema, Version
25
+ from openepd.model.validation.common import validate_version_compatibility, validate_version_format
26
+ from openepd.model.validation.numbers import QuantityValidator
27
+ from openepd.model.versioning import WithExtVersionMixin
28
+
29
+
30
+ class BaseOpenEpdSpec(BaseOpenEpdSchema):
31
+ """Base class for all OpenEPD specs."""
32
+
33
+ class Config:
34
+ use_enum_values = False # we need to store enums as strings and not values
35
+
36
+
37
+ class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
38
+ """Base class for new specs (hierarchical, versioned)."""
39
+
40
+ _QUANTITY_VALIDATOR: QuantityValidator | None = None
41
+
42
+ def __init__(self, **data: Any) -> None:
43
+ # ensure that all the concrete spec objects fail on creations if they dont have _EXT_VERSION declared to
44
+ # something meaningful
45
+ if not hasattr(self, "_EXT_VERSION") or self._EXT_VERSION is None:
46
+ raise ValueError(f"Class {self.__class__} must declare an extension version")
47
+ Version.parse_version(self._EXT_VERSION) # validate format correctness
48
+ super().__init__(**{"ext_version": self._EXT_VERSION, **data})
49
+
50
+ _version_format_validator = pyd.validator("ext_version", allow_reuse=True, check_fields=False)(
51
+ validate_version_format
52
+ )
53
+ _version_major_match_validator = pyd.validator("ext_version", allow_reuse=True, check_fields=False)(
54
+ validate_version_compatibility("_EXT_VERSION")
55
+ )
56
+
57
+
58
+ def setup_external_validators(quantity_validator: QuantityValidator):
59
+ """Set the implementation unit validator for specs."""
60
+ BaseOpenEpdHierarchicalSpec._QUANTITY_VALIDATOR = quantity_validator
@@ -18,10 +18,72 @@
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
20
  from enum import StrEnum
21
+ from typing import Literal
21
22
 
22
23
  import pydantic as pyd
23
24
 
24
- from openepd.model.base import BaseOpenEpdSpec
25
+ from openepd.model.base import BaseOpenEpdSchema
26
+ from openepd.model.common import OpenEPDUnit
27
+ from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec, BaseOpenEpdSpec
28
+ from openepd.model.validation.common import together_validator
29
+ from openepd.model.validation.numbers import RatioFloat, validate_unit_factory
30
+
31
+
32
+ class AciExposureClass(StrEnum):
33
+ """ACI Code (US)."""
34
+
35
+ F0 = "aci.F0"
36
+ F1 = "aci.F1"
37
+ F2 = "aci.F2"
38
+ F3 = "aci.F3"
39
+ S0 = "aci.S0"
40
+ S1 = "aci.S1"
41
+ S2 = "aci.S2"
42
+ S3 = "aci.S3"
43
+ C1 = "aci.C1"
44
+ C2 = "aci.C2"
45
+ W0 = "aci.W0"
46
+ W1 = "aci.W1"
47
+ W2 = "aci.W2"
48
+
49
+
50
+ class CsaExposureClass(StrEnum):
51
+ """CSA Code (Canada)."""
52
+
53
+ N = "csa.N"
54
+ F2 = "csa.F-2"
55
+ F_1 = "csa.F-1"
56
+ C_1 = "csa.C-1"
57
+ S_3 = "csa.S-3"
58
+ S_2 = "csa.S-2"
59
+ S_1 = "csa.S-1"
60
+ A_1 = "csa.A-1"
61
+ A_2 = "csa.A-2"
62
+ A_3 = "csa.A-3"
63
+ A_4 = "csa.A-4"
64
+
65
+
66
+ class EnExposureClass(StrEnum):
67
+ """EN 206 Class (Europe)."""
68
+
69
+ en206_0 = "en206.0"
70
+ F1 = "en206.F1"
71
+ F2 = "en206.F2"
72
+ F3 = "en206.F3"
73
+ F4 = "en206.F4"
74
+ A1 = "en206.A1"
75
+ A2 = "en206.A2"
76
+ A3 = "en206.A3"
77
+ D1 = "en206.D1"
78
+ D2 = "en206.D2"
79
+ D3 = "en206.D3"
80
+ S1 = "en206.S1"
81
+ S2 = "en206.S2"
82
+ S3 = "en206.S3"
83
+ C1 = "en206.C1"
84
+ C2 = "en206.C2"
85
+ C3 = "en206.C3"
86
+ C4 = "en206.C4"
25
87
 
26
88
 
27
89
  class CmuWeightClassification(StrEnum):
@@ -35,7 +97,7 @@ class CmuWeightClassification(StrEnum):
35
97
  """Lightweight CMU has a density less than 105 lbs/cu. ft."""
36
98
 
37
99
 
38
- class CmuOptions(BaseOpenEpdSpec):
100
+ class CmuOptions(BaseOpenEpdSchema):
39
101
  """Concrete Masonry Unit options."""
40
102
 
41
103
  load_bearing: bool | None = pyd.Field(
@@ -148,3 +210,107 @@ class CmuSpec(BaseOpenEpdSpec):
148
210
  options: CmuOptions = pyd.Field(
149
211
  description="Options for CMU. List of true/false properties", default_factory=CmuOptions
150
212
  )
213
+
214
+
215
+ class Cementitious(BaseOpenEpdSchema):
216
+ """List of cementitious materials, and proportion by mass."""
217
+
218
+ opc: RatioFloat | None = pyd.Field(default=None, description="Ordinary Gray Portland Cement")
219
+ wht: RatioFloat | None = pyd.Field(default=None, description="White Portland Cement")
220
+ ggbs: RatioFloat | None = pyd.Field(default=None, description="Ground Granulated Blast Furnace Slag")
221
+ flyAsh: RatioFloat | None = pyd.Field(default=None, description="Fly Ash, including types F, CL, and CH")
222
+ siFume: RatioFloat | None = pyd.Field(default=None, description="Silica Fume")
223
+ gg45: RatioFloat | None = pyd.Field(default=None, description="Ground Glass, 45um or smaller")
224
+ natPoz: RatioFloat | None = pyd.Field(default=None, description="Natural pozzolan")
225
+ mk: RatioFloat | None = pyd.Field(default=None, description="Metakaolin")
226
+ CaCO3: RatioFloat | None = pyd.Field(default=None, description="Limestone")
227
+ other: RatioFloat | None = pyd.Field(default=None, description="Other SCMs")
228
+
229
+
230
+ class TypicalApplication(BaseOpenEpdSchema):
231
+ """Concrete typical application."""
232
+
233
+ fnd: bool | None = pyd.Field(description="Foundation", default=None)
234
+ sog: bool | None = pyd.Field(description="Slab on Grade", default=None)
235
+ hrz: bool | None = pyd.Field(description="Elevated Horizontal", default=None)
236
+ vrt_wall: bool | None = pyd.Field(description="Vertical Wall", default=None)
237
+ vrt_column: bool | None = pyd.Field(description="Vertical Column", default=None)
238
+ vrt_other: bool | None = pyd.Field(description="Vertical Other", default=None)
239
+ sht: bool | None = pyd.Field(description="Shotcrete", default=None)
240
+ cdf: bool | None = pyd.Field(description="Flowable Fill (CDF,default=None)", default=None)
241
+ sac: bool | None = pyd.Field(description="Sidewalk and Curb", default=None)
242
+ pav: bool | None = pyd.Field(description="Paving", default=None)
243
+ oil: bool | None = pyd.Field(description="Oil Patch", default=None)
244
+ grt: bool | None = pyd.Field(description="Cement Grout", default=None)
245
+ ota: bool | None = pyd.Field(description="Other", default=None)
246
+
247
+
248
+ class ConcreteV1(BaseOpenEpdHierarchicalSpec):
249
+ """Concrete spec."""
250
+
251
+ _EXT_VERSION = "1.0"
252
+
253
+ class Options(BaseOpenEpdSchema):
254
+ lightweight: bool | None = pyd.Field(description="Lightweight", default=None)
255
+ plc: bool | None = pyd.Field(description="Portland Limestone Cement", default=None)
256
+ scc: bool | None = pyd.Field(description="Self Compacting", default=None)
257
+ finishable: bool | None = pyd.Field(description="Finishable", default=None)
258
+ air: bool | None = pyd.Field(description="Air Entrainment", default=None)
259
+ co2: bool | None = pyd.Field(description="CO2 Curing", default=None)
260
+ white: bool | None = pyd.Field(description="White Cement", default=None)
261
+ fiber_reinforced: bool | None = pyd.Field(description="Fiber reinforced", default=None)
262
+
263
+ strength_28d: str | None = pyd.Field(
264
+ default=None, title="Concrete Strength 28d", description="Concrete strength after 28 days"
265
+ )
266
+
267
+ strength_early: str | None = pyd.Field(
268
+ default=None,
269
+ title="Early Strength",
270
+ description="A strength spec which is to be reached earlier than 28 days (e.g. 3d)",
271
+ )
272
+ strength_early_d: Literal[3, 7, 14] | None = pyd.Field(
273
+ default=None, title="Test Days for Early Strength", description="Test Day for the Early Strength"
274
+ )
275
+ strength_late: str | None = pyd.Field(
276
+ default=None,
277
+ title="Late Strength",
278
+ description="A strength spec which is to be reached later than 28 days (e.g. 42d)",
279
+ )
280
+ strength_late_d: Literal[42, 56, 72, 96, 120] | None = pyd.Field(
281
+ default=None, title="Test Day for the Late Strength", description="Test Day for the Late Strength"
282
+ )
283
+ slump: str | None = pyd.Field(description="Minimum test slump", default=None)
284
+ w_c_ratio: RatioFloat | None = pyd.Field(description="Ratio of water to cement", default=None)
285
+ aci_exposure_classes: list[AciExposureClass] = pyd.Field(
286
+ description="List of ACI318-19 exposure classes this product meets", default=None
287
+ )
288
+ csa_exposure_classes: list[CsaExposureClass] = pyd.Field(
289
+ description="List of CSA A23.1 exposure classes this product meets", default=None
290
+ )
291
+ en_exposure_classes: list[EnExposureClass] = pyd.Field(
292
+ description="List of EN206 exposure classes this product meets", default=None
293
+ )
294
+
295
+ application: TypicalApplication | None = pyd.Field(description="Typical Application", default=None)
296
+
297
+ options: Options = pyd.Field(description="Concrete options", default=None)
298
+
299
+ cementitious: Cementitious | None = pyd.Field(
300
+ default=None,
301
+ title="Cementitious Materials",
302
+ description="List of cementitious materials, and proportion by mass",
303
+ )
304
+ _compressive_strength_unit_validator = pyd.validator("strength_28d", allow_reuse=True)(
305
+ validate_unit_factory(OpenEPDUnit.MPa)
306
+ )
307
+
308
+ @pyd.root_validator
309
+ def _late_validator(cls, values):
310
+ together_validator("strength_late", "strength_late_d", values)
311
+ return values
312
+
313
+ @pyd.root_validator
314
+ def _early_validator(cls, values):
315
+ together_validator("strength_early", "strength_early_d", values)
316
+ return values
@@ -0,0 +1,146 @@
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 enum import StrEnum
21
+
22
+ import pydantic as pyd
23
+
24
+ from openepd.model.base import BaseOpenEpdSchema
25
+ from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
26
+ from openepd.model.standard import Standard
27
+ from openepd.model.validation.numbers import RatioFloat
28
+
29
+
30
+ class SteelMakingRoute(BaseOpenEpdSchema):
31
+ """Steel making route."""
32
+
33
+ bof: bool | None = pyd.Field(default=None, description="Basic oxygen furnace")
34
+ eaf: bool | None = pyd.Field(default=None, description="Electric arc furnace")
35
+ ohf: bool | None = pyd.Field(default=None, description="Open hearth furnace")
36
+
37
+
38
+ class SteelComposition(StrEnum):
39
+ """Steel composition enum."""
40
+
41
+ CARBON = "Carbon"
42
+ ALLOY = "Alloy"
43
+ STAINLESS = "Stainless"
44
+ TOOL = "Tool"
45
+ OTHER = "Other"
46
+
47
+
48
+ class FabricatedOptionsMixin(pyd.BaseModel):
49
+ """Fabricated options mixin."""
50
+
51
+ fabricated: bool | None = pyd.Field(default=None, description="Fabricated")
52
+
53
+
54
+ class WireMeshSteelV1(BaseOpenEpdHierarchicalSpec):
55
+ """Spec for wire mesh steel."""
56
+
57
+ class Options(BaseOpenEpdSchema, FabricatedOptionsMixin):
58
+ """Wire Mesh Options."""
59
+
60
+ pass
61
+
62
+ options: Options = pyd.Field(description="Rebar Steel options", default_factory=Options)
63
+
64
+
65
+ class RebarSteelV1(BaseOpenEpdHierarchicalSpec):
66
+ """Rebar steel spec."""
67
+
68
+ _EXT_VERSION = "1.0"
69
+
70
+ class Options(BaseOpenEpdSchema, FabricatedOptionsMixin):
71
+ """Rebar Steel Options."""
72
+
73
+ epoxy: bool | None = pyd.Field(default=None, description="Epoxy Coated")
74
+
75
+ options: Options = pyd.Field(description="Rebar Steel options", default_factory=Options)
76
+
77
+
78
+ class PlateSteelV1(BaseOpenEpdHierarchicalSpec):
79
+ """Plate Steel Spec."""
80
+
81
+ class Options(BaseOpenEpdSchema, FabricatedOptionsMixin):
82
+ """Plate Steel Options."""
83
+
84
+ pass
85
+
86
+ options: Options = pyd.Field(description="Plate Steel options", default_factory=Options)
87
+
88
+
89
+ class HollowV1(BaseOpenEpdHierarchicalSpec):
90
+ """Hollow Sections Spec."""
91
+
92
+ class Options(FabricatedOptionsMixin, BaseOpenEpdSchema):
93
+ """Hollow Sections Options."""
94
+
95
+ pass
96
+
97
+ options: Options = pyd.Field(description="Hollow Steel options", default_factory=Options)
98
+
99
+
100
+ class HotRolledV1(BaseOpenEpdHierarchicalSpec):
101
+ """Hot Rolled spec."""
102
+
103
+ class Options(FabricatedOptionsMixin, BaseOpenEpdSchema):
104
+ """Hot Rolled options."""
105
+
106
+ pass
107
+
108
+ options: Options = pyd.Field(description="Hollow Steel options", default_factory=Options)
109
+
110
+
111
+ class SteelV1(BaseOpenEpdHierarchicalSpec):
112
+ """Steel spec."""
113
+
114
+ _EXT_VERSION = "1.0"
115
+
116
+ class Options(BaseOpenEpdSchema):
117
+ """Steel spec options."""
118
+
119
+ galvanized: bool | None = pyd.Field(default=None, description="Galvanized")
120
+ cold_finished: bool | None = pyd.Field(default=None, description="Cold Finished")
121
+
122
+ form_factor: str | None = pyd.Field(description="Product's form factor", example="Steel >> RebarSteel")
123
+ steel_composition: SteelComposition | None = pyd.Field(default=None, description="Basic chemical composition")
124
+ recycled_content: RatioFloat | None = pyd.Field(
125
+ default=None,
126
+ description="Scrap steel inputs from other processes. Includes "
127
+ "Post-Consumer content, if any. This percentage may be "
128
+ "used to evaluate the EPD w.r.t. targets or limits that are"
129
+ " different for primary and recycled content.",
130
+ )
131
+ ASTM: list[Standard] = pyd.Field(description="ASTM standard to which this product complies", default_factory=list)
132
+ SAE: list[Standard] = pyd.Field(
133
+ description="AISA/SAE standard to which this product complies", default_factory=list
134
+ )
135
+ EN: list[Standard] = pyd.Field(description="EN 10027 number(s)", default_factory=list)
136
+
137
+ options: Options | None = pyd.Field(description="Steel options", default_factory=Options)
138
+ making_route: SteelMakingRoute | None = pyd.Field(default=None, description="Steel making route")
139
+
140
+ # Nested specs
141
+
142
+ WireMeshSteel: WireMeshSteelV1 | None = None
143
+ RebarSteel: RebarSteelV1 | None = None
144
+ PlateSteel: PlateSteelV1 | None = None
145
+ Hollow: HollowV1 | None = None
146
+ HotRolled: HotRolledV1 | None = None
@@ -0,0 +1,19 @@
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
+ #
@@ -0,0 +1,52 @@
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 typing import Any, Callable, Type
21
+
22
+ from openepd.model.versioning import Version
23
+
24
+
25
+ def together_validator(field1: str, field2: Any, values: dict[str, Any]) -> Any:
26
+ """Shared validator to ensure that two fields are provided together or not provided at all."""
27
+ value1 = values.get(field1)
28
+ value2 = values.get(field2)
29
+ if value1 is not None and value2 is None or value1 is None and value2 is not None:
30
+ raise ValueError(f"Both or neither {field1} and {field2} days must be provided together")
31
+
32
+
33
+ def validate_version_format(v: str) -> str:
34
+ """Ensure that the extension version is valid."""
35
+ Version.parse_version(v) # will raise an error if not valid
36
+ return v
37
+
38
+
39
+ def validate_version_compatibility(class_version_attribute_name: str) -> Callable[[Type, str], str]:
40
+ """Ensure that the object which is passed for parsing and validation is compatible with the class."""
41
+
42
+ # we need closure to pass property name, since actual class will only be available in runtime
43
+ def internal_validate_version_compatibility(cls: Type, v: str) -> str:
44
+ if not hasattr(cls, class_version_attribute_name):
45
+ raise ValueError(f"Class {cls} must declare a class var extension var named {class_version_attribute_name}")
46
+
47
+ class_version = getattr(cls, class_version_attribute_name)
48
+ if Version.parse_version(v).major != Version.parse_version(class_version).major:
49
+ raise ValueError(f"Extension version {v} does not match class version {class_version}")
50
+ return v
51
+
52
+ return internal_validate_version_compatibility
@@ -0,0 +1,66 @@
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 abc import ABC, abstractmethod
21
+ from typing import TYPE_CHECKING, Annotated
22
+
23
+ import pydantic as pyd
24
+
25
+ from openepd.model.common import OpenEPDUnit
26
+
27
+ if TYPE_CHECKING:
28
+ from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
29
+
30
+ RatioFloat = Annotated[float, pyd.Field(ge=0, le=1, example=0.5)]
31
+ """Float field which represents a percentage ratio between 0 and 1."""
32
+
33
+
34
+ class QuantityValidator(ABC):
35
+ """
36
+ Interface for quantity validator.
37
+
38
+ The openEPD models are mapped using the simple types. Caller code should provide their own implementation of this
39
+ and set it with `set_unit_validator` function.
40
+ """
41
+
42
+ @abstractmethod
43
+ def validate(self, value: str, dimensionality: str) -> None:
44
+ """
45
+ Validate the given string value against the given dimensionality.
46
+
47
+ Args:
48
+ value: The value to validate, like "102.4 kg"
49
+ dimensionality: The dimensionality to validate against, like "kg"
50
+ Returns:
51
+ None if the value is valid, raises an error otherwise.
52
+ Raises:
53
+ ValueError: If the value is not valid.
54
+ """
55
+ pass
56
+
57
+
58
+ def validate_unit_factory(dimensionality: OpenEPDUnit | str):
59
+ """Create validator for unit field."""
60
+
61
+ def validator(cls: "BaseOpenEpdHierarchicalSpec", value: str) -> str:
62
+ if hasattr(cls, "_QUNATITY_VALIDATOR") and cls._QUANTITY_VALIDATOR is not None:
63
+ cls._QUANTITY_VALIDATOR.validate(value, dimensionality)
64
+ return value
65
+
66
+ return validator
@@ -0,0 +1,129 @@
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 abc import ABC
21
+ from enum import ReprEnum
22
+ from typing import ClassVar, NamedTuple
23
+
24
+ import pydantic as pyd
25
+ from pydantic import BaseModel
26
+
27
+
28
+ class WithExtVersionMixin(ABC, BaseModel):
29
+ """Mixin for extensions supporting versions: recommended way."""
30
+
31
+ _EXT_VERSION: ClassVar[str]
32
+ """Exact version (major, minor) of the spec extension"""
33
+
34
+ def __init_subclass__(cls):
35
+ """Set the default value for the ext_version field from _EXT_VERSION class var."""
36
+ super().__init_subclass__()
37
+ if hasattr(cls, "_EXT_VERSION"):
38
+ cls.__fields__["ext_version"].default = cls._EXT_VERSION
39
+
40
+ ext_version: str = pyd.Field(description="Extension version", example="3.22", default=None)
41
+
42
+
43
+ class Version(NamedTuple):
44
+ """Version of the object or specification."""
45
+
46
+ major: int
47
+ minor: int
48
+
49
+ @staticmethod
50
+ def parse_version(version: str) -> "Version":
51
+ """Parse the version of extension or the format.
52
+
53
+ Version is expected to be major.minor
54
+
55
+ :param version: The extension version.
56
+ :return: A tuple of major and minor version numbers.
57
+ """
58
+ splits = version.split(".", 1) if isinstance(version, str) else None
59
+ if len(splits) != 2:
60
+ raise ValueError(f"Invalid version: {version}")
61
+ if not splits[0].isdigit() or not splits[1].isdigit():
62
+ raise ValueError(f"Invalid version: {version}")
63
+ return Version(major=int(splits[0]), minor=int(splits[1]))
64
+
65
+ def __str__(self) -> str:
66
+ return self.as_str()
67
+
68
+ def __repr__(self) -> str:
69
+ return f"[Version] {self.as_str()}"
70
+
71
+ def as_str(self) -> str:
72
+ """Return the version as a string."""
73
+ return f"{self.major}.{self.minor}"
74
+
75
+
76
+ class OpenEpdVersions(Version, ReprEnum):
77
+ """
78
+ Enum of supported openEPD versions.
79
+
80
+ When adding a new version - make sure to add a new major version to the list of supported versions.
81
+ When doing non-breaking change - update minor version in a corresponding enum value.
82
+ """
83
+
84
+ Version0 = Version(major=0, minor=1)
85
+
86
+ @classmethod
87
+ def get_supported_versions(cls) -> list[Version]:
88
+ """Return a list of supported versions."""
89
+ return [x.value for x in cls]
90
+
91
+ @classmethod
92
+ def supported_versions_str(cls, major_only: bool = False) -> str:
93
+ """
94
+ Return a comma separated list of the supported versions.
95
+
96
+ This is a utility method, might be helpful for logging, building error messages, etc.
97
+
98
+ :param major_only: If True, minor component will be replaced with 'x'. E.g. `2.x` instead of `2.1`
99
+ """
100
+ if major_only:
101
+ return ", ".join(f"{x.major}.x" for x in cls.get_supported_versions())
102
+ return ", ".join(str(x) for x in cls.get_supported_versions())
103
+
104
+ @classmethod
105
+ def get_most_recent_version(cls, branch: int | None = 0) -> Version:
106
+ """
107
+ Return the most recent version of the openEPD format.
108
+
109
+ If branch is specified - returns the most recent version of the specified branch, otherwise returns
110
+ the most recent version among all branches.
111
+ """
112
+ if branch is None:
113
+ highest: Version = max(cls, key=lambda x: x.value.major) # type: ignore
114
+ if highest:
115
+ return highest
116
+ for x in cls:
117
+ if x.value.major == branch:
118
+ return x.value
119
+ raise ValueError(
120
+ f"No version {branch}.x is not supported. Supported versions are: {', '.join(str(x.value) for x in cls)}"
121
+ )
122
+
123
+ @classmethod
124
+ def get_current(cls) -> Version:
125
+ """Return the most recent stable version of the format."""
126
+ return cls.get_most_recent_version()
127
+
128
+ def __repr__(self):
129
+ return f"[OpenEpdVesion] {self.name} - {self.value}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 1.4.0
3
+ Version: 1.6.0
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
@@ -1,5 +1,5 @@
1
1
  openepd/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
2
- openepd/__version__.py,sha256=YSSiSbKfBLxnHr7oWf0EFSpUYGsAKTT6yx-ax_ef7Uc,855
2
+ openepd/__version__.py,sha256=dydPFQzMC85AwH6lLCuy1P3UT2PsGjNSUOyDiyVRnhA,855
3
3
  openepd/api/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
4
4
  openepd/api/base_sync_client.py,sha256=JcNpWsGoIK_1Eg27CAQd7nIjbcfD56jQ1nS6B48Q0cI,21142
5
5
  openepd/api/category/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
@@ -27,18 +27,26 @@ openepd/bundle/model.py,sha256=NWbpxorLzQ_OIFozV34wz76OWM_ORGTBkS0288chv0k,2647
27
27
  openepd/bundle/reader.py,sha256=z4v_UWyaosktN3DdmnRx8GpLq4DkejjoUsckFfCgUac,6904
28
28
  openepd/bundle/writer.py,sha256=AfvzzdAr9ybIbtiZfuX2mAGfddamTxXxUTxtHHrs2Gc,8353
29
29
  openepd/model/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
30
- openepd/model/base.py,sha256=vjcsMoNQNJuCK1ngh1RbOlYVzWgCw2LF9fhPaH_q2_o,5564
30
+ openepd/model/base.py,sha256=wLZUQSc6nOvoiESgxjEHTdGEV417SUolsXMNm2mA-po,8883
31
31
  openepd/model/category.py,sha256=_6yFyldaDrkZk2_9ZAO2oRTz7ocvkg4T-D_aw5ncgvo,1840
32
- openepd/model/common.py,sha256=SSTIvCjCkJEKf3FVnUvAw9SfFbkSdestq6YBX20eBQw,5386
33
- openepd/model/epd.py,sha256=xB11K3MQweB20yrv_iTUxgjn3fWC5hkt4c45KJa-xHk,13944
32
+ openepd/model/common.py,sha256=j-M_2T_Kl_Ea8l50pJT7LNatBW1fKrRcw4XNpeNvLU0,5635
33
+ openepd/model/epd.py,sha256=BxcKJd_LuApB2v_LGN0-hYLNaUNoqMu9ZH_V1Ffil-g,14269
34
+ openepd/model/factory.py,sha256=qQZNb4yFN1EQWHHVS-fHnJ2gB-vsKz9hVmXXJE7YaRU,1918
34
35
  openepd/model/lcia.py,sha256=2GOdcY6ASJu653SFwTakT9boYF-3CYCHLupyxTPUQ30,16711
35
- openepd/model/org.py,sha256=ov3nrSk36imug7ADarNWHIXW9jiFbmboswoXVQCNeDI,3662
36
+ openepd/model/org.py,sha256=quoYMrA6WXqs7ZRjdO1Bm47No4jGLqxz0QmnnqKgw7c,3741
36
37
  openepd/model/pcr.py,sha256=t861yFntmy3ewrnGLP47cv3afV-aCCVRlrbpGeRh-7I,4604
37
- openepd/model/specs/__init__.py,sha256=jdMXv4_FcVUrDafCqulELDx6T2KuZQyRUtgAuLZMVR4,1137
38
- openepd/model/specs/concrete.py,sha256=cwXv2O2FV6y5g76k_1Ao6uUlKrj0me7JzDOmL9CK2O8,6532
38
+ openepd/model/specs/README.md,sha256=W5LSMpZuW5x36cKS4HRfeFsClsRf8J9yHMMICghdc0s,862
39
+ openepd/model/specs/__init__.py,sha256=3DXyeFzH1bw4rf1KCFXPXl3X-ErMu0NpjvqqhorpnLc,1514
40
+ openepd/model/specs/base.py,sha256=hOE_hCuiY7xhR0yW_Pbxe_LQAnMWq8sq3rTqQqbeVOc,2564
41
+ openepd/model/specs/concrete.py,sha256=jdhuThy8QFaEs9HdJ53O7ua4Fw-ES2_YzoLDchyU8s0,13165
42
+ openepd/model/specs/steel.py,sha256=FoMYE6y_uvWg9Edkb7WV0f_hQNmviwtI1SM-UmdRh8E,4985
39
43
  openepd/model/standard.py,sha256=YjxpC9nyz6LM8m3z1s5S0exJ1qvA3n59-FBoScg-FL0,1519
44
+ openepd/model/validation/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
45
+ openepd/model/validation/common.py,sha256=nNwLtj5Uu7TsFmJXuD8aRcx1hB_nf-LQBfEvHOZM1Ao,2420
46
+ openepd/model/validation/numbers.py,sha256=AItjDAz9LODxLOH7shqfMMufT9ep_8ys2bVelIbME7g,2349
47
+ openepd/model/versioning.py,sha256=Z31TgCW0POsmXLrH-Rmpy-MrIw_bU_9wwh8T4Mq1yTM,4628
40
48
  openepd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- openepd-1.4.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
42
- openepd-1.4.0.dist-info/METADATA,sha256=dROT7H1EeFmmI5bluVzrSzC1yyOANMC9dY7yTfncLrk,7762
43
- openepd-1.4.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
44
- openepd-1.4.0.dist-info/RECORD,,
49
+ openepd-1.6.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
50
+ openepd-1.6.0.dist-info/METADATA,sha256=_zIXgd030Dco2ykLRwhes7xe6ygQhgmtjiyEXqVvUo0,7762
51
+ openepd-1.6.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
52
+ openepd-1.6.0.dist-info/RECORD,,