openepd 4.4.0__py3-none-any.whl → 4.5.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.
@@ -0,0 +1,101 @@
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
+ from typing import Any
17
+
18
+ from pydantic import utils as pydantic_utils
19
+
20
+ from openepd.compat.pydantic import pyd
21
+
22
+
23
+ def patch_pydantic_metaclass():
24
+ """
25
+ Patch pydantic's ModelMetaclass to restore class attribute lookup for fields.
26
+
27
+ In pydantic, while the model fields are defined in the model as class-level attributes, in runtime they disappear
28
+ due to ModelMetaclass logic. ModelMetaclass takes the defined attributes, removes them from class dict and puts
29
+ into a special __fields__ attribute to avoid naming conflict.
30
+
31
+ We would like to be able to access the attributes via dot notation in the runtimes, since it makes refactoring
32
+ easier.
33
+
34
+ This class exposes the original fields when accessed via class name. For example, one can call `Pcr.name` and get
35
+ `ModelField`, in addition to calling `pcr.__fields__` on an instance.
36
+ """
37
+
38
+ def model_metaclass__getattr__(cls, name: str) -> Any:
39
+ if name in cls.__fields__:
40
+ return cls.__fields__[name]
41
+ return getattr(super, name)
42
+
43
+ pyd.main.ModelMetaclass.__getattr__ = model_metaclass__getattr__
44
+
45
+
46
+ def patch_pydantic_metaclass_validator():
47
+ """
48
+ Patch the internal validator function used during model construction to support modified metaclass.
49
+
50
+ Pydantic has a special guard which stops execution if a model defines field which shadows a basemodel interface.
51
+ For example, if someone would define a field named `__fields__` this would break code.
52
+
53
+ With the modified metaclass functionality, we are exposing the original fields as class attributes, and this
54
+ breaks this check.
55
+
56
+ This patcher method modifies the pydantic internals so that the check is retained, but it is not causing exception
57
+ when doing the normal field inheritance.
58
+ """
59
+ model_field_classes = list()
60
+
61
+ try:
62
+ from pydantic.v1.fields import ModelField as ModelFieldV1
63
+
64
+ model_field_classes.append(ModelFieldV1)
65
+ except ImportError:
66
+ pass
67
+
68
+ try:
69
+ from pydantic_core.core_schema import ModelField as ModelFieldV2
70
+
71
+ model_field_classes.append(ModelFieldV2)
72
+ except ImportError:
73
+ pass
74
+ model_field_classes_tuple = tuple(model_field_classes)
75
+
76
+ def pydantic_utils__validate_field_name(bases: list[type[pyd.BaseModel]], field_name: str) -> None:
77
+ for base in bases:
78
+ if attr := getattr(base, field_name, None):
79
+ if isinstance(attr, model_field_classes_tuple):
80
+ continue
81
+
82
+ raise NameError(
83
+ f'Field name "{field_name}" shadows a BaseModel attribute; '
84
+ f"use a different field name with \"alias='{field_name}'\"."
85
+ )
86
+
87
+ pyd.main.validate_field_name = pydantic_utils__validate_field_name
88
+ pydantic_utils.validate_field_name = pydantic_utils__validate_field_name
89
+
90
+
91
+ def patch_pydantic():
92
+ """
93
+ Modify Pydantic to support field attribute access via class.
94
+
95
+ Example: Field the_field in the model TheModel(BaseModel). Before this patch, one can do
96
+ `TheModel().__fields__["the_field"]' to get field descriptor. After the fix, one can do TheModel.the_field.
97
+
98
+ To disable, set env variable `OPENEPD_DISABLE_PYDANTIC_PATCH` to "1", "yes" or "true".
99
+ """
100
+ patch_pydantic_metaclass()
101
+ patch_pydantic_metaclass_validator()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 4.4.0
3
+ Version: 4.5.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
@@ -60,9 +60,7 @@ documenting supply-chain specific data.
60
60
 
61
61
  ## Usage
62
62
 
63
- ## Usage
64
-
65
- **❗ ATTENTION**: Pick the right version. The cornerstone of this library models package representing openEPD models.
63
+ **❗ ATTENTION**: Pick the right version. The cornerstone of this library models package representing openEPD models.
66
64
  Models are defined with Pydantic library which is a dependency for openepd package. If you use Pydantic in your project
67
65
  carefully pick the version:
68
66
 
@@ -76,9 +74,11 @@ module. For mode details on the usage please refer to Pydantic documentation.
76
74
 
77
75
  ### API Client
78
76
 
79
- The library provides the API client to work with the OpenEPD API. The client is available in the `openepd.client` module.
77
+ The library provides the API client to work with the OpenEPD API. The client is available in the `openepd.client`
78
+ module.
80
79
  Currently, the only available implementation is based on synchronous [requests]() library. Client provides the following
81
80
  features:
81
+
82
82
  * Error handling - depending on HTTP status code the client raises different exceptions allowing to handle errors
83
83
  in a more granular way.
84
84
  * Throttling - the client is able to throttle the requests to the API to avoid hitting the rate limits.
@@ -144,12 +144,25 @@ with DefaultBundleWriter("my-bundle.epb") as writer, open("test-pcr.pdf", "rb")
144
144
  writer.write_blob_asset(pcr_pdf_file, "application/pdf", pcr_asset, RelType.Pdf)
145
145
  ```
146
146
 
147
- ### Mypy
147
+ ### Model attribute access
148
+
149
+ OpenEPD extends its pydantic models with extra functionality: field descriptors can be accessed via dot notation from
150
+ class name:
151
+
152
+ * Usual pydantic way: TheModel().__field__["the_field"]
153
+ * In openEPD: TheModel.the_field
154
+
155
+ Instances hold data as usual.
156
+
157
+ This behaviour is enabled by default. To disable, run the code with `OPENEPD_DISABLE_PYDANTIC_PATCH` set to `true`.
158
+
159
+ See src/openepd/patch_pydantic.py for details.
160
+
161
+ ### Generated enums
148
162
 
149
- OpenEPD uses a small modification to standard pydantic models (see PydanticClassAttributeExposeModelMetaclass). Mypy,
150
- in order to work correctly, requires a modified pydantic plugin. To enable it, add an
151
- `openepd.mypy.custom_pydantic_plugin` to list of mypy plugins in your `pyproject.toml` or other mypy-related config
152
- file. See [Mypy configuration](https://mypy.readthedocs.io/en/stable/extending_mypy.html)
163
+ The geography and country enums are generated from several sources, including pycountry list of 2-character country
164
+ codes, UN m49 codification, and special regions. To update the enums, first update any of these sources, then use
165
+ `make codegen`. See 'tools/openepd/codegen' for details.
153
166
 
154
167
  # Credits
155
168
 
@@ -1,5 +1,5 @@
1
- openepd/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
2
- openepd/__version__.py,sha256=8oxCZtughIDK-iiG2jwUL1oA4HclG-SZymHOYT1nbtE,638
1
+ openepd/__init__.py,sha256=Shkfh0Kun0YRhmRDw7LkUj2eQL3X-HnP55u2THOEALw,794
2
+ openepd/__version__.py,sha256=Hcw_oQZEZ9hzNn_8In_JiWYNOFLzNVvUkS7-nl2hWyg,638
3
3
  openepd/api/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
4
4
  openepd/api/base_sync_client.py,sha256=jviqtQgsOVdRq5x7_Yh_Tg8zIdWtVTIUqNCgebf6YDg,20925
5
5
  openepd/api/category/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
@@ -30,11 +30,13 @@ openepd/compat/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,62
30
30
  openepd/compat/compat_functional_validators.py,sha256=yz6DfWeg7knBHEN_enpCGGTLRknEsecXfpzD1FDlywY,834
31
31
  openepd/compat/pydantic.py,sha256=DOjSixsylLqMtFAIARu50sGcT4VPXN_c473q_2JwZQ0,1146
32
32
  openepd/model/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
33
- openepd/model/base.py,sha256=XGuXCWr0PTAeewQj8u75OcRWFumFP2wiSdJsrsL_ZGI,9959
33
+ openepd/model/base.py,sha256=uVEGndgLm8JFwffsCXdusfRE01DQSKJUJLxsd-GXba0,8988
34
34
  openepd/model/category.py,sha256=IQXNGQFQmFZ_H9PRONloX_UOSf1sTMDq1rM1yz8JR0Y,1639
35
35
  openepd/model/common.py,sha256=aa_bfotPybPoYyzHtwj5E5X1T-fCEyznMfVUWvpUhiM,5460
36
- openepd/model/epd.py,sha256=_PyVM3cKrOBS2vgLLPBZzKkNfiBEtp2rECP0imp49bc,14082
37
- openepd/model/factory.py,sha256=i45ZXG5RIMKrXvVH1li0ZlUwcBpSl5gEctcLc1MBM7M,1701
36
+ openepd/model/epd.py,sha256=2mdXxXZFoPOoXI_zSC1VzdBf0Q2sUNKg7vrtXZ8c8EE,14137
37
+ openepd/model/factory.py,sha256=StQUH7GNmRqskMnnRYkr_PLCl5X5sSLEPiYZOIQI9eo,2541
38
+ openepd/model/generic_estimate.py,sha256=txr2bCbrZPIaCNJZypkna0vzz7e0AizOp6MMhEU018A,3324
39
+ openepd/model/geography.py,sha256=amL_-0ojKhsiy_pVDbkM9Rm3yzBZC0KGpHQ_rZAYbM4,37417
38
40
  openepd/model/lcia.py,sha256=-5bMz5ZyoZJnggp66v9upTT0yhcyIZYlwfFh83-4ZvY,16968
39
41
  openepd/model/org.py,sha256=FHcYh2WOOQrCMyzm0Ow-iP79jMTBPcneidjH6NXIklA,3760
40
42
  openepd/model/pcr.py,sha256=SwqLWMj9k_jqIzxz5mh6ttqvtLCspKSpywF5YTBOMsA,5397
@@ -86,10 +88,9 @@ openepd/model/validation/common.py,sha256=FLYqK8gYFagx08LCkS0jy3qo4-Zq9VAv5i8ZwF
86
88
  openepd/model/validation/numbers.py,sha256=tgirqrDGgrSo6APGlW1ozNuVV8mJz_4HCAXS2OUENq0,888
87
89
  openepd/model/validation/quantity.py,sha256=kzug0MZ3Ao0zeVzN-aleyxUg5hA_7D5tNOOerverfRQ,7415
88
90
  openepd/model/versioning.py,sha256=R_zm6rCrgF3vlJQYbpyWhirdS_Oek16cv_mvZmpuE8I,4473
89
- openepd/mypy/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
90
- openepd/mypy/custom_pydantic_plugin.py,sha256=JpfQHAmqS95lKm68tTlQ12RFO_O2tvd4ZM_DFBNUa8s,4034
91
+ openepd/patch_pydantic.py,sha256=W4n5nmCeurGfJNxiUiiDs73YqbG4JpMdTc2vPv5_6Ag,3946
91
92
  openepd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
- openepd-4.4.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
93
- openepd-4.4.0.dist-info/METADATA,sha256=monlkvm4yIu0TbpTtV0iMUdxBGohmD-zBk5PSdPNc8o,8212
94
- openepd-4.4.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
95
- openepd-4.4.0.dist-info/RECORD,,
93
+ openepd-4.5.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
94
+ openepd-4.5.0.dist-info/METADATA,sha256=6RbVFGrMX0amNJi-F-by7jBBeUnVFaVB27guVslSiZ4,8534
95
+ openepd-4.5.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
96
+ openepd-4.5.0.dist-info/RECORD,,
openepd/mypy/__init__.py DELETED
@@ -1,15 +0,0 @@
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
- #
@@ -1,91 +0,0 @@
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
-
17
- from collections.abc import Callable
18
-
19
- from mypy.nodes import AssignmentStmt, CallExpr, MemberExpr, TypeInfo
20
- from mypy.plugin import ClassDefContext
21
- import pydantic.mypy
22
- from pydantic.mypy import (
23
- MODEL_METACLASS_FULLNAME,
24
- ModelConfigData,
25
- PydanticModelClassVar,
26
- PydanticModelField,
27
- PydanticModelTransformer,
28
- PydanticPlugin,
29
- )
30
-
31
- # Using this plugin fixes the issue.
32
-
33
- CUSTOM_OPENEPD_MODEL_METACLASS_FULLNAME = "openepd.model.base.PydanticClassAttributeExposeModelMetaclass"
34
- MODEL_METACLASSES_FULL_NAMES = (MODEL_METACLASS_FULLNAME, CUSTOM_OPENEPD_MODEL_METACLASS_FULLNAME)
35
-
36
- DECORATOR_FULLNAMES = pydantic.mypy.DECORATOR_FULLNAMES | {
37
- "pydantic.v1.class_validators.validator",
38
- }
39
-
40
-
41
- class CustomPydanticModelTransformer(PydanticModelTransformer):
42
- """Extension of the mypy/pydantic model transformer which also understands validator definitions via v1 compat."""
43
-
44
- def collect_field_or_class_var_from_stmt(
45
- self, stmt: AssignmentStmt, model_config: ModelConfigData, class_vars: dict[str, PydanticModelClassVar]
46
- ) -> PydanticModelField | PydanticModelClassVar | None:
47
- """Extend implementation of the original Pydantic method with one more case for validator."""
48
- if not stmt.new_syntax and (
49
- isinstance(stmt.rvalue, CallExpr)
50
- and isinstance(stmt.rvalue.callee, CallExpr)
51
- and isinstance(stmt.rvalue.callee.callee, MemberExpr)
52
- and stmt.rvalue.callee.callee.fullname in DECORATOR_FULLNAMES
53
- ):
54
- # Required to detect compat-imported v1 validators and not treat them as fields.
55
- return None
56
- return super().collect_field_or_class_var_from_stmt(stmt, model_config, class_vars)
57
-
58
-
59
- class CustomMetaclassPydanticPlugin(PydanticPlugin):
60
- """
61
- Custom metaclass pydantic plugin.
62
-
63
- Extends a standard pydantic mypy plugin, and adds certain behaviours required for us:
64
- 1. Support for a non-standard metaclass for pydantic models. We use it allow for access via Class.field notation
65
- 2. Support for our modified compat import of pydantic v1 when using this metaclass.
66
- """
67
-
68
- def get_metaclass_hook(self, fullname: str) -> Callable[[ClassDefContext], None] | None:
69
- """Update Pydantic `ModelMetaclass` definition."""
70
- if fullname in MODEL_METACLASSES_FULL_NAMES:
71
- return self._pydantic_model_metaclass_marker_callback
72
- return None
73
-
74
- def get_base_class_hook(self, fullname: str) -> Callable[[ClassDefContext], bool] | None: # type: ignore
75
- """Update Pydantic model class."""
76
- sym = self.lookup_fully_qualified(fullname)
77
- if sym and isinstance(sym.node, TypeInfo): # pragma: no branch
78
- # No branching may occur if the mypy cache has not been cleared
79
- if any(base.fullname in ["pydantic.main.BaseModel", "pydantic.v1.main.BaseModel"] for base in sym.node.mro):
80
- return self._pydantic_model_class_maker_callback
81
- return None
82
-
83
- def _pydantic_model_class_maker_callback(self, ctx: ClassDefContext) -> bool:
84
- # extended to replace the mypy-pydantic transformer with our custom transformer - see validator note.
85
- transformer = CustomPydanticModelTransformer(ctx.cls, ctx.reason, ctx.api, self.plugin_config)
86
- return transformer.transform()
87
-
88
-
89
- def plugin(version: str):
90
- """Entry point to the mypy plugin."""
91
- return CustomMetaclassPydanticPlugin