openepd 5.1.1__py3-none-any.whl → 5.3.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 +1 -1
- openepd/api/average_dataset/generic_estimate_sync_api.py +12 -5
- openepd/api/epd/sync_api.py +14 -5
- openepd/api/utils.py +40 -0
- openepd/model/common.py +43 -1
- openepd/model/specs/generated/concrete.py +11 -0
- openepd/model/specs/generated/enums.py +51 -3
- openepd/model/validation/enum.py +42 -0
- {openepd-5.1.1.dist-info → openepd-5.3.0.dist-info}/METADATA +1 -1
- {openepd-5.1.1.dist-info → openepd-5.3.0.dist-info}/RECORD +12 -11
- {openepd-5.1.1.dist-info → openepd-5.3.0.dist-info}/LICENSE +0 -0
- {openepd-5.1.1.dist-info → openepd-5.3.0.dist-info}/WHEEL +0 -0
    
        openepd/__version__.py
    CHANGED
    
    
| @@ -21,7 +21,7 @@ from openepd.api.base_sync_client import BaseApiMethodGroup | |
| 21 21 | 
             
            from openepd.api.common import StreamingListResponse, paging_meta_from_v1_api
         | 
| 22 22 | 
             
            from openepd.api.dto.common import BaseMeta, OpenEpdApiResponse
         | 
| 23 23 | 
             
            from openepd.api.dto.meta import PagingMetaMixin
         | 
| 24 | 
            -
            from openepd.api.utils import encode_path_param
         | 
| 24 | 
            +
            from openepd.api.utils import encode_path_param, remove_none_id_fields
         | 
| 25 25 | 
             
            from openepd.model.generic_estimate import (
         | 
| 26 26 | 
             
                GenericEstimate,
         | 
| 27 27 | 
             
                GenericEstimatePreview,
         | 
| @@ -57,26 +57,33 @@ class GenericEstimateApi(BaseApiMethodGroup): | |
| 57 57 |  | 
| 58 58 | 
             
                @overload
         | 
| 59 59 | 
             
                def post_with_refs(
         | 
| 60 | 
            -
                    self, ge: GenericEstimateWithDeps, with_response: Literal[True]
         | 
| 60 | 
            +
                    self, ge: GenericEstimateWithDeps, with_response: Literal[True], exclude_defaults: bool = True
         | 
| 61 61 | 
             
                ) -> tuple[GenericEstimate, Response]: ...
         | 
| 62 62 |  | 
| 63 63 | 
             
                @overload
         | 
| 64 | 
            -
                def post_with_refs( | 
| 64 | 
            +
                def post_with_refs(
         | 
| 65 | 
            +
                    self, ge: GenericEstimateWithDeps, with_response: Literal[False] = False, exclude_defaults: bool = True
         | 
| 66 | 
            +
                ) -> GenericEstimate: ...
         | 
| 65 67 |  | 
| 66 68 | 
             
                def post_with_refs(
         | 
| 67 | 
            -
                    self, ge: GenericEstimateWithDeps, with_response: bool = False
         | 
| 69 | 
            +
                    self, ge: GenericEstimateWithDeps, with_response: bool = False, exclude_defaults: bool = True
         | 
| 68 70 | 
             
                ) -> GenericEstimate | tuple[GenericEstimate, Response]:
         | 
| 69 71 | 
             
                    """
         | 
| 70 72 | 
             
                    Post an GenericEstimate with references.
         | 
| 71 73 |  | 
| 72 74 | 
             
                    :param ge: GenericEstimate
         | 
| 73 75 | 
             
                    :param with_response: return the response object togather with the GenericEstimate
         | 
| 76 | 
            +
                    :param exclude_defaults: If True, fields with default values are excluded from the payload
         | 
| 74 77 | 
             
                    :return: GenericEstimate alone, or GenericEstimate with HTTP Response object depending on parameter
         | 
| 75 78 | 
             
                    """
         | 
| 79 | 
            +
                    data = ge.to_serializable(exclude_unset=True, exclude_defaults=exclude_defaults, by_alias=True)
         | 
| 80 | 
            +
                    if exclude_defaults is False:
         | 
| 81 | 
            +
                        # Remove 'id' fields with None values, as 'id' cannot be None
         | 
| 82 | 
            +
                        data = remove_none_id_fields(data)
         | 
| 76 83 | 
             
                    response = self._client.do_request(
         | 
| 77 84 | 
             
                        "patch",
         | 
| 78 85 | 
             
                        "/generic_estimates/post_with_refs",
         | 
| 79 | 
            -
                        json= | 
| 86 | 
            +
                        json=data,
         | 
| 80 87 | 
             
                    )
         | 
| 81 88 | 
             
                    content = response.json()
         | 
| 82 89 | 
             
                    if with_response:
         | 
    
        openepd/api/epd/sync_api.py
    CHANGED
    
    | @@ -20,7 +20,7 @@ from requests import Response | |
| 20 20 | 
             
            from openepd.api.base_sync_client import BaseApiMethodGroup
         | 
| 21 21 | 
             
            from openepd.api.common import StreamingListResponse
         | 
| 22 22 | 
             
            from openepd.api.epd.dto import EpdSearchResponse, EpdStatisticsResponse, StatisticsDto
         | 
| 23 | 
            -
            from openepd.api.utils import encode_path_param
         | 
| 23 | 
            +
            from openepd.api.utils import encode_path_param, remove_none_id_fields
         | 
| 24 24 | 
             
            from openepd.model.epd import Epd
         | 
| 25 25 |  | 
| 26 26 |  | 
| @@ -106,23 +106,32 @@ class EpdApi(BaseApiMethodGroup): | |
| 106 106 | 
             
                    return self.get_statistics_raw(omf).payload
         | 
| 107 107 |  | 
| 108 108 | 
             
                @overload
         | 
| 109 | 
            -
                def post_with_refs( | 
| 109 | 
            +
                def post_with_refs(
         | 
| 110 | 
            +
                    self, epd: Epd, with_response: Literal[True], exclude_defaults: bool = True
         | 
| 111 | 
            +
                ) -> tuple[Epd, Response]: ...
         | 
| 110 112 |  | 
| 111 113 | 
             
                @overload
         | 
| 112 | 
            -
                def post_with_refs(self, epd: Epd, with_response: Literal[False] = False) -> Epd: ...
         | 
| 114 | 
            +
                def post_with_refs(self, epd: Epd, with_response: Literal[False] = False, exclude_defaults: bool = True) -> Epd: ...
         | 
| 113 115 |  | 
| 114 | 
            -
                def post_with_refs( | 
| 116 | 
            +
                def post_with_refs(
         | 
| 117 | 
            +
                    self, epd: Epd, with_response: bool = False, exclude_defaults: bool = True
         | 
| 118 | 
            +
                ) -> Epd | tuple[Epd, Response]:
         | 
| 115 119 | 
             
                    """
         | 
| 116 120 | 
             
                    Post an EPD with references.
         | 
| 117 121 |  | 
| 118 122 | 
             
                    :param epd: EPD
         | 
| 119 123 | 
             
                    :param with_response: return the response object togather with the EPD
         | 
| 124 | 
            +
                    :param exclude_defaults: If True, fields with default values are excluded from the payload
         | 
| 120 125 | 
             
                    :return: EPD or EPD with HTTP Response object depending on parameter
         | 
| 121 126 | 
             
                    """
         | 
| 127 | 
            +
                    epd_data = epd.to_serializable(exclude_unset=True, exclude_defaults=exclude_defaults, by_alias=True)
         | 
| 128 | 
            +
                    if exclude_defaults is False:
         | 
| 129 | 
            +
                        # Remove 'id' fields with None values, as 'id' cannot be None
         | 
| 130 | 
            +
                        epd_data = remove_none_id_fields(epd_data)
         | 
| 122 131 | 
             
                    response = self._client.do_request(
         | 
| 123 132 | 
             
                        "patch",
         | 
| 124 133 | 
             
                        "/epds/post-with-refs",
         | 
| 125 | 
            -
                        json= | 
| 134 | 
            +
                        json=epd_data,
         | 
| 126 135 | 
             
                    )
         | 
| 127 136 | 
             
                    content = response.json()
         | 
| 128 137 | 
             
                    if with_response:
         | 
    
        openepd/api/utils.py
    CHANGED
    
    | @@ -26,3 +26,43 @@ def encode_path_param(value: str) -> str: | |
| 26 26 | 
             
                :return: encoded value
         | 
| 27 27 | 
             
                """
         | 
| 28 28 | 
             
                return quote(value, safe="")
         | 
| 29 | 
            +
             | 
| 30 | 
            +
             | 
| 31 | 
            +
            def remove_none_id_fields(d: dict) -> dict:
         | 
| 32 | 
            +
                """
         | 
| 33 | 
            +
                Remove any key 'id' with a None value from the dictionary, including nested dicts.
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                :param d: the dict which may contain 'id' keys with None values.
         | 
| 36 | 
            +
                :return: a new dict with 'id' keys that have None values removed.
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                :note:
         | 
| 39 | 
            +
                    - This function does not modify the original dictionary (no side effects).
         | 
| 40 | 
            +
                    - It returns a new dictionary with the necessary modifications applied.
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                :example:
         | 
| 43 | 
            +
                    >>> data = {
         | 
| 44 | 
            +
                    ...     "id": None,
         | 
| 45 | 
            +
                    ...     "name": "item1",
         | 
| 46 | 
            +
                    ...     "details": {
         | 
| 47 | 
            +
                    ...         "id": None,
         | 
| 48 | 
            +
                    ...         "category": "tools",
         | 
| 49 | 
            +
                    ...         "nested": {
         | 
| 50 | 
            +
                    ...             "id": None,
         | 
| 51 | 
            +
                    ...             "value": 42
         | 
| 52 | 
            +
                    ...         }
         | 
| 53 | 
            +
                    ...     }
         | 
| 54 | 
            +
                    ... }
         | 
| 55 | 
            +
                    >>> remove_none_id_fields(data)
         | 
| 56 | 
            +
                    {'name': 'item1', 'details': {'category': 'tools', 'nested': {'value': 42}}}
         | 
| 57 | 
            +
                """
         | 
| 58 | 
            +
                if not isinstance(d, dict):
         | 
| 59 | 
            +
                    return d
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                cleaned_dict = {}
         | 
| 62 | 
            +
                for k, v in d.items():
         | 
| 63 | 
            +
                    if isinstance(v, dict):
         | 
| 64 | 
            +
                        v = remove_none_id_fields(v)
         | 
| 65 | 
            +
                    if not (k == "id" and v is None):
         | 
| 66 | 
            +
                        cleaned_dict[k] = v
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                return cleaned_dict
         | 
    
        openepd/model/common.py
    CHANGED
    
    | @@ -38,6 +38,32 @@ class Amount(BaseOpenEpdSchema): | |
| 38 38 | 
             
                    return f"{self.qty or ''} {self.unit or 'str'}".strip()
         | 
| 39 39 |  | 
| 40 40 |  | 
| 41 | 
            +
            class Distribution(StrEnum):
         | 
| 42 | 
            +
                """
         | 
| 43 | 
            +
                Distribution of the measured value.
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                 * log-normal: Probability distribution of any random parameter whose natural log is normally distributed (the
         | 
| 46 | 
            +
                   PDF is gaussian).
         | 
| 47 | 
            +
                 * normal: Probability distribution of any random parameter whose value is normally distributed around the mean
         | 
| 48 | 
            +
                   (the PDF is gaussian).
         | 
| 49 | 
            +
                 * Continuous uniform probability distribution between minimum value and maximum value and "0" probability beyond
         | 
| 50 | 
            +
                   these.
         | 
| 51 | 
            +
                 * Probability distribution of any random parameter between minimum value and maximum value with the highest
         | 
| 52 | 
            +
                   probability at the average value of minimum plus maximum value. Linear change of probability between minimum,
         | 
| 53 | 
            +
                   maximum and average value.
         | 
| 54 | 
            +
                 * Means Impact is not known, but with >95% certainty the true value is below the declared value.
         | 
| 55 | 
            +
                   So [1e-6,"kgCFC11e",0,"max"] means the ODP was not exactly measured, but it is guaranteed to be below
         | 
| 56 | 
            +
                   1E-6 kg CO2e.  It is acceptable to treat a 'max' distribution a normal or lognormal distribution with variation
         | 
| 57 | 
            +
                   0.1%.  This is conservative, because the 'max' value is usually much greater than the true impact.
         | 
| 58 | 
            +
                """
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                LOG_NORMAL = "log-normal"
         | 
| 61 | 
            +
                NORMAL = "normal"
         | 
| 62 | 
            +
                UNIFORM = "uniform"
         | 
| 63 | 
            +
                TRIANGULAR = "triangular"
         | 
| 64 | 
            +
                MAX = "max"
         | 
| 65 | 
            +
             | 
| 66 | 
            +
             | 
| 41 67 | 
             
            class Measurement(BaseOpenEpdSchema):
         | 
| 42 68 | 
             
                """A scientific value with units and uncertainty."""
         | 
| 43 69 |  | 
| @@ -46,7 +72,9 @@ class Measurement(BaseOpenEpdSchema): | |
| 46 72 | 
             
                rsd: pyd.PositiveFloat | None = pyd.Field(
         | 
| 47 73 | 
             
                    description="Relative standard deviation, i.e. standard_deviation/mean", default=None
         | 
| 48 74 | 
             
                )
         | 
| 49 | 
            -
                dist:  | 
| 75 | 
            +
                dist: Distribution | None = pyd.Field(
         | 
| 76 | 
            +
                    description="Statistical distribution of the measurement error.", default=None
         | 
| 77 | 
            +
                )
         | 
| 50 78 |  | 
| 51 79 |  | 
| 52 80 | 
             
            class Ingredient(BaseOpenEpdSchema):
         | 
| @@ -189,3 +217,17 @@ class RangeAmount(RangeFloat): | |
| 189 217 | 
             
                """Structure representing a range of quantities."""
         | 
| 190 218 |  | 
| 191 219 | 
             
                unit: str | None = pyd.Field(default=None, description="Unit of the range.")
         | 
| 220 | 
            +
             | 
| 221 | 
            +
             | 
| 222 | 
            +
            class EnumGroupingAware:
         | 
| 223 | 
            +
                """
         | 
| 224 | 
            +
                Mixin for enums to support groups.
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                With the groups, enum can group its values into more than one groups, so that the validator higher in code can check
         | 
| 227 | 
            +
                for mutual exclusivity, for example that only one value from the group is permitted at the same time.
         | 
| 228 | 
            +
                """
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                @classmethod
         | 
| 231 | 
            +
                def get_groupings(cls) -> list[list]:
         | 
| 232 | 
            +
                    """Return logical groupings of the values."""
         | 
| 233 | 
            +
                    return []
         | 
| @@ -19,6 +19,7 @@ from openepd.compat.pydantic import pyd | |
| 19 19 | 
             
            from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec, CodegenSpec
         | 
| 20 20 | 
             
            from openepd.model.specs.concrete import Cementitious, ConcreteTypicalApplication
         | 
| 21 21 | 
             
            from openepd.model.specs.generated.enums import AciExposureClass, CsaExposureClass, EnExposureClass
         | 
| 22 | 
            +
            from openepd.model.validation.enum import exclusive_groups_validator_factory
         | 
| 22 23 | 
             
            from openepd.model.validation.numbers import RatioFloat
         | 
| 23 24 | 
             
            from openepd.model.validation.quantity import (
         | 
| 24 25 | 
             
                LengthInchStr,
         | 
| @@ -172,3 +173,13 @@ class ConcreteV1(BaseOpenEpdHierarchicalSpec): | |
| 172 173 | 
             
                OilPatch: OilPatchV1 | None = None
         | 
| 173 174 | 
             
                ReadyMix: ReadyMixV1 | None = None
         | 
| 174 175 | 
             
                Shotcrete: ShotcreteV1 | None = None
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                _aci_exposure_classes_exclusive_groups_validator = pyd.validator("aci_exposure_classes", allow_reuse=True)(
         | 
| 178 | 
            +
                    exclusive_groups_validator_factory(AciExposureClass)
         | 
| 179 | 
            +
                )
         | 
| 180 | 
            +
                _en_exposure_classes_exclusive_groups_validator = pyd.validator("en_exposure_classes", allow_reuse=True)(
         | 
| 181 | 
            +
                    exclusive_groups_validator_factory(EnExposureClass)
         | 
| 182 | 
            +
                )
         | 
| 183 | 
            +
                _csa_exposure_classes_exclusive_groups_validator = pyd.validator("csa_exposure_classes", allow_reuse=True)(
         | 
| 184 | 
            +
                    exclusive_groups_validator_factory(CsaExposureClass)
         | 
| 185 | 
            +
                )
         | 
| @@ -15,6 +15,8 @@ | |
| 15 15 | 
             
            #
         | 
| 16 16 | 
             
            from enum import StrEnum
         | 
| 17 17 |  | 
| 18 | 
            +
            from openepd.model.common import EnumGroupingAware
         | 
| 19 | 
            +
             | 
| 18 20 | 
             
            # Enums used
         | 
| 19 21 |  | 
| 20 22 |  | 
| @@ -2182,7 +2184,7 @@ class FloorBoxFloorMaterial(StrEnum): | |
| 2182 2184 | 
             
                OTHER = "Other"
         | 
| 2183 2185 |  | 
| 2184 2186 |  | 
| 2185 | 
            -
            class AciExposureClass(StrEnum):
         | 
| 2187 | 
            +
            class AciExposureClass(EnumGroupingAware, StrEnum):
         | 
| 2186 2188 | 
             
                """
         | 
| 2187 2189 | 
             
                American Concrete Institute concrete exposure classes.
         | 
| 2188 2190 |  | 
| @@ -2196,6 +2198,7 @@ class AciExposureClass(StrEnum): | |
| 2196 2198 | 
             
                  * `aci.S2` - Exposed to <10000 ppm of SO4 in water and <2% SO4 in soil
         | 
| 2197 2199 | 
             
                  * `aci.S3` - Exposed to >10000 ppm of SO4 in water or >2% SO4 in soil
         | 
| 2198 2200 |  | 
| 2201 | 
            +
                  * `aci.C0` - Concrete dry or protected from moisture.
         | 
| 2199 2202 | 
             
                  * `aci.C1` - Concrete in contact with moisture, but the external source of chloride does not reach it.
         | 
| 2200 2203 | 
             
                  * `aci.C2` - Concrete subjected to moisture and an external source of chlorides such as deicing chemicals,
         | 
| 2201 2204 | 
             
                                salt, brackish water, seawater, or spray from these sources.
         | 
| @@ -2212,14 +2215,25 @@ class AciExposureClass(StrEnum): | |
| 2212 2215 | 
             
                S1 = "aci.S1"
         | 
| 2213 2216 | 
             
                S2 = "aci.S2"
         | 
| 2214 2217 | 
             
                S3 = "aci.S3"
         | 
| 2218 | 
            +
                C0 = "aci.C1"
         | 
| 2215 2219 | 
             
                C1 = "aci.C1"
         | 
| 2216 2220 | 
             
                C2 = "aci.C2"
         | 
| 2217 2221 | 
             
                W0 = "aci.W0"
         | 
| 2218 2222 | 
             
                W1 = "aci.W1"
         | 
| 2219 2223 | 
             
                W2 = "aci.W2"
         | 
| 2220 2224 |  | 
| 2225 | 
            +
                @classmethod
         | 
| 2226 | 
            +
                def get_groupings(cls) -> list[list]:
         | 
| 2227 | 
            +
                    """Return logical groupings of the values."""
         | 
| 2228 | 
            +
                    return [
         | 
| 2229 | 
            +
                        [AciExposureClass.F0, AciExposureClass.F1, AciExposureClass.F2, AciExposureClass.F3],
         | 
| 2230 | 
            +
                        [AciExposureClass.S0, AciExposureClass.S1, AciExposureClass.S2, AciExposureClass.S3],
         | 
| 2231 | 
            +
                        [AciExposureClass.W0, AciExposureClass.W1],
         | 
| 2232 | 
            +
                        [AciExposureClass.C0, AciExposureClass.C1, AciExposureClass.C2],
         | 
| 2233 | 
            +
                    ]
         | 
| 2234 | 
            +
             | 
| 2221 2235 |  | 
| 2222 | 
            -
            class CsaExposureClass(StrEnum):
         | 
| 2236 | 
            +
            class CsaExposureClass(EnumGroupingAware, StrEnum):
         | 
| 2223 2237 | 
             
                """
         | 
| 2224 2238 | 
             
                Canadian Standard Association concrete exposure classes.
         | 
| 2225 2239 |  | 
| @@ -2286,8 +2300,20 @@ class CsaExposureClass(StrEnum): | |
| 2286 2300 | 
             
                A_3 = "csa.A-3"
         | 
| 2287 2301 | 
             
                A_4 = "csa.A-4"
         | 
| 2288 2302 |  | 
| 2303 | 
            +
                @classmethod
         | 
| 2304 | 
            +
                def get_groupings(cls) -> list[list]:
         | 
| 2305 | 
            +
                    """Return logical groupings of the values."""
         | 
| 2306 | 
            +
                    return [
         | 
| 2307 | 
            +
                        [CsaExposureClass.C_XL],
         | 
| 2308 | 
            +
                        [CsaExposureClass.C_1, CsaExposureClass.C_2, CsaExposureClass.C_3, CsaExposureClass.C_4],
         | 
| 2309 | 
            +
                        [CsaExposureClass.F_1, CsaExposureClass.F2],
         | 
| 2310 | 
            +
                        [CsaExposureClass.N],
         | 
| 2311 | 
            +
                        [CsaExposureClass.A_1, CsaExposureClass.A_2, CsaExposureClass.A_3, CsaExposureClass.A_4],
         | 
| 2312 | 
            +
                        [CsaExposureClass.S_1, CsaExposureClass.S_2, CsaExposureClass.S_3],
         | 
| 2313 | 
            +
                    ]
         | 
| 2289 2314 |  | 
| 2290 | 
            -
             | 
| 2315 | 
            +
             | 
| 2316 | 
            +
            class EnExposureClass(EnumGroupingAware, StrEnum):
         | 
| 2291 2317 | 
             
                """
         | 
| 2292 2318 | 
             
                EN 206 Class (Europe).
         | 
| 2293 2319 |  | 
| @@ -2348,6 +2374,28 @@ class EnExposureClass(StrEnum): | |
| 2348 2374 | 
             
                en206_XA2 = "en206.XA2"
         | 
| 2349 2375 | 
             
                en206_XA3 = "en206.XA3"
         | 
| 2350 2376 |  | 
| 2377 | 
            +
                @classmethod
         | 
| 2378 | 
            +
                def get_groupings(cls) -> list[list]:
         | 
| 2379 | 
            +
                    """Return logical groupings of the values."""
         | 
| 2380 | 
            +
                    return [
         | 
| 2381 | 
            +
                        [EnExposureClass.en206_X0],
         | 
| 2382 | 
            +
                        [
         | 
| 2383 | 
            +
                            EnExposureClass.en206_XC1,
         | 
| 2384 | 
            +
                            EnExposureClass.en206_XC2,
         | 
| 2385 | 
            +
                            EnExposureClass.en206_XC3,
         | 
| 2386 | 
            +
                            EnExposureClass.en206_XC4,
         | 
| 2387 | 
            +
                        ],
         | 
| 2388 | 
            +
                        [EnExposureClass.en206_XD1, EnExposureClass.en206_XD2, EnExposureClass.en206_XD3],
         | 
| 2389 | 
            +
                        [EnExposureClass.en206_XS1, EnExposureClass.en206_XS2, EnExposureClass.en206_XS3],
         | 
| 2390 | 
            +
                        [
         | 
| 2391 | 
            +
                            EnExposureClass.en206_XF1,
         | 
| 2392 | 
            +
                            EnExposureClass.en206_XF2,
         | 
| 2393 | 
            +
                            EnExposureClass.en206_XF3,
         | 
| 2394 | 
            +
                            EnExposureClass.en206_XF4,
         | 
| 2395 | 
            +
                        ],
         | 
| 2396 | 
            +
                        [EnExposureClass.en206_XA1, EnExposureClass.en206_XA2, EnExposureClass.en206_XA3],
         | 
| 2397 | 
            +
                    ]
         | 
| 2398 | 
            +
             | 
| 2351 2399 |  | 
| 2352 2400 | 
             
            class AggregateWeightClassification(StrEnum):
         | 
| 2353 2401 | 
             
                """
         | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            #
         | 
| 2 | 
            +
            #  Copyright 2024 by C Change Labs Inc. www.c-change-labs.com
         | 
| 3 | 
            +
            #
         | 
| 4 | 
            +
            #  Licensed under the Apache License, Version 2.0 (the "License");
         | 
| 5 | 
            +
            #  you may not use this file except in compliance with the License.
         | 
| 6 | 
            +
            #  You may obtain a copy of the License at
         | 
| 7 | 
            +
            #
         | 
| 8 | 
            +
            #     http://www.apache.org/licenses/LICENSE-2.0
         | 
| 9 | 
            +
            #
         | 
| 10 | 
            +
            #  Unless required by applicable law or agreed to in writing, software
         | 
| 11 | 
            +
            #  distributed under the License is distributed on an "AS IS" BASIS,
         | 
| 12 | 
            +
            #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         | 
| 13 | 
            +
            #  See the License for the specific language governing permissions and
         | 
| 14 | 
            +
            #  limitations under the License.
         | 
| 15 | 
            +
            #
         | 
| 16 | 
            +
            from typing import Any, Callable
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            from openepd.model.common import EnumGroupingAware
         | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| 21 | 
            +
            def exclusive_groups_validator_factory(enum_type: type[EnumGroupingAware]) -> Callable[[type, Any], Any]:
         | 
| 22 | 
            +
                """
         | 
| 23 | 
            +
                Create an exclusive groups validator.
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                If we have a certain enum TheEnum, and a field of list[TheEnum], where list can contain only one value of each of
         | 
| 26 | 
            +
                the groups, this validator should be used. For example, ACI exposure classes for concrete specify various
         | 
| 27 | 
            +
                parameters such as water resistance, chemical resistance, etc., and the list of classes can have 0 or 1 from each
         | 
| 28 | 
            +
                group.
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                :param enum_type:Enum type which supports groupings.
         | 
| 31 | 
            +
                :return:value, or raises ValueError if not allowed combination is given.
         | 
| 32 | 
            +
                """
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def enum_exclusive_grouping_validator(cls, value: list | None) -> list | None:
         | 
| 35 | 
            +
                    for grouping in enum_type.get_groupings():
         | 
| 36 | 
            +
                        matching_from_group = [v for v in (value or []) if v in grouping]
         | 
| 37 | 
            +
                        if len(matching_from_group) > 1:
         | 
| 38 | 
            +
                            raise ValueError(f"Values {', '.join(matching_from_group)} are not allowed together.")
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    return value
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                return enum_exclusive_grouping_validator
         | 
| @@ -1,8 +1,8 @@ | |
| 1 1 | 
             
            openepd/__init__.py,sha256=Shkfh0Kun0YRhmRDw7LkUj2eQL3X-HnP55u2THOEALw,794
         | 
| 2 | 
            -
            openepd/__version__.py,sha256= | 
| 2 | 
            +
            openepd/__version__.py,sha256=eEVqmdbsGDJ4vyFA7va4C2zZ4rWy5ZZzKevZvLZTPtQ,638
         | 
| 3 3 | 
             
            openepd/api/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 4 4 | 
             
            openepd/api/average_dataset/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 5 | 
            -
            openepd/api/average_dataset/generic_estimate_sync_api.py,sha256= | 
| 5 | 
            +
            openepd/api/average_dataset/generic_estimate_sync_api.py,sha256=5dWtdyzs9WGO5-liQGZd0o5q0tSjoJrx1RCtjSnRrrk,8110
         | 
| 6 6 | 
             
            openepd/api/average_dataset/industry_epd_sync_api.py,sha256=xoicuZdclf4BTBa2ndG1K2Aog-iZd7_jOqdDPZSCLq4,6286
         | 
| 7 7 | 
             
            openepd/api/base_sync_client.py,sha256=IX-q6JdWLMqYHC-hKLXUFRPs-DZZ9Co0NRZ5pSAipAs,20992
         | 
| 8 8 | 
             
            openepd/api/category/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| @@ -17,13 +17,13 @@ openepd/api/dto/mf.py,sha256=tjsxF6yJhnOrcOepNVHBn18pF-kXkBoUYfTQwOTaY2g,1994 | |
| 17 17 | 
             
            openepd/api/dto/params.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 18 18 | 
             
            openepd/api/epd/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 19 19 | 
             
            openepd/api/epd/dto.py,sha256=NZ76vfUkCEkwDibQd2QCEQP5DZms_NFc9tjoP-mcw3o,4874
         | 
| 20 | 
            -
            openepd/api/epd/sync_api.py,sha256= | 
| 20 | 
            +
            openepd/api/epd/sync_api.py,sha256=EY9KVz8yDgmjgLzWWh2B3VobNswd9fZmfDFkiJTxog0,7522
         | 
| 21 21 | 
             
            openepd/api/errors.py,sha256=Iipd0QW2tbhUu_UjUbmiFWgmrPVgOFHc3uxZN-SX-g4,2868
         | 
| 22 22 | 
             
            openepd/api/pcr/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 23 23 | 
             
            openepd/api/pcr/sync_api.py,sha256=Riu77h8uLJngKpITOiXYmO7mzjAHpYskUJ6ynyfNG78,1557
         | 
| 24 24 | 
             
            openepd/api/sync_client.py,sha256=IurnhZrkBQoQIbbfon6TPuhjGpAV_CSTJNeXjIiN0QI,3105
         | 
| 25 25 | 
             
            openepd/api/test/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 26 | 
            -
            openepd/api/utils.py,sha256= | 
| 26 | 
            +
            openepd/api/utils.py,sha256=9kdWbJ-m0IqsrkPQpGkILIQlR1WPd9rRlko-ymj9gtI,2092
         | 
| 27 27 | 
             
            openepd/bundle/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 28 28 | 
             
            openepd/bundle/base.py,sha256=mOt3h3MuQyc1yunp9u6_CVYDtSdWQHZcx8cNfbNkqoY,6869
         | 
| 29 29 | 
             
            openepd/bundle/model.py,sha256=yICEHl7ZWDXHGx4rhFomCwa-ganjUtykyj_fpWw_6OI,2446
         | 
| @@ -35,7 +35,7 @@ openepd/compat/pydantic.py,sha256=DOjSixsylLqMtFAIARu50sGcT4VPXN_c473q_2JwZQ0,11 | |
| 35 35 | 
             
            openepd/model/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 36 36 | 
             
            openepd/model/base.py,sha256=OEYNFUTL4BivBNAt_LGowTlDuUjvKMHgf5U5ZBncZwQ,9805
         | 
| 37 37 | 
             
            openepd/model/category.py,sha256=IQXNGQFQmFZ_H9PRONloX_UOSf1sTMDq1rM1yz8JR0Y,1639
         | 
| 38 | 
            -
            openepd/model/common.py,sha256= | 
| 38 | 
            +
            openepd/model/common.py,sha256=kPN1MrFYCvnPNregds3CtFNvLxVrf0T-Y9nePdZspL4,8554
         | 
| 39 39 | 
             
            openepd/model/declaration.py,sha256=Wh5Ni6AG903lmPTV5sS627lNjSazH7km3OIr2lkKKJE,11072
         | 
| 40 40 | 
             
            openepd/model/epd.py,sha256=GU7OW6hp9GeLB9PUGqacDYMAJVWmAAHzGp3dZs2aGqo,7479
         | 
| 41 41 | 
             
            openepd/model/factory.py,sha256=XP7eeQNW5tqwX_4hfuEb3lK6BFQDb4KB0fSN0r8-lCU,2656
         | 
| @@ -60,12 +60,12 @@ openepd/model/specs/generated/cast_decks_and_underlayment.py,sha256=qZjryfTZc1lt | |
| 60 60 | 
             
            openepd/model/specs/generated/cladding.py,sha256=NEY2taEzXJeIR08oimzfXrHAaJ-EojC4TPI5tG6Xfi8,6598
         | 
| 61 61 | 
             
            openepd/model/specs/generated/cmu.py,sha256=Vv-aiT4Q7Zl5XTkSCnTeGOpE_ufTy63zt2Z5aXbv_Qo,1794
         | 
| 62 62 | 
             
            openepd/model/specs/generated/common.py,sha256=iNeJmVYZURVQWewx3zskwnwy7DBuaggCHFWQBThtY2A,1048
         | 
| 63 | 
            -
            openepd/model/specs/generated/concrete.py,sha256= | 
| 63 | 
            +
            openepd/model/specs/generated/concrete.py,sha256=gk-ZFUTJni4l32FRYGnqD9Vv6lT-ACgIEY7RKM266k4,7728
         | 
| 64 64 | 
             
            openepd/model/specs/generated/conveying_equipment.py,sha256=sQSnFccpTIw8sNGywW0KajE1lCHzbVBCISKyrYFW02U,3052
         | 
| 65 65 | 
             
            openepd/model/specs/generated/electrical.py,sha256=vX3k_dYRmFCQG0Rh4g8rYZ_hAiAE3Hxfk217jgsrz4g,11093
         | 
| 66 66 | 
             
            openepd/model/specs/generated/electrical_transmission_and_distribution_equipment.py,sha256=h6UQixACOHrquhQ2GFqqYWel_zqOXT-vAkI0o_RLf0A,1978
         | 
| 67 67 | 
             
            openepd/model/specs/generated/electricity.py,sha256=iGtN21K1MRVwoRfO6friVgiXc2b6cVdITbvnXqLmW3k,823
         | 
| 68 | 
            -
            openepd/model/specs/generated/enums.py,sha256= | 
| 68 | 
            +
            openepd/model/specs/generated/enums.py,sha256=rh35d0CZUIp1GDYopldu-dUJ3YQtXlKzJuMpyFLKT2w,63602
         | 
| 69 69 | 
             
            openepd/model/specs/generated/finishes.py,sha256=1nqZs2B2VRIt553ezCJSLT4qF8nJmfg457duNb1sxlI,22503
         | 
| 70 70 | 
             
            openepd/model/specs/generated/fire_and_smoke_protection.py,sha256=zkOlnNCnAZ9MUWk2sDqUX14YxNEDU3MGfUlePG3su0Q,3068
         | 
| 71 71 | 
             
            openepd/model/specs/generated/furnishings.py,sha256=QY_FDsFZaqjCiw2xHsD3kmyBGJA7jCHlSIvaw4TmqXI,2581
         | 
| @@ -127,12 +127,13 @@ openepd/model/specs/range/wood_joists.py,sha256=jSOP5s0sYVR4tqk0l8rYARDPHQE8UsbM | |
| 127 127 | 
             
            openepd/model/standard.py,sha256=QhGpWN3U27fDcS0Yy1Dk8ElJfD0etet6i_PzoTD6B48,1318
         | 
| 128 128 | 
             
            openepd/model/validation/__init__.py,sha256=UGmZGEyMnASrYwEBPHuXmVzHiuCUskUsJEPoHTIo-lg,620
         | 
| 129 129 | 
             
            openepd/model/validation/common.py,sha256=FLYqK8gYFagx08LCkS0jy3qo4-Zq9VAv5i8ZwF2svkc,2435
         | 
| 130 | 
            +
            openepd/model/validation/enum.py,sha256=06Fi2_sdjVhOElavtgvNXvkKU0NQ-bcAFx5_5MiDzdU,1793
         | 
| 130 131 | 
             
            openepd/model/validation/numbers.py,sha256=J2rVMwJTPr8uC1DrU80xpq2SZGMXvVKx7v74SgOVi0U,851
         | 
| 131 132 | 
             
            openepd/model/validation/quantity.py,sha256=8LbSn-Gfo6r1j6AKDjzPnt_9dwyXMFmN5Uaoxa6LIEU,16953
         | 
| 132 133 | 
             
            openepd/model/versioning.py,sha256=R_zm6rCrgF3vlJQYbpyWhirdS_Oek16cv_mvZmpuE8I,4473
         | 
| 133 134 | 
             
            openepd/patch_pydantic.py,sha256=xrkzblatmU9HBzukWkp1cPq9ZSuohoz1p0pQqVKSlKs,4122
         | 
| 134 135 | 
             
            openepd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 135 | 
            -
            openepd-5. | 
| 136 | 
            -
            openepd-5. | 
| 137 | 
            -
            openepd-5. | 
| 138 | 
            -
            openepd-5. | 
| 136 | 
            +
            openepd-5.3.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
         | 
| 137 | 
            +
            openepd-5.3.0.dist-info/METADATA,sha256=twGr4uGcA4Rn0osNRGYw0IdWeC53yVy5NlbzhZNLZ6c,8996
         | 
| 138 | 
            +
            openepd-5.3.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
         | 
| 139 | 
            +
            openepd-5.3.0.dist-info/RECORD,,
         | 
| 
            File without changes
         | 
| 
            File without changes
         |