openepd 1.4.0__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {openepd-1.4.0 → openepd-1.6.0}/PKG-INFO +1 -1
  2. {openepd-1.4.0 → openepd-1.6.0}/pyproject.toml +2 -2
  3. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/__version__.py +1 -1
  4. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/base.py +97 -13
  5. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/common.py +16 -0
  6. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/epd.py +37 -18
  7. openepd-1.6.0/src/openepd/model/factory.py +48 -0
  8. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/org.py +8 -2
  9. openepd-1.6.0/src/openepd/model/specs/README.md +19 -0
  10. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/specs/__init__.py +8 -2
  11. openepd-1.6.0/src/openepd/model/specs/base.py +60 -0
  12. openepd-1.6.0/src/openepd/model/specs/concrete.py +316 -0
  13. openepd-1.6.0/src/openepd/model/specs/steel.py +146 -0
  14. openepd-1.6.0/src/openepd/model/validation/__init__.py +19 -0
  15. openepd-1.6.0/src/openepd/model/validation/common.py +52 -0
  16. openepd-1.6.0/src/openepd/model/validation/numbers.py +66 -0
  17. openepd-1.6.0/src/openepd/model/versioning.py +129 -0
  18. openepd-1.4.0/src/openepd/model/specs/concrete.py +0 -150
  19. {openepd-1.4.0 → openepd-1.6.0}/LICENSE +0 -0
  20. {openepd-1.4.0 → openepd-1.6.0}/README.md +0 -0
  21. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/__init__.py +0 -0
  22. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/__init__.py +0 -0
  23. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/base_sync_client.py +0 -0
  24. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/category/__init__.py +0 -0
  25. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/category/dto.py +0 -0
  26. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/category/sync_api.py +0 -0
  27. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/common.py +0 -0
  28. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/dto/__init__.py +0 -0
  29. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/dto/base.py +0 -0
  30. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/dto/common.py +0 -0
  31. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/dto/meta.py +0 -0
  32. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/dto/mf.py +0 -0
  33. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/dto/params.py +0 -0
  34. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/epd/__init__.py +0 -0
  35. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/epd/dto.py +0 -0
  36. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/epd/sync_api.py +0 -0
  37. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/errors.py +0 -0
  38. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/pcr/__init__.py +0 -0
  39. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/pcr/dto.py +0 -0
  40. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/pcr/sync_api.py +0 -0
  41. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/sync_client.py +0 -0
  42. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/api/test/__init__.py +0 -0
  43. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/bundle/__init__.py +0 -0
  44. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/bundle/base.py +0 -0
  45. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/bundle/model.py +0 -0
  46. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/bundle/reader.py +0 -0
  47. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/bundle/writer.py +0 -0
  48. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/__init__.py +0 -0
  49. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/category.py +0 -0
  50. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/lcia.py +0 -0
  51. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/pcr.py +0 -0
  52. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/model/standard.py +0 -0
  53. {openepd-1.4.0 → openepd-1.6.0}/src/openepd/py.typed +0 -0
@@ -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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openepd"
3
- version = "1.4.0"
3
+ version = "1.6.0"
4
4
  license = "Apache-2.0"
5
5
  description = "Python library to work with OpenEPD format"
6
6
  authors = ["C-Change Labs <support@c-change-labs.com>"]
@@ -58,7 +58,7 @@ api_client = ["requests"]
58
58
 
59
59
 
60
60
  [tool.commitizen]
61
- version = "1.4.0"
61
+ version = "1.6.0"
62
62
  bump_version = "bump: version $current_version → $new_version"
63
63
  update_changelog_on_bump = true
64
64
  pre_bump_hooks = []
@@ -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"
@@ -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}")
@@ -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"
@@ -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)
@@ -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