openepd 6.4.1__tar.gz → 6.5.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 (142) hide show
  1. {openepd-6.4.1 → openepd-6.5.0}/PKG-INFO +1 -1
  2. {openepd-6.4.1 → openepd-6.5.0}/pyproject.toml +1 -1
  3. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/__version__.py +1 -1
  4. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/lcia.py +11 -3
  5. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/concrete.py +0 -91
  6. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/__init__.py +43 -1
  7. openepd-6.5.0/src/openepd/model/specs/singular/deprecated/__init__.py +79 -0
  8. openepd-6.5.0/src/openepd/model/specs/singular/deprecated/concrete.py +100 -0
  9. openepd-6.5.0/src/openepd/model/specs/singular/deprecated/steel.py +75 -0
  10. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/validation/quantity.py +1 -1
  11. {openepd-6.4.1 → openepd-6.5.0}/LICENSE +0 -0
  12. {openepd-6.4.1 → openepd-6.5.0}/README.md +0 -0
  13. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/__init__.py +0 -0
  14. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/__init__.py +0 -0
  15. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/average_dataset/__init__.py +0 -0
  16. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/average_dataset/generic_estimate_sync_api.py +0 -0
  17. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/average_dataset/industry_epd_sync_api.py +0 -0
  18. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/base_sync_client.py +0 -0
  19. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/category/__init__.py +0 -0
  20. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/category/dto.py +0 -0
  21. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/category/sync_api.py +0 -0
  22. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/common.py +0 -0
  23. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/dto/__init__.py +0 -0
  24. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/dto/base.py +0 -0
  25. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/dto/common.py +0 -0
  26. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/dto/meta.py +0 -0
  27. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/dto/mf.py +0 -0
  28. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/dto/params.py +0 -0
  29. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/epd/__init__.py +0 -0
  30. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/epd/dto.py +0 -0
  31. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/epd/sync_api.py +0 -0
  32. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/errors.py +0 -0
  33. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/pcr/__init__.py +0 -0
  34. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/pcr/sync_api.py +0 -0
  35. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/sync_client.py +0 -0
  36. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/test/__init__.py +0 -0
  37. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/api/utils.py +0 -0
  38. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/bundle/__init__.py +0 -0
  39. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/bundle/base.py +0 -0
  40. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/bundle/model.py +0 -0
  41. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/bundle/reader.py +0 -0
  42. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/bundle/writer.py +0 -0
  43. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/compat/__init__.py +0 -0
  44. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/compat/compat_functional_validators.py +0 -0
  45. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/compat/pydantic.py +0 -0
  46. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/__init__.py +0 -0
  47. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/base.py +0 -0
  48. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/category.py +0 -0
  49. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/common.py +0 -0
  50. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/declaration.py +0 -0
  51. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/epd.py +0 -0
  52. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/factory.py +0 -0
  53. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/generic_estimate.py +0 -0
  54. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/geography.py +0 -0
  55. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/industry_epd.py +0 -0
  56. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/org.py +0 -0
  57. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/pcr.py +0 -0
  58. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/README.md +0 -0
  59. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/__init__.py +0 -0
  60. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/asphalt.py +0 -0
  61. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/base.py +0 -0
  62. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/enums.py +0 -0
  63. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/__init__.py +0 -0
  64. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/accessories.py +0 -0
  65. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/aggregates.py +0 -0
  66. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/aluminium.py +0 -0
  67. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/asphalt.py +0 -0
  68. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/bulk_materials.py +0 -0
  69. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/cast_decks_and_underlayment.py +0 -0
  70. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/cladding.py +0 -0
  71. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/cmu.py +0 -0
  72. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/concrete.py +0 -0
  73. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/conveying_equipment.py +0 -0
  74. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/electrical.py +0 -0
  75. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +0 -0
  76. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/electricity.py +0 -0
  77. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/finishes.py +0 -0
  78. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/fire_and_smoke_protection.py +0 -0
  79. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/furnishings.py +0 -0
  80. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/grouting.py +0 -0
  81. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/manufacturing_inputs.py +0 -0
  82. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/masonry.py +0 -0
  83. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/material_handling.py +0 -0
  84. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/mechanical.py +0 -0
  85. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/mechanical_insulation.py +0 -0
  86. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/network_infrastructure.py +0 -0
  87. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/openings.py +0 -0
  88. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/other_electrical_equipment.py +0 -0
  89. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/other_materials.py +0 -0
  90. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/plumbing.py +0 -0
  91. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/precast_concrete.py +0 -0
  92. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/sheathing.py +0 -0
  93. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/steel.py +0 -0
  94. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/thermal_moisture_protection.py +0 -0
  95. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/utility_piping.py +0 -0
  96. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/wood.py +0 -0
  97. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/range/wood_joists.py +0 -0
  98. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/accessories.py +0 -0
  99. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/aggregates.py +0 -0
  100. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/aluminium.py +0 -0
  101. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/asphalt.py +0 -0
  102. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/bulk_materials.py +0 -0
  103. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/cast_decks_and_underlayment.py +0 -0
  104. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/cladding.py +0 -0
  105. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/cmu.py +0 -0
  106. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/common.py +0 -0
  107. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/concrete.py +0 -0
  108. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/conveying_equipment.py +0 -0
  109. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/electrical.py +0 -0
  110. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/electrical_transmission_and_distribution_equipment.py +0 -0
  111. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/electricity.py +0 -0
  112. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/finishes.py +0 -0
  113. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/fire_and_smoke_protection.py +0 -0
  114. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/furnishings.py +0 -0
  115. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/grouting.py +0 -0
  116. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/manufacturing_inputs.py +0 -0
  117. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/masonry.py +0 -0
  118. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/material_handling.py +0 -0
  119. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/mechanical.py +0 -0
  120. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/mechanical_insulation.py +0 -0
  121. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/mixins/__init__.py +0 -0
  122. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/mixins/conduit_mixin.py +0 -0
  123. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/network_infrastructure.py +0 -0
  124. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/openings.py +0 -0
  125. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/other_electrical_equipment.py +0 -0
  126. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/other_materials.py +0 -0
  127. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/plumbing.py +0 -0
  128. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/precast_concrete.py +0 -0
  129. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/sheathing.py +0 -0
  130. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/steel.py +0 -0
  131. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/thermal_moisture_protection.py +0 -0
  132. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/utility_piping.py +0 -0
  133. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/wood.py +0 -0
  134. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/specs/singular/wood_joists.py +0 -0
  135. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/standard.py +0 -0
  136. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/validation/__init__.py +0 -0
  137. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/validation/common.py +0 -0
  138. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/validation/enum.py +0 -0
  139. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/validation/numbers.py +0 -0
  140. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/model/versioning.py +0 -0
  141. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/patch_pydantic.py +0 -0
  142. {openepd-6.4.1 → openepd-6.5.0}/src/openepd/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 6.4.1
3
+ Version: 6.5.0
4
4
  Summary: Python library to work with OpenEPD format
5
5
  Home-page: https://github.com/cchangelabs/openepd
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openepd"
3
- version = "6.4.1"
3
+ version = "6.5.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 = "6.4.1"
16
+ VERSION = "6.5.0"
@@ -206,9 +206,17 @@ class ScopeSet(BaseOpenEpdSchema):
206
206
  if isinstance(v, Measurement):
207
207
  all_units.add(v.unit)
208
208
 
209
- # units should be the same across all measurements (textually)
210
- if len(all_units) > 1:
211
- raise ValueError("All scopes and measurements should be expressed in the same unit.")
209
+ if not cls.allowed_units:
210
+ # For unknown units - only units should be the same across all measurements (textually)
211
+ if len(all_units) > 1:
212
+ raise ValueError("All scopes and measurements should be expressed in the same unit.")
213
+ else:
214
+ # might be multiple variations of the same unit (kgCFC-11e, kgCFC11e)
215
+ if len(all_units) > 1 and ExternalValidationConfig.QUANTITY_VALIDATOR:
216
+ all_units_list = list(all_units)
217
+ first = all_units_list[0]
218
+ for unit in all_units_list[1:]:
219
+ ExternalValidationConfig.QUANTITY_VALIDATOR.validate_same_dimensionality(first, unit)
212
220
 
213
221
  # can correctly validate unit
214
222
  if cls.allowed_units is not None and len(all_units) == 1 and ExternalValidationConfig.QUANTITY_VALIDATOR:
@@ -13,7 +13,6 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- from enum import StrEnum
17
16
 
18
17
  from openepd.compat.pydantic import pyd
19
18
  from openepd.model.base import BaseOpenEpdSchema
@@ -21,56 +20,6 @@ from openepd.model.specs.base import BaseOpenEpdSpec
21
20
  from openepd.model.validation.numbers import RatioFloat
22
21
 
23
22
 
24
- class CmuWeightClassification(StrEnum):
25
- """Concrete Masonry Unit weight classification."""
26
-
27
- Normal = "Normal"
28
- """Normal weight CMU has a density of 125 lbs/cu. ft."""
29
- Medium = "Medium"
30
- """Medium weight CMU has a density of 105-125 lbs/cu. ft."""
31
- Light = "Light"
32
- """Lightweight CMU has a density less than 105 lbs/cu. ft."""
33
-
34
-
35
- class CmuOptions(BaseOpenEpdSchema):
36
- """Concrete Masonry Unit options."""
37
-
38
- load_bearing: bool | None = pyd.Field(
39
- description="Load-Bearing. CMUs intended to be loadbearing, rather than simply cosmetic",
40
- example=True,
41
- default=None,
42
- )
43
- aerated_concrete: bool | None = pyd.Field(
44
- description="AAC Aerated Concrete. Aerated Autoclaved Concrete, a foam concrete.", example=True, default=None
45
- )
46
- insulated: bool | None = pyd.Field(
47
- description="Insulated. CMUs with integral insulation", example=True, default=None
48
- )
49
- sound_absorbing: bool | None = pyd.Field(
50
- description="Sound Absorbing. CMUs structured for sound absorbtion", example=True, default=None
51
- )
52
- white: bool | None = pyd.Field(
53
- description="White. CMU using white cement and light-colored aggregate", example=True, default=None
54
- )
55
- recycled_aggregate: bool | None = pyd.Field(
56
- description="Recycled aggregate. CMU using primarily reycled aggregates", example=True, default=None
57
- )
58
- groundface: bool | None = pyd.Field(
59
- description="Ground Face. Ground or Honed facing, typically for improved appearance", example=True, default=None
60
- )
61
- splitface: bool | None = pyd.Field(
62
- description="Splitface. Rough surface texture via splitting; aggregate can be seen", example=True, default=None
63
- )
64
- smoothface: bool | None = pyd.Field(
65
- description="Smooth Face. Standard smooth-faced blocks", example=True, default=None
66
- )
67
- slumpstone: bool | None = pyd.Field(
68
- description="Slumpstone. A slightly rounded, random distortion with the look of rustic adobe.",
69
- example=True,
70
- default=None,
71
- )
72
-
73
-
74
23
  class ConcreteTypicalApplication(BaseOpenEpdSpec):
75
24
  """Typical Application for Concrete."""
76
25
 
@@ -138,15 +87,6 @@ class ConcreteTypicalApplication(BaseOpenEpdSpec):
138
87
  )
139
88
 
140
89
 
141
- class CmuSpec(BaseOpenEpdSpec):
142
- """Standardized Concrete Masonry Unit-specific extension for OpenEPD."""
143
-
144
- strength: str = pyd.Field(description="Compressive strength", example="4000 psi")
145
- options: CmuOptions = pyd.Field(
146
- description="Options for CMU. List of true/false properties", default_factory=CmuOptions
147
- )
148
-
149
-
150
90
  class Cementitious(BaseOpenEpdSchema):
151
91
  """List of cementitious materials, and proportion by mass."""
152
92
 
@@ -168,34 +108,3 @@ class Cementitious(BaseOpenEpdSchema):
168
108
  mk: RatioFloat | None = pyd.Field(default=None, description="Metakaolin", example=0.5, ge=0, le=1)
169
109
  CaCO3: RatioFloat | None = pyd.Field(default=None, description="Limestone", example=0.5, ge=0, le=1)
170
110
  other: RatioFloat | None = pyd.Field(default=None, description="Other SCMs", example=0.5, ge=0, le=1)
171
-
172
-
173
- class TypicalApplication(BaseOpenEpdSchema):
174
- """Concrete typical application."""
175
-
176
- fnd: bool | None = pyd.Field(description="Foundation", default=None)
177
- sog: bool | None = pyd.Field(description="Slab on Grade", default=None)
178
- hrz: bool | None = pyd.Field(description="Elevated Horizontal", default=None)
179
- vrt_wall: bool | None = pyd.Field(description="Vertical Wall", default=None)
180
- vrt_column: bool | None = pyd.Field(description="Vertical Column", default=None)
181
- vrt_other: bool | None = pyd.Field(description="Vertical Other", default=None)
182
- sht: bool | None = pyd.Field(description="Shotcrete", default=None)
183
- cdf: bool | None = pyd.Field(description="Flowable Fill (CDF,default=None)", default=None)
184
- sac: bool | None = pyd.Field(description="Sidewalk and Curb", default=None)
185
- pav: bool | None = pyd.Field(description="Paving", default=None)
186
- oil: bool | None = pyd.Field(description="Oil Patch", default=None)
187
- grt: bool | None = pyd.Field(description="Cement Grout", default=None)
188
- ota: bool | None = pyd.Field(description="Other", default=None)
189
-
190
-
191
- class ConcreteV1Options(BaseOpenEpdSchema):
192
- """Concrete options."""
193
-
194
- lightweight: bool | None = pyd.Field(description="Lightweight", default=None)
195
- plc: bool | None = pyd.Field(description="Portland Limestone Cement", default=None)
196
- scc: bool | None = pyd.Field(description="Self Compacting", default=None)
197
- finishable: bool | None = pyd.Field(description="Finishable", default=None)
198
- air: bool | None = pyd.Field(description="Air Entrainment", default=None)
199
- co2: bool | None = pyd.Field(description="CO2 Curing", default=None)
200
- white: bool | None = pyd.Field(description="White Cement", default=None)
201
- fiber_reinforced: bool | None = pyd.Field(description="Fiber reinforced", default=None)
@@ -13,7 +13,9 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- from openepd.model.base import BaseOpenEpdSchema
16
+ from typing import Any, ClassVar
17
+
18
+ from openepd.compat.pydantic import pyd
17
19
  from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
18
20
  from openepd.model.specs.singular.accessories import AccessoriesV1
19
21
  from openepd.model.specs.singular.aggregates import AggregatesV1
@@ -25,6 +27,9 @@ from openepd.model.specs.singular.cladding import CladdingV1
25
27
  from openepd.model.specs.singular.cmu import CMUV1
26
28
  from openepd.model.specs.singular.concrete import ConcreteV1
27
29
  from openepd.model.specs.singular.conveying_equipment import ConveyingEquipmentV1
30
+ from openepd.model.specs.singular.deprecated import BaseCompatibilitySpec, get_safely, set_safely
31
+ from openepd.model.specs.singular.deprecated.concrete import ConcreteOldSpec
32
+ from openepd.model.specs.singular.deprecated.steel import SteelOldSpec
28
33
  from openepd.model.specs.singular.electrical import ElectricalV1
29
34
  from openepd.model.specs.singular.electrical_transmission_and_distribution_equipment import (
30
35
  ElectricalTransmissionAndDistributionEquipmentV1,
@@ -58,6 +63,8 @@ __all__ = ("Specs",)
58
63
  class Specs(BaseOpenEpdHierarchicalSpec):
59
64
  """Material specific specs."""
60
65
 
66
+ COMPATIBILITY_SPECS: ClassVar[list[type[BaseCompatibilitySpec]]] = [ConcreteOldSpec, SteelOldSpec]
67
+
61
68
  _EXT_VERSION = "1.0"
62
69
 
63
70
  # Nested specs:
@@ -95,3 +102,38 @@ class Specs(BaseOpenEpdHierarchicalSpec):
95
102
  MechanicalInsulation: MechanicalInsulationV1 | None = None
96
103
  OtherElectricalEquipment: OtherElectricalEquipmentV1 | None = None
97
104
  WoodJoists: WoodJoistsV1 | None = None
105
+
106
+ # historical backward-compatible specs
107
+ concrete: ConcreteOldSpec | None = None
108
+ steel: SteelOldSpec | None = None
109
+
110
+ @pyd.root_validator(pre=True)
111
+ def _ensure_backward_compatibiltiy(cls, values: dict[str, Any]) -> dict[str, Any]:
112
+ """
113
+ Restore the functionality for backward-compatible specs.
114
+
115
+ Originally, we used to have 'concrete' and 'steel' specs non-hierarchical and manually maintained. Since we
116
+ introduced hierarchical specs, there is a need to retain the key mapping to the old structure.
117
+
118
+ :return: modified values
119
+ """
120
+ for compat_spec in cls.COMPATIBILITY_SPECS:
121
+ if (
122
+ compat_spec.COMPATIBILITY_SPECS_KEY_OLD not in values
123
+ and compat_spec.COMPATIBILITY_SPECS_KEY_NEW not in values
124
+ ):
125
+ continue
126
+ for old_key_spec, new_key_spec in compat_spec.COMPATIBILITY_MAPPING.items():
127
+ has_new_spec, new_value = get_safely(values, new_key_spec)
128
+
129
+ # new value is set, we should not override it but should return it in old spec
130
+ if has_new_spec:
131
+ set_safely(values, old_key_spec, new_value)
132
+ continue
133
+
134
+ has_old_spec, old_value = get_safely(values, old_key_spec)
135
+ # nothing to back
136
+ if not has_old_spec:
137
+ continue
138
+ set_safely(values, new_key_spec, old_value)
139
+ return values
@@ -0,0 +1,79 @@
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, ClassVar
17
+
18
+ from openepd.compat.pydantic import pyd
19
+ from openepd.model.base import BaseOpenEpdSchema
20
+
21
+
22
+ class BaseCompatibilitySpec(BaseOpenEpdSchema):
23
+ """
24
+ Base class for compatibility (legacy) specs.
25
+
26
+ See Specs model for implementation.
27
+ """
28
+
29
+ COMPATIBILITY_SPECS_KEY_OLD: ClassVar[str]
30
+ COMPATIBILITY_SPECS_KEY_NEW: ClassVar[str]
31
+ COMPATIBILITY_MAPPING: ClassVar[dict[str, str]]
32
+
33
+
34
+ def get_safely(d: dict, path: str) -> tuple[bool, Any]:
35
+ """
36
+ Get a value from a mixed object via dotted path.
37
+
38
+ Mixed object can be a combination of dicts and pydatnic Models on any hierarchy.
39
+ :param d: source dict/object to search in
40
+ :param path: dotted path in object like specs.Concrete.strength_28d
41
+ :return: tuple (WasFound, Value). First element tells if value was found, second - the value itself.
42
+ """
43
+ path_elements = path.split(".")
44
+ current: Any = d
45
+ for p in path_elements:
46
+ match current:
47
+ case dict():
48
+ if p not in current:
49
+ return False, None
50
+ current = current.get(p)
51
+ case pyd.BaseModel() as model:
52
+ if not hasattr(current, p) or p not in model.__fields_set__:
53
+ return False, None
54
+ current = getattr(current, p)
55
+ case _:
56
+ return False, None
57
+
58
+ return True, current
59
+
60
+
61
+ def set_safely(d: dict, path: str, value: Any) -> None:
62
+ """
63
+ Safely set an element in a dict.
64
+
65
+ Warning: this is asymmetric compared to get_safely, since it sets value in a dict, not in pydantic model object.
66
+
67
+ :param d: source dict/object to set value in
68
+ :param path: dotted path in object like specs.Concrete.strength_28d
69
+ :param value: value to set
70
+ :return: None
71
+ """
72
+ path_elements = path.split(".")
73
+ current = d
74
+ for p in path_elements[:-1]:
75
+ if current.get(p) is None:
76
+ current[p] = {}
77
+ current = current[p]
78
+
79
+ current[path_elements[-1]] = value
@@ -0,0 +1,100 @@
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 ClassVar, Literal
17
+
18
+ from openepd.compat.pydantic import pyd
19
+ from openepd.model.base import BaseOpenEpdSchema
20
+ from openepd.model.specs.concrete import Cementitious, ConcreteTypicalApplication
21
+ from openepd.model.specs.enums import AciExposureClass, CsaExposureClass, EnExposureClass
22
+ from openepd.model.specs.singular import BaseCompatibilitySpec
23
+ from openepd.model.validation.numbers import RatioFloat
24
+ from openepd.model.validation.quantity import LengthInchStr, PressureMPaStr
25
+
26
+
27
+ class ConcreteOptions(BaseOpenEpdSchema):
28
+ """Legacy Concrete options model."""
29
+
30
+ lightweight: bool | None = pyd.Field(
31
+ default=None, description="Lightweight. True if < 120lb/ft3 or 1900 kg/m3", example=True
32
+ )
33
+ scc: bool | None = pyd.Field(default=None, description="Self Compacting", example=True)
34
+ finishable: bool | None = pyd.Field(default=None, description="Finishable", example=True)
35
+ air: bool | None = pyd.Field(default=None, description="Air Entrainment", example=True)
36
+ co2_entrain: bool | None = pyd.Field(
37
+ default=None, description="CO2 Curing. Uses intentionally entrained CO2.", example=True
38
+ )
39
+ white_cement: bool | None = pyd.Field(default=None, description="White Cement", example=True)
40
+ plc: bool | None = pyd.Field(default=None, description="Portland Limestone Cement", example=True)
41
+ fiber_reinforced: bool | None = pyd.Field(default=None, description="fiber_reinforced", example=True)
42
+
43
+
44
+ class ConcreteOldSpec(BaseCompatibilitySpec):
45
+ """Old version of the Concrete spec, please use Concrete hierarchical version instead."""
46
+
47
+ COMPATIBILITY_SPECS_KEY_OLD: ClassVar[str] = "concrete"
48
+ COMPATIBILITY_SPECS_KEY_NEW: ClassVar[str] = "Concrete"
49
+ COMPATIBILITY_MAPPING: ClassVar[dict[str, str]] = {
50
+ "concrete.strength_28d": "Concrete.strength_28d",
51
+ "concrete.slump": "Concrete.min_slump",
52
+ "concrete.strength_other": "Concrete.strength_other",
53
+ "concrete.strength_other_d": "Concrete.strength_other_d",
54
+ "concrete.w_c_ratio": "Concrete.w_c_ratio",
55
+ "concrete.aci_exposure_classes": "Concrete.aci_exposure_classes",
56
+ "concrete.csa_exposure_classes": "Concrete.csa_exposure_classes",
57
+ "concrete.en_exposure_classes": "Concrete.en_exposure_classes",
58
+ "concrete.application": "Concrete.typical_application",
59
+ "concrete.options.lightweight": "Concrete.lightweight",
60
+ "concrete.options.scc": "Concrete.self_consolidating",
61
+ "concrete.options.finishable": "Concrete.finishable",
62
+ "concrete.options.air": "Concrete.air_entrain",
63
+ "concrete.options.co2_entrain": "Concrete.co2_entrain",
64
+ "concrete.options.white_cement": "Concrete.white_cement",
65
+ "concrete.options.plc": "Concrete.plc",
66
+ "concrete.options.fiber_reinforced": "Concrete.fiber_reinforced",
67
+ "concrete.cementitious": "Concrete.cementitious",
68
+ }
69
+
70
+ strength_28d: PressureMPaStr | None = pyd.Field(
71
+ default=None, description="Compressive Strength at 28 days", example="1 MPa"
72
+ )
73
+ slump: LengthInchStr | None = pyd.Field(default=None, description="Minimum test slump", example="2 in")
74
+ strength_other: PressureMPaStr | None = pyd.Field(
75
+ default=None,
76
+ description="One additional strength, which can be early (e.g. 3d) or late (e.g. 96d)",
77
+ example="30 MPa",
78
+ )
79
+ strength_other_d: Literal[3, 7, 14, 42, 56, 72, 96, 120] | None = pyd.Field(
80
+ default=None,
81
+ description="Days for the strength field above. Required IF strength_other is provided.",
82
+ example=42,
83
+ )
84
+ w_c_ratio: RatioFloat | None = pyd.Field(
85
+ default=None, description="Ratio of water to cement", example=0.5, ge=0, le=1
86
+ )
87
+ aci_exposure_classes: list[AciExposureClass] | None = pyd.Field(
88
+ default=None, description="List of ACI318-19 exposure classes this product meets", example=["aci.F0"]
89
+ )
90
+ csa_exposure_classes: list[CsaExposureClass] | None = pyd.Field(
91
+ default=None, description="List of CSA A23.1 exposure classes this product meets", example=["csa.C-2"]
92
+ )
93
+ en_exposure_classes: list[EnExposureClass] | None = pyd.Field(
94
+ default=None, description="List of EN206 exposure classes this product meets", example=["en206.X0"]
95
+ )
96
+ application: ConcreteTypicalApplication | None = pyd.Field(default=None, description="Typical Application")
97
+ options: ConcreteOptions | None = pyd.Field(default=None, description="List of true/false properties")
98
+ cementitious: Cementitious | None = pyd.Field(
99
+ default=None, description="List of cementitious materials, and proportion by mass"
100
+ )
@@ -0,0 +1,75 @@
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 ClassVar
17
+
18
+ from openepd.compat.pydantic import pyd
19
+ from openepd.model.base import BaseOpenEpdSchema
20
+ from openepd.model.specs.enums import SteelComposition
21
+ from openepd.model.specs.singular import BaseCompatibilitySpec
22
+ from openepd.model.specs.singular.steel import SteelMakingRoute
23
+ from openepd.model.standard import Standard
24
+ from openepd.model.validation.quantity import PressureMPaStr
25
+
26
+
27
+ class SteelOldOptions(BaseOpenEpdSchema):
28
+ """Legacy Steel options model."""
29
+
30
+ cold_finished: bool | None = pyd.Field(default=None, description="Cold Finished", example=True)
31
+ galvanized: bool | None = pyd.Field(default=None, description="Galvanized", example=True)
32
+ epoxy: bool | None = pyd.Field(default=None, description="Epoxy Coated", example=True)
33
+ steel_fabricated: bool | None = pyd.Field(default=None, example=True, description="Fabricated")
34
+
35
+
36
+ class SteelOldSpec(BaseCompatibilitySpec):
37
+ """Legacy Steel spec."""
38
+
39
+ COMPATIBILITY_SPECS_KEY_OLD: ClassVar[str] = "steel"
40
+ COMPATIBILITY_SPECS_KEY_NEW: ClassVar[str] = "Steel"
41
+ COMPATIBILITY_MAPPING: ClassVar[dict[str, str]] = {
42
+ "steel.steel_composition": "Steel.composition",
43
+ "steel.Fy": "Steel.yield_tensile_str",
44
+ "steel.making_route": "Steel.making_route",
45
+ "steel.ASTM": "Steel.astm_standards",
46
+ "steel.EN": "Steel.en_standards",
47
+ "steel.SAE": "Steel.sae_standards",
48
+ "steel.options.cold_finished": "Steel.cold_finished",
49
+ "steel.options.galvanized": "Steel.galvanized",
50
+ "steel.options.epoxy": "Steel.RebarSteel.epoxy_coated", # moved to sub-spec
51
+ "steel.options.steel_fabricated": "Steel.RebarSteel.fabricated",
52
+ # last one is tricky as fabricated might be in multiple cases; thus limited support
53
+ }
54
+ form_factor: str | None = pyd.Field(default=None, description="Product's form factor, read-only.")
55
+ steel_composition: SteelComposition | None = pyd.Field(
56
+ default=None,
57
+ description="Basic chemical composition. Generally the ASTM or EN grade is a subcategory of one of these.",
58
+ example="Carbon",
59
+ )
60
+ Fy: PressureMPaStr | None = pyd.Field(
61
+ default=None,
62
+ description="Minimum Yield Strength",
63
+ example="2000 psi",
64
+ )
65
+ making_route: SteelMakingRoute | None = pyd.Field(
66
+ default=None, description="List of true/false properties for steelmaking route"
67
+ )
68
+ ASTM: list[Standard] | None = pyd.Field(
69
+ default=None, description="ASTM standard(s) to which this product complies."
70
+ )
71
+ SAE: list[Standard] | None = pyd.Field(
72
+ default=None, description="AISA/SAE standard(s) to which this product complies."
73
+ )
74
+ EN: list[Standard] | None = pyd.Field(default=None, description="EN 10027 number(s).")
75
+ options: SteelOldOptions | None = pyd.Field(default=None, description="List of true/false properties")
@@ -33,7 +33,7 @@ class QuantityValidator(ABC):
33
33
  """
34
34
 
35
35
  @abstractmethod
36
- def validate_same_dimensionality(self, unit: str | None, dimensionality_unit: str) -> None:
36
+ def validate_same_dimensionality(self, unit: str | None, dimensionality_unit: str | None) -> None:
37
37
  """
38
38
  Validate that a given unit ('kg') has the same dimesnionality as provided dimensionality_unit ('g').
39
39
 
File without changes
File without changes
File without changes
File without changes