openepd 1.5.0__py3-none-any.whl → 1.6.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.
openepd/__version__.py CHANGED
@@ -17,4 +17,4 @@
17
17
  # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
- VERSION = "1.5.0"
20
+ VERSION = "1.6.0"
openepd/model/base.py CHANGED
@@ -20,7 +20,7 @@
20
20
  import abc
21
21
  from enum import StrEnum
22
22
  import json
23
- from typing import Any, Generic, Optional, Type, TypeVar
23
+ from typing import Any, Callable, Generic, Optional, Type, TypeVar
24
24
 
25
25
  import pydantic as pyd
26
26
  from pydantic.generics import GenericModel
@@ -41,6 +41,22 @@ class OpenEpdDoctypes(StrEnum):
41
41
  Epd = "openEPD"
42
42
 
43
43
 
44
+ def modify_pydantic_schema(schema_dict: dict, cls: type) -> dict:
45
+ """
46
+ Modify the schema dictionary to add the required fields.
47
+
48
+ :param schema_dict: schema dictionary
49
+ :param cls: class for which the schema was generated
50
+ :return: modified schema dictionary
51
+ """
52
+ ext = schema_dict.get("properties", {}).get("ext")
53
+ # move to bottom
54
+ if ext is not None:
55
+ del schema_dict["properties"]["ext"]
56
+ schema_dict["properties"]["ext"] = ext
57
+ return schema_dict
58
+
59
+
44
60
  class BaseOpenEpdSchema(pyd.BaseModel):
45
61
  """Base class for all OpenEPD models."""
46
62
 
@@ -51,6 +67,7 @@ class BaseOpenEpdSchema(pyd.BaseModel):
51
67
  validate_assignment = False
52
68
  allow_population_by_field_name = True
53
69
  use_enum_values = True
70
+ schema_extra: Callable | dict = modify_pydantic_schema
54
71
 
55
72
  def to_serializable(self, *args, **kwargs) -> dict[str, Any]:
56
73
  """
openepd/model/common.py CHANGED
@@ -17,6 +17,7 @@
17
17
  # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
+ from enum import StrEnum
20
21
  from typing import Annotated, Any
21
22
 
22
23
  import pydantic as pyd
@@ -137,3 +138,18 @@ class WithAltIdsMixin(BaseModel):
137
138
  """Set an alt_id if value is not None."""
138
139
  if value is not None:
139
140
  self.set_alt_id(domain_name, value)
141
+
142
+
143
+ class OpenEPDUnit(StrEnum):
144
+ """OpenEPD allowed units."""
145
+
146
+ kg = "kg"
147
+ m2 = "m2"
148
+ m = "m"
149
+ M2_RSI = "m2 * RSI"
150
+ MJ = "MJ"
151
+ t_km = "t * km"
152
+ MPa = "MPa"
153
+ item = "item"
154
+ W = "W"
155
+ use = "use"
openepd/model/org.py CHANGED
@@ -25,8 +25,8 @@ from openepd.model.base import BaseOpenEpdSchema
25
25
  from openepd.model.common import Location, WithAltIdsMixin, WithAttachmentsMixin
26
26
 
27
27
 
28
- class Org(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
29
- """Represent an organization."""
28
+ class OrgRef(BaseOpenEpdSchema):
29
+ """Represents Organisation with minimal data."""
30
30
 
31
31
  web_domain: str | None = pyd.Field(
32
32
  description="A web domain owned by organization. Typically is the org's home website address", default=None
@@ -37,12 +37,18 @@ class Org(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
37
37
  example="C Change Labs",
38
38
  default=None,
39
39
  )
40
+
41
+
42
+ class Org(WithAttachmentsMixin, WithAltIdsMixin, OrgRef):
43
+ """Represent an organization."""
44
+
40
45
  alt_names: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
41
46
  description="List of other names for organization",
42
47
  example=["C-Change Labs", "C-Change Labs inc."],
43
48
  default=None,
44
49
  )
45
50
  # TODO: NEW field, not in the spec
51
+
46
52
  owner: Optional["Org"] = pyd.Field(description="Organization that controls this organization", default=None)
47
53
  subsidiaries: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
48
54
  description="Organizations controlled by this organization",
@@ -17,6 +17,7 @@
17
17
  # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
+
20
21
  import pydantic as pyd
21
22
 
22
23
  from openepd.model.base import BaseOpenEpdSchema
@@ -28,5 +29,7 @@ class Specs(BaseOpenEpdSchema):
28
29
 
29
30
  cmu: concrete.CmuSpec | None = pyd.Field(default=None, description="Concrete Masonry Unit-specific (CMU) specs")
30
31
  CMU: concrete.CmuSpec | None = pyd.Field(default=None, description="Concrete Masonry Unit-specific (CMU) specs")
31
- Steel: steel.SteelV1 | None = pyd.Field(default=None, description="Steel-specific specs")
32
- Concrete: concrete.ConcreteV1 | None = pyd.Field(default=None, description="Concrete-specific specs")
32
+ Steel: steel.SteelV1 | None = pyd.Field(default=None, title="SteelV1", description="Steel-specific specs")
33
+ Concrete: concrete.ConcreteV1 | None = pyd.Field(
34
+ default=None, title="ConcreteV1", description="Concrete-specific specs"
35
+ )
@@ -23,6 +23,7 @@ import pydantic as pyd
23
23
 
24
24
  from openepd.model.base import BaseOpenEpdSchema, Version
25
25
  from openepd.model.validation.common import validate_version_compatibility, validate_version_format
26
+ from openepd.model.validation.numbers import QuantityValidator
26
27
  from openepd.model.versioning import WithExtVersionMixin
27
28
 
28
29
 
@@ -36,6 +37,8 @@ class BaseOpenEpdSpec(BaseOpenEpdSchema):
36
37
  class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
37
38
  """Base class for new specs (hierarchical, versioned)."""
38
39
 
40
+ _QUANTITY_VALIDATOR: QuantityValidator | None = None
41
+
39
42
  def __init__(self, **data: Any) -> None:
40
43
  # ensure that all the concrete spec objects fail on creations if they dont have _EXT_VERSION declared to
41
44
  # something meaningful
@@ -50,3 +53,8 @@ class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
50
53
  _version_major_match_validator = pyd.validator("ext_version", allow_reuse=True, check_fields=False)(
51
54
  validate_version_compatibility("_EXT_VERSION")
52
55
  )
56
+
57
+
58
+ def setup_external_validators(quantity_validator: QuantityValidator):
59
+ """Set the implementation unit validator for specs."""
60
+ BaseOpenEpdHierarchicalSpec._QUANTITY_VALIDATOR = quantity_validator
@@ -23,9 +23,67 @@ from typing import Literal
23
23
  import pydantic as pyd
24
24
 
25
25
  from openepd.model.base import BaseOpenEpdSchema
26
+ from openepd.model.common import OpenEPDUnit
26
27
  from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec, BaseOpenEpdSpec
27
28
  from openepd.model.validation.common import together_validator
28
- from openepd.model.validation.numbers import RatioFloat
29
+ from openepd.model.validation.numbers import RatioFloat, validate_unit_factory
30
+
31
+
32
+ class AciExposureClass(StrEnum):
33
+ """ACI Code (US)."""
34
+
35
+ F0 = "aci.F0"
36
+ F1 = "aci.F1"
37
+ F2 = "aci.F2"
38
+ F3 = "aci.F3"
39
+ S0 = "aci.S0"
40
+ S1 = "aci.S1"
41
+ S2 = "aci.S2"
42
+ S3 = "aci.S3"
43
+ C1 = "aci.C1"
44
+ C2 = "aci.C2"
45
+ W0 = "aci.W0"
46
+ W1 = "aci.W1"
47
+ W2 = "aci.W2"
48
+
49
+
50
+ class CsaExposureClass(StrEnum):
51
+ """CSA Code (Canada)."""
52
+
53
+ N = "csa.N"
54
+ F2 = "csa.F-2"
55
+ F_1 = "csa.F-1"
56
+ C_1 = "csa.C-1"
57
+ S_3 = "csa.S-3"
58
+ S_2 = "csa.S-2"
59
+ S_1 = "csa.S-1"
60
+ A_1 = "csa.A-1"
61
+ A_2 = "csa.A-2"
62
+ A_3 = "csa.A-3"
63
+ A_4 = "csa.A-4"
64
+
65
+
66
+ class EnExposureClass(StrEnum):
67
+ """EN 206 Class (Europe)."""
68
+
69
+ en206_0 = "en206.0"
70
+ F1 = "en206.F1"
71
+ F2 = "en206.F2"
72
+ F3 = "en206.F3"
73
+ F4 = "en206.F4"
74
+ A1 = "en206.A1"
75
+ A2 = "en206.A2"
76
+ A3 = "en206.A3"
77
+ D1 = "en206.D1"
78
+ D2 = "en206.D2"
79
+ D3 = "en206.D3"
80
+ S1 = "en206.S1"
81
+ S2 = "en206.S2"
82
+ S3 = "en206.S3"
83
+ C1 = "en206.C1"
84
+ C2 = "en206.C2"
85
+ C3 = "en206.C3"
86
+ C4 = "en206.C4"
29
87
 
30
88
 
31
89
  class CmuWeightClassification(StrEnum):
@@ -157,16 +215,34 @@ class CmuSpec(BaseOpenEpdSpec):
157
215
  class Cementitious(BaseOpenEpdSchema):
158
216
  """List of cementitious materials, and proportion by mass."""
159
217
 
160
- opc: RatioFloat | None = pyd.Field(default=None, title="Ordinary Gray Portland Cement")
161
- wht: RatioFloat | None = pyd.Field(default=None, title="White Portland Cement")
162
- ggbs: RatioFloat | None = pyd.Field(default=None, title="Ground Granulated Blast Furnace Slag")
163
- flyAsh: RatioFloat | None = pyd.Field(default=None, title="Fly Ash, including types F, CL, and CH")
164
- siFume: RatioFloat | None = pyd.Field(default=None, title="Silica Fume")
165
- gg45: RatioFloat | None = pyd.Field(default=None, title="Ground Glass, 45um or smaller")
166
- natPoz: RatioFloat | None = pyd.Field(default=None, title="Natural pozzolan")
167
- mk: RatioFloat | None = pyd.Field(default=None, title="Metakaolin")
168
- CaCO3: RatioFloat | None = pyd.Field(default=None, title="Limestone")
169
- other: RatioFloat | None = pyd.Field(default=None, title="Other SCMs")
218
+ opc: RatioFloat | None = pyd.Field(default=None, description="Ordinary Gray Portland Cement")
219
+ wht: RatioFloat | None = pyd.Field(default=None, description="White Portland Cement")
220
+ ggbs: RatioFloat | None = pyd.Field(default=None, description="Ground Granulated Blast Furnace Slag")
221
+ flyAsh: RatioFloat | None = pyd.Field(default=None, description="Fly Ash, including types F, CL, and CH")
222
+ siFume: RatioFloat | None = pyd.Field(default=None, description="Silica Fume")
223
+ gg45: RatioFloat | None = pyd.Field(default=None, description="Ground Glass, 45um or smaller")
224
+ natPoz: RatioFloat | None = pyd.Field(default=None, description="Natural pozzolan")
225
+ mk: RatioFloat | None = pyd.Field(default=None, description="Metakaolin")
226
+ CaCO3: RatioFloat | None = pyd.Field(default=None, description="Limestone")
227
+ other: RatioFloat | None = pyd.Field(default=None, description="Other SCMs")
228
+
229
+
230
+ class TypicalApplication(BaseOpenEpdSchema):
231
+ """Concrete typical application."""
232
+
233
+ fnd: bool | None = pyd.Field(description="Foundation", default=None)
234
+ sog: bool | None = pyd.Field(description="Slab on Grade", default=None)
235
+ hrz: bool | None = pyd.Field(description="Elevated Horizontal", default=None)
236
+ vrt_wall: bool | None = pyd.Field(description="Vertical Wall", default=None)
237
+ vrt_column: bool | None = pyd.Field(description="Vertical Column", default=None)
238
+ vrt_other: bool | None = pyd.Field(description="Vertical Other", default=None)
239
+ sht: bool | None = pyd.Field(description="Shotcrete", default=None)
240
+ cdf: bool | None = pyd.Field(description="Flowable Fill (CDF,default=None)", default=None)
241
+ sac: bool | None = pyd.Field(description="Sidewalk and Curb", default=None)
242
+ pav: bool | None = pyd.Field(description="Paving", default=None)
243
+ oil: bool | None = pyd.Field(description="Oil Patch", default=None)
244
+ grt: bool | None = pyd.Field(description="Cement Grout", default=None)
245
+ ota: bool | None = pyd.Field(description="Other", default=None)
170
246
 
171
247
 
172
248
  class ConcreteV1(BaseOpenEpdHierarchicalSpec):
@@ -174,6 +250,16 @@ class ConcreteV1(BaseOpenEpdHierarchicalSpec):
174
250
 
175
251
  _EXT_VERSION = "1.0"
176
252
 
253
+ class Options(BaseOpenEpdSchema):
254
+ lightweight: bool | None = pyd.Field(description="Lightweight", default=None)
255
+ plc: bool | None = pyd.Field(description="Portland Limestone Cement", default=None)
256
+ scc: bool | None = pyd.Field(description="Self Compacting", default=None)
257
+ finishable: bool | None = pyd.Field(description="Finishable", default=None)
258
+ air: bool | None = pyd.Field(description="Air Entrainment", default=None)
259
+ co2: bool | None = pyd.Field(description="CO2 Curing", default=None)
260
+ white: bool | None = pyd.Field(description="White Cement", default=None)
261
+ fiber_reinforced: bool | None = pyd.Field(description="Fiber reinforced", default=None)
262
+
177
263
  strength_28d: str | None = pyd.Field(
178
264
  default=None, title="Concrete Strength 28d", description="Concrete strength after 28 days"
179
265
  )
@@ -194,11 +280,30 @@ class ConcreteV1(BaseOpenEpdHierarchicalSpec):
194
280
  strength_late_d: Literal[42, 56, 72, 96, 120] | None = pyd.Field(
195
281
  default=None, title="Test Day for the Late Strength", description="Test Day for the Late Strength"
196
282
  )
283
+ slump: str | None = pyd.Field(description="Minimum test slump", default=None)
284
+ w_c_ratio: RatioFloat | None = pyd.Field(description="Ratio of water to cement", default=None)
285
+ aci_exposure_classes: list[AciExposureClass] = pyd.Field(
286
+ description="List of ACI318-19 exposure classes this product meets", default=None
287
+ )
288
+ csa_exposure_classes: list[CsaExposureClass] = pyd.Field(
289
+ description="List of CSA A23.1 exposure classes this product meets", default=None
290
+ )
291
+ en_exposure_classes: list[EnExposureClass] = pyd.Field(
292
+ description="List of EN206 exposure classes this product meets", default=None
293
+ )
294
+
295
+ application: TypicalApplication | None = pyd.Field(description="Typical Application", default=None)
296
+
297
+ options: Options = pyd.Field(description="Concrete options", default=None)
298
+
197
299
  cementitious: Cementitious | None = pyd.Field(
198
300
  default=None,
199
301
  title="Cementitious Materials",
200
302
  description="List of cementitious materials, and proportion by mass",
201
303
  )
304
+ _compressive_strength_unit_validator = pyd.validator("strength_28d", allow_reuse=True)(
305
+ validate_unit_factory(OpenEPDUnit.MPa)
306
+ )
202
307
 
203
308
  @pyd.root_validator
204
309
  def _late_validator(cls, values):
@@ -23,15 +23,16 @@ import pydantic as pyd
23
23
 
24
24
  from openepd.model.base import BaseOpenEpdSchema
25
25
  from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
26
+ from openepd.model.standard import Standard
26
27
  from openepd.model.validation.numbers import RatioFloat
27
28
 
28
29
 
29
30
  class SteelMakingRoute(BaseOpenEpdSchema):
30
31
  """Steel making route."""
31
32
 
32
- bof: bool | None = pyd.Field(default=None, title="Steel Making Route BOF", description="Basic oxygen furnace")
33
- eaf: bool | None = pyd.Field(default=None, title="Steel Making Route EAF", description="Electric arc furnace")
34
- ohf: bool | None = pyd.Field(default=None, title="Steel Making Route OHF", description="Open hearth furnace")
33
+ bof: bool | None = pyd.Field(default=None, description="Basic oxygen furnace")
34
+ eaf: bool | None = pyd.Field(default=None, description="Electric arc furnace")
35
+ ohf: bool | None = pyd.Field(default=None, description="Open hearth furnace")
35
36
 
36
37
 
37
38
  class SteelComposition(StrEnum):
@@ -44,20 +45,67 @@ class SteelComposition(StrEnum):
44
45
  OTHER = "Other"
45
46
 
46
47
 
48
+ class FabricatedOptionsMixin(pyd.BaseModel):
49
+ """Fabricated options mixin."""
50
+
51
+ fabricated: bool | None = pyd.Field(default=None, description="Fabricated")
52
+
53
+
54
+ class WireMeshSteelV1(BaseOpenEpdHierarchicalSpec):
55
+ """Spec for wire mesh steel."""
56
+
57
+ class Options(BaseOpenEpdSchema, FabricatedOptionsMixin):
58
+ """Wire Mesh Options."""
59
+
60
+ pass
61
+
62
+ options: Options = pyd.Field(description="Rebar Steel options", default_factory=Options)
63
+
64
+
47
65
  class RebarSteelV1(BaseOpenEpdHierarchicalSpec):
48
66
  """Rebar steel spec."""
49
67
 
50
68
  _EXT_VERSION = "1.0"
51
69
 
52
- class Options(BaseOpenEpdSchema):
53
- """Rebar steel options."""
70
+ class Options(BaseOpenEpdSchema, FabricatedOptionsMixin):
71
+ """Rebar Steel Options."""
54
72
 
55
- epoxy: bool | None = pyd.Field(default=None, title="Epoxy Coated")
56
- fabricated: bool | None = pyd.Field(default=None, title="Fabricated", description="Fabricated")
73
+ epoxy: bool | None = pyd.Field(default=None, description="Epoxy Coated")
57
74
 
58
- options: Options = pyd.Field(
59
- title="Rebar Steel Options", description="Rebar Steel options", default_factory=Options
60
- )
75
+ options: Options = pyd.Field(description="Rebar Steel options", default_factory=Options)
76
+
77
+
78
+ class PlateSteelV1(BaseOpenEpdHierarchicalSpec):
79
+ """Plate Steel Spec."""
80
+
81
+ class Options(BaseOpenEpdSchema, FabricatedOptionsMixin):
82
+ """Plate Steel Options."""
83
+
84
+ pass
85
+
86
+ options: Options = pyd.Field(description="Plate Steel options", default_factory=Options)
87
+
88
+
89
+ class HollowV1(BaseOpenEpdHierarchicalSpec):
90
+ """Hollow Sections Spec."""
91
+
92
+ class Options(FabricatedOptionsMixin, BaseOpenEpdSchema):
93
+ """Hollow Sections Options."""
94
+
95
+ pass
96
+
97
+ options: Options = pyd.Field(description="Hollow Steel options", default_factory=Options)
98
+
99
+
100
+ class HotRolledV1(BaseOpenEpdHierarchicalSpec):
101
+ """Hot Rolled spec."""
102
+
103
+ class Options(FabricatedOptionsMixin, BaseOpenEpdSchema):
104
+ """Hot Rolled options."""
105
+
106
+ pass
107
+
108
+ options: Options = pyd.Field(description="Hollow Steel options", default_factory=Options)
61
109
 
62
110
 
63
111
  class SteelV1(BaseOpenEpdHierarchicalSpec):
@@ -68,24 +116,31 @@ class SteelV1(BaseOpenEpdHierarchicalSpec):
68
116
  class Options(BaseOpenEpdSchema):
69
117
  """Steel spec options."""
70
118
 
71
- galvanized: bool | None = pyd.Field(default=None, title="Galvanized")
72
- cold_finished: bool | None = pyd.Field(default=None, title="Cold Finished")
119
+ galvanized: bool | None = pyd.Field(default=None, description="Galvanized")
120
+ cold_finished: bool | None = pyd.Field(default=None, description="Cold Finished")
73
121
 
122
+ form_factor: str | None = pyd.Field(description="Product's form factor", example="Steel >> RebarSteel")
123
+ steel_composition: SteelComposition | None = pyd.Field(default=None, description="Basic chemical composition")
74
124
  recycled_content: RatioFloat | None = pyd.Field(
75
125
  default=None,
76
- title="Scrap Recycled Content",
77
126
  description="Scrap steel inputs from other processes. Includes "
78
127
  "Post-Consumer content, if any. This percentage may be "
79
128
  "used to evaluate the EPD w.r.t. targets or limits that are"
80
129
  " different for primary and recycled content.",
81
130
  )
82
- steel_composition: SteelComposition | None = pyd.Field(
83
- default=None, title="Steel Composition", description="Basic chemical composition"
84
- )
85
- making_route: SteelMakingRoute | None = pyd.Field(
86
- default=None, title="Steel Making Route", description="Steel making route"
131
+ ASTM: list[Standard] = pyd.Field(description="ASTM standard to which this product complies", default_factory=list)
132
+ SAE: list[Standard] = pyd.Field(
133
+ description="AISA/SAE standard to which this product complies", default_factory=list
87
134
  )
135
+ EN: list[Standard] = pyd.Field(description="EN 10027 number(s)", default_factory=list)
136
+
88
137
  options: Options | None = pyd.Field(description="Steel options", default_factory=Options)
138
+ making_route: SteelMakingRoute | None = pyd.Field(default=None, description="Steel making route")
89
139
 
90
140
  # Nested specs
141
+
142
+ WireMeshSteel: WireMeshSteelV1 | None = None
91
143
  RebarSteel: RebarSteelV1 | None = None
144
+ PlateSteel: PlateSteelV1 | None = None
145
+ Hollow: HollowV1 | None = None
146
+ HotRolled: HotRolledV1 | None = None
@@ -17,9 +17,50 @@
17
17
  # Charles Pankow Foundation, Microsoft Sustainability Fund, Interface, MKA Foundation, and others.
18
18
  # Find out more at www.BuildingTransparency.org
19
19
  #
20
- from typing import Annotated
20
+ from abc import ABC, abstractmethod
21
+ from typing import TYPE_CHECKING, Annotated
21
22
 
22
23
  import pydantic as pyd
23
24
 
25
+ from openepd.model.common import OpenEPDUnit
26
+
27
+ if TYPE_CHECKING:
28
+ from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
29
+
24
30
  RatioFloat = Annotated[float, pyd.Field(ge=0, le=1, example=0.5)]
25
31
  """Float field which represents a percentage ratio between 0 and 1."""
32
+
33
+
34
+ class QuantityValidator(ABC):
35
+ """
36
+ Interface for quantity validator.
37
+
38
+ The openEPD models are mapped using the simple types. Caller code should provide their own implementation of this
39
+ and set it with `set_unit_validator` function.
40
+ """
41
+
42
+ @abstractmethod
43
+ def validate(self, value: str, dimensionality: str) -> None:
44
+ """
45
+ Validate the given string value against the given dimensionality.
46
+
47
+ Args:
48
+ value: The value to validate, like "102.4 kg"
49
+ dimensionality: The dimensionality to validate against, like "kg"
50
+ Returns:
51
+ None if the value is valid, raises an error otherwise.
52
+ Raises:
53
+ ValueError: If the value is not valid.
54
+ """
55
+ pass
56
+
57
+
58
+ def validate_unit_factory(dimensionality: OpenEPDUnit | str):
59
+ """Create validator for unit field."""
60
+
61
+ def validator(cls: "BaseOpenEpdHierarchicalSpec", value: str) -> str:
62
+ if hasattr(cls, "_QUNATITY_VALIDATOR") and cls._QUANTITY_VALIDATOR is not None:
63
+ cls._QUANTITY_VALIDATOR.validate(value, dimensionality)
64
+ return value
65
+
66
+ return validator
@@ -37,7 +37,7 @@ class WithExtVersionMixin(ABC, BaseModel):
37
37
  if hasattr(cls, "_EXT_VERSION"):
38
38
  cls.__fields__["ext_version"].default = cls._EXT_VERSION
39
39
 
40
- ext_version: str = pyd.Field(title="Extension version", example="3.22", default=None)
40
+ ext_version: str = pyd.Field(description="Extension version", example="3.22", default=None)
41
41
 
42
42
 
43
43
  class Version(NamedTuple):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 1.5.0
3
+ Version: 1.6.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,5 +1,5 @@
1
1
  openepd/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
2
- openepd/__version__.py,sha256=39xThneFQ4W_HcH1Mw2OPiRHanB4Qxl2McAEAZ_HV6w,855
2
+ openepd/__version__.py,sha256=dydPFQzMC85AwH6lLCuy1P3UT2PsGjNSUOyDiyVRnhA,855
3
3
  openepd/api/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
4
4
  openepd/api/base_sync_client.py,sha256=JcNpWsGoIK_1Eg27CAQd7nIjbcfD56jQ1nS6B48Q0cI,21142
5
5
  openepd/api/category/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
@@ -27,26 +27,26 @@ openepd/bundle/model.py,sha256=NWbpxorLzQ_OIFozV34wz76OWM_ORGTBkS0288chv0k,2647
27
27
  openepd/bundle/reader.py,sha256=z4v_UWyaosktN3DdmnRx8GpLq4DkejjoUsckFfCgUac,6904
28
28
  openepd/bundle/writer.py,sha256=AfvzzdAr9ybIbtiZfuX2mAGfddamTxXxUTxtHHrs2Gc,8353
29
29
  openepd/model/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
30
- openepd/model/base.py,sha256=rR86DJ2x94mHaVwNf8wcD5Z7l8BLo2oS16gQaS9AI4s,8310
30
+ openepd/model/base.py,sha256=wLZUQSc6nOvoiESgxjEHTdGEV417SUolsXMNm2mA-po,8883
31
31
  openepd/model/category.py,sha256=_6yFyldaDrkZk2_9ZAO2oRTz7ocvkg4T-D_aw5ncgvo,1840
32
- openepd/model/common.py,sha256=SSTIvCjCkJEKf3FVnUvAw9SfFbkSdestq6YBX20eBQw,5386
32
+ openepd/model/common.py,sha256=j-M_2T_Kl_Ea8l50pJT7LNatBW1fKrRcw4XNpeNvLU0,5635
33
33
  openepd/model/epd.py,sha256=BxcKJd_LuApB2v_LGN0-hYLNaUNoqMu9ZH_V1Ffil-g,14269
34
34
  openepd/model/factory.py,sha256=qQZNb4yFN1EQWHHVS-fHnJ2gB-vsKz9hVmXXJE7YaRU,1918
35
35
  openepd/model/lcia.py,sha256=2GOdcY6ASJu653SFwTakT9boYF-3CYCHLupyxTPUQ30,16711
36
- openepd/model/org.py,sha256=ov3nrSk36imug7ADarNWHIXW9jiFbmboswoXVQCNeDI,3662
36
+ openepd/model/org.py,sha256=quoYMrA6WXqs7ZRjdO1Bm47No4jGLqxz0QmnnqKgw7c,3741
37
37
  openepd/model/pcr.py,sha256=t861yFntmy3ewrnGLP47cv3afV-aCCVRlrbpGeRh-7I,4604
38
38
  openepd/model/specs/README.md,sha256=W5LSMpZuW5x36cKS4HRfeFsClsRf8J9yHMMICghdc0s,862
39
- openepd/model/specs/__init__.py,sha256=iyubvilvNaoL7Olh1pM8RxZstfFM2tQdJvWdQ3FuzMA,1462
40
- openepd/model/specs/base.py,sha256=pbSE_RBDKiVarhM7wTUP4bGmAuHtKrF-DRMzvlHqYok,2239
41
- openepd/model/specs/concrete.py,sha256=wZUfpYAjX0zNew719AKo0gfqxHq9qnZFkWTsB48Tbxw,9232
42
- openepd/model/specs/steel.py,sha256=4azufmyD_h2HIIVJEc_mw6i3Fv-DdWVTOI5rzQ3Raxo,3325
39
+ openepd/model/specs/__init__.py,sha256=3DXyeFzH1bw4rf1KCFXPXl3X-ErMu0NpjvqqhorpnLc,1514
40
+ openepd/model/specs/base.py,sha256=hOE_hCuiY7xhR0yW_Pbxe_LQAnMWq8sq3rTqQqbeVOc,2564
41
+ openepd/model/specs/concrete.py,sha256=jdhuThy8QFaEs9HdJ53O7ua4Fw-ES2_YzoLDchyU8s0,13165
42
+ openepd/model/specs/steel.py,sha256=FoMYE6y_uvWg9Edkb7WV0f_hQNmviwtI1SM-UmdRh8E,4985
43
43
  openepd/model/standard.py,sha256=YjxpC9nyz6LM8m3z1s5S0exJ1qvA3n59-FBoScg-FL0,1519
44
44
  openepd/model/validation/__init__.py,sha256=rqQJWF5jpYAgRbbAycUfWMGsr5kGtfjmwzsTeqbElJw,837
45
45
  openepd/model/validation/common.py,sha256=nNwLtj5Uu7TsFmJXuD8aRcx1hB_nf-LQBfEvHOZM1Ao,2420
46
- openepd/model/validation/numbers.py,sha256=YIJFcw1oXQC7MzHKBk1Z9vkfkWhxnndoX8YH5bY5qR4,1028
47
- openepd/model/versioning.py,sha256=tJ4XNuV8vO3Aa6WqRpVfG258tX_9nDIZfMjqjoMyy2M,4622
46
+ openepd/model/validation/numbers.py,sha256=AItjDAz9LODxLOH7shqfMMufT9ep_8ys2bVelIbME7g,2349
47
+ openepd/model/versioning.py,sha256=Z31TgCW0POsmXLrH-Rmpy-MrIw_bU_9wwh8T4Mq1yTM,4628
48
48
  openepd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
- openepd-1.5.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
50
- openepd-1.5.0.dist-info/METADATA,sha256=96X-W4zaXleOzdJZECfbcijqJvgaYO8ZsPTwxLuTOq8,7762
51
- openepd-1.5.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
52
- openepd-1.5.0.dist-info/RECORD,,
49
+ openepd-1.6.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
50
+ openepd-1.6.0.dist-info/METADATA,sha256=_zIXgd030Dco2ykLRwhes7xe6ygQhgmtjiyEXqVvUo0,7762
51
+ openepd-1.6.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
52
+ openepd-1.6.0.dist-info/RECORD,,