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 +1 -1
- openepd/model/base.py +97 -13
- openepd/model/common.py +16 -0
- openepd/model/epd.py +37 -18
- openepd/model/factory.py +48 -0
- openepd/model/org.py +8 -2
- openepd/model/specs/README.md +19 -0
- openepd/model/specs/__init__.py +8 -2
- openepd/model/specs/base.py +60 -0
- openepd/model/specs/concrete.py +168 -2
- openepd/model/specs/steel.py +146 -0
- openepd/model/validation/__init__.py +19 -0
- openepd/model/validation/common.py +52 -0
- openepd/model/validation/numbers.py +66 -0
- openepd/model/versioning.py +129 -0
- {openepd-1.4.0.dist-info → openepd-1.6.0.dist-info}/METADATA +1 -1
- {openepd-1.4.0.dist-info → openepd-1.6.0.dist-info}/RECORD +19 -11
- {openepd-1.4.0.dist-info → openepd-1.6.0.dist-info}/LICENSE +0 -0
- {openepd-1.4.0.dist-info → openepd-1.6.0.dist-info}/WHEEL +0 -0
openepd/__version__.py
CHANGED
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
|
-
|
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
|
-
|
59
|
+
|
60
|
+
class BaseOpenEpdSchema(pyd.BaseModel):
|
32
61
|
"""Base class for all OpenEPD models."""
|
33
62
|
|
34
|
-
ext: dict[str, AnySerializable] | 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
|
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,
|
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
|
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
|
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.
|
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}
|
openepd/model/factory.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
# This software was developed with support from the Skanska USA,
|
17
|
+
# Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
|
18
|
+
# Find out more at www.BuildingTransparency.org
|
19
|
+
#
|
20
|
+
from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes, RootDocument
|
21
|
+
from openepd.model.epd import EpdFactory
|
22
|
+
|
23
|
+
|
24
|
+
class DocumentFactory:
|
25
|
+
"""A factory for creating documents regardless of the type."""
|
26
|
+
|
27
|
+
DOCTYPE_TO_FACTORY: dict[str, type[BaseDocumentFactory]] = {
|
28
|
+
OpenEpdDoctypes.Epd: EpdFactory,
|
29
|
+
}
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def from_dict(cls, data: dict) -> RootDocument:
|
33
|
+
"""
|
34
|
+
Create a document from the dictionary.
|
35
|
+
|
36
|
+
Type of the document will be recognized from the `doctype` field.
|
37
|
+
:raise ValueError: if the document type is not specified or not supported.
|
38
|
+
"""
|
39
|
+
doctype = data.get("doctype")
|
40
|
+
if doctype is None:
|
41
|
+
raise ValueError("The document type is not specified.")
|
42
|
+
factory = cls.DOCTYPE_TO_FACTORY.get(doctype)
|
43
|
+
if factory is None:
|
44
|
+
raise ValueError(
|
45
|
+
f"The document of type `{doctype}` is not supported. Supported documents are: "
|
46
|
+
+ ", ".join(cls.DOCTYPE_TO_FACTORY.keys())
|
47
|
+
)
|
48
|
+
return factory.from_dict(data)
|
openepd/model/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
|
29
|
-
"""
|
28
|
+
class OrgRef(BaseOpenEpdSchema):
|
29
|
+
"""Represents Organisation with minimal data."""
|
30
30
|
|
31
31
|
web_domain: str | None = pyd.Field(
|
32
32
|
description="A web domain owned by organization. Typically is the org's home website address", default=None
|
@@ -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.
|
openepd/model/specs/__init__.py
CHANGED
@@ -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
|
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
|
openepd/model/specs/concrete.py
CHANGED
@@ -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
|
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(
|
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,5 +1,5 @@
|
|
1
1
|
openepd/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
|
2
|
-
openepd/__version__.py,sha256=
|
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=
|
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=
|
33
|
-
openepd/model/epd.py,sha256=
|
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=
|
36
|
+
openepd/model/org.py,sha256=quoYMrA6WXqs7ZRjdO1Bm47No4jGLqxz0QmnnqKgw7c,3741
|
36
37
|
openepd/model/pcr.py,sha256=t861yFntmy3ewrnGLP47cv3afV-aCCVRlrbpGeRh-7I,4604
|
37
|
-
openepd/model/specs/
|
38
|
-
openepd/model/specs/
|
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.
|
42
|
-
openepd-1.
|
43
|
-
openepd-1.
|
44
|
-
openepd-1.
|
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,,
|
File without changes
|
File without changes
|