openepd 4.11.3__py3-none-any.whl → 4.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
openepd/__version__.py CHANGED
@@ -13,4 +13,4 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- VERSION = "4.11.3"
16
+ VERSION = "4.12.0"
@@ -151,6 +151,7 @@ class SyncHttpClient:
151
151
 
152
152
  HTTP_DATE_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S %Z"
153
153
  DEFAULT_RETRY_INTERVAL_SEC = 10
154
+ DEFAULT_TIMEOUT_SEC = (15, 2 * 60)
154
155
 
155
156
  def __init__(
156
157
  self,
@@ -183,7 +184,7 @@ class SyncHttpClient:
183
184
  else throttle_retry_timeout.total_seconds()
184
185
  )
185
186
  self.user_agent = user_agent
186
- self.timeout = timeout_sec
187
+ self.timeout = timeout_sec or self.DEFAULT_TIMEOUT_SEC
187
188
  self._session: Session | None = None
188
189
  self._auth: AuthBase | None = auth
189
190
  self._retry_count: int = retry_count
openepd/api/errors.py CHANGED
@@ -55,9 +55,22 @@ class ValidationError(ApiError):
55
55
  super().__init__(http_status, error_summary, response, error_code)
56
56
  self.validation_errors: dict[str, list[str]] = validation_errors or {}
57
57
 
58
+ @staticmethod
59
+ def __flatten(nested_errors: dict[str, Any], parent_key: str = "") -> dict[str, list[str]]:
60
+ result: dict[str, list[str]] = {}
61
+ for key, value in nested_errors.items():
62
+ full_key = f"{parent_key}.{key}" if parent_key else key
63
+ if isinstance(value, dict):
64
+ nested_result = ValidationError.__flatten(value, full_key)
65
+ for nested_key, nested_errors in nested_result.items(): # type: ignore[assignment]
66
+ result.setdefault(nested_key, []).extend(nested_errors)
67
+ else:
68
+ result.setdefault(full_key, []).extend(value)
69
+ return result
70
+
58
71
  def __str__(self) -> str:
59
72
  result: list[str] = ["Validation errors:"]
60
- for code, errors in self.validation_errors.items():
73
+ for code, errors in self.__flatten(self.validation_errors).items(): # type: ignore[arg-type]
61
74
  result.append(f"{code}:")
62
75
  for e in errors:
63
76
  result.append(f" {e}")
openepd/model/base.py CHANGED
@@ -183,7 +183,7 @@ class RootDocument(abc.ABC, BaseOpenEpdSchema):
183
183
 
184
184
  doctype: str = pyd.Field(
185
185
  description='Describes the type and schema of the document. Must always always read "openEPD".',
186
- default="OpenEPD",
186
+ default="openEPD",
187
187
  )
188
188
  openepd_version: str = pyd.Field(
189
189
  description="Version of the document format, related to /doctype",
@@ -24,6 +24,7 @@ from openepd.model.org import Org
24
24
  from openepd.model.pcr import Pcr
25
25
  from openepd.model.standard import Standard
26
26
  from openepd.model.validation.common import ReferenceStr
27
+ from openepd.model.validation.quantity import AmountMass
27
28
 
28
29
  DEVELOPER_DESCRIPTION = "The organization responsible for the underlying LCA (and subsequent summarization as EPD)."
29
30
  PROGRAM_OPERATOR_DESCRIPTION = "JSON object for program operator Org"
@@ -61,7 +62,7 @@ class BaseDeclaration(RootDocument, abc.ABC):
61
62
  "utilized, the declared unit shall refer to the amount of "
62
63
  "product associated with the A1-A3 life cycle stage."
63
64
  )
64
- kg_per_declared_unit: Amount | None = pyd.Field(
65
+ kg_per_declared_unit: AmountMass | None = pyd.Field(
65
66
  default=None,
66
67
  description="Mass of the product, in kilograms, per declared unit",
67
68
  example=Amount(qty=12.5, unit="kg").to_serializable(exclude_unset=True),
openepd/model/epd.py CHANGED
@@ -30,6 +30,7 @@ from openepd.model.declaration import (
30
30
  from openepd.model.lcia import WithLciaMixin
31
31
  from openepd.model.org import Org, Plant
32
32
  from openepd.model.specs import Specs
33
+ from openepd.model.validation.quantity import AmountMass
33
34
  from openepd.model.versioning import OpenEpdVersions, Version
34
35
 
35
36
  MANUFACTURER_DESCRIPTION = (
@@ -53,6 +54,11 @@ class EpdPreviewV0(
53
54
 
54
55
  """
55
56
 
57
+ doctype: str = pyd.Field(
58
+ description='Describes the type and schema of the document. Must always always read "openEPD".',
59
+ default="openEPD",
60
+ )
61
+
56
62
  product_name: str | None = pyd.Field(
57
63
  max_length=200, description="The name of the product described by this EPD", example="Mix 12345AC", default=None
58
64
  )
@@ -79,14 +85,14 @@ class EpdPreviewV0(
79
85
  description="List of object(s) for one or more plant(s) that this declaration applies to.",
80
86
  default_factory=list,
81
87
  )
82
- kg_C_per_declared_unit: Amount | None = pyd.Field(
88
+ kg_C_per_declared_unit: AmountMass | None = pyd.Field(
83
89
  default=None,
84
90
  description="Mass of elemental carbon, per declared unit, contained in the product itself at the manufacturing "
85
91
  "facility gate. Used (among other things) to check a carbon balance or calculate incineration "
86
92
  "emissions. The source of carbon (e.g. biogenic) is not relevant in this field.",
87
93
  example=Amount(qty=8.76, unit="kg"),
88
94
  )
89
- kg_C_biogenic_per_declared_unit: Amount | None = pyd.Field(
95
+ kg_C_biogenic_per_declared_unit: AmountMass | None = pyd.Field(
90
96
  default=None,
91
97
  description="Mass of elemental carbon from biogenic sources, per declared unit, contained in the product "
92
98
  "itself at the manufacturing facility gate. It may be presumed that any biogenic carbon content "
@@ -149,6 +155,17 @@ class EpdPreviewV0(
149
155
  default_factory=list,
150
156
  )
151
157
 
158
+ @pyd.validator("doctype")
159
+ def validate_doctype(cls, v: str | None) -> str:
160
+ """
161
+ Handle possible mixed case options for doctype.
162
+
163
+ Required for backward compatibility as some code might have already used 'doctype: OpenEPD' instead of 'openEPD'
164
+ """
165
+ if not v or v.lower() == "openepd":
166
+ return "openEPD"
167
+ raise ValueError("Invalid doctype")
168
+
152
169
 
153
170
  EpdPreview = EpdPreviewV0
154
171
 
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  from enum import StrEnum
17
+ from typing import Literal
17
18
 
18
19
  from openepd.compat.pydantic import pyd
19
20
  from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes
@@ -60,7 +61,7 @@ class GenericEstimatePreviewV0(
60
61
 
61
62
  _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
62
63
 
63
- doctype: str = pyd.Field(
64
+ doctype: Literal["openGenericEstimate"] = pyd.Field(
64
65
  description='Describes the type and schema of the document. Must always be "openGenericEstimate"',
65
66
  default="openGenericEstimate",
66
67
  )
@@ -13,6 +13,8 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
+ from typing import Literal
17
+
16
18
  from openepd.compat.pydantic import pyd
17
19
  from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes
18
20
  from openepd.model.common import WithAltIdsMixin, WithAttachmentsMixin
@@ -51,7 +53,7 @@ class IndustryEpdPreviewV0(
51
53
 
52
54
  _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
53
55
 
54
- doctype: str = pyd.Field(
56
+ doctype: Literal["openIndustryEpd"] = pyd.Field(
55
57
  description='Describes the type and schema of the document. Must always be "openIndustryEpd"',
56
58
  default="openIndustryEpd",
57
59
  )
openepd/model/lcia.py CHANGED
@@ -204,7 +204,7 @@ class ScopesetByNameBase(BaseOpenEpdSchema):
204
204
 
205
205
  :return: set of names, for example ['gwp', 'odp]
206
206
  """
207
- return [self.__fields__[f].alias or f for f in self.__fields_set__]
207
+ return [self.__fields__[f].alias or f for f in self.__fields_set__ if f not in ("ext",)]
208
208
 
209
209
  def get_scopeset_by_name(self, name: str) -> ScopeSet | None:
210
210
  """
@@ -14,9 +14,10 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  from abc import ABC, abstractmethod
17
- from typing import TYPE_CHECKING, Callable, ClassVar
17
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar
18
18
 
19
- from openepd.model.common import OpenEPDUnit
19
+ from openepd.compat.pydantic import pyd
20
+ from openepd.model.common import Amount, OpenEPDUnit
20
21
 
21
22
  if TYPE_CHECKING:
22
23
  from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
@@ -127,6 +128,32 @@ def validate_quantity_for_new_validator(max_value: str) -> Callable:
127
128
  # todo these types should be replaced by Annotated[str, AfterValidator...] as we move completely to pydantic 2
128
129
 
129
130
 
131
+ class AmountWithDimensionality(Amount, ABC):
132
+ """Class for dimensionality-validated amounts."""
133
+
134
+ dimensionality_unit: ClassVar[str | None] = None
135
+
136
+ # Unit for dimensionality to validate against, for example "kg"
137
+
138
+ @pyd.root_validator
139
+ def check_dimensionality_matches(cls, values: dict[str, Any]) -> dict[str, Any]:
140
+ """Check that this amount conforms to the same dimensionality as dimensionality_unit."""
141
+ if not cls.dimensionality_unit:
142
+ return values
143
+
144
+ from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
145
+
146
+ str_repr = f"{values['qty']} {values['unit']}"
147
+ validate_unit_factory(cls.dimensionality_unit)(BaseOpenEpdHierarchicalSpec, str_repr)
148
+ return values
149
+
150
+
151
+ class AmountMass(AmountWithDimensionality):
152
+ """Amount of mass, measured in kg, t, etc."""
153
+
154
+ dimensionality_unit = OpenEPDUnit.kg
155
+
156
+
130
157
  class QuantityStr(str):
131
158
  """
132
159
  Quantity string type.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 4.11.3
3
+ Version: 4.12.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,10 +1,10 @@
1
1
  openepd/__init__.py,sha256=Shkfh0Kun0YRhmRDw7LkUj2eQL3X-HnP55u2THOEALw,794
2
- openepd/__version__.py,sha256=3ww6aECKcVyN52aVZ0we7ffz83m6WDgt2jI21Jsz_lw,639
2
+ openepd/__version__.py,sha256=21tmVPyiMCbot4ApEc5ODeqH2q7qPuAfknYig3smndc,639
3
3
  openepd/api/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
4
4
  openepd/api/average_dataset/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
5
5
  openepd/api/average_dataset/generic_estimate_sync_api.py,sha256=DM7i8gugUIEAutEsakEUxFjwgtKmcytkjvNAvfhVYUs,7690
6
6
  openepd/api/average_dataset/industry_epd_sync_api.py,sha256=xoicuZdclf4BTBa2ndG1K2Aog-iZd7_jOqdDPZSCLq4,6286
7
- openepd/api/base_sync_client.py,sha256=jviqtQgsOVdRq5x7_Yh_Tg8zIdWtVTIUqNCgebf6YDg,20925
7
+ openepd/api/base_sync_client.py,sha256=IX-q6JdWLMqYHC-hKLXUFRPs-DZZ9Co0NRZ5pSAipAs,20992
8
8
  openepd/api/category/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
9
9
  openepd/api/category/dto.py,sha256=tDojagSwT7CTtcYq31Qe_c0P3xKKUWXKdzT5iN6odtk,850
10
10
  openepd/api/category/sync_api.py,sha256=VHHOVbblZGyc4AtbsgQza00trSLuaCO6KfQw6r8vzgg,1371
@@ -18,7 +18,7 @@ openepd/api/dto/params.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
18
18
  openepd/api/epd/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
19
19
  openepd/api/epd/dto.py,sha256=NZ76vfUkCEkwDibQd2QCEQP5DZms_NFc9tjoP-mcw3o,4874
20
20
  openepd/api/epd/sync_api.py,sha256=5wuPiM_zVciVtTvsGaaRdf7T6q2CUp7QMZCW0DAjQng,7072
21
- openepd/api/errors.py,sha256=Pcg2JqjLoUXUFJTVdIFbd8rgjGoAT5HpnivTpFRACVU,2159
21
+ openepd/api/errors.py,sha256=Iipd0QW2tbhUu_UjUbmiFWgmrPVgOFHc3uxZN-SX-g4,2868
22
22
  openepd/api/pcr/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
23
23
  openepd/api/pcr/sync_api.py,sha256=Riu77h8uLJngKpITOiXYmO7mzjAHpYskUJ6ynyfNG78,1557
24
24
  openepd/api/sync_client.py,sha256=IurnhZrkBQoQIbbfon6TPuhjGpAV_CSTJNeXjIiN0QI,3105
@@ -33,16 +33,16 @@ openepd/compat/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,62
33
33
  openepd/compat/compat_functional_validators.py,sha256=yz6DfWeg7knBHEN_enpCGGTLRknEsecXfpzD1FDlywY,834
34
34
  openepd/compat/pydantic.py,sha256=DOjSixsylLqMtFAIARu50sGcT4VPXN_c473q_2JwZQ0,1146
35
35
  openepd/model/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
36
- openepd/model/base.py,sha256=o6miTbb4d2BRQ6epN5Jn8mIMkAeoAucd_3op7aEMELc,9044
36
+ openepd/model/base.py,sha256=h3lUv5XuROAuCyJ68FZ0hkoyiqDRxXpjfxp2c0CGvOU,9044
37
37
  openepd/model/category.py,sha256=IQXNGQFQmFZ_H9PRONloX_UOSf1sTMDq1rM1yz8JR0Y,1639
38
38
  openepd/model/common.py,sha256=hXnz2QiQDx3Z7F4BMHlHQ2iddq3OonKrveltl-nOiEI,5468
39
- openepd/model/declaration.py,sha256=RFkjoEKD9QRzOMzd-svXVezzHZsrbPziZhME8SjmV44,8938
40
- openepd/model/epd.py,sha256=FfU36K5B1iVYksPPvKsg8rZgaO1EKxQar9QcAjAGyiE,8731
39
+ openepd/model/declaration.py,sha256=W4bTm423_vMf8i0nsrEy9iw0fwNGOD-9yvpuRlXt-4o,8999
40
+ openepd/model/epd.py,sha256=fZJxgbpPUc8IINfrVpHG9fUWSf_XWJfMPK9lMhN0qME,9368
41
41
  openepd/model/factory.py,sha256=XP7eeQNW5tqwX_4hfuEb3lK6BFQDb4KB0fSN0r8-lCU,2656
42
- openepd/model/generic_estimate.py,sha256=kBRtW6yAXgV3YkflXOI1jy8Tav5k5y8M3MPnuKPCcA4,3938
42
+ openepd/model/generic_estimate.py,sha256=bbU0cR4izSqjZcfxUHNbdO4pllqqd8OaUFikrEgCFoA,3992
43
43
  openepd/model/geography.py,sha256=eCt15zXKDtiteNwXQ675cFwBXQqSpiGpIqwDo4nkOek,42091
44
- openepd/model/industry_epd.py,sha256=39D8WlLrEUwHLwhUcOQgjcLFQq5xz3fpwkpIfTaXRek,3234
45
- openepd/model/lcia.py,sha256=bnDVsEo429Go_EneVOkXYIxDu2soxrgxyfzU4OhDR3Q,18827
44
+ openepd/model/industry_epd.py,sha256=rgXhCUDAgzZ9eGio7ExqE3ymP3zTXnrrwcIDvg5YP1A,3285
45
+ openepd/model/lcia.py,sha256=pVh4LbZgzDuLuM3epdCNLlAp5B0BnUrwggaaDiuPccM,18848
46
46
  openepd/model/org.py,sha256=FHcYh2WOOQrCMyzm0Ow-iP79jMTBPcneidjH6NXIklA,3760
47
47
  openepd/model/pcr.py,sha256=SwqLWMj9k_jqIzxz5mh6ttqvtLCspKSpywF5YTBOMsA,5397
48
48
  openepd/model/specs/README.md,sha256=W5LSMpZuW5x36cKS4HRfeFsClsRf8J9yHMMICghdc0s,862
@@ -93,11 +93,11 @@ openepd/model/standard.py,sha256=QhGpWN3U27fDcS0Yy1Dk8ElJfD0etet6i_PzoTD6B48,131
93
93
  openepd/model/validation/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
94
94
  openepd/model/validation/common.py,sha256=FLYqK8gYFagx08LCkS0jy3qo4-Zq9VAv5i8ZwF2svkc,2435
95
95
  openepd/model/validation/numbers.py,sha256=tgirqrDGgrSo6APGlW1ozNuVV8mJz_4HCAXS2OUENq0,888
96
- openepd/model/validation/quantity.py,sha256=kzug0MZ3Ao0zeVzN-aleyxUg5hA_7D5tNOOerverfRQ,7415
96
+ openepd/model/validation/quantity.py,sha256=h4AXXMijFtJRFUb5bi_iRzK2v7v1CtggLi7CyK5bA4s,8349
97
97
  openepd/model/versioning.py,sha256=R_zm6rCrgF3vlJQYbpyWhirdS_Oek16cv_mvZmpuE8I,4473
98
98
  openepd/patch_pydantic.py,sha256=xrkzblatmU9HBzukWkp1cPq9ZSuohoz1p0pQqVKSlKs,4122
99
99
  openepd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
- openepd-4.11.3.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
101
- openepd-4.11.3.dist-info/METADATA,sha256=yParHG3ytGnOqmdUk5pDgyVhPBiwwHF-NOxqjl3aZaM,8705
102
- openepd-4.11.3.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
103
- openepd-4.11.3.dist-info/RECORD,,
100
+ openepd-4.12.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
101
+ openepd-4.12.0.dist-info/METADATA,sha256=1mNyCyep7uh_aBL0WoQIM303D5gIkA30Upv_E3pHSx4,8705
102
+ openepd-4.12.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
103
+ openepd-4.12.0.dist-info/RECORD,,