openepd 5.1.1__tar.gz → 5.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.
Files changed (140) hide show
  1. {openepd-5.1.1 → openepd-5.3.0}/PKG-INFO +1 -1
  2. {openepd-5.1.1 → openepd-5.3.0}/pyproject.toml +1 -1
  3. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/__version__.py +1 -1
  4. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/average_dataset/generic_estimate_sync_api.py +12 -5
  5. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/epd/sync_api.py +14 -5
  6. openepd-5.3.0/src/openepd/api/utils.py +68 -0
  7. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/common.py +43 -1
  8. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/concrete.py +11 -0
  9. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/enums.py +51 -3
  10. openepd-5.3.0/src/openepd/model/validation/enum.py +42 -0
  11. openepd-5.1.1/src/openepd/api/utils.py +0 -28
  12. {openepd-5.1.1 → openepd-5.3.0}/LICENSE +0 -0
  13. {openepd-5.1.1 → openepd-5.3.0}/README.md +0 -0
  14. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/__init__.py +0 -0
  15. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/__init__.py +0 -0
  16. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/average_dataset/__init__.py +0 -0
  17. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/average_dataset/industry_epd_sync_api.py +0 -0
  18. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/base_sync_client.py +0 -0
  19. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/category/__init__.py +0 -0
  20. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/category/dto.py +0 -0
  21. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/category/sync_api.py +0 -0
  22. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/common.py +0 -0
  23. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/dto/__init__.py +0 -0
  24. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/dto/base.py +0 -0
  25. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/dto/common.py +0 -0
  26. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/dto/meta.py +0 -0
  27. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/dto/mf.py +0 -0
  28. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/dto/params.py +0 -0
  29. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/epd/__init__.py +0 -0
  30. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/epd/dto.py +0 -0
  31. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/errors.py +0 -0
  32. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/pcr/__init__.py +0 -0
  33. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/pcr/sync_api.py +0 -0
  34. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/sync_client.py +0 -0
  35. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/api/test/__init__.py +0 -0
  36. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/bundle/__init__.py +0 -0
  37. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/bundle/base.py +0 -0
  38. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/bundle/model.py +0 -0
  39. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/bundle/reader.py +0 -0
  40. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/bundle/writer.py +0 -0
  41. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/compat/__init__.py +0 -0
  42. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/compat/compat_functional_validators.py +0 -0
  43. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/compat/pydantic.py +0 -0
  44. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/__init__.py +0 -0
  45. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/base.py +0 -0
  46. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/category.py +0 -0
  47. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/declaration.py +0 -0
  48. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/epd.py +0 -0
  49. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/factory.py +0 -0
  50. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/generic_estimate.py +0 -0
  51. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/geography.py +0 -0
  52. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/industry_epd.py +0 -0
  53. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/lcia.py +0 -0
  54. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/org.py +0 -0
  55. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/pcr.py +0 -0
  56. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/README.md +0 -0
  57. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/__init__.py +0 -0
  58. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/asphalt.py +0 -0
  59. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/base.py +0 -0
  60. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/concrete.py +0 -0
  61. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/__init__.py +0 -0
  62. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/accessories.py +0 -0
  63. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/aggregates.py +0 -0
  64. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/aluminium.py +0 -0
  65. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/asphalt.py +0 -0
  66. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/bulk_materials.py +0 -0
  67. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/cast_decks_and_underlayment.py +0 -0
  68. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/cladding.py +0 -0
  69. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/cmu.py +0 -0
  70. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/common.py +0 -0
  71. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/conveying_equipment.py +0 -0
  72. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/electrical.py +0 -0
  73. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/electrical_transmission_and_distribution_equipment.py +0 -0
  74. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/electricity.py +0 -0
  75. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/finishes.py +0 -0
  76. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/fire_and_smoke_protection.py +0 -0
  77. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/furnishings.py +0 -0
  78. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/grouting.py +0 -0
  79. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/manufacturing_inputs.py +0 -0
  80. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/masonry.py +0 -0
  81. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/material_handling.py +0 -0
  82. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/mechanical.py +0 -0
  83. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/mechanical_insulation.py +0 -0
  84. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/mixins/__init__.py +0 -0
  85. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/mixins/conduit_mixin.py +0 -0
  86. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/network_infrastructure.py +0 -0
  87. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/openings.py +0 -0
  88. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/other_electrical_equipment.py +0 -0
  89. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/other_materials.py +0 -0
  90. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/plumbing.py +0 -0
  91. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/precast_concrete.py +0 -0
  92. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/sheathing.py +0 -0
  93. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/steel.py +0 -0
  94. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/thermal_moisture_protection.py +0 -0
  95. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/utility_piping.py +0 -0
  96. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/wood.py +0 -0
  97. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/generated/wood_joists.py +0 -0
  98. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/__init__.py +0 -0
  99. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/accessories.py +0 -0
  100. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/aggregates.py +0 -0
  101. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/aluminium.py +0 -0
  102. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/asphalt.py +0 -0
  103. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/bulk_materials.py +0 -0
  104. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/cast_decks_and_underlayment.py +0 -0
  105. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/cladding.py +0 -0
  106. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/cmu.py +0 -0
  107. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/concrete.py +0 -0
  108. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/conveying_equipment.py +0 -0
  109. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/electrical.py +0 -0
  110. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +0 -0
  111. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/electricity.py +0 -0
  112. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/finishes.py +0 -0
  113. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/fire_and_smoke_protection.py +0 -0
  114. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/furnishings.py +0 -0
  115. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/grouting.py +0 -0
  116. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/manufacturing_inputs.py +0 -0
  117. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/masonry.py +0 -0
  118. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/material_handling.py +0 -0
  119. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/mechanical.py +0 -0
  120. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/mechanical_insulation.py +0 -0
  121. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/network_infrastructure.py +0 -0
  122. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/openings.py +0 -0
  123. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/other_electrical_equipment.py +0 -0
  124. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/other_materials.py +0 -0
  125. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/plumbing.py +0 -0
  126. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/precast_concrete.py +0 -0
  127. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/sheathing.py +0 -0
  128. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/steel.py +0 -0
  129. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/thermal_moisture_protection.py +0 -0
  130. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/utility_piping.py +0 -0
  131. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/wood.py +0 -0
  132. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/specs/range/wood_joists.py +0 -0
  133. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/standard.py +0 -0
  134. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/validation/__init__.py +0 -0
  135. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/validation/common.py +0 -0
  136. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/validation/numbers.py +0 -0
  137. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/validation/quantity.py +0 -0
  138. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/model/versioning.py +0 -0
  139. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/patch_pydantic.py +0 -0
  140. {openepd-5.1.1 → openepd-5.3.0}/src/openepd/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 5.1.1
3
+ Version: 5.3.0
4
4
  Summary: Python library to work with OpenEPD format
5
5
  Home-page: https://github.com/cchangelabs/openepd
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openepd"
3
- version = "5.1.1"
3
+ version = "5.3.0"
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>"]
@@ -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 = "5.1.1"
16
+ VERSION = "5.3.0"
@@ -21,7 +21,7 @@ from openepd.api.base_sync_client import BaseApiMethodGroup
21
21
  from openepd.api.common import StreamingListResponse, paging_meta_from_v1_api
22
22
  from openepd.api.dto.common import BaseMeta, OpenEpdApiResponse
23
23
  from openepd.api.dto.meta import PagingMetaMixin
24
- from openepd.api.utils import encode_path_param
24
+ from openepd.api.utils import encode_path_param, remove_none_id_fields
25
25
  from openepd.model.generic_estimate import (
26
26
  GenericEstimate,
27
27
  GenericEstimatePreview,
@@ -57,26 +57,33 @@ class GenericEstimateApi(BaseApiMethodGroup):
57
57
 
58
58
  @overload
59
59
  def post_with_refs(
60
- self, ge: GenericEstimateWithDeps, with_response: Literal[True]
60
+ self, ge: GenericEstimateWithDeps, with_response: Literal[True], exclude_defaults: bool = True
61
61
  ) -> tuple[GenericEstimate, Response]: ...
62
62
 
63
63
  @overload
64
- def post_with_refs(self, ge: GenericEstimateWithDeps, with_response: Literal[False] = False) -> GenericEstimate: ...
64
+ def post_with_refs(
65
+ self, ge: GenericEstimateWithDeps, with_response: Literal[False] = False, exclude_defaults: bool = True
66
+ ) -> GenericEstimate: ...
65
67
 
66
68
  def post_with_refs(
67
- self, ge: GenericEstimateWithDeps, with_response: bool = False
69
+ self, ge: GenericEstimateWithDeps, with_response: bool = False, exclude_defaults: bool = True
68
70
  ) -> GenericEstimate | tuple[GenericEstimate, Response]:
69
71
  """
70
72
  Post an GenericEstimate with references.
71
73
 
72
74
  :param ge: GenericEstimate
73
75
  :param with_response: return the response object togather with the GenericEstimate
76
+ :param exclude_defaults: If True, fields with default values are excluded from the payload
74
77
  :return: GenericEstimate alone, or GenericEstimate with HTTP Response object depending on parameter
75
78
  """
79
+ data = ge.to_serializable(exclude_unset=True, exclude_defaults=exclude_defaults, by_alias=True)
80
+ if exclude_defaults is False:
81
+ # Remove 'id' fields with None values, as 'id' cannot be None
82
+ data = remove_none_id_fields(data)
76
83
  response = self._client.do_request(
77
84
  "patch",
78
85
  "/generic_estimates/post_with_refs",
79
- json=ge.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
86
+ json=data,
80
87
  )
81
88
  content = response.json()
82
89
  if with_response:
@@ -20,7 +20,7 @@ from requests import Response
20
20
  from openepd.api.base_sync_client import BaseApiMethodGroup
21
21
  from openepd.api.common import StreamingListResponse
22
22
  from openepd.api.epd.dto import EpdSearchResponse, EpdStatisticsResponse, StatisticsDto
23
- from openepd.api.utils import encode_path_param
23
+ from openepd.api.utils import encode_path_param, remove_none_id_fields
24
24
  from openepd.model.epd import Epd
25
25
 
26
26
 
@@ -106,23 +106,32 @@ class EpdApi(BaseApiMethodGroup):
106
106
  return self.get_statistics_raw(omf).payload
107
107
 
108
108
  @overload
109
- def post_with_refs(self, epd: Epd, with_response: Literal[True]) -> tuple[Epd, Response]: ...
109
+ def post_with_refs(
110
+ self, epd: Epd, with_response: Literal[True], exclude_defaults: bool = True
111
+ ) -> tuple[Epd, Response]: ...
110
112
 
111
113
  @overload
112
- def post_with_refs(self, epd: Epd, with_response: Literal[False] = False) -> Epd: ...
114
+ def post_with_refs(self, epd: Epd, with_response: Literal[False] = False, exclude_defaults: bool = True) -> Epd: ...
113
115
 
114
- def post_with_refs(self, epd: Epd, with_response: bool = False) -> Epd | tuple[Epd, Response]:
116
+ def post_with_refs(
117
+ self, epd: Epd, with_response: bool = False, exclude_defaults: bool = True
118
+ ) -> Epd | tuple[Epd, Response]:
115
119
  """
116
120
  Post an EPD with references.
117
121
 
118
122
  :param epd: EPD
119
123
  :param with_response: return the response object togather with the EPD
124
+ :param exclude_defaults: If True, fields with default values are excluded from the payload
120
125
  :return: EPD or EPD with HTTP Response object depending on parameter
121
126
  """
127
+ epd_data = epd.to_serializable(exclude_unset=True, exclude_defaults=exclude_defaults, by_alias=True)
128
+ if exclude_defaults is False:
129
+ # Remove 'id' fields with None values, as 'id' cannot be None
130
+ epd_data = remove_none_id_fields(epd_data)
122
131
  response = self._client.do_request(
123
132
  "patch",
124
133
  "/epds/post-with-refs",
125
- json=epd.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
134
+ json=epd_data,
126
135
  )
127
136
  content = response.json()
128
137
  if with_response:
@@ -0,0 +1,68 @@
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
+ __all__ = ("encode_path_param",)
17
+
18
+ from urllib.parse import quote
19
+
20
+
21
+ def encode_path_param(value: str) -> str:
22
+ """
23
+ Encode a path parameter value.
24
+
25
+ :param value: parameter value
26
+ :return: encoded value
27
+ """
28
+ return quote(value, safe="")
29
+
30
+
31
+ def remove_none_id_fields(d: dict) -> dict:
32
+ """
33
+ Remove any key 'id' with a None value from the dictionary, including nested dicts.
34
+
35
+ :param d: the dict which may contain 'id' keys with None values.
36
+ :return: a new dict with 'id' keys that have None values removed.
37
+
38
+ :note:
39
+ - This function does not modify the original dictionary (no side effects).
40
+ - It returns a new dictionary with the necessary modifications applied.
41
+
42
+ :example:
43
+ >>> data = {
44
+ ... "id": None,
45
+ ... "name": "item1",
46
+ ... "details": {
47
+ ... "id": None,
48
+ ... "category": "tools",
49
+ ... "nested": {
50
+ ... "id": None,
51
+ ... "value": 42
52
+ ... }
53
+ ... }
54
+ ... }
55
+ >>> remove_none_id_fields(data)
56
+ {'name': 'item1', 'details': {'category': 'tools', 'nested': {'value': 42}}}
57
+ """
58
+ if not isinstance(d, dict):
59
+ return d
60
+
61
+ cleaned_dict = {}
62
+ for k, v in d.items():
63
+ if isinstance(v, dict):
64
+ v = remove_none_id_fields(v)
65
+ if not (k == "id" and v is None):
66
+ cleaned_dict[k] = v
67
+
68
+ return cleaned_dict
@@ -38,6 +38,32 @@ class Amount(BaseOpenEpdSchema):
38
38
  return f"{self.qty or ''} {self.unit or 'str'}".strip()
39
39
 
40
40
 
41
+ class Distribution(StrEnum):
42
+ """
43
+ Distribution of the measured value.
44
+
45
+ * log-normal: Probability distribution of any random parameter whose natural log is normally distributed (the
46
+ PDF is gaussian).
47
+ * normal: Probability distribution of any random parameter whose value is normally distributed around the mean
48
+ (the PDF is gaussian).
49
+ * Continuous uniform probability distribution between minimum value and maximum value and "0" probability beyond
50
+ these.
51
+ * Probability distribution of any random parameter between minimum value and maximum value with the highest
52
+ probability at the average value of minimum plus maximum value. Linear change of probability between minimum,
53
+ maximum and average value.
54
+ * Means Impact is not known, but with >95% certainty the true value is below the declared value.
55
+ So [1e-6,"kgCFC11e",0,"max"] means the ODP was not exactly measured, but it is guaranteed to be below
56
+ 1E-6 kg CO2e. It is acceptable to treat a 'max' distribution a normal or lognormal distribution with variation
57
+ 0.1%. This is conservative, because the 'max' value is usually much greater than the true impact.
58
+ """
59
+
60
+ LOG_NORMAL = "log-normal"
61
+ NORMAL = "normal"
62
+ UNIFORM = "uniform"
63
+ TRIANGULAR = "triangular"
64
+ MAX = "max"
65
+
66
+
41
67
  class Measurement(BaseOpenEpdSchema):
42
68
  """A scientific value with units and uncertainty."""
43
69
 
@@ -46,7 +72,9 @@ class Measurement(BaseOpenEpdSchema):
46
72
  rsd: pyd.PositiveFloat | None = pyd.Field(
47
73
  description="Relative standard deviation, i.e. standard_deviation/mean", default=None
48
74
  )
49
- dist: str | None = pyd.Field(description="Statistical distribution of the measurement error.", default=None)
75
+ dist: Distribution | None = pyd.Field(
76
+ description="Statistical distribution of the measurement error.", default=None
77
+ )
50
78
 
51
79
 
52
80
  class Ingredient(BaseOpenEpdSchema):
@@ -189,3 +217,17 @@ class RangeAmount(RangeFloat):
189
217
  """Structure representing a range of quantities."""
190
218
 
191
219
  unit: str | None = pyd.Field(default=None, description="Unit of the range.")
220
+
221
+
222
+ class EnumGroupingAware:
223
+ """
224
+ Mixin for enums to support groups.
225
+
226
+ With the groups, enum can group its values into more than one groups, so that the validator higher in code can check
227
+ for mutual exclusivity, for example that only one value from the group is permitted at the same time.
228
+ """
229
+
230
+ @classmethod
231
+ def get_groupings(cls) -> list[list]:
232
+ """Return logical groupings of the values."""
233
+ return []
@@ -19,6 +19,7 @@ from openepd.compat.pydantic import pyd
19
19
  from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec, CodegenSpec
20
20
  from openepd.model.specs.concrete import Cementitious, ConcreteTypicalApplication
21
21
  from openepd.model.specs.generated.enums import AciExposureClass, CsaExposureClass, EnExposureClass
22
+ from openepd.model.validation.enum import exclusive_groups_validator_factory
22
23
  from openepd.model.validation.numbers import RatioFloat
23
24
  from openepd.model.validation.quantity import (
24
25
  LengthInchStr,
@@ -172,3 +173,13 @@ class ConcreteV1(BaseOpenEpdHierarchicalSpec):
172
173
  OilPatch: OilPatchV1 | None = None
173
174
  ReadyMix: ReadyMixV1 | None = None
174
175
  Shotcrete: ShotcreteV1 | None = None
176
+
177
+ _aci_exposure_classes_exclusive_groups_validator = pyd.validator("aci_exposure_classes", allow_reuse=True)(
178
+ exclusive_groups_validator_factory(AciExposureClass)
179
+ )
180
+ _en_exposure_classes_exclusive_groups_validator = pyd.validator("en_exposure_classes", allow_reuse=True)(
181
+ exclusive_groups_validator_factory(EnExposureClass)
182
+ )
183
+ _csa_exposure_classes_exclusive_groups_validator = pyd.validator("csa_exposure_classes", allow_reuse=True)(
184
+ exclusive_groups_validator_factory(CsaExposureClass)
185
+ )
@@ -15,6 +15,8 @@
15
15
  #
16
16
  from enum import StrEnum
17
17
 
18
+ from openepd.model.common import EnumGroupingAware
19
+
18
20
  # Enums used
19
21
 
20
22
 
@@ -2182,7 +2184,7 @@ class FloorBoxFloorMaterial(StrEnum):
2182
2184
  OTHER = "Other"
2183
2185
 
2184
2186
 
2185
- class AciExposureClass(StrEnum):
2187
+ class AciExposureClass(EnumGroupingAware, StrEnum):
2186
2188
  """
2187
2189
  American Concrete Institute concrete exposure classes.
2188
2190
 
@@ -2196,6 +2198,7 @@ class AciExposureClass(StrEnum):
2196
2198
  * `aci.S2` - Exposed to <10000 ppm of SO4 in water and <2% SO4 in soil
2197
2199
  * `aci.S3` - Exposed to >10000 ppm of SO4 in water or >2% SO4 in soil
2198
2200
 
2201
+ * `aci.C0` - Concrete dry or protected from moisture.
2199
2202
  * `aci.C1` - Concrete in contact with moisture, but the external source of chloride does not reach it.
2200
2203
  * `aci.C2` - Concrete subjected to moisture and an external source of chlorides such as deicing chemicals,
2201
2204
  salt, brackish water, seawater, or spray from these sources.
@@ -2212,14 +2215,25 @@ class AciExposureClass(StrEnum):
2212
2215
  S1 = "aci.S1"
2213
2216
  S2 = "aci.S2"
2214
2217
  S3 = "aci.S3"
2218
+ C0 = "aci.C1"
2215
2219
  C1 = "aci.C1"
2216
2220
  C2 = "aci.C2"
2217
2221
  W0 = "aci.W0"
2218
2222
  W1 = "aci.W1"
2219
2223
  W2 = "aci.W2"
2220
2224
 
2225
+ @classmethod
2226
+ def get_groupings(cls) -> list[list]:
2227
+ """Return logical groupings of the values."""
2228
+ return [
2229
+ [AciExposureClass.F0, AciExposureClass.F1, AciExposureClass.F2, AciExposureClass.F3],
2230
+ [AciExposureClass.S0, AciExposureClass.S1, AciExposureClass.S2, AciExposureClass.S3],
2231
+ [AciExposureClass.W0, AciExposureClass.W1],
2232
+ [AciExposureClass.C0, AciExposureClass.C1, AciExposureClass.C2],
2233
+ ]
2234
+
2221
2235
 
2222
- class CsaExposureClass(StrEnum):
2236
+ class CsaExposureClass(EnumGroupingAware, StrEnum):
2223
2237
  """
2224
2238
  Canadian Standard Association concrete exposure classes.
2225
2239
 
@@ -2286,8 +2300,20 @@ class CsaExposureClass(StrEnum):
2286
2300
  A_3 = "csa.A-3"
2287
2301
  A_4 = "csa.A-4"
2288
2302
 
2303
+ @classmethod
2304
+ def get_groupings(cls) -> list[list]:
2305
+ """Return logical groupings of the values."""
2306
+ return [
2307
+ [CsaExposureClass.C_XL],
2308
+ [CsaExposureClass.C_1, CsaExposureClass.C_2, CsaExposureClass.C_3, CsaExposureClass.C_4],
2309
+ [CsaExposureClass.F_1, CsaExposureClass.F2],
2310
+ [CsaExposureClass.N],
2311
+ [CsaExposureClass.A_1, CsaExposureClass.A_2, CsaExposureClass.A_3, CsaExposureClass.A_4],
2312
+ [CsaExposureClass.S_1, CsaExposureClass.S_2, CsaExposureClass.S_3],
2313
+ ]
2289
2314
 
2290
- class EnExposureClass(StrEnum):
2315
+
2316
+ class EnExposureClass(EnumGroupingAware, StrEnum):
2291
2317
  """
2292
2318
  EN 206 Class (Europe).
2293
2319
 
@@ -2348,6 +2374,28 @@ class EnExposureClass(StrEnum):
2348
2374
  en206_XA2 = "en206.XA2"
2349
2375
  en206_XA3 = "en206.XA3"
2350
2376
 
2377
+ @classmethod
2378
+ def get_groupings(cls) -> list[list]:
2379
+ """Return logical groupings of the values."""
2380
+ return [
2381
+ [EnExposureClass.en206_X0],
2382
+ [
2383
+ EnExposureClass.en206_XC1,
2384
+ EnExposureClass.en206_XC2,
2385
+ EnExposureClass.en206_XC3,
2386
+ EnExposureClass.en206_XC4,
2387
+ ],
2388
+ [EnExposureClass.en206_XD1, EnExposureClass.en206_XD2, EnExposureClass.en206_XD3],
2389
+ [EnExposureClass.en206_XS1, EnExposureClass.en206_XS2, EnExposureClass.en206_XS3],
2390
+ [
2391
+ EnExposureClass.en206_XF1,
2392
+ EnExposureClass.en206_XF2,
2393
+ EnExposureClass.en206_XF3,
2394
+ EnExposureClass.en206_XF4,
2395
+ ],
2396
+ [EnExposureClass.en206_XA1, EnExposureClass.en206_XA2, EnExposureClass.en206_XA3],
2397
+ ]
2398
+
2351
2399
 
2352
2400
  class AggregateWeightClassification(StrEnum):
2353
2401
  """
@@ -0,0 +1,42 @@
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, Callable
17
+
18
+ from openepd.model.common import EnumGroupingAware
19
+
20
+
21
+ def exclusive_groups_validator_factory(enum_type: type[EnumGroupingAware]) -> Callable[[type, Any], Any]:
22
+ """
23
+ Create an exclusive groups validator.
24
+
25
+ If we have a certain enum TheEnum, and a field of list[TheEnum], where list can contain only one value of each of
26
+ the groups, this validator should be used. For example, ACI exposure classes for concrete specify various
27
+ parameters such as water resistance, chemical resistance, etc., and the list of classes can have 0 or 1 from each
28
+ group.
29
+
30
+ :param enum_type:Enum type which supports groupings.
31
+ :return:value, or raises ValueError if not allowed combination is given.
32
+ """
33
+
34
+ def enum_exclusive_grouping_validator(cls, value: list | None) -> list | None:
35
+ for grouping in enum_type.get_groupings():
36
+ matching_from_group = [v for v in (value or []) if v in grouping]
37
+ if len(matching_from_group) > 1:
38
+ raise ValueError(f"Values {', '.join(matching_from_group)} are not allowed together.")
39
+
40
+ return value
41
+
42
+ return enum_exclusive_grouping_validator
@@ -1,28 +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
- __all__ = ("encode_path_param",)
17
-
18
- from urllib.parse import quote
19
-
20
-
21
- def encode_path_param(value: str) -> str:
22
- """
23
- Encode a path parameter value.
24
-
25
- :param value: parameter value
26
- :return: encoded value
27
- """
28
- return quote(value, safe="")
File without changes
File without changes
File without changes
File without changes