openepd 6.13.1__py3-none-any.whl → 7.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. openepd/__init__.py +4 -4
  2. openepd/__version__.py +1 -1
  3. openepd/api/average_dataset/generic_estimate_sync_api.py +11 -10
  4. openepd/api/average_dataset/industry_epd_sync_api.py +9 -8
  5. openepd/api/base_sync_client.py +53 -9
  6. openepd/api/category/sync_api.py +1 -1
  7. openepd/api/dto/base.py +4 -4
  8. openepd/api/dto/common.py +24 -16
  9. openepd/api/dto/meta.py +15 -11
  10. openepd/api/dto/mf.py +9 -8
  11. openepd/api/epd/dto.py +43 -33
  12. openepd/api/epd/sync_api.py +9 -9
  13. openepd/api/pcr/sync_api.py +2 -2
  14. openepd/bundle/model.py +11 -10
  15. openepd/bundle/reader.py +12 -5
  16. openepd/bundle/writer.py +17 -6
  17. openepd/m49/__init__.py +2 -0
  18. openepd/m49/const.py +5 -2
  19. openepd/m49/{geo_converter.py → utils.py} +24 -2
  20. openepd/model/base.py +60 -43
  21. openepd/model/category.py +13 -10
  22. openepd/model/common.py +100 -55
  23. openepd/model/declaration.py +93 -64
  24. openepd/model/epd.py +51 -43
  25. openepd/model/generic_estimate.py +28 -13
  26. openepd/model/industry_epd.py +15 -9
  27. openepd/model/lcia.py +132 -113
  28. openepd/model/org.py +54 -33
  29. openepd/model/pcr.py +38 -32
  30. openepd/model/specs/asphalt.py +31 -22
  31. openepd/model/specs/base.py +11 -9
  32. openepd/model/specs/concrete.py +60 -39
  33. openepd/model/specs/range/aggregates.py +9 -9
  34. openepd/model/specs/range/aluminium.py +7 -7
  35. openepd/model/specs/range/asphalt.py +22 -19
  36. openepd/model/specs/range/cladding.py +16 -16
  37. openepd/model/specs/range/cmu.py +10 -9
  38. openepd/model/specs/range/concrete.py +36 -27
  39. openepd/model/specs/range/conveying_equipment.py +16 -15
  40. openepd/model/specs/range/electrical.py +24 -22
  41. openepd/model/specs/range/finishes.py +109 -104
  42. openepd/model/specs/range/fire_and_smoke_protection.py +7 -7
  43. openepd/model/specs/range/furnishings.py +16 -12
  44. openepd/model/specs/range/manufacturing_inputs.py +16 -16
  45. openepd/model/specs/range/masonry.py +16 -16
  46. openepd/model/specs/range/mechanical.py +47 -47
  47. openepd/model/specs/range/mechanical_insulation.py +7 -7
  48. openepd/model/specs/range/network_infrastructure.py +54 -46
  49. openepd/model/specs/range/openings.py +36 -31
  50. openepd/model/specs/range/plumbing.py +15 -13
  51. openepd/model/specs/range/precast_concrete.py +20 -16
  52. openepd/model/specs/range/sheathing.py +18 -18
  53. openepd/model/specs/range/steel.py +25 -25
  54. openepd/model/specs/range/thermal_moisture_protection.py +20 -20
  55. openepd/model/specs/range/utility_piping.py +9 -9
  56. openepd/model/specs/range/wood.py +19 -19
  57. openepd/model/specs/range/wood_joists.py +8 -8
  58. openepd/model/specs/singular/__init__.py +9 -5
  59. openepd/model/specs/singular/aggregates.py +22 -15
  60. openepd/model/specs/singular/aluminium.py +20 -5
  61. openepd/model/specs/singular/asphalt.py +44 -20
  62. openepd/model/specs/singular/cladding.py +38 -23
  63. openepd/model/specs/singular/cmu.py +26 -11
  64. openepd/model/specs/singular/common.py +3 -2
  65. openepd/model/specs/singular/concrete.py +85 -48
  66. openepd/model/specs/singular/conveying_equipment.py +30 -17
  67. openepd/model/specs/singular/deprecated/__init__.py +3 -2
  68. openepd/model/specs/singular/deprecated/concrete.py +68 -33
  69. openepd/model/specs/singular/deprecated/steel.py +28 -15
  70. openepd/model/specs/singular/electrical.py +69 -41
  71. openepd/model/specs/singular/finishes.py +250 -140
  72. openepd/model/specs/singular/fire_and_smoke_protection.py +9 -6
  73. openepd/model/specs/singular/furnishings.py +16 -14
  74. openepd/model/specs/singular/manufacturing_inputs.py +23 -14
  75. openepd/model/specs/singular/masonry.py +66 -21
  76. openepd/model/specs/singular/mechanical.py +48 -47
  77. openepd/model/specs/singular/mechanical_insulation.py +7 -6
  78. openepd/model/specs/singular/mixins/conduit_mixin.py +13 -10
  79. openepd/model/specs/singular/network_infrastructure.py +111 -52
  80. openepd/model/specs/singular/openings.py +127 -95
  81. openepd/model/specs/singular/plumbing.py +15 -12
  82. openepd/model/specs/singular/precast_concrete.py +68 -54
  83. openepd/model/specs/singular/sheathing.py +47 -27
  84. openepd/model/specs/singular/steel.py +69 -45
  85. openepd/model/specs/singular/thermal_moisture_protection.py +36 -20
  86. openepd/model/specs/singular/utility_piping.py +11 -8
  87. openepd/model/specs/singular/wood.py +48 -24
  88. openepd/model/specs/singular/wood_joists.py +19 -6
  89. openepd/model/standard.py +15 -8
  90. openepd/model/validation/common.py +9 -3
  91. openepd/model/validation/numbers.py +0 -13
  92. openepd/model/validation/quantity.py +53 -25
  93. openepd/model/versioning.py +9 -6
  94. openepd/patch_pydantic.py +0 -93
  95. {openepd-6.13.1.dist-info → openepd-7.0.0.dist-info}/METADATA +1 -1
  96. openepd-7.0.0.dist-info/RECORD +142 -0
  97. openepd/compat/__init__.py +0 -15
  98. openepd/compat/compat_functional_validators.py +0 -25
  99. openepd/compat/pydantic.py +0 -30
  100. openepd-6.13.1.dist-info/RECORD +0 -145
  101. {openepd-6.13.1.dist-info → openepd-7.0.0.dist-info}/LICENSE +0 -0
  102. {openepd-6.13.1.dist-info → openepd-7.0.0.dist-info}/WHEEL +0 -0
@@ -15,30 +15,58 @@
15
15
  #
16
16
  from typing import ClassVar, Literal
17
17
 
18
- from openepd.compat.pydantic import pyd
18
+ import pydantic
19
+
19
20
  from openepd.model.base import BaseOpenEpdSchema
20
21
  from openepd.model.specs.concrete import Cementitious, ConcreteTypicalApplication
21
22
  from openepd.model.specs.enums import AciExposureClass, CsaExposureClass, EnExposureClass
22
23
  from openepd.model.specs.singular import BaseCompatibilitySpec
23
- from openepd.model.validation.numbers import RatioFloat
24
24
  from openepd.model.validation.quantity import LengthInchStr, PressureMPaStr
25
25
 
26
26
 
27
27
  class ConcreteOptions(BaseOpenEpdSchema):
28
28
  """Legacy Concrete options model."""
29
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)
30
+ lightweight: bool | None = pydantic.Field(
31
+ default=None,
32
+ description="Lightweight. True if < 120lb/ft3 or 1900 kg/m3",
33
+ examples=[True],
34
+ )
35
+ scc: bool | None = pydantic.Field(
36
+ default=None,
37
+ description="Self Compacting",
38
+ examples=[True],
39
+ )
40
+ finishable: bool | None = pydantic.Field(
41
+ default=None,
42
+ description="Finishable",
43
+ examples=[True],
44
+ )
45
+ air: bool | None = pydantic.Field(
46
+ default=None,
47
+ description="Air Entrainment",
48
+ examples=[True],
49
+ )
50
+ co2_entrain: bool | None = pydantic.Field(
51
+ default=None,
52
+ description="CO2 Curing. Uses intentionally entrained CO2.",
53
+ examples=[True],
54
+ )
55
+ white_cement: bool | None = pydantic.Field(
56
+ default=None,
57
+ description="White Cement",
58
+ examples=[True],
59
+ )
60
+ plc: bool | None = pydantic.Field(
61
+ default=None,
62
+ description="Portland Limestone Cement",
63
+ examples=[True],
64
+ )
65
+ fiber_reinforced: bool | None = pydantic.Field(
66
+ default=None,
67
+ description="fiber_reinforced",
68
+ examples=[True],
69
+ )
42
70
 
43
71
 
44
72
  class ConcreteOldSpec(BaseCompatibilitySpec):
@@ -67,34 +95,41 @@ class ConcreteOldSpec(BaseCompatibilitySpec):
67
95
  "concrete.cementitious": "Concrete.cementitious",
68
96
  }
69
97
 
70
- strength_28d: PressureMPaStr | None = pyd.Field(
71
- default=None, description="Compressive Strength at 28 days", example="1 MPa"
98
+ strength_28d: PressureMPaStr | None = pydantic.Field(
99
+ default=None, description="Compressive Strength at 28 days", examples=["1 MPa"]
72
100
  )
73
- slump: LengthInchStr | None = pyd.Field(default=None, description="Minimum test slump", example="2 in")
74
- strength_other: PressureMPaStr | None = pyd.Field(
101
+ slump: LengthInchStr | None = pydantic.Field(default=None, description="Minimum test slump", examples=["2 in"])
102
+ strength_other: PressureMPaStr | None = pydantic.Field(
75
103
  default=None,
76
104
  description="One additional strength, which can be early (e.g. 3d) or late (e.g. 96d)",
77
- example="30 MPa",
105
+ examples=["30 MPa"],
78
106
  )
79
- strength_other_d: Literal[3, 7, 14, 42, 56, 72, 96, 120] | None = pyd.Field(
107
+ strength_other_d: Literal[3, 7, 14, 42, 56, 72, 96, 120] | None = pydantic.Field(
80
108
  default=None,
81
109
  description="Days for the strength field above. Required IF strength_other is provided.",
82
- example=42,
110
+ examples=[42],
83
111
  )
84
- w_c_ratio: RatioFloat | None = pyd.Field(
85
- default=None, description="Ratio of water to cement", example=0.5, ge=0, le=1
112
+ w_c_ratio: float | None = pydantic.Field(
113
+ default=None, description="Ratio of water to cement", examples=[0.5], ge=0, le=1
86
114
  )
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"]
115
+ aci_exposure_classes: list[AciExposureClass] | None = pydantic.Field(
116
+ default=None,
117
+ description="List of ACI318-19 exposure classes this product meets",
118
+ examples=[["aci.F0"]],
89
119
  )
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"]
120
+ csa_exposure_classes: list[CsaExposureClass] | None = pydantic.Field(
121
+ default=None,
122
+ description="List of CSA A23.1 exposure classes this product meets",
123
+ examples=[["csa.C-2"]],
92
124
  )
93
- en_exposure_classes: list[EnExposureClass] | None = pyd.Field(
94
- default=None, description="List of EN206 exposure classes this product meets", example=["en206.X0"]
125
+ en_exposure_classes: list[EnExposureClass] | None = pydantic.Field(
126
+ default=None,
127
+ description="List of EN206 exposure classes this product meets",
128
+ examples=[["en206.X0"]],
95
129
  )
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"
130
+ application: ConcreteTypicalApplication | None = pydantic.Field(default=None, description="Typical Application")
131
+ options: ConcreteOptions | None = pydantic.Field(default=None, description="List of true/false properties")
132
+ cementitious: Cementitious | None = pydantic.Field(
133
+ default=None,
134
+ description="List of cementitious materials, and proportion by mass",
100
135
  )
@@ -15,7 +15,8 @@
15
15
  #
16
16
  from typing import ClassVar
17
17
 
18
- from openepd.compat.pydantic import pyd
18
+ import pydantic
19
+
19
20
  from openepd.model.base import BaseOpenEpdSchema
20
21
  from openepd.model.specs.enums import SteelComposition
21
22
  from openepd.model.specs.singular import BaseCompatibilitySpec
@@ -27,10 +28,22 @@ from openepd.model.validation.quantity import PressureMPaStr
27
28
  class SteelOldOptions(BaseOpenEpdSchema):
28
29
  """Legacy Steel options model."""
29
30
 
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")
31
+ cold_finished: bool | None = pydantic.Field(
32
+ default=None,
33
+ description="Cold Finished",
34
+ examples=[True],
35
+ )
36
+ galvanized: bool | None = pydantic.Field(
37
+ default=None,
38
+ description="Galvanized",
39
+ examples=[True],
40
+ )
41
+ epoxy: bool | None = pydantic.Field(
42
+ default=None,
43
+ description="Epoxy Coated",
44
+ examples=[True],
45
+ )
46
+ steel_fabricated: bool | None = pydantic.Field(default=None, examples=[True], description="Fabricated")
34
47
 
35
48
 
36
49
  class SteelOldSpec(BaseCompatibilitySpec):
@@ -51,25 +64,25 @@ class SteelOldSpec(BaseCompatibilitySpec):
51
64
  "steel.options.steel_fabricated": "Steel.RebarSteel.fabricated",
52
65
  # last one is tricky as fabricated might be in multiple cases; thus limited support
53
66
  }
54
- form_factor: str | None = pyd.Field(default=None, description="Product's form factor, read-only.")
55
- steel_composition: SteelComposition | None = pyd.Field(
67
+ form_factor: str | None = pydantic.Field(default=None, description="Product's form factor, read-only.")
68
+ steel_composition: SteelComposition | None = pydantic.Field(
56
69
  default=None,
57
70
  description="Basic chemical composition. Generally the ASTM or EN grade is a subcategory of one of these.",
58
- example="Carbon",
71
+ examples=["Carbon"],
59
72
  )
60
- Fy: PressureMPaStr | None = pyd.Field(
73
+ Fy: PressureMPaStr | None = pydantic.Field(
61
74
  default=None,
62
75
  description="Minimum Yield Strength",
63
- example="2000 psi",
76
+ examples=["2000 psi"],
64
77
  )
65
- making_route: SteelMakingRoute | None = pyd.Field(
78
+ making_route: SteelMakingRoute | None = pydantic.Field(
66
79
  default=None, description="List of true/false properties for steelmaking route"
67
80
  )
68
- ASTM: list[Standard] | None = pyd.Field(
81
+ ASTM: list[Standard] | None = pydantic.Field(
69
82
  default=None, description="ASTM standard(s) to which this product complies."
70
83
  )
71
- SAE: list[Standard] | None = pyd.Field(
84
+ SAE: list[Standard] | None = pydantic.Field(
72
85
  default=None, description="AISA/SAE standard(s) to which this product complies."
73
86
  )
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")
87
+ EN: list[Standard] | None = pydantic.Field(default=None, description="EN 10027 number(s).")
88
+ options: SteelOldOptions | None = pydantic.Field(default=None, description="List of true/false properties")
@@ -13,7 +13,8 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- from openepd.compat.pydantic import pyd
16
+ import pydantic
17
+
17
18
  from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
18
19
  from openepd.model.specs.enums import CableTraysMaterial, EnergySource, RacewaysMaterial
19
20
  from openepd.model.specs.singular.mixins.conduit_mixin import ConduitMixin
@@ -61,14 +62,18 @@ class CableTraysV1(BaseOpenEpdHierarchicalSpec):
61
62
  _EXT_VERSION = "1.0"
62
63
 
63
64
  # Own fields:
64
- height: LengthMmStr | None = pyd.Field(default=None, description="", example="100 mm")
65
- width: LengthMmStr | None = pyd.Field(default=None, description="", example="100 mm")
66
- depth: LengthMmStr | None = pyd.Field(default=None, description="", example="100 mm")
67
- static_load: MassKgStr | None = pyd.Field(default=None, description="", example="1 kg")
68
- ventilated: bool | None = pyd.Field(
69
- default=None, description="At least 40% of the tray base is open to air flow", example=True
65
+ height: LengthMmStr | None = pydantic.Field(default=None, description="", examples=["100 mm"])
66
+ width: LengthMmStr | None = pydantic.Field(default=None, description="", examples=["100 mm"])
67
+ depth: LengthMmStr | None = pydantic.Field(default=None, description="", examples=["100 mm"])
68
+ static_load: MassKgStr | None = pydantic.Field(default=None, description="", examples=["1 kg"])
69
+ ventilated: bool | None = pydantic.Field(
70
+ default=None,
71
+ description="At least 40% of the tray base is open to air flow",
72
+ examples=[True],
73
+ )
74
+ cable_trays_material: CableTraysMaterial | None = pydantic.Field(
75
+ default=None, description="", examples=["Stainless Steel"]
70
76
  )
71
- cable_trays_material: CableTraysMaterial | None = pyd.Field(default=None, description="", example="Stainless Steel")
72
77
 
73
78
 
74
79
  class ElectricalBusesV1(BaseOpenEpdHierarchicalSpec):
@@ -104,11 +109,19 @@ class RacewaysV1(BaseOpenEpdHierarchicalSpec):
104
109
  _EXT_VERSION = "1.0"
105
110
 
106
111
  # Own fields:
107
- width: LengthMStr | None = pyd.Field(default=None, description="", example="100 mm")
108
- depth: LengthMStr | None = pyd.Field(default=None, description="", example="100 mm")
109
- painted: bool | None = pyd.Field(default=None, description="", example=True)
110
- divided: bool | None = pyd.Field(default=None, description="", example=True)
111
- raceways_material: RacewaysMaterial | None = pyd.Field(default=None, description="", example="Aluminum")
112
+ width: LengthMStr | None = pydantic.Field(default=None, description="", examples=["100 mm"])
113
+ depth: LengthMStr | None = pydantic.Field(default=None, description="", examples=["100 mm"])
114
+ painted: bool | None = pydantic.Field(
115
+ default=None,
116
+ description="",
117
+ examples=[True],
118
+ )
119
+ divided: bool | None = pydantic.Field(
120
+ default=None,
121
+ description="",
122
+ examples=[True],
123
+ )
124
+ raceways_material: RacewaysMaterial | None = pydantic.Field(default=None, description="", examples=["Aluminum"])
112
125
 
113
126
 
114
127
  class FueledElectricalGeneratorsV1(BaseOpenEpdHierarchicalSpec):
@@ -147,7 +160,7 @@ class ElectricityFromSpecificGeneratorV1(BaseOpenEpdHierarchicalSpec):
147
160
  _EXT_VERSION = "1.0"
148
161
 
149
162
  # Own fields:
150
- energy_source: EnergySource | None = pyd.Field(default=None, description="", example="Grid")
163
+ energy_source: EnergySource | None = pydantic.Field(default=None, description="", examples=["Grid"])
151
164
 
152
165
 
153
166
  class PowerPurchaseAgreementsV1(BaseOpenEpdHierarchicalSpec):
@@ -160,7 +173,7 @@ class PowerPurchaseAgreementsV1(BaseOpenEpdHierarchicalSpec):
160
173
  _EXT_VERSION = "1.0"
161
174
 
162
175
  # Own fields:
163
- energy_source: EnergySource | None = pyd.Field(default=None, description="", example="Grid")
176
+ energy_source: EnergySource | None = pydantic.Field(default=None, description="", examples=["Grid"])
164
177
 
165
178
 
166
179
  class LightbulbsV1(BaseOpenEpdHierarchicalSpec):
@@ -255,33 +268,48 @@ class LightingV1(BaseOpenEpdHierarchicalSpec):
255
268
  _EXT_VERSION = "1.0"
256
269
 
257
270
  # Own fields:
258
- color_temperature: ColorTemperatureStr | None = pyd.Field(default=None, description="", example="1 K")
259
- typical_utilization: UtilizationStr | None = pyd.Field(default=None, description="", example="1 h / yr")
260
- luminosity: LuminosityStr | None = pyd.Field(default=None, description="", example="1 lumen")
261
- wattage: PowerStr | None = pyd.Field(default=None, description="")
262
- color_rendering_index: float | None = pyd.Field(default=None, description="", example=2.3)
263
- dimmable: bool | None = pyd.Field(default=None, description="", example=True)
264
-
265
- _color_temperature_quantity_ge_validator = pyd.validator("color_temperature", allow_reuse=True)(
266
- validate_quantity_ge_factory("1E+03 K")
267
- )
268
- _color_temperature_quantity_le_validator = pyd.validator("color_temperature", allow_reuse=True)(
269
- validate_quantity_le_factory("1E+04 K")
270
- )
271
- _typical_utilization_unit_validator = pyd.validator("typical_utilization", allow_reuse=True)(
272
- validate_quantity_unit_factory("h / yr")
273
- )
274
- _typical_utilization_quantity_ge_validator = pyd.validator("typical_utilization", allow_reuse=True)(
275
- validate_quantity_ge_factory("25 h / yr")
276
- )
277
- _luminosity_quantity_ge_validator = pyd.validator("luminosity", allow_reuse=True)(
278
- validate_quantity_ge_factory("450 lumen")
279
- )
280
- _luminosity_quantity_le_validator = pyd.validator("luminosity", allow_reuse=True)(
281
- validate_quantity_le_factory("2.6E+03 lumen")
271
+ color_temperature: ColorTemperatureStr | None = pydantic.Field(default=None, description="", examples=["1 K"])
272
+ typical_utilization: UtilizationStr | None = pydantic.Field(default=None, description="", examples=["1 h / yr"])
273
+ luminosity: LuminosityStr | None = pydantic.Field(default=None, description="", examples=["1 lumen"])
274
+ wattage: PowerStr | None = pydantic.Field(default=None, description="")
275
+ color_rendering_index: float | None = pydantic.Field(default=None, description="", examples=[2.3])
276
+ dimmable: bool | None = pydantic.Field(
277
+ default=None,
278
+ description="",
279
+ examples=[True],
282
280
  )
283
- _wattage_quantity_ge_validator = pyd.validator("wattage", allow_reuse=True)(validate_quantity_ge_factory("5 W"))
284
- _wattage_quantity_le_validator = pyd.validator("wattage", allow_reuse=True)(validate_quantity_le_factory("100 W"))
281
+
282
+ @pydantic.field_validator("color_temperature")
283
+ def _color_temperature_quantity_ge_validator(cls, value):
284
+ return validate_quantity_ge_factory("1E+03 K")(cls, value)
285
+
286
+ @pydantic.field_validator("color_temperature")
287
+ def _color_temperature_quantity_le_validator(cls, value):
288
+ return validate_quantity_le_factory("1E+04 K")(cls, value)
289
+
290
+ @pydantic.field_validator("typical_utilization")
291
+ def _typical_utilization_unit_validator(cls, value):
292
+ return validate_quantity_unit_factory("h / yr")(cls, value)
293
+
294
+ @pydantic.field_validator("typical_utilization")
295
+ def _typical_utilization_quantity_ge_validator(cls, value):
296
+ return validate_quantity_ge_factory("25 h / yr")(cls, value)
297
+
298
+ @pydantic.field_validator("luminosity")
299
+ def _luminosity_quantity_ge_validator(cls, value):
300
+ return validate_quantity_ge_factory("450 lumen")(cls, value)
301
+
302
+ @pydantic.field_validator("luminosity")
303
+ def _luminosity_quantity_le_validator(cls, value):
304
+ return validate_quantity_le_factory("2.6E+03 lumen")(cls, value)
305
+
306
+ @pydantic.field_validator("wattage")
307
+ def _wattage_quantity_ge_validator(cls, value):
308
+ return validate_quantity_ge_factory("5 W")(cls, value)
309
+
310
+ @pydantic.field_validator("wattage")
311
+ def _wattage_quantity_le_validator(cls, value):
312
+ return validate_quantity_le_factory("100 W")(cls, value)
285
313
 
286
314
  # Nested specs:
287
315
  Lightbulbs: LightbulbsV1 | None = None