openepd 2.0.0__py3-none-any.whl → 3.0.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/__init__.py +1 -1
- openepd/__version__.py +2 -2
- openepd/api/__init__.py +19 -0
- openepd/api/base_sync_client.py +550 -0
- openepd/api/category/__init__.py +19 -0
- openepd/api/category/dto.py +25 -0
- openepd/api/category/sync_api.py +44 -0
- openepd/api/common.py +239 -0
- openepd/api/dto/__init__.py +19 -0
- openepd/api/dto/base.py +41 -0
- openepd/api/dto/common.py +115 -0
- openepd/api/dto/meta.py +69 -0
- openepd/api/dto/mf.py +59 -0
- openepd/api/dto/params.py +19 -0
- openepd/api/epd/__init__.py +19 -0
- openepd/api/epd/dto.py +121 -0
- openepd/api/epd/sync_api.py +105 -0
- openepd/api/errors.py +86 -0
- openepd/api/pcr/__init__.py +19 -0
- openepd/api/pcr/dto.py +41 -0
- openepd/api/pcr/sync_api.py +49 -0
- openepd/api/sync_client.py +67 -0
- openepd/api/test/__init__.py +19 -0
- openepd/bundle/__init__.py +1 -1
- openepd/bundle/base.py +1 -1
- openepd/bundle/model.py +5 -6
- openepd/bundle/reader.py +5 -5
- openepd/bundle/writer.py +5 -4
- openepd/compat/__init__.py +19 -0
- openepd/compat/pydantic.py +29 -0
- openepd/model/__init__.py +1 -1
- openepd/model/base.py +114 -15
- openepd/model/category.py +39 -0
- openepd/model/common.py +33 -25
- openepd/model/epd.py +97 -78
- openepd/model/factory.py +48 -0
- openepd/model/lcia.py +24 -13
- openepd/model/org.py +28 -18
- openepd/model/pcr.py +42 -14
- openepd/model/specs/README.md +19 -0
- openepd/model/specs/__init__.py +20 -4
- openepd/model/specs/aluminium.py +67 -0
- openepd/model/specs/asphalt.py +87 -0
- openepd/model/specs/base.py +60 -0
- openepd/model/specs/concrete.py +453 -23
- openepd/model/specs/glass.py +404 -0
- openepd/model/specs/steel.py +193 -0
- openepd/model/specs/wood.py +130 -0
- openepd/model/standard.py +2 -3
- openepd/model/validation/__init__.py +19 -0
- openepd/model/validation/common.py +59 -0
- openepd/model/validation/numbers.py +26 -0
- openepd/model/validation/quantity.py +131 -0
- openepd/model/versioning.py +129 -0
- {openepd-2.0.0.dist-info → openepd-3.0.0.dist-info}/METADATA +36 -5
- openepd-3.0.0.dist-info/RECORD +59 -0
- openepd-2.0.0.dist-info/RECORD +0 -22
- {openepd-2.0.0.dist-info → openepd-3.0.0.dist-info}/LICENSE +0 -0
- {openepd-2.0.0.dist-info → openepd-3.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,29 @@
|
|
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
|
+
try:
|
21
|
+
from pydantic import v1 as pyd # type: ignore
|
22
|
+
from pydantic.v1 import generics as pyd_generics # type: ignore
|
23
|
+
except ImportError:
|
24
|
+
import pydantic as pyd # type: ignore[no-redef]
|
25
|
+
from pydantic import generics as pyd_generics # type: ignore[no-redef]
|
26
|
+
|
27
|
+
pydantic = pyd
|
28
|
+
|
29
|
+
__all__ = ["pyd", "pydantic", "pyd_generics"]
|
openepd/model/__init__.py
CHANGED
openepd/model/base.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright
|
2
|
+
# Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
|
3
3
|
#
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
5
|
# you may not use this file except in compliance with the License.
|
@@ -18,32 +18,71 @@
|
|
18
18
|
# Find out more at www.BuildingTransparency.org
|
19
19
|
#
|
20
20
|
import abc
|
21
|
-
from
|
21
|
+
from enum import StrEnum
|
22
|
+
import json
|
23
|
+
from typing import Any, Callable, Generic, Optional, Type, TypeAlias, TypeVar
|
22
24
|
|
23
|
-
import
|
25
|
+
from openepd.compat.pydantic import pyd, pyd_generics
|
26
|
+
from openepd.model.validation.common import validate_version_compatibility, validate_version_format
|
27
|
+
from openepd.model.versioning import OpenEpdVersions, Version
|
24
28
|
|
25
|
-
AnySerializable = int | str | bool | float | list | dict |
|
29
|
+
AnySerializable: TypeAlias = int | str | bool | float | list | dict | pyd.BaseModel | None
|
26
30
|
TAnySerializable = TypeVar("TAnySerializable", bound=AnySerializable)
|
27
31
|
|
32
|
+
OPENEPD_VERSION_FIELD = "openepd_version"
|
33
|
+
"""Field name for the openEPD format version."""
|
28
34
|
|
29
|
-
|
35
|
+
OPENAPI_SCHEMA_SERVICE_PROPERTIES = ["ext_version", "ext"]
|
36
|
+
"""OpenAPI properties which should be moved to the bottom of specification if present. """
|
37
|
+
|
38
|
+
|
39
|
+
class OpenEpdDoctypes(StrEnum):
|
40
|
+
"""Enum of supported openEPD document types."""
|
41
|
+
|
42
|
+
Epd = "openEPD"
|
43
|
+
|
44
|
+
|
45
|
+
def modify_pydantic_schema(schema_dict: dict, cls: type) -> dict:
|
46
|
+
"""
|
47
|
+
Modify the schema dictionary to add the required fields.
|
48
|
+
|
49
|
+
:param schema_dict: schema dictionary
|
50
|
+
:param cls: class for which the schema was generated
|
51
|
+
:return: modified schema dictionary
|
52
|
+
"""
|
53
|
+
for prop_name in OPENAPI_SCHEMA_SERVICE_PROPERTIES:
|
54
|
+
prop = schema_dict.get("properties", {}).get(prop_name, None)
|
55
|
+
# move to bottom
|
56
|
+
if prop is not None:
|
57
|
+
del schema_dict["properties"][prop_name]
|
58
|
+
schema_dict["properties"][prop_name] = prop
|
59
|
+
|
60
|
+
return schema_dict
|
61
|
+
|
62
|
+
|
63
|
+
class BaseOpenEpdSchema(pyd.BaseModel):
|
30
64
|
"""Base class for all OpenEPD models."""
|
31
65
|
|
32
|
-
|
66
|
+
ext: dict[str, AnySerializable] | None = pyd.Field(alias="ext", default=None)
|
33
67
|
|
34
|
-
|
68
|
+
class Config:
|
69
|
+
allow_mutation = True
|
70
|
+
validate_assignment = False
|
71
|
+
allow_population_by_field_name = True
|
72
|
+
use_enum_values = True
|
73
|
+
schema_extra: Callable | dict = modify_pydantic_schema
|
35
74
|
|
36
75
|
def to_serializable(self, *args, **kwargs) -> dict[str, Any]:
|
37
76
|
"""
|
38
77
|
Return a serializable dict representation of the DTO.
|
39
78
|
|
40
|
-
It expects the same arguments as the
|
79
|
+
It expects the same arguments as the pyd.BaseModel.json() method.
|
41
80
|
"""
|
42
|
-
return self.
|
81
|
+
return json.loads(self.json(*args, **kwargs))
|
43
82
|
|
44
83
|
def has_values(self) -> bool:
|
45
84
|
"""Return True if the model has any values."""
|
46
|
-
return len(self.
|
85
|
+
return len(self.dict(exclude_unset=True, exclude_none=True)) > 0
|
47
86
|
|
48
87
|
def set_ext(self, ext: "OpenEpdExtension") -> None:
|
49
88
|
"""Set the extension field."""
|
@@ -77,7 +116,7 @@ class BaseOpenEpdSchema(pydantic.BaseModel):
|
|
77
116
|
value = self.get_ext_field(key, default)
|
78
117
|
if value is None:
|
79
118
|
return None # type: ignore
|
80
|
-
if issubclass(target_type,
|
119
|
+
if issubclass(target_type, pyd.BaseModel) and isinstance(value, dict):
|
81
120
|
return target_type.construct(**value) # type: ignore
|
82
121
|
elif isinstance(value, target_type):
|
83
122
|
return value
|
@@ -98,10 +137,10 @@ class BaseOpenEpdSchema(pydantic.BaseModel):
|
|
98
137
|
|
99
138
|
Both property name and aliases are checked.
|
100
139
|
"""
|
101
|
-
if field_name in cls.
|
140
|
+
if field_name in cls.__fields__:
|
102
141
|
return True
|
103
142
|
else:
|
104
|
-
for x in cls.
|
143
|
+
for x in cls.__fields__.values():
|
105
144
|
if x.alias == field_name:
|
106
145
|
return True
|
107
146
|
return False
|
@@ -117,8 +156,8 @@ class BaseOpenEpdSchema(pydantic.BaseModel):
|
|
117
156
|
return None
|
118
157
|
|
119
158
|
|
120
|
-
class
|
121
|
-
"""Base class for all OpenEPD
|
159
|
+
class BaseOpenEpdGenericSchema(pyd_generics.GenericModel, BaseOpenEpdSchema):
|
160
|
+
"""Base class for all OpenEPD generic models."""
|
122
161
|
|
123
162
|
pass
|
124
163
|
|
@@ -136,3 +175,63 @@ class OpenEpdExtension(BaseOpenEpdSchema, metaclass=abc.ABCMeta):
|
|
136
175
|
TOpenEpdExtension = TypeVar("TOpenEpdExtension", bound=OpenEpdExtension)
|
137
176
|
TOpenEpdObject = TypeVar("TOpenEpdObject", bound=BaseOpenEpdSchema)
|
138
177
|
TOpenEpdObjectClass = TypeVar("TOpenEpdObjectClass", bound=Type[BaseOpenEpdSchema])
|
178
|
+
|
179
|
+
|
180
|
+
class RootDocument(abc.ABC, BaseOpenEpdSchema):
|
181
|
+
"""Base class for all objects representing openEPD root element. E.g. Epd, IndustryEpd, GenericEstimate, etc."""
|
182
|
+
|
183
|
+
_FORMAT_VERSION: str
|
184
|
+
"""Version of this document format. Must be defined in the concrete class."""
|
185
|
+
|
186
|
+
doctype: str = pyd.Field(
|
187
|
+
description='Describes the type and schema of the document. Must always always read "openEPD".',
|
188
|
+
default="OpenEPD",
|
189
|
+
)
|
190
|
+
openepd_version: str = pyd.Field(
|
191
|
+
description="Version of the document format, related to /doctype",
|
192
|
+
default=OpenEpdVersions.get_current().as_str(),
|
193
|
+
)
|
194
|
+
|
195
|
+
_version_format_validator = pyd.validator(OPENEPD_VERSION_FIELD, allow_reuse=True, check_fields=False)(
|
196
|
+
validate_version_format
|
197
|
+
)
|
198
|
+
_version_major_match_validator = pyd.validator(OPENEPD_VERSION_FIELD, allow_reuse=True, check_fields=False)(
|
199
|
+
validate_version_compatibility("_FORMAT_VERSION")
|
200
|
+
)
|
201
|
+
|
202
|
+
|
203
|
+
TRootDocument = TypeVar("TRootDocument", bound=RootDocument)
|
204
|
+
|
205
|
+
|
206
|
+
class BaseDocumentFactory(Generic[TRootDocument]):
|
207
|
+
"""
|
208
|
+
Base class for document factories.
|
209
|
+
|
210
|
+
Extend it to create a factory for a specific document type e.g. for industry epd, epd, etc.
|
211
|
+
"""
|
212
|
+
|
213
|
+
DOCTYPE_CONSTRAINT: str = ""
|
214
|
+
VERSION_MAP: dict[Version, type[TRootDocument]] = {}
|
215
|
+
|
216
|
+
@classmethod
|
217
|
+
def from_dict(cls, data: dict) -> TRootDocument:
|
218
|
+
"""Create a document from a dictionary."""
|
219
|
+
doctype: str | None = data.get("doctype")
|
220
|
+
if doctype is None:
|
221
|
+
raise ValueError("Doctype not found in the data.")
|
222
|
+
if doctype.lower() != cls.DOCTYPE_CONSTRAINT.lower():
|
223
|
+
raise ValueError(
|
224
|
+
f"Document type {doctype} not supported. This factory supports {cls.DOCTYPE_CONSTRAINT} only."
|
225
|
+
)
|
226
|
+
version = Version.parse_version(data.get(OPENEPD_VERSION_FIELD, ""))
|
227
|
+
for x, doc_cls in cls.VERSION_MAP.items():
|
228
|
+
if x.major == version.major:
|
229
|
+
if version.minor <= x.minor:
|
230
|
+
return doc_cls(**data)
|
231
|
+
else:
|
232
|
+
raise ValueError(
|
233
|
+
f"Unsupported version: {version}. "
|
234
|
+
f"The highest supported version from branch {x.major}.x is {x}"
|
235
|
+
)
|
236
|
+
supported_versions = ", ".join(f"{v.major}.x" for v in cls.VERSION_MAP.keys())
|
237
|
+
raise ValueError(f"Version {version} is not supported. Supported versions are: {supported_versions}")
|
@@ -0,0 +1,39 @@
|
|
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.compat.pydantic import pyd
|
21
|
+
from openepd.model.base import BaseOpenEpdSchema
|
22
|
+
from openepd.model.common import Amount
|
23
|
+
|
24
|
+
|
25
|
+
class Category(BaseOpenEpdSchema):
|
26
|
+
"""DTO for Category model, recursive."""
|
27
|
+
|
28
|
+
id: str = pyd.Field(description="Category short ID (readable unique string)")
|
29
|
+
name: str = pyd.Field(description="Category display name (user-friendly)")
|
30
|
+
short_name: str = pyd.Field(description="Category short user-friendly name")
|
31
|
+
openepd_hierarchical_name: str = pyd.Field(
|
32
|
+
"Special form of hierarchical category ID where the >> is hierarchy separator"
|
33
|
+
)
|
34
|
+
masterformat: str | None = pyd.Field(description="Default category code in Masterformat")
|
35
|
+
description: str | None = pyd.Field(description="Category verbose description")
|
36
|
+
declared_unit: Amount | None = pyd.Field(description="Declared unit of category, for example 1 kg")
|
37
|
+
subcategories: list["Category"] = pyd.Field(
|
38
|
+
description="List of subcategories. This makes categories tree-like structure"
|
39
|
+
)
|
openepd/model/common.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright
|
2
|
+
# Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
|
3
3
|
#
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
5
|
# you may not use this file except in compliance with the License.
|
@@ -17,11 +17,10 @@
|
|
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
|
-
from pydantic import BaseModel, model_validator
|
24
|
-
|
23
|
+
from openepd.compat.pydantic import pyd
|
25
24
|
from openepd.model.base import BaseOpenEpdSchema
|
26
25
|
|
27
26
|
|
@@ -29,13 +28,9 @@ class Amount(BaseOpenEpdSchema):
|
|
29
28
|
"""A value-and-unit pairing for amounts that do not have an uncertainty."""
|
30
29
|
|
31
30
|
qty: float | None = pyd.Field(description="How much of this in the amount.", default=None)
|
32
|
-
unit: str | None = pyd.Field(
|
33
|
-
description="Which unit. SI units are preferred.",
|
34
|
-
default=None,
|
35
|
-
json_schema_extra=dict(example="kg"),
|
36
|
-
)
|
31
|
+
unit: str | None = pyd.Field(description="Which unit. SI units are preferred.", example="kg", default=None)
|
37
32
|
|
38
|
-
@
|
33
|
+
@pyd.root_validator
|
39
34
|
def check_qty_or_unit(cls, values: dict[str, Any]):
|
40
35
|
"""Ensure that qty or unit is provided."""
|
41
36
|
if values["qty"] is None and values["unit"] is None:
|
@@ -80,8 +75,8 @@ class Ingredient(BaseOpenEpdSchema):
|
|
80
75
|
class LatLng(BaseOpenEpdSchema):
|
81
76
|
"""A latitude and longitude."""
|
82
77
|
|
83
|
-
lat: float = pyd.Field(description="Latitude",
|
84
|
-
lng: float = pyd.Field(description="Longitude",
|
78
|
+
lat: float = pyd.Field(description="Latitude", example=47.6062)
|
79
|
+
lng: float = pyd.Field(description="Longitude", example=-122.3321)
|
85
80
|
|
86
81
|
|
87
82
|
class Location(BaseOpenEpdSchema):
|
@@ -96,18 +91,16 @@ class Location(BaseOpenEpdSchema):
|
|
96
91
|
)
|
97
92
|
|
98
93
|
|
99
|
-
class WithAttachmentsMixin(BaseModel):
|
94
|
+
class WithAttachmentsMixin(pyd.BaseModel):
|
100
95
|
"""Mixin for objects that can have attachments."""
|
101
96
|
|
102
97
|
attachments: dict[Annotated[str, pyd.Field(max_length=200)], pyd.AnyUrl] | None = pyd.Field(
|
103
98
|
description="Dict of URLs relevant to this entry",
|
104
|
-
|
105
|
-
|
106
|
-
"
|
107
|
-
"Contact Us": "https://www.c-change-labs.com/en/contact-us/",
|
108
|
-
"LinkedIn": "https://www.linkedin.com/company/c-change-labs/",
|
109
|
-
}
|
99
|
+
example={
|
100
|
+
"Contact Us": "https://www.c-change-labs.com/en/contact-us/",
|
101
|
+
"LinkedIn": "https://www.linkedin.com/company/c-change-labs/",
|
110
102
|
},
|
103
|
+
default=None,
|
111
104
|
)
|
112
105
|
|
113
106
|
def set_attachment(self, name: str, url: str):
|
@@ -122,17 +115,15 @@ class WithAttachmentsMixin(BaseModel):
|
|
122
115
|
self.set_attachment(name, url)
|
123
116
|
|
124
117
|
|
125
|
-
class WithAltIdsMixin(BaseModel):
|
118
|
+
class WithAltIdsMixin(pyd.BaseModel):
|
126
119
|
"""Mixin for objects that can have alt_ids."""
|
127
120
|
|
128
121
|
alt_ids: dict[Annotated[str, pyd.Field(max_length=200)], str] | None = pyd.Field(
|
129
122
|
description="Dict identifiers for this entry.",
|
123
|
+
example={
|
124
|
+
"oekobau.dat": "bdda4364-451f-4df2-a68b-5912469ee4c9",
|
125
|
+
},
|
130
126
|
default=None,
|
131
|
-
json_schema_extra=dict(
|
132
|
-
example={
|
133
|
-
"oekobau.dat": "bdda4364-451f-4df2-a68b-5912469ee4c9",
|
134
|
-
}
|
135
|
-
),
|
136
127
|
)
|
137
128
|
|
138
129
|
def set_alt_id(self, domain_name: str, value: str):
|
@@ -145,3 +136,20 @@ class WithAltIdsMixin(BaseModel):
|
|
145
136
|
"""Set an alt_id if value is not None."""
|
146
137
|
if value is not None:
|
147
138
|
self.set_alt_id(domain_name, value)
|
139
|
+
|
140
|
+
|
141
|
+
class OpenEPDUnit(StrEnum):
|
142
|
+
"""OpenEPD allowed units."""
|
143
|
+
|
144
|
+
kg = "kg"
|
145
|
+
m2 = "m2"
|
146
|
+
m = "m"
|
147
|
+
M2_RSI = "m2 * RSI"
|
148
|
+
MJ = "MJ"
|
149
|
+
t_km = "t * km"
|
150
|
+
MPa = "MPa"
|
151
|
+
item = "item"
|
152
|
+
W = "W"
|
153
|
+
use = "use"
|
154
|
+
degree_c = "°C"
|
155
|
+
kg_co2 = "kgCO2e"
|