openepd 7.1.0__tar.gz → 7.3.0__tar.gz
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-7.1.0 → openepd-7.3.0}/PKG-INFO +23 -13
- {openepd-7.1.0 → openepd-7.3.0}/README.md +19 -8
- {openepd-7.1.0 → openepd-7.3.0}/pyproject.toml +36 -15
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/__version__.py +1 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/average_dataset/generic_estimate_sync_api.py +2 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/average_dataset/industry_epd_sync_api.py +2 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/base_sync_client.py +10 -8
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/common.py +17 -11
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/dto/common.py +1 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/epd/sync_api.py +2 -1
- openepd-7.3.0/src/openepd/api/org/sync_api.py +79 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/pcr/sync_api.py +35 -0
- openepd-7.3.0/src/openepd/api/plant/sync_api.py +79 -0
- openepd-7.3.0/src/openepd/api/standard/sync_api.py +79 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/sync_client.py +27 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/bundle/base.py +47 -6
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/bundle/model.py +1 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/bundle/reader.py +35 -10
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/bundle/writer.py +21 -12
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/m49/__init__.py +2 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/m49/const.py +1 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/m49/utils.py +16 -10
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/base.py +20 -15
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/common.py +10 -5
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/declaration.py +2 -2
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/epd.py +2 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/factory.py +5 -3
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/generic_estimate.py +4 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/lcia.py +10 -10
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/org.py +14 -7
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/pcr.py +2 -2
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/__init__.py +37 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/asphalt.py +3 -3
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/base.py +2 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/enums.py +9 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/__init__.py +5 -3
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/cmu.py +0 -2
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/concrete.py +25 -2
- openepd-7.3.0/src/openepd/model/specs/range/exterior_improvements.py +47 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/finishes.py +19 -40
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/manufacturing_inputs.py +17 -5
- openepd-7.3.0/src/openepd/model/specs/range/mixins/__init__.py +15 -0
- openepd-7.3.0/src/openepd/model/specs/range/mixins/access_flooring_mixin.py +43 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/steel.py +18 -9
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/wood.py +4 -6
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/__init__.py +119 -2
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/aluminium.py +2 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/concrete.py +25 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/deprecated/__init__.py +1 -1
- openepd-7.3.0/src/openepd/model/specs/singular/exterior_improvements.py +47 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/finishes.py +3 -59
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/manufacturing_inputs.py +13 -1
- openepd-7.3.0/src/openepd/model/specs/singular/mixins/__init__.py +15 -0
- openepd-7.3.0/src/openepd/model/specs/singular/mixins/access_flooring_mixin.py +55 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/steel.py +10 -2
- openepd-7.3.0/src/openepd/model/validation/__init__.py +15 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/validation/common.py +10 -6
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/validation/enum.py +4 -2
- openepd-7.3.0/src/openepd/model/validation/numbers.py +15 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/validation/quantity.py +13 -6
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/versioning.py +8 -6
- {openepd-7.1.0 → openepd-7.3.0}/LICENSE +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/__init__.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/__init__.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/average_dataset/__init__.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/category/__init__.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/category/dto.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/category/sync_api.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/dto/__init__.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/dto/base.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/dto/meta.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/dto/mf.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/dto/params.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/epd/__init__.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/epd/dto.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/errors.py +0 -0
- {openepd-7.1.0/src/openepd/api/pcr → openepd-7.3.0/src/openepd/api/org}/__init__.py +0 -0
- {openepd-7.1.0/src/openepd/api/test → openepd-7.3.0/src/openepd/api/pcr}/__init__.py +0 -0
- {openepd-7.1.0/src/openepd/bundle → openepd-7.3.0/src/openepd/api/plant}/__init__.py +0 -0
- {openepd-7.1.0/src/openepd/model → openepd-7.3.0/src/openepd/api/standard}/__init__.py +0 -0
- {openepd-7.1.0/src/openepd/model/specs/singular/mixins → openepd-7.3.0/src/openepd/api/test}/__init__.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/utils.py +0 -0
- {openepd-7.1.0/src/openepd/model/validation → openepd-7.3.0/src/openepd/bundle}/__init__.py +0 -0
- openepd-7.1.0/src/openepd/model/validation/numbers.py → openepd-7.3.0/src/openepd/model/__init__.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/category.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/geography.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/industry_epd.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/README.md +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/concrete.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/accessories.py +1 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/aggregates.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/aluminium.py +1 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/asphalt.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/bulk_materials.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/cast_decks_and_underlayment.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/cladding.py +10 -10
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/conveying_equipment.py +2 -2
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/electrical.py +18 -18
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +1 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/electricity.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/fire_and_smoke_protection.py +3 -3
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/furnishings.py +7 -7
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/grouting.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/masonry.py +1 -1
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/material_handling.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/mechanical.py +6 -6
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/mechanical_insulation.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/network_infrastructure.py +3 -3
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/openings.py +17 -17
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/other_electrical_equipment.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/other_materials.py +4 -4
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/plumbing.py +5 -5
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/precast_concrete.py +2 -2
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/sheathing.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/thermal_moisture_protection.py +12 -12
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/utility_piping.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/range/wood_joists.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/accessories.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/aggregates.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/asphalt.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/bulk_materials.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/cast_decks_and_underlayment.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/cladding.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/cmu.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/common.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/conveying_equipment.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/deprecated/concrete.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/deprecated/steel.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/electrical.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/electrical_transmission_and_distribution_equipment.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/electricity.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/fire_and_smoke_protection.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/furnishings.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/grouting.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/masonry.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/material_handling.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/mechanical.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/mechanical_insulation.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/mixins/conduit_mixin.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/network_infrastructure.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/openings.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/other_electrical_equipment.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/other_materials.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/plumbing.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/precast_concrete.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/sheathing.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/thermal_moisture_protection.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/utility_piping.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/wood.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/specs/singular/wood_joists.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/model/standard.py +0 -0
- {openepd-7.1.0 → openepd-7.3.0}/src/openepd/py.typed +0 -0
@@ -1,15 +1,14 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: openepd
|
3
|
-
Version: 7.
|
3
|
+
Version: 7.3.0
|
4
4
|
Summary: Python library to work with OpenEPD format
|
5
|
-
Home-page: https://github.com/cchangelabs/openepd
|
6
5
|
License: Apache-2.0
|
7
6
|
Author: C-Change Labs
|
8
7
|
Author-email: support@c-change-labs.com
|
9
8
|
Maintainer: C-Change Labs
|
10
9
|
Maintainer-email: open-source@c-change-labs.com
|
11
10
|
Requires-Python: >=3.11,<4.0
|
12
|
-
Classifier: Development Status ::
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
13
12
|
Classifier: Intended Audience :: Developers
|
14
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
15
14
|
Classifier: Operating System :: OS Independent
|
@@ -21,7 +20,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
20
|
Provides-Extra: api-client
|
22
21
|
Requires-Dist: email-validator (>=1.3.1)
|
23
22
|
Requires-Dist: idna (>=3.7)
|
24
|
-
Requires-Dist: open-xpd-uuid (>=0.2.1,<
|
23
|
+
Requires-Dist: open-xpd-uuid (>=0.2.1,<2)
|
25
24
|
Requires-Dist: openlocationcode (>=1.0.1)
|
26
25
|
Requires-Dist: pydantic (>=2.0,<3)
|
27
26
|
Requires-Dist: requests (>=2.0) ; extra == "api-client"
|
@@ -45,6 +44,24 @@ Description-Content-Type: text/markdown
|
|
45
44
|
|
46
45
|
This library is a Python library to work with OpenEPD format.
|
47
46
|
|
47
|
+
> ⚠️ **Version Warning**
|
48
|
+
>
|
49
|
+
> This application is currently developed in **two major versions** in parallel:
|
50
|
+
>
|
51
|
+
> - **v6.x (>=6.0.0)** — Stable and production-ready. Maintains support for Pydantic v1 and v2 through a compatibility layer.
|
52
|
+
> - **v7.x (>=7.0.0)** — Public beta. Fully functional, with native support for Pydantic v2. Still experimental and may introduce breaking changes in **internal and integration interfaces**.
|
53
|
+
>
|
54
|
+
> ⚠️ No breaking changes are expected in the **public standard or data model**, only in internal APIs and integration points.
|
55
|
+
>
|
56
|
+
> Both versions currently offer the same set of features.
|
57
|
+
> We recommend using **v6** for most production use cases as the more mature and stable option.
|
58
|
+
> **v7** is suitable for production environments that can tolerate some level of interface instability and want to adopt the latest internals.
|
59
|
+
>
|
60
|
+
> 💡 Only the **latest version of v7** is guaranteed to contain all the features and updates from v6. Earlier v7 releases may lack some recent improvements.
|
61
|
+
>
|
62
|
+
> Once **v7 is promoted to stable**, all earlier **pre-stable (beta) v7 releases** will be **marked as yanked** to prevent accidental usage in production.
|
63
|
+
>
|
64
|
+
|
48
65
|
## About OpenEPD
|
49
66
|
|
50
67
|
[openEPD](https://www.buildingtransparency.org/programs/openepd/) is an open data format for passing digital
|
@@ -60,17 +77,10 @@ including uniqueness of organizations/plants, precise PCR references, and dated
|
|
60
77
|
The openEPD format is **extensible**. Standard extensions exist for concrete products, and for
|
61
78
|
documenting supply-chain specific data.
|
62
79
|
|
63
|
-
[Read More about OpenEPD format here](https://www.
|
80
|
+
[Read More about OpenEPD format here](https://www.open-epd-forum.org).
|
64
81
|
|
65
82
|
## Usage
|
66
83
|
|
67
|
-
**❗ ATTENTION**: Pick the right version. The cornerstone of this library models package representing openEPD models.
|
68
|
-
Models are defined with Pydantic library which is a dependency for openepd package. If you use Pydantic in your project
|
69
|
-
carefully pick the version:
|
70
|
-
|
71
|
-
* Use version **below** `2.0.0` if your project uses Pydantic version below `2.0.0`
|
72
|
-
* Use version `2.x.x` or higher if your project uses Pydantic version `2.0.0` or above
|
73
|
-
|
74
84
|
### Models
|
75
85
|
|
76
86
|
The library provides the Pydantic models for all the OpenEPD entities. The models are available in the `openepd.models`
|
@@ -15,6 +15,24 @@
|
|
15
15
|
|
16
16
|
This library is a Python library to work with OpenEPD format.
|
17
17
|
|
18
|
+
> ⚠️ **Version Warning**
|
19
|
+
>
|
20
|
+
> This application is currently developed in **two major versions** in parallel:
|
21
|
+
>
|
22
|
+
> - **v6.x (>=6.0.0)** — Stable and production-ready. Maintains support for Pydantic v1 and v2 through a compatibility layer.
|
23
|
+
> - **v7.x (>=7.0.0)** — Public beta. Fully functional, with native support for Pydantic v2. Still experimental and may introduce breaking changes in **internal and integration interfaces**.
|
24
|
+
>
|
25
|
+
> ⚠️ No breaking changes are expected in the **public standard or data model**, only in internal APIs and integration points.
|
26
|
+
>
|
27
|
+
> Both versions currently offer the same set of features.
|
28
|
+
> We recommend using **v6** for most production use cases as the more mature and stable option.
|
29
|
+
> **v7** is suitable for production environments that can tolerate some level of interface instability and want to adopt the latest internals.
|
30
|
+
>
|
31
|
+
> 💡 Only the **latest version of v7** is guaranteed to contain all the features and updates from v6. Earlier v7 releases may lack some recent improvements.
|
32
|
+
>
|
33
|
+
> Once **v7 is promoted to stable**, all earlier **pre-stable (beta) v7 releases** will be **marked as yanked** to prevent accidental usage in production.
|
34
|
+
>
|
35
|
+
|
18
36
|
## About OpenEPD
|
19
37
|
|
20
38
|
[openEPD](https://www.buildingtransparency.org/programs/openepd/) is an open data format for passing digital
|
@@ -30,17 +48,10 @@ including uniqueness of organizations/plants, precise PCR references, and dated
|
|
30
48
|
The openEPD format is **extensible**. Standard extensions exist for concrete products, and for
|
31
49
|
documenting supply-chain specific data.
|
32
50
|
|
33
|
-
[Read More about OpenEPD format here](https://www.
|
51
|
+
[Read More about OpenEPD format here](https://www.open-epd-forum.org).
|
34
52
|
|
35
53
|
## Usage
|
36
54
|
|
37
|
-
**❗ ATTENTION**: Pick the right version. The cornerstone of this library models package representing openEPD models.
|
38
|
-
Models are defined with Pydantic library which is a dependency for openepd package. If you use Pydantic in your project
|
39
|
-
carefully pick the version:
|
40
|
-
|
41
|
-
* Use version **below** `2.0.0` if your project uses Pydantic version below `2.0.0`
|
42
|
-
* Use version `2.x.x` or higher if your project uses Pydantic version `2.0.0` or above
|
43
|
-
|
44
55
|
### Models
|
45
56
|
|
46
57
|
The library provides the Pydantic models for all the OpenEPD entities. The models are available in the `openepd.models`
|
@@ -1,11 +1,6 @@
|
|
1
|
-
[tool.ruff]
|
2
|
-
line-length = 120
|
3
|
-
target-version = "py311"
|
4
|
-
exclude = [".*pyi"]
|
5
|
-
|
6
1
|
[tool.poetry]
|
7
2
|
name = "openepd"
|
8
|
-
version = "7.
|
3
|
+
version = "7.3.0"
|
9
4
|
license = "Apache-2.0"
|
10
5
|
description = "Python library to work with OpenEPD format"
|
11
6
|
authors = ["C-Change Labs <support@c-change-labs.com>"]
|
@@ -13,7 +8,7 @@ maintainers = ["C-Change Labs <open-source@c-change-labs.com>"]
|
|
13
8
|
repository = "https://github.com/cchangelabs/openepd"
|
14
9
|
keywords = []
|
15
10
|
classifiers = [
|
16
|
-
"Development Status ::
|
11
|
+
"Development Status :: 4 - Beta",
|
17
12
|
"Intended Audience :: Developers",
|
18
13
|
"License :: OSI Approved :: Apache Software License",
|
19
14
|
"Operating System :: OS Independent",
|
@@ -23,6 +18,7 @@ classifiers = [
|
|
23
18
|
readme = "README.md"
|
24
19
|
packages = [{ include = "openepd", from = "src" }]
|
25
20
|
exclude = ["**/test_*.py", "**/tests/**"]
|
21
|
+
requires-poetry = ">=2.0.0"
|
26
22
|
|
27
23
|
[tool.poetry.dependencies]
|
28
24
|
python = "^3.11"
|
@@ -30,13 +26,13 @@ pydantic = ">=2.0,<3"
|
|
30
26
|
email-validator = ">=1.3.1"
|
31
27
|
requests = { version = ">=2.0", optional = true }
|
32
28
|
idna = ">=3.7"
|
33
|
-
open-xpd-uuid = "
|
29
|
+
open-xpd-uuid = ">=0.2.1,<2"
|
34
30
|
openlocationcode = ">=1.0.1"
|
35
31
|
|
36
32
|
# Optional dependencies
|
37
33
|
# lxml = { version = "~=4.9.2", optional = true }
|
38
34
|
|
39
|
-
[tool.poetry.dev
|
35
|
+
[tool.poetry.group.dev.dependencies]
|
40
36
|
# Unit tests
|
41
37
|
coverage = { version = "=6.5", extras = ["toml"] }
|
42
38
|
pytest = "~=7.2"
|
@@ -47,12 +43,8 @@ wheel = "~=0.40.0"
|
|
47
43
|
click = "~=8.1.7"
|
48
44
|
|
49
45
|
# Dev tools
|
50
|
-
|
46
|
+
ruff = ">=0.11.8"
|
51
47
|
licenseheaders = "~=0.8"
|
52
|
-
flake8 = "~=4.0"
|
53
|
-
flake8-import-graph = "~=0.1.3"
|
54
|
-
flake8-docstrings = "~=1.7.0"
|
55
|
-
isort = "~=5.11"
|
56
48
|
mypy = ">=1.0.1"
|
57
49
|
pre-commit = "~=2.19"
|
58
50
|
commitizen = "~=3.16.0"
|
@@ -71,7 +63,6 @@ jinja2 = ">=3.1.4"
|
|
71
63
|
[tool.poetry.extras]
|
72
64
|
api_client = ["requests"]
|
73
65
|
|
74
|
-
|
75
66
|
[tool.commitizen]
|
76
67
|
version_provider = "poetry"
|
77
68
|
bump_version = "bump: version $current_version → $new_version"
|
@@ -151,3 +142,33 @@ follow_imports = "skip"
|
|
151
142
|
[[tool.mypy.overrides]]
|
152
143
|
module = "openlocationcode.*"
|
153
144
|
ignore_missing_imports = true
|
145
|
+
|
146
|
+
[tool.ruff]
|
147
|
+
line-length = 120
|
148
|
+
target-version = "py311"
|
149
|
+
exclude = [".*pyi", "tools/**.py"]
|
150
|
+
|
151
|
+
[tool.ruff.lint.isort]
|
152
|
+
force-sort-within-sections = true
|
153
|
+
|
154
|
+
[tool.ruff.lint]
|
155
|
+
extend-ignore = [
|
156
|
+
"S101", # Use of assert statement. We have a lot of asserts for mypy type checking.
|
157
|
+
"W291", # W291 trailing whitespace
|
158
|
+
"W391", # W391 blank line at end of file
|
159
|
+
"E501", # E501: line too long
|
160
|
+
"E203", # E704: Multiple statements on one line (def)
|
161
|
+
"F403", #F403 'from module import *' used; unable to detect undefined names (F403)
|
162
|
+
##### DOCSTRINGS #####
|
163
|
+
"D100", # Missing docstring in public module
|
164
|
+
"D101", # Missing docstring in public class
|
165
|
+
"D102", # Docstring of prublic method
|
166
|
+
"D107", # Missing docstring in __init__
|
167
|
+
"D105", # Missing docstring in magic method
|
168
|
+
"D104", # Missing docstring in public package
|
169
|
+
"D106", # Missing docstring in public nested class
|
170
|
+
"D202", # D202 No blank lines allowed after function docstring
|
171
|
+
"D203", # We want to have blank line before class
|
172
|
+
"D212", # We want to require second line for multiline docstrings
|
173
|
+
]
|
174
|
+
extend-select = ["S", "E", "B", "A", "EM", "UP", "LOG", "G", "I", "D"]
|
{openepd-7.1.0 → openepd-7.3.0}/src/openepd/api/average_dataset/generic_estimate_sync_api.py
RENAMED
@@ -135,7 +135,8 @@ class GenericEstimateApi(BaseApiMethodGroup):
|
|
135
135
|
"""
|
136
136
|
ge_id = ge.id
|
137
137
|
if not ge_id:
|
138
|
-
|
138
|
+
msg = "The ID must be set to edit a GenericEstimate."
|
139
|
+
raise ValueError(msg)
|
139
140
|
|
140
141
|
response = self._client.do_request(
|
141
142
|
"put",
|
@@ -90,7 +90,8 @@ class IndustryEpdApi(BaseApiMethodGroup):
|
|
90
90
|
"""
|
91
91
|
iepd_id = iepd.id
|
92
92
|
if not iepd_id:
|
93
|
-
|
93
|
+
msg = "The ID must be set to edit a IndustryEpd."
|
94
|
+
raise ValueError(msg)
|
94
95
|
|
95
96
|
response = self._client.do_request(
|
96
97
|
"put",
|
@@ -14,14 +14,15 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
__all__ = (
|
17
|
-
"
|
18
|
-
"
|
17
|
+
"USER_AGENT_DEFAULT",
|
18
|
+
"BaseApiMethodGroup",
|
19
19
|
"DoRequest",
|
20
|
+
"HttpStreamReader",
|
20
21
|
"RetryHandler",
|
21
|
-
"
|
22
|
-
"USER_AGENT_DEFAULT",
|
22
|
+
"SyncHttpClient",
|
23
23
|
)
|
24
24
|
|
25
|
+
from collections.abc import Callable
|
25
26
|
import datetime
|
26
27
|
from functools import partial, wraps
|
27
28
|
from io import IOBase
|
@@ -29,7 +30,7 @@ import logging
|
|
29
30
|
import random
|
30
31
|
import shutil
|
31
32
|
import time
|
32
|
-
from typing import IO, Any, BinaryIO,
|
33
|
+
from typing import IO, Any, BinaryIO, Final, NamedTuple
|
33
34
|
|
34
35
|
import requests
|
35
36
|
from requests import PreparedRequest, Response, Session, Timeout
|
@@ -180,7 +181,7 @@ class SyncHttpClient:
|
|
180
181
|
self._throttler = Throttler(rate_per_sec=requests_per_sec)
|
181
182
|
self._throttle_retry_timeout: float = (
|
182
183
|
float(throttle_retry_timeout)
|
183
|
-
if isinstance(throttle_retry_timeout,
|
184
|
+
if isinstance(throttle_retry_timeout, float | int)
|
184
185
|
else throttle_retry_timeout.total_seconds()
|
185
186
|
)
|
186
187
|
self.user_agent = user_agent
|
@@ -412,7 +413,8 @@ class SyncHttpClient:
|
|
412
413
|
|
413
414
|
response.raise_for_status()
|
414
415
|
# This can't be handled by static checker because of the dynamic nature of the raise_for_status method
|
415
|
-
|
416
|
+
msg = "This line should never be reached"
|
417
|
+
raise RuntimeError(msg)
|
416
418
|
|
417
419
|
def _get_url_for_request(self, path_or_url: str) -> str:
|
418
420
|
"""
|
@@ -471,7 +473,7 @@ class SyncHttpClient:
|
|
471
473
|
exception = e
|
472
474
|
|
473
475
|
if exception or response.status_code == requests_codes.service_unavailable:
|
474
|
-
secs = random.randint(60, 60 * 5)
|
476
|
+
secs = random.randint(60, 60 * 5) # noqa: S311
|
475
477
|
logger.warning(
|
476
478
|
"%s %s is unavailable. Attempts left: %s. Waiting %s seconds...",
|
477
479
|
method,
|
@@ -13,12 +13,12 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
|
-
from collections.abc import Iterable
|
16
|
+
from collections.abc import Callable, Iterable, Iterator
|
17
17
|
from contextlib import contextmanager
|
18
18
|
from datetime import datetime, timedelta
|
19
19
|
import threading
|
20
20
|
from time import sleep
|
21
|
-
from typing import
|
21
|
+
from typing import Generic, cast
|
22
22
|
|
23
23
|
from requests import Response
|
24
24
|
|
@@ -126,20 +126,26 @@ class StreamingListResponse(Iterable[TOpenEpdObject], Generic[TOpenEpdObject]):
|
|
126
126
|
:return: list of items on the page
|
127
127
|
"""
|
128
128
|
if page_num <= 0:
|
129
|
-
|
129
|
+
msg = "Page number must be positive"
|
130
|
+
raise ValueError(msg)
|
130
131
|
if self.__current_page != page_num or force_reload:
|
131
132
|
self.__recent_response = self.__fetch_handler(page_num, self.__page_size)
|
132
133
|
self.__current_page = page_num
|
133
134
|
if self.__recent_response is None:
|
134
|
-
|
135
|
+
msg = "Response is empty, this should not happen, check if fetch_handler is compatible"
|
136
|
+
raise RuntimeError(msg)
|
135
137
|
if self.__recent_response.payload is None:
|
136
|
-
|
138
|
+
msg = "Response does not contain payload"
|
139
|
+
raise ValueError(msg)
|
137
140
|
if not isinstance(self.__recent_response.payload, list):
|
138
|
-
|
141
|
+
msg = "Response does not contain a list"
|
142
|
+
raise ValueError(msg)
|
139
143
|
if self.__recent_response.meta is None:
|
140
|
-
|
144
|
+
msg = "Response does not contain meta"
|
145
|
+
raise ValueError(msg)
|
141
146
|
if not isinstance(self.__recent_response.meta, PagingMetaMixin):
|
142
|
-
|
147
|
+
msg = "Response does not contain paging meta"
|
148
|
+
raise ValueError(msg)
|
143
149
|
return self.__recent_response.payload
|
144
150
|
|
145
151
|
def get_paging_meta(self) -> PagingMeta:
|
@@ -152,7 +158,8 @@ class StreamingListResponse(Iterable[TOpenEpdObject], Generic[TOpenEpdObject]):
|
|
152
158
|
"""
|
153
159
|
paging_meta = cast(PagingMetaMixin, self.get_meta()).paging
|
154
160
|
if paging_meta is None:
|
155
|
-
|
161
|
+
msg = "Response does not contain paging meta"
|
162
|
+
raise ValueError(msg)
|
156
163
|
return paging_meta
|
157
164
|
|
158
165
|
def get_meta(self) -> MetaCollectionDto:
|
@@ -206,8 +213,7 @@ class StreamingListResponse(Iterable[TOpenEpdObject], Generic[TOpenEpdObject]):
|
|
206
213
|
self.goto_page(start_from_page)
|
207
214
|
while True:
|
208
215
|
items = self.goto_page(self.current_page)
|
209
|
-
|
210
|
-
yield x
|
216
|
+
yield from items
|
211
217
|
if not self.has_next_page():
|
212
218
|
return # no more pages
|
213
219
|
else:
|
@@ -63,7 +63,7 @@ TMetaExtension = TypeVar("TMetaExtension", bound=MetaExtensionBase)
|
|
63
63
|
|
64
64
|
class MetaCollectionDto(BaseOpenEpdApiModel, Generic[TMetaExtension]):
|
65
65
|
"""
|
66
|
-
|
66
|
+
Use this class as a container for different meta objects.
|
67
67
|
|
68
68
|
From a specific controller, you should return a specific subclass of MetaCollectionDto and appropriate mixins. This
|
69
69
|
would allow to retain type information to generate schema for meta section of response.
|
@@ -173,7 +173,8 @@ class EpdApi(BaseApiMethodGroup):
|
|
173
173
|
"""
|
174
174
|
epd_id = epd.id
|
175
175
|
if not epd_id:
|
176
|
-
|
176
|
+
msg = "The EPD ID must be set to edit an EPD."
|
177
|
+
raise ValueError(msg)
|
177
178
|
response = self._client.do_request(
|
178
179
|
"put",
|
179
180
|
f"/epds/{encode_path_param(epd_id)}",
|
@@ -0,0 +1,79 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2025 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 Literal, overload
|
17
|
+
|
18
|
+
from requests import Response
|
19
|
+
|
20
|
+
from openepd.api.base_sync_client import BaseApiMethodGroup
|
21
|
+
from openepd.api.utils import encode_path_param
|
22
|
+
from openepd.model.org import Org, OrgRef
|
23
|
+
|
24
|
+
|
25
|
+
class OrgApi(BaseApiMethodGroup):
|
26
|
+
"""API methods for Orgs."""
|
27
|
+
|
28
|
+
@overload
|
29
|
+
def create(self, to_create: Org, with_response: Literal[True]) -> tuple[OrgRef, Response]: ...
|
30
|
+
|
31
|
+
@overload
|
32
|
+
def create(self, to_create: Org, with_response: Literal[False] = False) -> OrgRef: ...
|
33
|
+
|
34
|
+
def create(self, to_create: Org, with_response: bool = False) -> OrgRef | tuple[OrgRef, Response]:
|
35
|
+
"""
|
36
|
+
Create a new organization.
|
37
|
+
|
38
|
+
:param to_create: Organization to create
|
39
|
+
:param with_response: if True, return a tuple of (OrgRef, Response), otherwise return only OrgRef
|
40
|
+
:return: Organization reference or Organization reference with HTTP Response object depending on parameter
|
41
|
+
:raise ValidationError: if given object Org is invalid
|
42
|
+
"""
|
43
|
+
response = self._client.do_request("post", "/orgs", json=to_create.to_serializable())
|
44
|
+
content = response.json()
|
45
|
+
ref = OrgRef.model_validate(content)
|
46
|
+
if with_response:
|
47
|
+
return ref, response
|
48
|
+
return ref
|
49
|
+
|
50
|
+
@overload
|
51
|
+
def edit(self, to_edit: Org, with_response: Literal[True]) -> tuple[OrgRef, Response]: ...
|
52
|
+
|
53
|
+
@overload
|
54
|
+
def edit(self, to_edit: Org, with_response: Literal[False] = False) -> OrgRef: ...
|
55
|
+
|
56
|
+
def edit(self, to_edit: Org, with_response: bool = False) -> OrgRef | tuple[OrgRef, Response]:
|
57
|
+
"""
|
58
|
+
Edit an organization.
|
59
|
+
|
60
|
+
:param to_edit: Organization to edit
|
61
|
+
:param with_response: if True, return a tuple of (OrgRef, Response), otherwise return only Org
|
62
|
+
:return: Organization reference or Organization reference with HTTP Response object depending on parameter
|
63
|
+
:raise ValueError: if the organization web_domain is not set
|
64
|
+
"""
|
65
|
+
entity_id = to_edit.web_domain
|
66
|
+
if not entity_id:
|
67
|
+
msg = "The organization web_domain must be set to edit an organization."
|
68
|
+
raise ValueError(msg)
|
69
|
+
response = self._client.do_request(
|
70
|
+
"put",
|
71
|
+
f"/orgs/{encode_path_param(entity_id)}",
|
72
|
+
json=to_edit.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
|
73
|
+
)
|
74
|
+
response.raise_for_status()
|
75
|
+
content = response.json()
|
76
|
+
ref = OrgRef.model_validate(content)
|
77
|
+
if with_response:
|
78
|
+
return ref, response
|
79
|
+
return ref
|
@@ -13,7 +13,12 @@
|
|
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, overload
|
17
|
+
|
18
|
+
from requests import Response
|
19
|
+
|
16
20
|
from openepd.api.base_sync_client import BaseApiMethodGroup
|
21
|
+
from openepd.api.utils import encode_path_param
|
17
22
|
from openepd.model.pcr import Pcr, PcrRef
|
18
23
|
|
19
24
|
|
@@ -42,3 +47,33 @@ class PcrApi(BaseApiMethodGroup):
|
|
42
47
|
"""
|
43
48
|
pcr_ref_obj = self._client.do_request("post", "/pcrs", json=pcr.to_serializable()).json()
|
44
49
|
return PcrRef.model_validate(pcr_ref_obj)
|
50
|
+
|
51
|
+
@overload
|
52
|
+
def edit(self, to_edit: Pcr, with_response: Literal[True]) -> tuple[PcrRef, Response]: ...
|
53
|
+
|
54
|
+
@overload
|
55
|
+
def edit(self, to_edit: Pcr, with_response: Literal[False] = False) -> PcrRef: ...
|
56
|
+
|
57
|
+
def edit(self, to_edit: Pcr, with_response: bool = False) -> PcrRef | tuple[PcrRef, Response]:
|
58
|
+
"""
|
59
|
+
Edit a pcr.
|
60
|
+
|
61
|
+
:param to_edit: Pcr to edit
|
62
|
+
:param with_response: if True, return a tuple of (PcrRef, Response), otherwise return only PcrRef
|
63
|
+
:return: Pcr reference or Pcr reference with HTTP Response object depending on parameter
|
64
|
+
:raise ValueError: if the pcr ID is not set
|
65
|
+
"""
|
66
|
+
entity_id = to_edit.id
|
67
|
+
if not entity_id:
|
68
|
+
msg = "The pcr ID must be set to edit a pcr."
|
69
|
+
raise ValueError(msg)
|
70
|
+
response = self._client.do_request(
|
71
|
+
"put",
|
72
|
+
f"/pcrs/{encode_path_param(entity_id)}",
|
73
|
+
json=to_edit.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
|
74
|
+
)
|
75
|
+
content = response.json()
|
76
|
+
ref = PcrRef.model_validate(content)
|
77
|
+
if with_response:
|
78
|
+
return ref, response
|
79
|
+
return ref
|
@@ -0,0 +1,79 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2025 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 Literal, overload
|
17
|
+
|
18
|
+
from requests import Response
|
19
|
+
|
20
|
+
from openepd.api.base_sync_client import BaseApiMethodGroup
|
21
|
+
from openepd.api.utils import encode_path_param
|
22
|
+
from openepd.model.org import Plant, PlantRef
|
23
|
+
|
24
|
+
|
25
|
+
class PlantApi(BaseApiMethodGroup):
|
26
|
+
"""API methods for Plants."""
|
27
|
+
|
28
|
+
@overload
|
29
|
+
def create(self, to_create: Plant, with_response: Literal[True]) -> tuple[PlantRef, Response]: ...
|
30
|
+
|
31
|
+
@overload
|
32
|
+
def create(self, to_create: Plant, with_response: Literal[False] = False) -> PlantRef: ...
|
33
|
+
|
34
|
+
def create(self, to_create: Plant, with_response: bool = False) -> PlantRef | tuple[PlantRef, Response]:
|
35
|
+
"""
|
36
|
+
Create a new plant.
|
37
|
+
|
38
|
+
:param to_create: Plant to create
|
39
|
+
:param with_response: if True, return a tuple of (PlantRef, Response), otherwise return only PlantRef
|
40
|
+
:return: Plant reference or Plant reference with HTTP Response object depending on parameter
|
41
|
+
:raise ValidationError: if given object Plant is invalid
|
42
|
+
"""
|
43
|
+
response = self._client.do_request("post", "/plants", json=to_create.to_serializable())
|
44
|
+
content = response.json()
|
45
|
+
ref = PlantRef.model_validate(content)
|
46
|
+
if with_response:
|
47
|
+
return ref, response
|
48
|
+
return ref
|
49
|
+
|
50
|
+
@overload
|
51
|
+
def edit(self, to_edit: Plant, with_response: Literal[True]) -> tuple[PlantRef, Response]: ...
|
52
|
+
|
53
|
+
@overload
|
54
|
+
def edit(self, to_edit: Plant, with_response: Literal[False] = False) -> PlantRef: ...
|
55
|
+
|
56
|
+
def edit(self, to_edit: Plant, with_response: bool = False) -> PlantRef | tuple[PlantRef, Response]:
|
57
|
+
"""
|
58
|
+
Edit a plant.
|
59
|
+
|
60
|
+
:param to_edit: Plant to edit
|
61
|
+
:param with_response: if True, return a tuple of (PlantRef, Response), otherwise return only PlantRef
|
62
|
+
:return: Plant reference or Plant reference with HTTP Response object depending on parameter
|
63
|
+
:raise ValueError: if the plant ID is not set
|
64
|
+
"""
|
65
|
+
entity_id = to_edit.id
|
66
|
+
if not entity_id:
|
67
|
+
msg = "The plant ID must be set to edit a plant."
|
68
|
+
raise ValueError(msg)
|
69
|
+
response = self._client.do_request(
|
70
|
+
"put",
|
71
|
+
f"/plants/{encode_path_param(entity_id)}",
|
72
|
+
json=to_edit.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
|
73
|
+
)
|
74
|
+
response.raise_for_status()
|
75
|
+
content = response.json()
|
76
|
+
ref = PlantRef.model_validate(content)
|
77
|
+
if with_response:
|
78
|
+
return ref, response
|
79
|
+
return ref
|