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.
Files changed (59) hide show
  1. openepd/__init__.py +1 -1
  2. openepd/__version__.py +2 -2
  3. openepd/api/__init__.py +19 -0
  4. openepd/api/base_sync_client.py +550 -0
  5. openepd/api/category/__init__.py +19 -0
  6. openepd/api/category/dto.py +25 -0
  7. openepd/api/category/sync_api.py +44 -0
  8. openepd/api/common.py +239 -0
  9. openepd/api/dto/__init__.py +19 -0
  10. openepd/api/dto/base.py +41 -0
  11. openepd/api/dto/common.py +115 -0
  12. openepd/api/dto/meta.py +69 -0
  13. openepd/api/dto/mf.py +59 -0
  14. openepd/api/dto/params.py +19 -0
  15. openepd/api/epd/__init__.py +19 -0
  16. openepd/api/epd/dto.py +121 -0
  17. openepd/api/epd/sync_api.py +105 -0
  18. openepd/api/errors.py +86 -0
  19. openepd/api/pcr/__init__.py +19 -0
  20. openepd/api/pcr/dto.py +41 -0
  21. openepd/api/pcr/sync_api.py +49 -0
  22. openepd/api/sync_client.py +67 -0
  23. openepd/api/test/__init__.py +19 -0
  24. openepd/bundle/__init__.py +1 -1
  25. openepd/bundle/base.py +1 -1
  26. openepd/bundle/model.py +5 -6
  27. openepd/bundle/reader.py +5 -5
  28. openepd/bundle/writer.py +5 -4
  29. openepd/compat/__init__.py +19 -0
  30. openepd/compat/pydantic.py +29 -0
  31. openepd/model/__init__.py +1 -1
  32. openepd/model/base.py +114 -15
  33. openepd/model/category.py +39 -0
  34. openepd/model/common.py +33 -25
  35. openepd/model/epd.py +97 -78
  36. openepd/model/factory.py +48 -0
  37. openepd/model/lcia.py +24 -13
  38. openepd/model/org.py +28 -18
  39. openepd/model/pcr.py +42 -14
  40. openepd/model/specs/README.md +19 -0
  41. openepd/model/specs/__init__.py +20 -4
  42. openepd/model/specs/aluminium.py +67 -0
  43. openepd/model/specs/asphalt.py +87 -0
  44. openepd/model/specs/base.py +60 -0
  45. openepd/model/specs/concrete.py +453 -23
  46. openepd/model/specs/glass.py +404 -0
  47. openepd/model/specs/steel.py +193 -0
  48. openepd/model/specs/wood.py +130 -0
  49. openepd/model/standard.py +2 -3
  50. openepd/model/validation/__init__.py +19 -0
  51. openepd/model/validation/common.py +59 -0
  52. openepd/model/validation/numbers.py +26 -0
  53. openepd/model/validation/quantity.py +131 -0
  54. openepd/model/versioning.py +129 -0
  55. {openepd-2.0.0.dist-info → openepd-3.0.0.dist-info}/METADATA +36 -5
  56. openepd-3.0.0.dist-info/RECORD +59 -0
  57. openepd-2.0.0.dist-info/RECORD +0 -22
  58. {openepd-2.0.0.dist-info → openepd-3.0.0.dist-info}/LICENSE +0 -0
  59. {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
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2023 by C Change Labs Inc. www.c-change-labs.com
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.
openepd/model/base.py CHANGED
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2023 by C Change Labs Inc. www.c-change-labs.com
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 typing import Any, Optional, Type, TypeVar
21
+ from enum import StrEnum
22
+ import json
23
+ from typing import Any, Callable, Generic, Optional, Type, TypeAlias, TypeVar
22
24
 
23
- import pydantic
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 | pydantic.BaseModel | None
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
- class BaseOpenEpdSchema(pydantic.BaseModel):
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
- model_config = pydantic.ConfigDict(validate_assignment=False, populate_by_name=True, use_enum_values=True)
66
+ ext: dict[str, AnySerializable] | None = pyd.Field(alias="ext", default=None)
33
67
 
34
- ext: dict[str, AnySerializable] | None = pydantic.Field(alias="ext", default=None)
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 pydantic.BaseModel.json() method.
79
+ It expects the same arguments as the pyd.BaseModel.json() method.
41
80
  """
42
- return self.model_dump(mode="json", *args, **kwargs)
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.model_dump(mode="python", exclude_unset=True, exclude_none=True)) > 0
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, pydantic.BaseModel) and isinstance(value, dict):
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.model_fields:
140
+ if field_name in cls.__fields__:
102
141
  return True
103
142
  else:
104
- for x in cls.model_fields.values():
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 BaseOpenEpdSpec(BaseOpenEpdSchema):
121
- """Base class for all OpenEPD specs."""
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 2023 by C Change Labs Inc. www.c-change-labs.com
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
- import pydantic as pyd
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
- @model_validator(mode="before")
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", json_schema_extra=dict(example=47.6062))
84
- lng: float = pyd.Field(description="Longitude", json_schema_extra=dict(example=-122.3321))
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
- default=None,
105
- json_schema_extra={
106
- "example": {
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"