openepd 6.13.2__py3-none-any.whl → 7.0.1__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 (99) hide show
  1. openepd/__init__.py +0 -6
  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/model/base.py +61 -44
  18. openepd/model/category.py +13 -10
  19. openepd/model/common.py +107 -59
  20. openepd/model/declaration.py +93 -64
  21. openepd/model/epd.py +51 -43
  22. openepd/model/generic_estimate.py +28 -13
  23. openepd/model/industry_epd.py +15 -9
  24. openepd/model/lcia.py +161 -136
  25. openepd/model/org.py +70 -37
  26. openepd/model/pcr.py +38 -32
  27. openepd/model/specs/asphalt.py +31 -22
  28. openepd/model/specs/base.py +14 -11
  29. openepd/model/specs/concrete.py +60 -39
  30. openepd/model/specs/range/aggregates.py +9 -9
  31. openepd/model/specs/range/aluminium.py +7 -7
  32. openepd/model/specs/range/asphalt.py +22 -19
  33. openepd/model/specs/range/cladding.py +16 -16
  34. openepd/model/specs/range/cmu.py +10 -9
  35. openepd/model/specs/range/concrete.py +36 -27
  36. openepd/model/specs/range/conveying_equipment.py +16 -15
  37. openepd/model/specs/range/electrical.py +24 -22
  38. openepd/model/specs/range/finishes.py +109 -104
  39. openepd/model/specs/range/fire_and_smoke_protection.py +7 -7
  40. openepd/model/specs/range/furnishings.py +16 -12
  41. openepd/model/specs/range/manufacturing_inputs.py +16 -16
  42. openepd/model/specs/range/masonry.py +16 -16
  43. openepd/model/specs/range/mechanical.py +47 -47
  44. openepd/model/specs/range/mechanical_insulation.py +7 -7
  45. openepd/model/specs/range/network_infrastructure.py +54 -46
  46. openepd/model/specs/range/openings.py +36 -31
  47. openepd/model/specs/range/plumbing.py +15 -13
  48. openepd/model/specs/range/precast_concrete.py +20 -16
  49. openepd/model/specs/range/sheathing.py +18 -18
  50. openepd/model/specs/range/steel.py +27 -25
  51. openepd/model/specs/range/thermal_moisture_protection.py +20 -20
  52. openepd/model/specs/range/utility_piping.py +9 -9
  53. openepd/model/specs/range/wood.py +19 -19
  54. openepd/model/specs/range/wood_joists.py +8 -8
  55. openepd/model/specs/singular/__init__.py +9 -5
  56. openepd/model/specs/singular/aggregates.py +22 -15
  57. openepd/model/specs/singular/aluminium.py +20 -5
  58. openepd/model/specs/singular/asphalt.py +44 -20
  59. openepd/model/specs/singular/cladding.py +38 -23
  60. openepd/model/specs/singular/cmu.py +26 -11
  61. openepd/model/specs/singular/common.py +3 -2
  62. openepd/model/specs/singular/concrete.py +85 -48
  63. openepd/model/specs/singular/conveying_equipment.py +30 -17
  64. openepd/model/specs/singular/deprecated/__init__.py +3 -2
  65. openepd/model/specs/singular/deprecated/concrete.py +68 -33
  66. openepd/model/specs/singular/deprecated/steel.py +28 -15
  67. openepd/model/specs/singular/electrical.py +69 -41
  68. openepd/model/specs/singular/finishes.py +250 -140
  69. openepd/model/specs/singular/fire_and_smoke_protection.py +9 -6
  70. openepd/model/specs/singular/furnishings.py +16 -14
  71. openepd/model/specs/singular/manufacturing_inputs.py +23 -14
  72. openepd/model/specs/singular/masonry.py +66 -21
  73. openepd/model/specs/singular/mechanical.py +48 -47
  74. openepd/model/specs/singular/mechanical_insulation.py +7 -6
  75. openepd/model/specs/singular/mixins/conduit_mixin.py +13 -10
  76. openepd/model/specs/singular/network_infrastructure.py +111 -52
  77. openepd/model/specs/singular/openings.py +127 -95
  78. openepd/model/specs/singular/plumbing.py +15 -12
  79. openepd/model/specs/singular/precast_concrete.py +68 -54
  80. openepd/model/specs/singular/sheathing.py +47 -27
  81. openepd/model/specs/singular/steel.py +69 -45
  82. openepd/model/specs/singular/thermal_moisture_protection.py +36 -20
  83. openepd/model/specs/singular/utility_piping.py +11 -8
  84. openepd/model/specs/singular/wood.py +48 -24
  85. openepd/model/specs/singular/wood_joists.py +19 -6
  86. openepd/model/standard.py +15 -8
  87. openepd/model/validation/common.py +9 -3
  88. openepd/model/validation/numbers.py +0 -13
  89. openepd/model/validation/quantity.py +88 -55
  90. openepd/model/versioning.py +9 -6
  91. {openepd-6.13.2.dist-info → openepd-7.0.1.dist-info}/METADATA +2 -2
  92. openepd-7.0.1.dist-info/RECORD +141 -0
  93. openepd/compat/__init__.py +0 -15
  94. openepd/compat/compat_functional_validators.py +0 -25
  95. openepd/compat/pydantic.py +0 -30
  96. openepd/patch_pydantic.py +0 -108
  97. openepd-6.13.2.dist-info/RECORD +0 -145
  98. {openepd-6.13.2.dist-info → openepd-7.0.1.dist-info}/LICENSE +0 -0
  99. {openepd-6.13.2.dist-info → openepd-7.0.1.dist-info}/WHEEL +0 -0
openepd/model/org.py CHANGED
@@ -13,11 +13,12 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- from typing import Annotated, Optional
16
+ from typing import Any, Optional
17
17
 
18
18
  from openlocationcode import openlocationcode
19
+ import pydantic
20
+ from pydantic import ConfigDict
19
21
 
20
- from openepd.compat.pydantic import pyd
21
22
  from openepd.model.base import BaseOpenEpdSchema
22
23
  from openepd.model.common import Location, WithAltIdsMixin, WithAttachmentsMixin
23
24
  from openepd.model.validation.common import ReferenceStr
@@ -26,18 +27,19 @@ from openepd.model.validation.common import ReferenceStr
26
27
  class OrgRef(BaseOpenEpdSchema):
27
28
  """Represents Organisation with minimal data."""
28
29
 
29
- web_domain: str | None = pyd.Field(
30
- description="A web domain owned by organization. Typically is the org's home website address", default=None
30
+ web_domain: str | None = pydantic.Field(
31
+ description="A web domain owned by organization. Typically is the org's home website address",
32
+ default=None,
31
33
  )
32
- name: str | None = pyd.Field(
34
+ name: str | None = pydantic.Field(
33
35
  max_length=200,
34
36
  description="Common name for organization",
35
- example="C Change Labs",
37
+ examples=["C Change Labs"],
36
38
  default=None,
37
39
  )
38
- ref: ReferenceStr | None = pyd.Field(
40
+ ref: ReferenceStr | None = pydantic.Field(
39
41
  default=None,
40
- example="https://openepd.buildingtransparency.org/api/orgs/c-change-labs.com",
42
+ examples=["https://openepd.buildingtransparency.org/api/orgs/c-change-labs.com"],
41
43
  description="Reference to this Org's JSON object",
42
44
  )
43
45
 
@@ -45,19 +47,47 @@ class OrgRef(BaseOpenEpdSchema):
45
47
  class Org(WithAttachmentsMixin, WithAltIdsMixin, OrgRef):
46
48
  """Represent an organization."""
47
49
 
48
- alt_names: Annotated[list[str], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
50
+ alt_names: list[str] | None = pydantic.Field(
49
51
  description="List of other names for organization",
50
- example=["C-Change Labs", "C-Change Labs inc."],
52
+ examples=[["C-Change Labs", "C-Change Labs inc."]],
51
53
  default=None,
54
+ max_length=255,
52
55
  )
53
56
  # TODO: NEW field, not in the spec
54
57
 
55
- owner: Optional["OrgRef"] = pyd.Field(description="Organization that controls this organization", default=None)
56
- subsidiaries: Annotated[list["OrgRef"], pyd.conlist(pyd.constr(max_length=200), max_items=255)] | None = pyd.Field(
57
- description="Organizations controlled by this organization",
58
- default=None,
58
+ @pydantic.field_validator("alt_names", mode="before")
59
+ def _validate_alt_names(cls, value: Any) -> list[str] | None:
60
+ if value is None:
61
+ return value
62
+
63
+ if not isinstance(value, list):
64
+ raise TypeError(f"Expected type list or None, got {type(value)}")
65
+
66
+ if any((len(item) > 200) for item in value):
67
+ raise ValueError("One or more alt_names are longer than 200 characters")
68
+
69
+ return value
70
+
71
+ owner: Optional["OrgRef"] = pydantic.Field(description="Organization that controls this organization", default=None)
72
+ subsidiaries: list["OrgRef"] | None = pydantic.Field(
73
+ description="Organizations controlled by this organization", default=None, max_length=255
59
74
  )
60
- hq_location: Location | None = pyd.Field(
75
+
76
+ @pydantic.field_validator("subsidiaries", mode="before")
77
+ def _validate_subsidiaries(cls, value: Any) -> list[str] | None:
78
+ if value is None:
79
+ return value
80
+
81
+ if not isinstance(value, list):
82
+ raise TypeError(f"Expected type list or None, got {type(value)}")
83
+
84
+ for item in value:
85
+ if len(item.name) > 200:
86
+ raise ValueError("One or more subsidiaries name are longer than 200 characters")
87
+
88
+ return value
89
+
90
+ hq_location: Location | None = pydantic.Field(
61
91
  default=None,
62
92
  description="Location of a place of business, preferably the corporate headquarters.",
63
93
  )
@@ -66,21 +96,21 @@ class Org(WithAttachmentsMixin, WithAltIdsMixin, OrgRef):
66
96
  class PlantRef(BaseOpenEpdSchema):
67
97
  """Represents Plant with minimal data."""
68
98
 
69
- id: str | None = pyd.Field(
99
+ id: str | None = pydantic.Field(
70
100
  description="Plus code (aka Open Location Code) of plant's location and "
71
101
  "owner's web domain joined with `.`(dot).",
72
- example="865P2W3V+3W.interface.com",
102
+ examples=["865P2W3V+3W.interface.com"],
73
103
  default=None,
74
104
  )
75
- name: str | None = pyd.Field(
105
+ name: str | None = pydantic.Field(
76
106
  max_length=200,
77
107
  description="Manufacturer's name for plant. Recommended < 40 chars",
78
- example="Dalton, GA",
108
+ examples=["Dalton, GA"],
79
109
  default=None,
80
110
  )
81
- ref: ReferenceStr | None = pyd.Field(
111
+ ref: ReferenceStr | None = pydantic.Field(
82
112
  default=None,
83
- example="https://openepd.buildingtransparency.org/api/orgs/c-change-labs.com",
113
+ examples=["https://openepd.buildingtransparency.org/api/orgs/c-change-labs.com"],
84
114
  description="Reference to this Plant's JSON object",
85
115
  )
86
116
 
@@ -88,36 +118,40 @@ class PlantRef(BaseOpenEpdSchema):
88
118
  class Plant(PlantRef, WithAttachmentsMixin, WithAltIdsMixin):
89
119
  """Represent a manufacturing plant."""
90
120
 
91
- pluscode: str | None = pyd.Field(
121
+ pluscode: str | None = pydantic.Field(
92
122
  default=None,
93
123
  description="(deprecated) Plus code (aka Open Location Code) of plant's location",
94
- deprecated="Pluscode field is deprecated. If users need a pluscode they can obtain it from "
95
- "`id` like this: `id.spit('.', maxsplit=1)[0]`",
124
+ json_schema_extra={
125
+ "deprecated": "Pluscode field is deprecated. If users need a pluscode they can obtain it from "
126
+ "`id` like this: `id.spit('.', maxsplit=1)[0]`",
127
+ },
96
128
  )
97
- latitude: float | None = pyd.Field(
98
- default=None, description="(deprecated) Latitude of the plant location. Use 'location' fields instead."
129
+ latitude: float | None = pydantic.Field(
130
+ default=None,
131
+ description="(deprecated) Latitude of the plant location. Use 'location' fields instead.",
99
132
  )
100
- longitude: float | None = pyd.Field(
101
- default=None, description="(deprecated) Longitude of the plant location. Use 'location' fields instead."
133
+ longitude: float | None = pydantic.Field(
134
+ default=None,
135
+ description="(deprecated) Longitude of the plant location. Use 'location' fields instead.",
102
136
  )
103
- owner: Org | None = pyd.Field(description="Organization that owns the plant", default=None)
104
- address: str | None = pyd.Field(
137
+ owner: Org | None = pydantic.Field(description="Organization that owns the plant", default=None)
138
+ address: str | None = pydantic.Field(
105
139
  max_length=200,
106
140
  default=None,
107
141
  description="(deprecated) Text address, preferably geocoded. Use 'location' fields instead",
108
- example="1503 Orchard Hill Rd, LaGrange, GA 30240, United States",
142
+ examples=["1503 Orchard Hill Rd, LaGrange, GA 30240, United States"],
109
143
  )
110
- contact_email: pyd.EmailStr | None = pyd.Field(
111
- description="Email contact", example="info@interface.com", default=None
144
+ contact_email: pydantic.EmailStr | None = pydantic.Field(
145
+ description="Email contact", examples=["info@interface.com"], default=None
112
146
  )
113
- location: Location | None = pyd.Field(description="Location of the plant", default=None)
147
+ location: Location | None = pydantic.Field(description="Location of the plant", default=None)
114
148
 
115
149
  @classmethod
116
150
  def get_asset_type(cls) -> str | None:
117
151
  """Return the asset type of this class (see BaseOpenEpdSchema.get_asset_type for details)."""
118
152
  return "org"
119
153
 
120
- @pyd.validator("id")
154
+ @pydantic.field_validator("id")
121
155
  def _validate_id(cls, v: str) -> str:
122
156
  if not v:
123
157
  return v
@@ -133,5 +167,4 @@ class Plant(PlantRef, WithAttachmentsMixin, WithAltIdsMixin):
133
167
  raise ValueError("Incorrect web_domain for plant")
134
168
  return v
135
169
 
136
- class Config(BaseOpenEpdSchema.Config):
137
- allow_population_by_field_name = True
170
+ model_config = ConfigDict(populate_by_name=True)
openepd/model/pcr.py CHANGED
@@ -17,7 +17,8 @@ import datetime
17
17
  from enum import StrEnum
18
18
  from typing import Annotated, Optional
19
19
 
20
- from openepd.compat.pydantic import pyd
20
+ import pydantic
21
+
21
22
  from openepd.model.base import BaseOpenEpdSchema, OpenXpdUUID
22
23
  from openepd.model.common import Amount, WithAltIdsMixin, WithAttachmentsMixin
23
24
  from openepd.model.org import Org
@@ -36,87 +37,92 @@ class PcrStatus(StrEnum):
36
37
  class PcrRef(BaseOpenEpdSchema):
37
38
  """Reference to a PCR."""
38
39
 
39
- id: OpenXpdUUID | None = pyd.Field(
40
+ id: OpenXpdUUID | None = pydantic.Field(
40
41
  description="The unique ID for this PCR. To ensure global uniqueness, should be registered "
41
42
  "at open-xpd-uuid.cqd.io/register or a coordinating registry.",
42
- example="ec3xpgq2",
43
+ examples=["ec3xpgq2"],
43
44
  default=None,
44
45
  )
45
- name: str | None = pyd.Field(
46
+ name: str | None = pydantic.Field(
46
47
  max_length=200,
47
48
  description="Full document name as listed in source document",
48
- example="c-PCR-003 Concrete and concrete elements (EN 16757)",
49
+ examples=["c-PCR-003 Concrete and concrete elements (EN 16757)"],
49
50
  )
50
- ref: pyd.AnyUrl | None = pyd.Field(
51
+ ref: pydantic.AnyUrl | None = pydantic.Field(
51
52
  description="Reference to this PCR's JSON object",
52
- example="https://openepd.buildingtransparency.org/api/pcrs/1u7zsed8",
53
+ examples=["https://openepd.buildingtransparency.org/api/pcrs/1u7zsed8"],
53
54
  )
54
55
 
55
56
 
56
57
  class Pcr(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
57
58
  """Represent a PCR (Product Category Rules)."""
58
59
 
59
- id: OpenXpdUUID | None = pyd.Field(
60
+ id: OpenXpdUUID | None = pydantic.Field(
60
61
  description="The unique ID for this PCR. To ensure global uniqueness, should be registered "
61
62
  "at open-xpd-uuid.cqd.io/register or a coordinating registry.",
62
- example="ec3xpgq2",
63
+ examples=["ec3xpgq2"],
63
64
  default=None,
64
65
  )
65
- issuer: Org | None = pyd.Field(description="Organization issuing this PCR", default=None)
66
- issuer_doc_id: str | None = pyd.Field(
66
+ issuer: Org | None = pydantic.Field(description="Organization issuing this PCR", default=None)
67
+ issuer_doc_id: str | None = pydantic.Field(
67
68
  max_length=40,
68
69
  default=None,
69
70
  description="Document ID or code created by issuer",
70
- example="c-PCR-003",
71
+ examples=["c-PCR-003"],
71
72
  )
72
- name: str | None = pyd.Field(
73
+ name: str | None = pydantic.Field(
73
74
  max_length=200,
74
75
  default=None,
75
76
  description="Full document name as listed in source document",
76
- example="c-PCR-003 Concrete and concrete elements (EN 16757)",
77
+ examples=["c-PCR-003 Concrete and concrete elements (EN 16757)"],
77
78
  )
78
- short_name: str | None = pyd.Field(
79
+ short_name: str | None = pydantic.Field(
79
80
  default=None,
80
81
  description="A shortened name without boilerplate text.",
81
- example="Concrete and Concrete Elements",
82
+ examples=["Concrete and Concrete Elements"],
82
83
  )
83
- declared_units: list[Amount] | None = pyd.Field(
84
+ declared_units: list[Amount] | None = pydantic.Field(
84
85
  description="SI declared units for this PCR. If a functional unit is "
85
86
  "utilized, the declared unit shall refer to the amount of "
86
- "product associated with the A1-A3 life cycle stage."
87
+ "product associated with the A1-A3 life cycle stage.",
88
+ default=None,
87
89
  )
88
- version: str | None = pyd.Field(
90
+ version: str | None = pydantic.Field(
89
91
  description="Document version, as expressed in document.",
90
- example="1.0.2",
92
+ examples=["1.0.2"],
91
93
  default=None,
92
94
  )
93
- date_of_issue: datetime.datetime | None = pyd.Field(
94
- example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
95
+ date_of_issue: datetime.datetime | None = pydantic.Field(
96
+ examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc)],
95
97
  default=None,
96
98
  description="First day on which the document is valid",
97
99
  )
98
- valid_until: datetime.datetime | None = pyd.Field(
99
- example=datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc),
100
+ valid_until: datetime.datetime | None = pydantic.Field(
101
+ examples=[datetime.datetime(day=11, month=9, year=2019, tzinfo=datetime.timezone.utc)],
100
102
  default=None,
101
103
  description="Last day on which the document is valid",
102
104
  )
103
- doc: str | None = pyd.Field(default=None, description="URL to original document, preferably directly to a PDF.")
104
- parent: Optional["Pcr"] = pyd.Field(
105
+ doc: str | None = pydantic.Field(
106
+ default=None,
107
+ description="URL to original document, preferably directly to a PDF.",
108
+ )
109
+ parent: Optional["Pcr"] = pydantic.Field(
105
110
  description="The parent PCR, base PCR, `Part A` PCR",
106
111
  default=None,
107
112
  )
108
- status: PcrStatus | None = pyd.Field(
113
+ status: PcrStatus | None = pydantic.Field(
109
114
  default=None,
110
115
  description="The current release status of this PCR. "
111
116
  "A PCR with valid_until in the past must have status Expired or Sunset; a PCR with valid_until "
112
117
  "more than 5 years in the past must have status Sunset. Compliant systems should automatically "
113
118
  "update these fields within 24 hours.",
114
119
  )
115
- product_classes: dict[str, str | list[str]] = pyd.Field(
116
- description="List of classifications, including Masterformat and UNSPC", default_factory=dict
120
+ product_classes: dict[str, str | list[str]] = pydantic.Field(
121
+ description="List of classifications, including Masterformat and UNSPC",
122
+ default_factory=dict,
117
123
  )
118
- applicable_in: list[Annotated[str, pyd.Field(min_length=2, max_length=2)]] | None = pyd.Field(
119
- max_items=100,
124
+ applicable_in: list[Annotated[str, pydantic.Field(min_length=2, max_length=2)]] | None = pydantic.Field(
125
+ max_length=100,
120
126
  default=None,
121
127
  description="Jurisdiction(s) in which EPD is applicable. An empty array, or absent properties, "
122
128
  "implies global applicability. Accepts "
@@ -124,7 +130,7 @@ class Pcr(WithAttachmentsMixin, WithAltIdsMixin, BaseOpenEpdSchema):
124
130
  "[M49 region codes](https://unstats.un.org/unsd/methodology/m49/), "
125
131
  'or the alias "EU27" for the 27 members of the Euro bloc, or the alias "NAFTA" '
126
132
  "for the members of North American Free Trade Agreement",
127
- example=["US", "CA", "MX", "EU27", "NAFTA"],
133
+ examples=[["US", "CA", "MX", "EU27", "NAFTA"]],
128
134
  )
129
135
 
130
136
  @classmethod
@@ -15,10 +15,10 @@
15
15
  #
16
16
  from enum import StrEnum
17
17
 
18
- from openepd.compat.pydantic import pyd
18
+ import pydantic
19
+
19
20
  from openepd.model.common import OpenEPDUnit
20
21
  from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
21
- from openepd.model.validation.numbers import RatioFloat
22
22
  from openepd.model.validation.quantity import LengthMmStr, TemperatureCStr, validate_quantity_unit_factory
23
23
 
24
24
 
@@ -45,42 +45,51 @@ class AsphaltV1(BaseOpenEpdHierarchicalSpec):
45
45
 
46
46
  _EXT_VERSION = "1.1"
47
47
 
48
- asphalt_aggregate_size_max: LengthMmStr | None = pyd.Field(
49
- default=None, example="5mm", description="Max aggregate size"
48
+ asphalt_aggregate_size_max: LengthMmStr | None = pydantic.Field(
49
+ default=None, examples=["5mm"], description="Max aggregate size"
50
50
  )
51
51
 
52
- asphalt_rap: RatioFloat | None = pyd.Field(
53
- default=None, description="Percent of mixture that has been replaced by recycled " "asphalt pavement (RAP)."
52
+ asphalt_rap: float | None = pydantic.Field(
53
+ default=None,
54
+ description="Percent of mixture that has been replaced by recycled " "asphalt pavement (RAP).",
55
+ ge=0,
56
+ le=1,
54
57
  )
55
- asphalt_ras: RatioFloat | None = pyd.Field(
56
- default=None, description="Percent of mixture that has been replaced by recycled " "asphalt shingles (RAS)."
58
+ asphalt_ras: float | None = pydantic.Field(
59
+ default=None,
60
+ description="Percent of mixture that has been replaced by recycled " "asphalt shingles (RAS).",
61
+ ge=0,
62
+ le=1,
57
63
  )
58
- asphalt_ground_tire_rubber: RatioFloat | None = pyd.Field(
59
- default=None, description="Percent of mixture that has been replaced " "by ground tire rubber (GTR)."
64
+ asphalt_ground_tire_rubber: float | None = pydantic.Field(
65
+ default=None,
66
+ description="Percent of mixture that has been replaced " "by ground tire rubber (GTR).",
67
+ ge=0,
68
+ le=1,
60
69
  )
61
70
 
62
- asphalt_max_temperature: TemperatureCStr | None = pyd.Field(
71
+ asphalt_max_temperature: TemperatureCStr | None = pydantic.Field(
63
72
  default=None,
64
73
  description="The upper threshold temperature to which an asphalt "
65
74
  "binder can be heated preventing the asphalt mixture "
66
75
  "from rutting",
67
76
  )
68
- asphalt_min_temperature: TemperatureCStr | None = pyd.Field(
77
+ asphalt_min_temperature: TemperatureCStr | None = pydantic.Field(
69
78
  default=None,
70
79
  description="The lower threshold temperature for an asphalt "
71
80
  "binder to prevent thermal cracking of the asphalt"
72
81
  " mixture.",
73
82
  )
74
83
 
75
- asphalt_mix_type: AsphaltMixType | None = pyd.Field(default=None, description="Asphalt mix type")
76
- asphalt_gradation: AsphaltGradation | None = pyd.Field(default=None, description="Asphalt gradation")
84
+ asphalt_mix_type: AsphaltMixType | None = pydantic.Field(default=None, description="Asphalt mix type")
85
+ asphalt_gradation: AsphaltGradation | None = pydantic.Field(default=None, description="Asphalt gradation")
77
86
 
78
- asphalt_sbr: bool | None = pyd.Field(default=None, description="Styrene-butadiene rubber (SBR)")
79
- asphalt_sbs: bool | None = pyd.Field(default=None, description="Styrene-butadiene-styrene (SBS)")
80
- asphalt_ppa: bool | None = pyd.Field(default=None, description="Polyphosphoric acid (PPA)")
81
- asphalt_gtr: bool | None = pyd.Field(default=None, description="Ground tire rubber (GTR)")
82
- asphalt_pmb: bool | None = pyd.Field(default=None, description="Polymer modified bitumen (PMB)")
87
+ asphalt_sbr: bool | None = pydantic.Field(default=None, description="Styrene-butadiene rubber (SBR)")
88
+ asphalt_sbs: bool | None = pydantic.Field(default=None, description="Styrene-butadiene-styrene (SBS)")
89
+ asphalt_ppa: bool | None = pydantic.Field(default=None, description="Polyphosphoric acid (PPA)")
90
+ asphalt_gtr: bool | None = pydantic.Field(default=None, description="Ground tire rubber (GTR)")
91
+ asphalt_pmb: bool | None = pydantic.Field(default=None, description="Polymer modified bitumen (PMB)")
83
92
 
84
- _aggregate_size_max_validator = pyd.validator("asphalt_aggregate_size_max", allow_reuse=True)(
85
- validate_quantity_unit_factory(OpenEPDUnit.m)
86
- )
93
+ @pydantic.field_validator("asphalt_aggregate_size_max", mode="before")
94
+ def _aggregate_size_max_validator(cls, value):
95
+ return validate_quantity_unit_factory(OpenEPDUnit.m)(cls, value)
@@ -17,23 +17,25 @@ import dataclasses
17
17
  from types import UnionType
18
18
  from typing import Any
19
19
 
20
- from openepd.compat.pydantic import pyd
21
- from openepd.model.base import BaseOpenEpdSchema, Version
20
+ import pydantic
21
+ from pydantic import ConfigDict
22
+
23
+ from openepd.model.base import BaseOpenEpdSchema
22
24
  from openepd.model.validation.common import validate_version_compatibility, validate_version_format
23
25
  from openepd.model.validation.quantity import ExternalValidationConfig, QuantityValidator
24
- from openepd.model.versioning import WithExtVersionMixin
26
+ from openepd.model.versioning import Version, WithExtVersionMixin
25
27
 
26
28
 
27
29
  class BaseOpenEpdSpec(BaseOpenEpdSchema):
28
30
  """Base class for all OpenEPD specs."""
29
31
 
30
- class Config:
31
- use_enum_values = False # we need to store enums as strings and not values
32
+ model_config = ConfigDict(use_enum_values=False)
32
33
 
33
34
 
34
35
  class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
35
36
  """Base class for new specs (hierarchical, versioned)."""
36
37
 
38
+ # TODO: Refactor work with class-based and instance-based _EXT_VERSION
37
39
  def __init__(self, **data: Any) -> None:
38
40
  # ensure that all the concrete spec objects fail on creations if they dont have _EXT_VERSION declared to
39
41
  # something meaningful
@@ -42,12 +44,13 @@ class BaseOpenEpdHierarchicalSpec(BaseOpenEpdSpec, WithExtVersionMixin):
42
44
  Version.parse_version(self._EXT_VERSION) # validate format correctness
43
45
  super().__init__(**{"ext_version": self._EXT_VERSION, **data})
44
46
 
45
- _version_format_validator = pyd.validator("ext_version", allow_reuse=True, check_fields=False)(
46
- validate_version_format
47
- )
48
- _version_major_match_validator = pyd.validator("ext_version", allow_reuse=True, check_fields=False)(
49
- validate_version_compatibility("_EXT_VERSION")
50
- )
47
+ @pydantic.field_validator("ext_version", mode="before")
48
+ def _version_format_validator(cls, v: str) -> str:
49
+ return validate_version_format(v)
50
+
51
+ @pydantic.field_validator("ext_version", mode="before")
52
+ def _version_major_match_validator(cls, v: str) -> str:
53
+ return validate_version_compatibility("_EXT_VERSION")(cls, v) # type: ignore[arg-type]
51
54
 
52
55
 
53
56
  def setup_external_validators(quantity_validator: QuantityValidator):
@@ -13,98 +13,119 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
+ import pydantic
16
17
 
17
- from openepd.compat.pydantic import pyd
18
18
  from openepd.model.base import BaseOpenEpdSchema
19
19
  from openepd.model.specs.base import BaseOpenEpdSpec
20
- from openepd.model.validation.numbers import RatioFloat
21
20
 
22
21
 
23
22
  class ConcreteTypicalApplication(BaseOpenEpdSpec):
24
23
  """Typical Application for Concrete."""
25
24
 
26
- fnd: bool | None = pyd.Field(
25
+ fnd: bool | None = pydantic.Field(
27
26
  description="Foundation. Typically used in direct contact with soil, e.g. footings, piles, mass concrete, "
28
27
  "mat foundations, and similar applications.",
29
- example=True,
28
+ examples=[True],
30
29
  default=None,
31
30
  )
32
- sog: bool | None = pyd.Field(
31
+ sog: bool | None = pydantic.Field(
33
32
  description="Slab on Grade. Typically used in continuously supported horizontal "
34
33
  "applications e.g. slab on grade, topping slabs, sidewalks, and roadways.",
35
- example=True,
34
+ examples=[True],
36
35
  default=None,
37
36
  )
38
- hrz: bool | None = pyd.Field(
37
+ hrz: bool | None = pydantic.Field(
39
38
  description="Elevated Horizontal. Typically used in elevated horizontal applications, either on metal deck or "
40
39
  "where soffit formwork must be removed, e.g. post-tension plates, rebar plates, beams and slabs, "
41
40
  "waffle slabs.",
42
- example=True,
41
+ examples=[True],
43
42
  default=None,
44
43
  )
45
- vrt_wall: bool | None = pyd.Field(description="Vertical Wall.", example=True, default=None)
46
- vrt_column: bool | None = pyd.Field(description="Vertical Column.", example=True, default=None)
47
- vrt_other: bool | None = pyd.Field(
44
+ vrt_wall: bool | None = pydantic.Field(description="Vertical Wall.", examples=[True], default=None)
45
+ vrt_column: bool | None = pydantic.Field(description="Vertical Column.", examples=[True], default=None)
46
+ vrt_other: bool | None = pydantic.Field(
48
47
  description="Vertical Other. Typically used in vertical applications other than "
49
48
  "walls or columns, e.g. sloped surfaces where formwork is required "
50
49
  "on multiple faces.",
51
- example=True,
50
+ examples=[True],
52
51
  default=None,
53
52
  )
54
- sht: bool | None = pyd.Field(
55
- description="Shotcrete. Pneumatically applied, without formwork on all sides.", example=True, default=None
53
+ sht: bool | None = pydantic.Field(
54
+ description="Shotcrete. Pneumatically applied, without formwork on all sides.",
55
+ examples=[True],
56
+ default=None,
56
57
  )
57
- cdf: bool | None = pyd.Field(
58
+ cdf: bool | None = pydantic.Field(
58
59
  description="Flowable Fill (CDF). Typically used to fill voids, backfill retaining "
59
60
  "walls, as a sub-base, and similar applications. Also called Controlled "
60
61
  "Density Fill (CDF) or Controlled Low Strength Materials (CLSM).",
61
- example=True,
62
+ examples=[True],
62
63
  default=None,
63
64
  )
64
- sac: bool | None = pyd.Field(
65
- description="Typically used in concrete sidewalks and barrier curbs.", example=True, default=None
65
+ sac: bool | None = pydantic.Field(
66
+ description="Typically used in concrete sidewalks and barrier curbs.",
67
+ examples=[True],
68
+ default=None,
66
69
  )
67
- pav: bool | None = pyd.Field(description="Typically used in pervious concrete", example=True, default=None)
68
- oil: bool | None = pyd.Field(
70
+ pav: bool | None = pydantic.Field(description="Typically used in pervious concrete", examples=[True], default=None)
71
+ oil: bool | None = pydantic.Field(
69
72
  description="Concretes for use in creation, maintenance, and decommissioning of "
70
73
  "petroleum extraction wells and similar applications. Includes foamed "
71
74
  "cement; often called cement in the drilling industry. Differs from "
72
75
  "flowable fill and grout in that it contains no sand or other aggregates.",
73
- example=True,
76
+ examples=[True],
74
77
  default=None,
75
78
  )
76
- grt: bool | None = pyd.Field(
79
+ grt: bool | None = pydantic.Field(
77
80
  description="Cement grouting is a slurry that is placed as a flowable liquid. It is "
78
81
  "an effective material for filling and strengthening granular soils, "
79
82
  "voids in rocks, foundation underpinnings, and other underground voids. "
80
83
  "Also called structural grout, these materials typically impart"
81
84
  " significant strength to the system",
82
- example=True,
85
+ examples=[True],
83
86
  default=None,
84
87
  )
85
- ota: bool | None = pyd.Field(
86
- description="Typical application not covered by other values.", example=True, default=None
88
+ ota: bool | None = pydantic.Field(
89
+ description="Typical application not covered by other values.",
90
+ examples=[True],
91
+ default=None,
87
92
  )
88
93
 
89
94
 
90
95
  class Cementitious(BaseOpenEpdSchema):
91
96
  """List of cementitious materials, and proportion by mass."""
92
97
 
93
- opc: RatioFloat | None = pyd.Field(
94
- default=None, description="Ordinary Gray Portland Cement", example=0.5, ge=0, le=1
98
+ opc: float | None = pydantic.Field(
99
+ default=None,
100
+ description="Ordinary Gray Portland Cement",
101
+ examples=[0.5],
102
+ ge=0,
103
+ le=1,
95
104
  )
96
- wht: RatioFloat | None = pyd.Field(default=None, description="White Portland Cement", example=0.5, ge=0, le=1)
97
- ggbs: RatioFloat | None = pyd.Field(
98
- default=None, description="Ground Granulated Blast Furnace Slag", example=0.5, ge=0, le=1
105
+ wht: float | None = pydantic.Field(default=None, description="White Portland Cement", examples=[0.5], ge=0, le=1)
106
+ ggbs: float | None = pydantic.Field(
107
+ default=None,
108
+ description="Ground Granulated Blast Furnace Slag",
109
+ examples=[0.5],
110
+ ge=0,
111
+ le=1,
99
112
  )
100
- flyAsh: RatioFloat | None = pyd.Field(
101
- default=None, description="Fly Ash, including types F, CL, and CH", example=0.5, ge=0, le=1
113
+ flyAsh: float | None = pydantic.Field(
114
+ default=None,
115
+ description="Fly Ash, including types F, CL, and CH",
116
+ examples=[0.5],
117
+ ge=0,
118
+ le=1,
102
119
  )
103
- siFume: RatioFloat | None = pyd.Field(default=None, description="Silica Fume", example=0.5, ge=0, le=1)
104
- gg45: RatioFloat | None = pyd.Field(
105
- default=None, description="Ground Glass, 45um or smaller", example=0.5, ge=0, le=1
120
+ siFume: float | None = pydantic.Field(default=None, description="Silica Fume", examples=[0.5], ge=0, le=1)
121
+ gg45: float | None = pydantic.Field(
122
+ default=None,
123
+ description="Ground Glass, 45um or smaller",
124
+ examples=[0.5],
125
+ ge=0,
126
+ le=1,
106
127
  )
107
- natPoz: RatioFloat | None = pyd.Field(default=None, description="Natural pozzolan", example=0.5, ge=0, le=1)
108
- mk: RatioFloat | None = pyd.Field(default=None, description="Metakaolin", example=0.5, ge=0, le=1)
109
- CaCO3: RatioFloat | None = pyd.Field(default=None, description="Limestone", example=0.5, ge=0, le=1)
110
- other: RatioFloat | None = pyd.Field(default=None, description="Other SCMs", example=0.5, ge=0, le=1)
128
+ natPoz: float | None = pydantic.Field(default=None, description="Natural pozzolan", examples=[0.5], ge=0, le=1)
129
+ mk: float | None = pydantic.Field(default=None, description="Metakaolin", examples=[0.5], ge=0, le=1)
130
+ CaCO3: float | None = pydantic.Field(default=None, description="Limestone", examples=[0.5], ge=0, le=1)
131
+ other: float | None = pydantic.Field(default=None, description="Other SCMs", examples=[0.5], ge=0, le=1)