openepd 4.4.0__tar.gz → 4.5.1__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.
Files changed (99) hide show
  1. {openepd-4.4.0 → openepd-4.5.1}/PKG-INFO +23 -10
  2. {openepd-4.4.0 → openepd-4.5.1}/README.md +22 -9
  3. {openepd-4.4.0 → openepd-4.5.1}/pyproject.toml +8 -2
  4. openepd-4.5.1/src/openepd/__init__.py +21 -0
  5. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/__version__.py +1 -1
  6. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/base.py +3 -23
  7. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/epd.py +101 -97
  8. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/factory.py +26 -6
  9. openepd-4.5.1/src/openepd/model/generic_estimate.py +85 -0
  10. openepd-4.5.1/src/openepd/model/geography.py +1739 -0
  11. openepd-4.5.1/src/openepd/patch_pydantic.py +108 -0
  12. openepd-4.4.0/src/openepd/model/validation/__init__.py +0 -15
  13. openepd-4.4.0/src/openepd/mypy/__init__.py +0 -15
  14. openepd-4.4.0/src/openepd/mypy/custom_pydantic_plugin.py +0 -91
  15. {openepd-4.4.0 → openepd-4.5.1}/LICENSE +0 -0
  16. {openepd-4.4.0/src/openepd → openepd-4.5.1/src/openepd/api}/__init__.py +0 -0
  17. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/base_sync_client.py +0 -0
  18. {openepd-4.4.0/src/openepd/api → openepd-4.5.1/src/openepd/api/category}/__init__.py +0 -0
  19. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/category/dto.py +0 -0
  20. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/category/sync_api.py +0 -0
  21. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/common.py +0 -0
  22. {openepd-4.4.0/src/openepd/api/category → openepd-4.5.1/src/openepd/api/dto}/__init__.py +0 -0
  23. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/dto/base.py +0 -0
  24. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/dto/common.py +0 -0
  25. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/dto/meta.py +0 -0
  26. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/dto/mf.py +0 -0
  27. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/dto/params.py +0 -0
  28. {openepd-4.4.0/src/openepd/api/dto → openepd-4.5.1/src/openepd/api/epd}/__init__.py +0 -0
  29. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/epd/dto.py +0 -0
  30. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/epd/sync_api.py +0 -0
  31. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/errors.py +0 -0
  32. {openepd-4.4.0/src/openepd/api/epd → openepd-4.5.1/src/openepd/api/pcr}/__init__.py +0 -0
  33. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/pcr/sync_api.py +0 -0
  34. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/sync_client.py +0 -0
  35. {openepd-4.4.0/src/openepd/api/pcr → openepd-4.5.1/src/openepd/api/test}/__init__.py +0 -0
  36. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/api/utils.py +0 -0
  37. {openepd-4.4.0/src/openepd/api/test → openepd-4.5.1/src/openepd/bundle}/__init__.py +0 -0
  38. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/bundle/base.py +0 -0
  39. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/bundle/model.py +0 -0
  40. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/bundle/reader.py +0 -0
  41. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/bundle/writer.py +0 -0
  42. {openepd-4.4.0/src/openepd/bundle → openepd-4.5.1/src/openepd/compat}/__init__.py +0 -0
  43. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/compat/compat_functional_validators.py +0 -0
  44. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/compat/pydantic.py +0 -0
  45. {openepd-4.4.0/src/openepd/compat → openepd-4.5.1/src/openepd/model}/__init__.py +0 -0
  46. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/category.py +0 -0
  47. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/common.py +0 -0
  48. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/lcia.py +0 -0
  49. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/org.py +0 -0
  50. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/pcr.py +0 -0
  51. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/README.md +0 -0
  52. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/__init__.py +0 -0
  53. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/asphalt.py +0 -0
  54. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/base.py +0 -0
  55. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/concrete.py +0 -0
  56. {openepd-4.4.0/src/openepd/model → openepd-4.5.1/src/openepd/model/specs/generated}/__init__.py +0 -0
  57. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/accessories.py +0 -0
  58. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/aggregates.py +0 -0
  59. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/aluminium.py +0 -0
  60. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/asphalt.py +0 -0
  61. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/bulk_materials.py +0 -0
  62. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/cast_decks_and_underlayment.py +0 -0
  63. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/cladding.py +0 -0
  64. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/cmu.py +0 -0
  65. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/common.py +0 -0
  66. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/concrete.py +0 -0
  67. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/conveying_equipment.py +0 -0
  68. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/electrical.py +0 -0
  69. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/electrical_transmission_and_distribution_equipment.py +0 -0
  70. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/electricity.py +0 -0
  71. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/enums.py +0 -0
  72. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/finishes.py +0 -0
  73. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/fire_and_smoke_protection.py +0 -0
  74. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/furnishings.py +0 -0
  75. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/grouting.py +0 -0
  76. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/manufacturing_inputs.py +0 -0
  77. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/masonry.py +0 -0
  78. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/material_handling.py +0 -0
  79. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/mechanical.py +0 -0
  80. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/mechanical_insulation.py +0 -0
  81. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/network_infrastructure.py +0 -0
  82. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/openings.py +0 -0
  83. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/other_electrical_equipment.py +0 -0
  84. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/other_materials.py +0 -0
  85. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/plumbing.py +0 -0
  86. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/precast_concrete.py +0 -0
  87. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/sheathing.py +0 -0
  88. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/steel.py +0 -0
  89. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/thermal_moisture_protection.py +0 -0
  90. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/utility_piping.py +0 -0
  91. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/wood.py +0 -0
  92. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/specs/generated/wood_joists.py +0 -0
  93. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/standard.py +0 -0
  94. {openepd-4.4.0/src/openepd/model/specs/generated → openepd-4.5.1/src/openepd/model/validation}/__init__.py +0 -0
  95. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/validation/common.py +0 -0
  96. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/validation/numbers.py +0 -0
  97. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/validation/quantity.py +0 -0
  98. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/model/versioning.py +0 -0
  99. {openepd-4.4.0 → openepd-4.5.1}/src/openepd/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 4.4.0
3
+ Version: 4.5.1
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
 
@@ -34,9 +34,7 @@ documenting supply-chain specific data.
34
34
 
35
35
  ## Usage
36
36
 
37
- ## Usage
38
-
39
- **❗ ATTENTION**: Pick the right version. The cornerstone of this library models package representing openEPD models.
37
+ **❗ ATTENTION**: Pick the right version. The cornerstone of this library models package representing openEPD models.
40
38
  Models are defined with Pydantic library which is a dependency for openepd package. If you use Pydantic in your project
41
39
  carefully pick the version:
42
40
 
@@ -50,9 +48,11 @@ module. For mode details on the usage please refer to Pydantic documentation.
50
48
 
51
49
  ### API Client
52
50
 
53
- The library provides the API client to work with the OpenEPD API. The client is available in the `openepd.client` module.
51
+ The library provides the API client to work with the OpenEPD API. The client is available in the `openepd.client`
52
+ module.
54
53
  Currently, the only available implementation is based on synchronous [requests]() library. Client provides the following
55
54
  features:
55
+
56
56
  * Error handling - depending on HTTP status code the client raises different exceptions allowing to handle errors
57
57
  in a more granular way.
58
58
  * Throttling - the client is able to throttle the requests to the API to avoid hitting the rate limits.
@@ -118,12 +118,25 @@ with DefaultBundleWriter("my-bundle.epb") as writer, open("test-pcr.pdf", "rb")
118
118
  writer.write_blob_asset(pcr_pdf_file, "application/pdf", pcr_asset, RelType.Pdf)
119
119
  ```
120
120
 
121
- ### Mypy
121
+ ### Model attribute access
122
+
123
+ OpenEPD extends its pydantic models with extra functionality: field descriptors can be accessed via dot notation from
124
+ class name:
125
+
126
+ * Usual pydantic way: TheModel().__field__["the_field"]
127
+ * In openEPD: TheModel.the_field
128
+
129
+ Instances hold data as usual.
130
+
131
+ This behaviour is enabled by default. To disable, run the code with `OPENEPD_DISABLE_PYDANTIC_PATCH` set to `true`.
132
+
133
+ See src/openepd/patch_pydantic.py for details.
134
+
135
+ ### Generated enums
122
136
 
123
- OpenEPD uses a small modification to standard pydantic models (see PydanticClassAttributeExposeModelMetaclass). Mypy,
124
- in order to work correctly, requires a modified pydantic plugin. To enable it, add an
125
- `openepd.mypy.custom_pydantic_plugin` to list of mypy plugins in your `pyproject.toml` or other mypy-related config
126
- file. See [Mypy configuration](https://mypy.readthedocs.io/en/stable/extending_mypy.html)
137
+ The geography and country enums are generated from several sources, including pycountry list of 2-character country
138
+ codes, UN m49 codification, and special regions. To update the enums, first update any of these sources, then use
139
+ `make codegen`. See 'tools/openepd/codegen' for details.
127
140
 
128
141
  # Credits
129
142
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openepd"
3
- version = "4.4.0"
3
+ version = "4.5.1"
4
4
  license = "Apache-2.0"
5
5
  description = "Python library to work with OpenEPD format"
6
6
  authors = ["C-Change Labs <support@c-change-labs.com>"]
@@ -16,7 +16,7 @@ classifiers = [
16
16
  "Topic :: Software Development :: Libraries :: Python Modules",
17
17
  ]
18
18
  readme = "README.md"
19
- packages = [{include = "openepd", from = "src"}]
19
+ packages = [{ include = "openepd", from = "src" }]
20
20
  exclude = ["**/test_*.py", "**/tests/**"]
21
21
 
22
22
  [tool.poetry.dependencies]
@@ -54,6 +54,12 @@ types-dateparser = ">=1.1.3"
54
54
  types-deprecated = ">=1.2.9"
55
55
  types-requests = ">=2.0"
56
56
 
57
+ # Code generation
58
+ # For list of countries
59
+ pycountry = ">=24.6.1"
60
+ jinja2 = ">=2.10.3"
61
+
62
+
57
63
  [tool.poetry.extras]
58
64
  api_client = ["requests"]
59
65
 
@@ -0,0 +1,21 @@
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
+ import os
17
+
18
+ from .patch_pydantic import patch_pydantic
19
+
20
+ if os.environ.get("OPENEPD_DISABLE_PYDANTIC_PATCH", "false").lower() not in ("true", "1", "yes"):
21
+ patch_pydantic()
@@ -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.4.0"
16
+ VERSION = "4.5.1"
@@ -36,6 +36,7 @@ class OpenEpdDoctypes(StrEnum):
36
36
  """Enum of supported openEPD document types."""
37
37
 
38
38
  Epd = "openEPD"
39
+ GenericEstimate = "openGenericEstimate"
39
40
 
40
41
 
41
42
  def modify_pydantic_schema(schema_dict: dict, cls: type) -> dict:
@@ -56,28 +57,7 @@ def modify_pydantic_schema(schema_dict: dict, cls: type) -> dict:
56
57
  return schema_dict
57
58
 
58
59
 
59
- class PydanticClassAttributeExposeModelMetaclass(pyd.main.ModelMetaclass):
60
- """
61
- Extension of the pydantic's ModelMetaclass which restores class attribute lookup for fields.
62
-
63
- In pydantic, while the model fields are defined in the model as class-level attributes, in runtime they disappear
64
- due to ModelMetaclass logic. ModelMetaclass takes the defined attributes, removes them from class dict and puts
65
- into a special __fields__ attribute to avoid naming conflict.
66
-
67
- We would like to be able to access the attributes via dot notation in the runtimes, since it makes refactoring
68
- easier.
69
-
70
- This class exposes the original fields when accessed via class name. For example, one can call `Pcr.name` and get
71
- `ModelField`, in addition to calling `pcr.__fields__` on an instance.
72
- """
73
-
74
- def __getattr__(cls, name: str) -> Any:
75
- if name in cls.__fields__:
76
- return cls.__fields__[name]
77
- return getattr(super, name)
78
-
79
-
80
- class BaseOpenEpdSchema(pyd.BaseModel, metaclass=PydanticClassAttributeExposeModelMetaclass):
60
+ class BaseOpenEpdSchema(pyd.BaseModel):
81
61
  """Base class for all OpenEPD models."""
82
62
 
83
63
  ext: dict[str, AnySerializable] | None = pyd.Field(alias="ext", default=None)
@@ -227,7 +207,7 @@ class BaseDocumentFactory(Generic[TRootDocument]):
227
207
  Extend it to create a factory for a specific document type e.g. for industry epd, epd, etc.
228
208
  """
229
209
 
230
- DOCTYPE_CONSTRAINT: str = ""
210
+ DOCTYPE_CONSTRAINT: OpenEpdDoctypes
231
211
  VERSION_MAP: dict[Version, type[TRootDocument]] = {}
232
212
 
233
213
  @classmethod
@@ -17,7 +17,7 @@ import datetime
17
17
  from typing import Annotated
18
18
 
19
19
  from openepd.compat.pydantic import pyd
20
- from openepd.model.base import BaseDocumentFactory, RootDocument
20
+ from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes, RootDocument
21
21
  from openepd.model.common import Amount, Ingredient, WithAltIdsMixin, WithAttachmentsMixin
22
22
  from openepd.model.lcia import Impacts, OutputFlowSet, ResourceUseSet
23
23
  from openepd.model.org import Org, Plant
@@ -27,54 +27,47 @@ from openepd.model.standard import Standard
27
27
  from openepd.model.versioning import OpenEpdVersions, Version
28
28
 
29
29
 
30
- class BaseEpd(RootDocument):
30
+ class BaseDeclaration(RootDocument):
31
31
  """
32
32
  Base class for EPD documents.
33
33
 
34
34
  This class should not be used directly. Use Epd or EpdVx instead.
35
35
  """
36
36
 
37
- pass
38
-
39
-
40
- class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseEpd):
41
- """Represent an EPD."""
42
-
43
- _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
44
-
45
- # TODO: Add validator for open-xpd-uuid on this field
46
37
  id: str | None = pyd.Field(
47
38
  description="The unique ID for this EPD. To ensure global uniqueness, should be registered at "
48
39
  "open-xpd-uuid.cqd.io/register or a coordinating registry.",
49
40
  example="1u7zsed8",
50
41
  default=None,
51
42
  )
52
- product_name: str | None = pyd.Field(
53
- max_length=200, description="The name of the product described by this EPD", example="Mix 12345AC", default=None
43
+ date_of_issue: datetime.datetime | None = pyd.Field(
44
+ example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
45
+ description="Date the EPD was issued. This should be the first day on which the EPD is valid.",
54
46
  )
55
- product_sku: str | None = pyd.Field(
56
- max_length=200, description="Unique stock keeping identifier assigned by manufacturer"
47
+ valid_until: datetime.datetime | None = pyd.Field(
48
+ example=datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.timezone.utc),
49
+ description="Last date the EPD is valid on, including any extensions.",
57
50
  )
58
- product_description: str | None = pyd.Field(
59
- max_length=2000,
60
- description="1-paragraph description of product. Supports plain text or github flavored markdown.",
51
+
52
+ declared_unit: Amount | None = pyd.Field(
53
+ description="SI declared unit for this EPD. If a functional unit is "
54
+ "utilized, the declared unit shall refer to the amount of "
55
+ "product associated with the A1-A3 life cycle stage."
56
+ )
57
+ kg_per_declared_unit: Amount | None = pyd.Field(
58
+ default=None,
59
+ description="Mass of the product, in kilograms, per declared unit",
60
+ example=Amount(qty=12.5, unit="kg"),
61
61
  )
62
+ compliance: list[Standard] = pyd.Field(
63
+ description="Standard(s) to which this declaration is compliant.", default_factory=list
64
+ )
65
+
62
66
  # TODO: add product_alt_names? E.g. ILCD has a list of synonymous names
63
67
  product_classes: dict[str, str | list[str]] = pyd.Field(
64
68
  description="List of classifications, including Masterformat and UNSPC", default_factory=dict
65
69
  )
66
- product_image_small: pyd.AnyUrl | None = pyd.Field(
67
- description="Pointer to image illustrating the product, which is no more than 200x200 pixels", default=None
68
- )
69
- product_image: pyd.AnyUrl | pyd.FileUrl | None = pyd.Field(
70
- description="pointer to image illustrating the product no more than 10MB", default=None
71
- )
72
- version: pyd.PositiveInt | None = pyd.Field(
73
- description="Version of this document. The document's issuer should increment it anytime even a single "
74
- "character changes, as this value is used to determine the most recent version.",
75
- example=1,
76
- default=None,
77
- )
70
+
78
71
  language: str | None = pyd.Field(
79
72
  min_length=2,
80
73
  max_length=2,
@@ -91,6 +84,80 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseEpd):
91
84
  "number of required fields, to allow for multiple systems to coordinate "
92
85
  "incomplete EPDs.",
93
86
  )
87
+ impacts: Impacts | None = pyd.Field(
88
+ description="List of environmental impacts, compiled per one of the standard Impact Assessment methods"
89
+ )
90
+ resource_uses: ResourceUseSet | None = pyd.Field(
91
+ description="Set of Resource Use Indicators, over various LCA scopes"
92
+ )
93
+ output_flows: OutputFlowSet | None = pyd.Field(
94
+ description="Set of Waste and Output Flow indicators which describe the waste categories "
95
+ "and other material output flows derived from the LCI."
96
+ )
97
+
98
+ pcr: Pcr | None = pyd.Field(
99
+ description="JSON object for product category rules. Should point to the "
100
+ "most-specific PCR that applies; the PCR entry should point to any "
101
+ "parent PCR.",
102
+ default=None,
103
+ )
104
+ lca_discussion: str | None = pyd.Field(
105
+ max_length=20000,
106
+ description="""A rich text description containing information for experts reviewing the EPD contents.
107
+ Text descriptions required by ISO 14025, ISO 21930, EN 15804,, relevant PCRs, or program instructions and which do not
108
+ have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
109
+ separated by github flavored markdown formatting.""",
110
+ example="""# Packaging
111
+
112
+ Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
113
+ strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
114
+ scenario assumptions, including disposition pathways for each packaging material by reuse, recycling, or landfill
115
+ disposal based on packaging type.*
116
+
117
+ # Product Installation
118
+
119
+ A description of the type of processing, machinery, tools, dust extraction equipment, auxiliary materials, etc.
120
+ to be used during installation shall be included. Information on industrial and environmental protection may be
121
+ included in this section. Any waste treatment included within the system boundary of installation waste should be
122
+ specified.
123
+
124
+ # Use Conditions
125
+
126
+ Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
127
+ Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
128
+ In the absence of primary data, cleaning assumptions shall be documented.
129
+ """,
130
+ )
131
+
132
+
133
+ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
134
+ """Represent an EPD."""
135
+
136
+ _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
137
+
138
+ # TODO: Add validator for open-xpd-uuid on this field
139
+ product_name: str | None = pyd.Field(
140
+ max_length=200, description="The name of the product described by this EPD", example="Mix 12345AC", default=None
141
+ )
142
+ product_sku: str | None = pyd.Field(
143
+ max_length=200, description="Unique stock keeping identifier assigned by manufacturer"
144
+ )
145
+ product_description: str | None = pyd.Field(
146
+ max_length=2000,
147
+ description="1-paragraph description of product. Supports plain text or github flavored markdown.",
148
+ )
149
+ product_image_small: pyd.AnyUrl | None = pyd.Field(
150
+ description="Pointer to image illustrating the product, which is no more than 200x200 pixels", default=None
151
+ )
152
+ product_image: pyd.AnyUrl | pyd.FileUrl | None = pyd.Field(
153
+ description="pointer to image illustrating the product no more than 10MB", default=None
154
+ )
155
+ version: pyd.PositiveInt | None = pyd.Field(
156
+ description="Version of this document. The document's issuer should increment it anytime even a single "
157
+ "character changes, as this value is used to determine the most recent version.",
158
+ example=1,
159
+ default=None,
160
+ )
94
161
  declaration_url: str | None = pyd.Field(
95
162
  description="Link to data object on original registrar's site",
96
163
  example="https://epd-online.com/EmbeddedEpdList/Download/6029",
@@ -131,30 +198,6 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseEpd):
131
198
  third_party_verifier_email: pyd.EmailStr | None = pyd.Field(
132
199
  description="Email address of the third party verifier", example="john.doe@example.com", default=None
133
200
  )
134
- date_of_issue: datetime.datetime | None = pyd.Field(
135
- example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
136
- description="Date the EPD was issued. This should be the first day on which the EPD is valid.",
137
- )
138
- valid_until: datetime.datetime | None = pyd.Field(
139
- example=datetime.datetime(day=11, month=9, year=2028, tzinfo=datetime.timezone.utc),
140
- description="Last date the EPD is valid on, including any extensions.",
141
- )
142
- pcr: Pcr | None = pyd.Field(
143
- description="JSON object for product category rules. Should point to the "
144
- "most-specific PCR that applies; the PCR entry should point to any "
145
- "parent PCR.",
146
- default=None,
147
- )
148
- declared_unit: Amount | None = pyd.Field(
149
- description="SI declared unit for this EPD. If a functional unit is "
150
- "utilized, the declared unit shall refer to the amount of "
151
- "product associated with the A1-A3 life cycle stage."
152
- )
153
- kg_per_declared_unit: Amount | None = pyd.Field(
154
- default=None,
155
- description="Mass of the product, in kilograms, per declared unit",
156
- example=Amount(qty=12.5, unit="kg"),
157
- )
158
201
  kg_C_per_declared_unit: Amount | None = pyd.Field(
159
202
  default=None,
160
203
  description="Mass of elemental carbon, per declared unit, contained in the product itself at the manufacturing "
@@ -213,19 +256,7 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseEpd):
213
256
  manufacturing_image: pyd.AnyUrl | None = pyd.Field(
214
257
  description="Pointer (url) to an image illustrating the manufacturing process. No more than 10MB.", default=None
215
258
  )
216
- impacts: Impacts | None = pyd.Field(
217
- description="List of environmental impacts, compiled per one of the standard Impact Assessment methods"
218
- )
219
- resource_uses: ResourceUseSet | None = pyd.Field(
220
- description="Set of Resource Use Indicators, over various LCA scopes"
221
- )
222
- output_flows: OutputFlowSet | None = pyd.Field(
223
- description="Set of Waste and Output Flow indicators which describe the waste categories "
224
- "and other material output flows derived from the LCI."
225
- )
226
- compliance: list[Standard] = pyd.Field(
227
- description="Standard(s) to which this declaration is compliant.", default_factory=list
228
- )
259
+
229
260
  specs: Specs = pyd.Field(
230
261
  default_factory=Specs,
231
262
  description="Data structure(s) describing performance specs of product. Unique for each material type.",
@@ -236,33 +267,6 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseEpd):
236
267
  "Each one should be an EPD or digitized LCI process.",
237
268
  default_factory=list,
238
269
  )
239
- lca_discussion: str | None = pyd.Field(
240
- max_length=20000,
241
- description="""A rich text description containing information for experts reviewing the EPD contents.
242
- Text descriptions required by ISO 14025, ISO 21930, EN 15804,, relevant PCRs, or program instructions and which do not
243
- have specific openEPD fields should be entered here. This field may be large, and may contain multiple sections
244
- separated by github flavored markdown formatting.""",
245
- example="""# Packaging
246
-
247
- Information on product-specific packaging: type, composition and possible reuse of packaging materials (paper,
248
- strapping, pallets, foils, drums, etc.) shall be included in this Section. The EPD shall describe specific packaging
249
- scenario assumptions, including disposition pathways for each packaging material by reuse, recycling, or landfill
250
- disposal based on packaging type.*
251
-
252
- # Product Installation
253
-
254
- A description of the type of processing, machinery, tools, dust extraction equipment, auxiliary materials, etc.
255
- to be used during installation shall be included. Information on industrial and environmental protection may be
256
- included in this section. Any waste treatment included within the system boundary of installation waste should be
257
- specified.
258
-
259
- # Use Conditions
260
-
261
- Use-stage environmental impacts of flooring products during building operations depend on product cleaning assumptions.
262
- Information on cleaning frequency and cleaning products shall be provided based on the manufacturer’s recommendations.
263
- In the absence of primary data, cleaning assumptions shall be documented.
264
- """,
265
- )
266
270
 
267
271
  @classmethod
268
272
  def get_asset_type(cls) -> str | None:
@@ -287,8 +291,8 @@ class EpdV0(WithAttachmentsMixin, WithAltIdsMixin, BaseEpd):
287
291
  Epd = EpdV0
288
292
 
289
293
 
290
- class EpdFactory(BaseDocumentFactory[BaseEpd]):
294
+ class EpdFactory(BaseDocumentFactory[BaseDeclaration]):
291
295
  """Factory for EPD objects."""
292
296
 
293
- DOCTYPE_CONSTRAINT = "openEPD"
294
- VERSION_MAP: dict[Version, type[BaseEpd]] = {OpenEpdVersions.Version0: EpdV0}
297
+ DOCTYPE_CONSTRAINT = OpenEpdDoctypes.Epd
298
+ VERSION_MAP: dict[Version, type[BaseDeclaration]] = {OpenEpdVersions.Version0: EpdV0}
@@ -15,24 +15,26 @@
15
15
  #
16
16
  from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes, RootDocument
17
17
  from openepd.model.epd import EpdFactory
18
+ from openepd.model.generic_estimate import GenericEstimateFactory
18
19
 
19
20
 
20
21
  class DocumentFactory:
21
22
  """A factory for creating documents regardless of the type."""
22
23
 
23
- DOCTYPE_TO_FACTORY: dict[str, type[BaseDocumentFactory]] = {
24
+ DOCTYPE_TO_FACTORY: dict[OpenEpdDoctypes, type[BaseDocumentFactory]] = {
24
25
  OpenEpdDoctypes.Epd: EpdFactory,
26
+ OpenEpdDoctypes.GenericEstimate: GenericEstimateFactory,
25
27
  }
26
28
 
27
29
  @classmethod
28
- def from_dict(cls, data: dict) -> RootDocument:
30
+ def get_factory(cls, doctype: OpenEpdDoctypes | None) -> type[BaseDocumentFactory]:
29
31
  """
30
- Create a document from the dictionary.
32
+ Get a document factory by given doctype.
31
33
 
32
- Type of the document will be recognized from the `doctype` field.
33
- :raise ValueError: if the document type is not specified or not supported.
34
+ :param doctype: doctype
35
+ :return document factory
36
+ :raise ValueError if doctype not supported or not found.
34
37
  """
35
- doctype = data.get("doctype")
36
38
  if doctype is None:
37
39
  raise ValueError("The document type is not specified.")
38
40
  factory = cls.DOCTYPE_TO_FACTORY.get(doctype)
@@ -41,4 +43,22 @@ class DocumentFactory:
41
43
  f"The document of type `{doctype}` is not supported. Supported documents are: "
42
44
  + ", ".join(cls.DOCTYPE_TO_FACTORY.keys())
43
45
  )
46
+ return factory
47
+
48
+ @classmethod
49
+ def from_dict(cls, data: dict) -> RootDocument:
50
+ """
51
+ Create a document from the dictionary.
52
+
53
+ Type of the document will be recognized from the `doctype` field.
54
+ :raise ValueError: if the document type is not specified or not supported.
55
+ """
56
+ doctype = data.get("doctype")
57
+ if doctype is None or not isinstance(doctype, (str, OpenEpdDoctypes)):
58
+ raise ValueError(
59
+ f"The document type is not specified or not supported. "
60
+ f"Please specify it in `doctype` field. Supported are: {','.join(cls.DOCTYPE_TO_FACTORY)}"
61
+ )
62
+
63
+ factory = cls.get_factory(OpenEpdDoctypes(doctype))
44
64
  return factory.from_dict(data)
@@ -0,0 +1,85 @@
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 enum import StrEnum
17
+
18
+ from openepd.compat.pydantic import pyd
19
+ from openepd.model.base import BaseDocumentFactory, OpenEpdDoctypes
20
+ from openepd.model.common import WithAltIdsMixin, WithAttachmentsMixin
21
+ from openepd.model.epd import BaseDeclaration
22
+ from openepd.model.geography import Geography
23
+ from openepd.model.org import Org
24
+ from openepd.model.versioning import OpenEpdVersions, Version
25
+
26
+
27
+ class LicenseTerms(StrEnum):
28
+ """Licensing terms."""
29
+
30
+ CC_BY = "CC-BY"
31
+ """
32
+ Creative Commons attribution-only license https://creativecommons.org/licenses/by/4.0/.
33
+ """
34
+ ODbL = "ODbL"
35
+ """
36
+ Open Database License per https://opendatacommons.org/licenses/odbl/
37
+ """
38
+ Government = "Government"
39
+ """
40
+ The data is offered at no charge by a government body such as the USDA or Okobaudat.
41
+ """
42
+ Other = "Other"
43
+ """
44
+ Review estimate documentation to determine license terms.
45
+ """
46
+
47
+
48
+ class GenericEstimateV0(WithAttachmentsMixin, WithAltIdsMixin, BaseDeclaration):
49
+ """Represent a Generic Estimate."""
50
+
51
+ _FORMAT_VERSION = OpenEpdVersions.Version0.as_str()
52
+
53
+ doctype: str = pyd.Field(
54
+ description='Describes the type and schema of the document. Must always be "openGenericEstimate"',
55
+ default="openGenericEstimate",
56
+ )
57
+
58
+ name: str | None = pyd.Field(max_length=200, description="", default=None)
59
+ description: str | None = pyd.Field(
60
+ max_length=2000,
61
+ description="1-paragraph description of the Generic Estimate. Supports plain text or github flavored markdown.",
62
+ )
63
+
64
+ publisher: Org | None = pyd.Field(description="Organization that published the LCA results.")
65
+ reviewer_email: pyd.EmailStr | None = pyd.Field(
66
+ description="Email address of the third party verifier", example="john.doe@example.com", default=None
67
+ )
68
+ reviewer: Org | None = pyd.Field(description="Org that performed a critical review of the LCA.")
69
+ license_terms: LicenseTerms | None = pyd.Field(description="The license terms for use of the data.")
70
+ geography: list[Geography] | None = pyd.Field(
71
+ "Jurisdiction(s) in which the LCA result is applicable. An empty array, or absent properties, implies global applicability."
72
+ )
73
+ model_repository: pyd.AnyUrl | None = pyd.Field(
74
+ default=None, description="A link to the shared git repository containing the LCA model used for this estimate."
75
+ )
76
+
77
+
78
+ GenericEstimate = GenericEstimateV0
79
+
80
+
81
+ class GenericEstimateFactory(BaseDocumentFactory[GenericEstimate]):
82
+ """Factory for EPD objects."""
83
+
84
+ DOCTYPE_CONSTRAINT = OpenEpdDoctypes.GenericEstimate
85
+ VERSION_MAP: dict[Version, type[GenericEstimate]] = {OpenEpdVersions.Version0: GenericEstimateV0}