openepd 6.19.0__py3-none-any.whl → 6.21.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
@@ -13,4 +13,4 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- VERSION = "6.19.0"
16
+ VERSION = "6.21.0"
@@ -0,0 +1,15 @@
1
+ #
2
+ # Copyright 2025 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
+ #
@@ -0,0 +1,79 @@
1
+ #
2
+ # Copyright 2025 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 Literal, overload
17
+
18
+ from requests import Response
19
+
20
+ from openepd.api.base_sync_client import BaseApiMethodGroup
21
+ from openepd.api.utils import encode_path_param
22
+ from openepd.model.org import Org, OrgRef
23
+
24
+
25
+ class OrgApi(BaseApiMethodGroup):
26
+ """API methods for Orgs."""
27
+
28
+ @overload
29
+ def create(self, to_create: Org, with_response: Literal[True]) -> tuple[OrgRef, Response]: ...
30
+
31
+ @overload
32
+ def create(self, to_create: Org, with_response: Literal[False] = False) -> OrgRef: ...
33
+
34
+ def create(self, to_create: Org, with_response: bool = False) -> OrgRef | tuple[OrgRef, Response]:
35
+ """
36
+ Create a new organization.
37
+
38
+ :param to_create: Organization to create
39
+ :param with_response: if True, return a tuple of (OrgRef, Response), otherwise return only OrgRef
40
+ :return: Organization reference or Organization reference with HTTP Response object depending on parameter
41
+ :raise ValidationError: if given object Org is invalid
42
+ """
43
+ response = self._client.do_request("post", "/orgs", json=to_create.to_serializable())
44
+ content = response.json()
45
+ ref = OrgRef.parse_obj(content)
46
+ if with_response:
47
+ return ref, response
48
+ return ref
49
+
50
+ @overload
51
+ def edit(self, to_edit: Org, with_response: Literal[True]) -> tuple[OrgRef, Response]: ...
52
+
53
+ @overload
54
+ def edit(self, to_edit: Org, with_response: Literal[False] = False) -> OrgRef: ...
55
+
56
+ def edit(self, to_edit: Org, with_response: bool = False) -> OrgRef | tuple[OrgRef, Response]:
57
+ """
58
+ Edit an organization.
59
+
60
+ :param to_edit: Organization to edit
61
+ :param with_response: if True, return a tuple of (OrgRef, Response), otherwise return only Org
62
+ :return: Organization reference or Organization reference with HTTP Response object depending on parameter
63
+ :raise ValueError: if the organization web_domain is not set
64
+ """
65
+ entity_id = to_edit.web_domain
66
+ if not entity_id:
67
+ msg = "The organization web_domain must be set to edit an organization."
68
+ raise ValueError(msg)
69
+ response = self._client.do_request(
70
+ "put",
71
+ f"/orgs/{encode_path_param(entity_id)}",
72
+ json=to_edit.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
73
+ )
74
+ response.raise_for_status()
75
+ content = response.json()
76
+ ref = OrgRef.parse_obj(content)
77
+ if with_response:
78
+ return ref, response
79
+ return ref
@@ -13,7 +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 Literal, overload
17
+
18
+ from requests import Response
19
+
16
20
  from openepd.api.base_sync_client import BaseApiMethodGroup
21
+ from openepd.api.utils import encode_path_param
17
22
  from openepd.model.pcr import Pcr, PcrRef
18
23
 
19
24
 
@@ -42,3 +47,33 @@ class PcrApi(BaseApiMethodGroup):
42
47
  """
43
48
  pcr_ref_obj = self._client.do_request("post", "/pcrs", json=pcr.to_serializable()).json()
44
49
  return PcrRef.parse_obj(pcr_ref_obj)
50
+
51
+ @overload
52
+ def edit(self, to_edit: Pcr, with_response: Literal[True]) -> tuple[PcrRef, Response]: ...
53
+
54
+ @overload
55
+ def edit(self, to_edit: Pcr, with_response: Literal[False] = False) -> PcrRef: ...
56
+
57
+ def edit(self, to_edit: Pcr, with_response: bool = False) -> PcrRef | tuple[PcrRef, Response]:
58
+ """
59
+ Edit a pcr.
60
+
61
+ :param to_edit: Pcr to edit
62
+ :param with_response: if True, return a tuple of (PcrRef, Response), otherwise return only PcrRef
63
+ :return: Pcr reference or Pcr reference with HTTP Response object depending on parameter
64
+ :raise ValueError: if the pcr ID is not set
65
+ """
66
+ entity_id = to_edit.id
67
+ if not entity_id:
68
+ msg = "The pcr ID must be set to edit a pcr."
69
+ raise ValueError(msg)
70
+ response = self._client.do_request(
71
+ "put",
72
+ f"/pcrs/{encode_path_param(entity_id)}",
73
+ json=to_edit.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
74
+ )
75
+ content = response.json()
76
+ ref = PcrRef.parse_obj(content)
77
+ if with_response:
78
+ return ref, response
79
+ return ref
@@ -0,0 +1,15 @@
1
+ #
2
+ # Copyright 2025 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
+ #
@@ -0,0 +1,79 @@
1
+ #
2
+ # Copyright 2025 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 Literal, overload
17
+
18
+ from requests import Response
19
+
20
+ from openepd.api.base_sync_client import BaseApiMethodGroup
21
+ from openepd.api.utils import encode_path_param
22
+ from openepd.model.org import Plant, PlantRef
23
+
24
+
25
+ class PlantApi(BaseApiMethodGroup):
26
+ """API methods for Plants."""
27
+
28
+ @overload
29
+ def create(self, to_create: Plant, with_response: Literal[True]) -> tuple[PlantRef, Response]: ...
30
+
31
+ @overload
32
+ def create(self, to_create: Plant, with_response: Literal[False] = False) -> PlantRef: ...
33
+
34
+ def create(self, to_create: Plant, with_response: bool = False) -> PlantRef | tuple[PlantRef, Response]:
35
+ """
36
+ Create a new plant.
37
+
38
+ :param to_create: Plant to create
39
+ :param with_response: if True, return a tuple of (PlantRef, Response), otherwise return only PlantRef
40
+ :return: Plant reference or Plant reference with HTTP Response object depending on parameter
41
+ :raise ValidationError: if given object Plant is invalid
42
+ """
43
+ response = self._client.do_request("post", "/plants", json=to_create.to_serializable())
44
+ content = response.json()
45
+ ref = PlantRef.parse_obj(content)
46
+ if with_response:
47
+ return ref, response
48
+ return ref
49
+
50
+ @overload
51
+ def edit(self, to_edit: Plant, with_response: Literal[True]) -> tuple[PlantRef, Response]: ...
52
+
53
+ @overload
54
+ def edit(self, to_edit: Plant, with_response: Literal[False] = False) -> PlantRef: ...
55
+
56
+ def edit(self, to_edit: Plant, with_response: bool = False) -> PlantRef | tuple[PlantRef, Response]:
57
+ """
58
+ Edit a plant.
59
+
60
+ :param to_edit: Plant to edit
61
+ :param with_response: if True, return a tuple of (PlantRef, Response), otherwise return only PlantRef
62
+ :return: Plant reference or Plant reference with HTTP Response object depending on parameter
63
+ :raise ValueError: if the plant ID is not set
64
+ """
65
+ entity_id = to_edit.id
66
+ if not entity_id:
67
+ msg = "The plant ID must be set to edit a plant."
68
+ raise ValueError(msg)
69
+ response = self._client.do_request(
70
+ "put",
71
+ f"/plants/{encode_path_param(entity_id)}",
72
+ json=to_edit.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
73
+ )
74
+ response.raise_for_status()
75
+ content = response.json()
76
+ ref = PlantRef.parse_obj(content)
77
+ if with_response:
78
+ return ref, response
79
+ return ref
@@ -0,0 +1,15 @@
1
+ #
2
+ # Copyright 2025 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
+ #
@@ -0,0 +1,79 @@
1
+ #
2
+ # Copyright 2025 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 Literal, overload
17
+
18
+ from requests import Response
19
+
20
+ from openepd.api.base_sync_client import BaseApiMethodGroup
21
+ from openepd.api.utils import encode_path_param
22
+ from openepd.model.standard import Standard, StandardRef
23
+
24
+
25
+ class StandardApi(BaseApiMethodGroup):
26
+ """API methods for Standards."""
27
+
28
+ @overload
29
+ def create(self, to_create: Standard, with_response: Literal[True]) -> tuple[StandardRef, Response]: ...
30
+
31
+ @overload
32
+ def create(self, to_create: Standard, with_response: Literal[False] = False) -> StandardRef: ...
33
+
34
+ def create(self, to_create: Standard, with_response: bool = False) -> StandardRef | tuple[StandardRef, Response]:
35
+ """
36
+ Create a new standard.
37
+
38
+ :param to_create: Standard to create
39
+ :param with_response: if True, return a tuple of (StandardRef, Response), otherwise return only StandardRef
40
+ :return: Standard reference or Standard reference with HTTP Response object depending on parameter
41
+ :raise ValidationError: if given object Standard is invalid
42
+ """
43
+ response = self._client.do_request("post", "/standards", json=to_create.to_serializable())
44
+ content = response.json()
45
+ ref = StandardRef.parse_obj(content)
46
+ if with_response:
47
+ return ref, response
48
+ return ref
49
+
50
+ @overload
51
+ def edit(self, to_edit: Standard, with_response: Literal[True]) -> tuple[StandardRef, Response]: ...
52
+
53
+ @overload
54
+ def edit(self, to_edit: Standard, with_response: Literal[False] = False) -> StandardRef: ...
55
+
56
+ def edit(self, to_edit: Standard, with_response: bool = False) -> StandardRef | tuple[StandardRef, Response]:
57
+ """
58
+ Edit a standard.
59
+
60
+ :param to_edit: Standard to edit
61
+ :param with_response: if True, return a tuple of (StandardRef, Response), otherwise return only StandardRef
62
+ :return: Standard reference or Standard reference with HTTP Response object depending on parameter
63
+ :raise ValueError: if the standard short_name is not set
64
+ """
65
+ entity_id = to_edit.short_name
66
+ if not entity_id:
67
+ msg = "The standard short_name must be set to edit a standard."
68
+ raise ValueError(msg)
69
+ response = self._client.do_request(
70
+ "put",
71
+ f"/standards/{encode_path_param(entity_id)}",
72
+ json=to_edit.to_serializable(exclude_unset=True, exclude_defaults=True, by_alias=True),
73
+ )
74
+ response.raise_for_status()
75
+ content = response.json()
76
+ ref = StandardRef.parse_obj(content)
77
+ if with_response:
78
+ return ref, response
79
+ return ref
@@ -22,7 +22,10 @@ from openepd.api.average_dataset.industry_epd_sync_api import IndustryEpdApi
22
22
  from openepd.api.base_sync_client import SyncHttpClient, TokenAuth
23
23
  from openepd.api.category.sync_api import CategoryApi
24
24
  from openepd.api.epd.sync_api import EpdApi
25
+ from openepd.api.org.sync_api import OrgApi
25
26
  from openepd.api.pcr.sync_api import PcrApi
27
+ from openepd.api.plant.sync_api import PlantApi
28
+ from openepd.api.standard.sync_api import StandardApi
26
29
 
27
30
 
28
31
  class OpenEpdApiClientSync:
@@ -41,6 +44,9 @@ class OpenEpdApiClientSync:
41
44
  self._http_client = SyncHttpClient(base_url, auth=auth, **kwargs)
42
45
  self.__epd_api: EpdApi | None = None
43
46
  self.__pcr_api: PcrApi | None = None
47
+ self.__org_api: OrgApi | None = None
48
+ self.__plant_api: PlantApi | None = None
49
+ self.__standard_api: StandardApi | None = None
44
50
  self.__category_api: CategoryApi | None = None
45
51
  self.__generic_estimate_api: GenericEstimateApi | None = None
46
52
  self.__industry_epd_api: IndustryEpdApi | None = None
@@ -59,6 +65,27 @@ class OpenEpdApiClientSync:
59
65
  self.__pcr_api = PcrApi(self._http_client)
60
66
  return self.__pcr_api
61
67
 
68
+ @property
69
+ def orgs(self) -> OrgApi:
70
+ """Get the Org API."""
71
+ if self.__org_api is None:
72
+ self.__org_api = OrgApi(self._http_client)
73
+ return self.__org_api
74
+
75
+ @property
76
+ def plants(self) -> PlantApi:
77
+ """Get the Plant API."""
78
+ if self.__plant_api is None:
79
+ self.__plant_api = PlantApi(self._http_client)
80
+ return self.__plant_api
81
+
82
+ @property
83
+ def standards(self) -> StandardApi:
84
+ """Get the Standard API."""
85
+ if self.__standard_api is None:
86
+ self.__standard_api = StandardApi(self._http_client)
87
+ return self.__standard_api
88
+
62
89
  @property
63
90
  def categories(self) -> CategoryApi:
64
91
  """Get the Category API."""
openepd/bundle/base.py CHANGED
@@ -64,6 +64,46 @@ class BundleMixin:
64
64
  else:
65
65
  return asset_ref
66
66
 
67
+ @classmethod
68
+ def _asset_refs_to_str(cls, asset_refs: AssetRef | list[AssetRef] | None) -> list[str] | str | None:
69
+ """Convert single or multiple asset references to strings."""
70
+ if asset_refs is None:
71
+ return None
72
+ if isinstance(asset_refs, list):
73
+ return [cls._asset_ref_to_str(asset_ref) for asset_ref in asset_refs]
74
+ else:
75
+ return cls._asset_ref_to_str(asset_refs)
76
+
77
+ @classmethod
78
+ def _serialize_rel_asset_for_csv(cls, rel_asset: list[str] | str | None) -> str | None:
79
+ """
80
+ Serialize rel_asset for CSV storage.
81
+
82
+ Multiple assets are joined with semicolons.
83
+ """
84
+ if rel_asset is None:
85
+ return None
86
+ if isinstance(rel_asset, list):
87
+ # For empty list, return None. For non-empty list, join with semicolons
88
+ return ";".join(rel_asset) if rel_asset else None
89
+ else:
90
+ # Single asset - return as string
91
+ return str(rel_asset)
92
+
93
+ @classmethod
94
+ def _deserialize_rel_asset_from_csv(cls, rel_asset_str: str | None) -> list[str] | str | None:
95
+ """
96
+ Deserialize rel_asset from CSV storage.
97
+
98
+ Semicolon-separated values become lists.
99
+ """
100
+ if rel_asset_str is None or rel_asset_str == "":
101
+ return None
102
+ if ";" in rel_asset_str:
103
+ return rel_asset_str.split(";")
104
+ else:
105
+ return rel_asset_str
106
+
67
107
 
68
108
  class BaseBundleReader(BundleMixin, metaclass=abc.ABCMeta):
69
109
  """Base class for bundle readers."""
@@ -180,7 +220,7 @@ class BaseBundleWriter(BundleMixin, metaclass=abc.ABCMeta):
180
220
  self,
181
221
  data: IO[bytes],
182
222
  content_type: str | None,
183
- rel_asset: AssetRef | None = None,
223
+ rel_asset: AssetRef | list[AssetRef] | None = None,
184
224
  rel_type: str | None = None,
185
225
  file_name: str | None = None,
186
226
  name: str | None = None,
@@ -196,7 +236,7 @@ class BaseBundleWriter(BundleMixin, metaclass=abc.ABCMeta):
196
236
  def write_object_asset(
197
237
  self,
198
238
  obj: TOpenEpdObject,
199
- rel_asset: AssetRef | None = None,
239
+ rel_asset: AssetRef | list[AssetRef] | None = None,
200
240
  rel_type: str | None = None,
201
241
  file_name: str | None = None,
202
242
  name: str | None = None,
openepd/bundle/model.py CHANGED
@@ -79,6 +79,7 @@ class AssetInfo(BaseOpenEpdSchema):
79
79
  """The language of the asset."""
80
80
  rel_type: str | None
81
81
  rel_asset: str | None
82
+ """The related asset reference (serialized as semicolon-separated for multiple assets)."""
82
83
  comment: str | None = pyd.Field(default=None)
83
84
  content_type: str | None = pyd.Field(default=None)
84
85
  size: int | None = pyd.Field(default=None)
openepd/bundle/reader.py CHANGED
@@ -61,8 +61,12 @@ class DefaultBundleReader(BaseBundleReader):
61
61
  return False
62
62
  if name is not None and a.name != name:
63
63
  return False
64
- if parent_ref is not None and a.rel_asset != parent_ref:
65
- return False
64
+ if parent_ref is not None:
65
+ parent_ref_str = self._asset_ref_to_str(parent_ref)
66
+ # Get the actual list of related assets
67
+ rel_asset_list = self._get_rel_asset_list(a)
68
+ if parent_ref_str not in rel_asset_list:
69
+ return False
66
70
  if ref_type is not None and a.rel_type != ref_type:
67
71
  return False
68
72
  if is_translated is not None and a.lang is not None and "translated" in a.lang:
@@ -78,6 +82,20 @@ class DefaultBundleReader(BaseBundleReader):
78
82
  input_dict[x] = None
79
83
  return input_dict
80
84
 
85
+ def _get_rel_asset_list(self, asset_info: AssetInfo) -> list[str]:
86
+ """Get the list of related asset references from an AssetInfo object."""
87
+ if asset_info.rel_asset is None:
88
+ return []
89
+
90
+ # Deserialize from CSV format (semicolon-separated values)
91
+ deserialized = self._deserialize_rel_asset_from_csv(asset_info.rel_asset)
92
+ if isinstance(deserialized, list):
93
+ return deserialized
94
+ elif isinstance(deserialized, str):
95
+ return [deserialized]
96
+ else:
97
+ return []
98
+
81
99
  def assets_iter(self) -> Iterator[AssetInfo]:
82
100
  """Iterate over all assets in the bundle."""
83
101
  with self._bundle_archive.open("toc", "r") as toc_stream:
@@ -125,7 +143,8 @@ class DefaultBundleReader(BaseBundleReader):
125
143
  rel_type = [rel_type]
126
144
  asset_ref = self._asset_ref_to_str(asset)
127
145
  for x in self.assets_iter():
128
- if x.rel_asset == asset_ref:
146
+ rel_asset_list = self._get_rel_asset_list(x)
147
+ if asset_ref in rel_asset_list:
129
148
  if rel_type is None or x.rel_type in rel_type:
130
149
  yield x
131
150
 
openepd/bundle/writer.py CHANGED
@@ -50,7 +50,7 @@ class DefaultBundleWriter(BaseBundleWriter):
50
50
  self,
51
51
  data: IO[bytes],
52
52
  content_type: str | None = None,
53
- rel_asset: AssetRef | None = None,
53
+ rel_asset: AssetRef | list[AssetRef] | None = None,
54
54
  rel_type: str | None = None,
55
55
  file_name: str | None = None,
56
56
  name: str | None = None,
@@ -60,7 +60,10 @@ class DefaultBundleWriter(BaseBundleWriter):
60
60
  custom_data: str | None = None,
61
61
  ) -> AssetInfo:
62
62
  """Write a blob asset to the bundle."""
63
- rel_ref_str = self._asset_ref_to_str(rel_asset) if rel_asset is not None else None
63
+ # Convert multiple rel_asset to proper format and serialize for storage
64
+ rel_ref_converted = self._asset_refs_to_str(rel_asset)
65
+ rel_ref_serialized = self._serialize_rel_asset_for_csv(rel_ref_converted)
66
+
64
67
  ref_str = self.__generate_entry_name(
65
68
  AssetType.Blob, self.__get_ext_for_content_type(content_type, "bin"), file_name
66
69
  )
@@ -70,7 +73,7 @@ class DefaultBundleWriter(BaseBundleWriter):
70
73
  type=AssetType.Blob,
71
74
  lang=lang,
72
75
  rel_type=rel_type,
73
- rel_asset=rel_ref_str,
76
+ rel_asset=rel_ref_serialized,
74
77
  content_type=content_type,
75
78
  comment=comment,
76
79
  custom_type=custom_type,
@@ -83,7 +86,7 @@ class DefaultBundleWriter(BaseBundleWriter):
83
86
  def write_object_asset(
84
87
  self,
85
88
  obj: TOpenEpdObject,
86
- rel_asset: AssetRef | None = None,
89
+ rel_asset: list[AssetRef] | AssetRef | None = None,
87
90
  rel_type: str | None = None,
88
91
  file_name: str | None = None,
89
92
  name: str | None = None,
@@ -98,7 +101,11 @@ class DefaultBundleWriter(BaseBundleWriter):
98
101
  msg = f"Object {obj} does not have a valid asset type and can't be written to a bundle."
99
102
  raise ValueError(msg)
100
103
  asset_type = AssetType(asset_type_str)
101
- rel_ref_str = self._asset_ref_to_str(rel_asset) if rel_asset is not None else None
104
+
105
+ # Convert multiple rel_asset to proper format and serialize for storage
106
+ rel_ref_converted = self._asset_refs_to_str(rel_asset)
107
+ rel_ref_serialized = self._serialize_rel_asset_for_csv(rel_ref_converted)
108
+
102
109
  ref_str = self.__generate_entry_name(
103
110
  asset_type, self.__get_ext_for_content_type("application/json", "json"), file_name
104
111
  )
@@ -107,7 +114,7 @@ class DefaultBundleWriter(BaseBundleWriter):
107
114
  name=name,
108
115
  type=asset_type,
109
116
  lang=lang,
110
- rel_asset=rel_ref_str,
117
+ rel_asset=rel_ref_serialized,
111
118
  rel_type=rel_type,
112
119
  content_type="application/json",
113
120
  comment=comment,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openepd
3
- Version: 6.19.0
3
+ Version: 6.21.0
4
4
  Summary: Python library to work with OpenEPD format
5
5
  License: Apache-2.0
6
6
  Author: C-Change Labs
@@ -8,7 +8,7 @@ Author-email: support@c-change-labs.com
8
8
  Maintainer: C-Change Labs
9
9
  Maintainer-email: open-source@c-change-labs.com
10
10
  Requires-Python: >=3.11,<4.0
11
- Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: Apache Software License
14
14
  Classifier: Operating System :: OS Independent
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Provides-Extra: api-client
21
21
  Requires-Dist: email-validator (>=1.3.1)
22
22
  Requires-Dist: idna (>=3.7)
23
- Requires-Dist: open-xpd-uuid (>=0.2.1,<0.3.0)
23
+ Requires-Dist: open-xpd-uuid (>=0.2.1,<2)
24
24
  Requires-Dist: openlocationcode (>=1.0.1)
25
25
  Requires-Dist: pydantic (>=1.10,<3.0)
26
26
  Requires-Dist: requests (>=2.0) ; extra == "api-client"
@@ -44,6 +44,24 @@ Description-Content-Type: text/markdown
44
44
 
45
45
  This library is a Python library to work with OpenEPD format.
46
46
 
47
+ > ⚠️ **Version Warning**
48
+ >
49
+ > This application is currently developed in **two major versions** in parallel:
50
+ >
51
+ > - **v6.x (>=6.0.0)** — Stable and production-ready. Maintains support for Pydantic v1 and v2 through a compatibility layer.
52
+ > - **v7.x (>=7.0.0)** — Public beta. Fully functional, with native support for Pydantic v2. Still experimental and may introduce breaking changes in **internal and integration interfaces**.
53
+ >
54
+ > ⚠️ No breaking changes are expected in the **public standard or data model**, only in internal APIs and integration points.
55
+ >
56
+ > Both versions currently offer the same set of features.
57
+ > We recommend using **v6** for most production use cases as the more mature and stable option.
58
+ > **v7** is suitable for production environments that can tolerate some level of interface instability and want to adopt the latest internals.
59
+ >
60
+ > 💡 Only the **latest version of v7** is guaranteed to contain all the features and updates from v6. Earlier v7 releases may lack some recent improvements.
61
+ >
62
+ > Once **v7 is promoted to stable**, all earlier **pre-stable (beta) v7 releases** will be **marked as yanked** to prevent accidental usage in production.
63
+ >
64
+
47
65
  ## About OpenEPD
48
66
 
49
67
  [openEPD](https://www.buildingtransparency.org/programs/openepd/) is an open data format for passing digital
@@ -63,13 +81,6 @@ documenting supply-chain specific data.
63
81
 
64
82
  ## Usage
65
83
 
66
- **❗ ATTENTION**: Pick the right version. The cornerstone of this library models package representing openEPD models.
67
- Models are defined with Pydantic library which is a dependency for openepd package. If you use Pydantic in your project
68
- carefully pick the version:
69
-
70
- * Use version **below** `2.0.0` if your project uses Pydantic version below `2.0.0`
71
- * Use version `2.x.x` or higher if your project uses Pydantic version `2.0.0` or above
72
-
73
84
  ### Models
74
85
 
75
86
  The library provides the Pydantic models for all the OpenEPD entities. The models are available in the `openepd.models`
@@ -1,5 +1,5 @@
1
1
  openepd/__init__.py,sha256=fhxfEyEurLvSfvQci-vb3njzl_lvhcLXiZrecCOaMU8,794
2
- openepd/__version__.py,sha256=3bpnsguYhHUBbEqDHO2zgvwFAf_zr5yjuOpqcce4MLo,639
2
+ openepd/__version__.py,sha256=k7n_fLDUizaOEiiaI-LYyq8UKQvC3djSX4Z3eV0EjcI,639
3
3
  openepd/api/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
4
4
  openepd/api/average_dataset/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
5
5
  openepd/api/average_dataset/generic_estimate_sync_api.py,sha256=_eZt_jGVL1a3p9cr-EF39Ve9Vl5sB8zwzTc_slnRL50,7975
@@ -19,16 +19,22 @@ openepd/api/epd/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,6
19
19
  openepd/api/epd/dto.py,sha256=MqhHjaNdtOc-KT2zNI88EB9-1d2a6CS2zzSus8HefBo,4874
20
20
  openepd/api/epd/sync_api.py,sha256=kBsx43q0cBm51hl3HVvzMIDrMMRi8NMyudPmHYd0qqU,7342
21
21
  openepd/api/errors.py,sha256=BgZeNfMNAKVPfhpuiVapCXNBSsXygAOWql-gy7m9j7E,2868
22
+ openepd/api/org/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
23
+ openepd/api/org/sync_api.py,sha256=VzOrd3eB1xPVLyrKlZl3OwXIQ5nT3808sA6N7MNBu7w,3167
22
24
  openepd/api/pcr/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
23
- openepd/api/pcr/sync_api.py,sha256=MyBOlBqdJt35-fXk-Vr-RpiKUbN_XlilUwwFGvVROdQ,1557
24
- openepd/api/sync_client.py,sha256=reD_OXsaVOvUvnC-cyp5aq8sKSOH2Z8xmImxV2ClQIM,3105
25
+ openepd/api/pcr/sync_api.py,sha256=JWiegxoSnD2JElYORp6QdkbO3jDNhrNKxJR6orsD1TI,2849
26
+ openepd/api/plant/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
27
+ openepd/api/plant/sync_api.py,sha256=cryGfKojyXV78RxIPRTGscWuLnkdgTNJAw9RkxrbZWI,3121
28
+ openepd/api/standard/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
29
+ openepd/api/standard/sync_api.py,sha256=Oj_Os3yBPk7y7hXDvQbzr-AyX-z2b82jzbN7AuiK3Go,3264
30
+ openepd/api/sync_client.py,sha256=DiDSQU0kBd9gU17KrPUvo07pyLv15rGozuWXbkM1JzA,4037
25
31
  openepd/api/test/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
26
32
  openepd/api/utils.py,sha256=xOU8ihC0eghsoaCFhC85PU4WYRwNxVEpfK3gzq4e9ik,2092
27
33
  openepd/bundle/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
28
- openepd/bundle/base.py,sha256=ovURf721k9XWe0C8-tmik_bEOrHdOtF1jbN03T5G7M4,6918
29
- openepd/bundle/model.py,sha256=_fQWW1KsxKquFFB6JtU3dyc82re1vqmwMpxe6zUEfSE,2605
30
- openepd/bundle/reader.py,sha256=YvTXglxp8HLxkaOUdbvOuyA9qeN3vt_PjlAroOSzNO0,6822
31
- openepd/bundle/writer.py,sha256=qfQWlGnbQxzim8mMbDKRyBpdEXDe60nyNbgjAbG0eq8,8245
34
+ openepd/bundle/base.py,sha256=2tv7KbxG9zC_nuMOGudorxZcTl5rU6ABJmEbO2WTPas,8400
35
+ openepd/bundle/model.py,sha256=uQto8zfI4wdCOpNAhhMPrsGIeyoKNMMJSmXJk6LlwC8,2700
36
+ openepd/bundle/reader.py,sha256=Qu6KM68tbaoSPRYSqkEKLgWjdl_c7pVAWIDHJK32aaQ,7653
37
+ openepd/bundle/writer.py,sha256=kX1vJ3z96W1TxYwy152hHbmHul_7jcxUXV8psOhtr18,8564
32
38
  openepd/compat/__init__.py,sha256=9THJcV3LT7JDBOMz1px-QFf_sdJ0LOqJ5dmA9Dvvtd4,620
33
39
  openepd/compat/compat_functional_validators.py,sha256=aWg3a80fqT8zjN0S260N-Ad2WFKAaB8ByN7ArBW3NMA,834
34
40
  openepd/compat/pydantic.py,sha256=HZJmAiYO7s-LLcFHuj3iXS4MGOITExZYn2rPmteoqNI,1146
@@ -144,7 +150,7 @@ openepd/model/validation/quantity.py,sha256=mP4gIkeOGZuHRhprsf_BX11Cic75NssYxOTk
144
150
  openepd/model/versioning.py,sha256=wBZdOVL3ND9FMIRU9PS3vx9M_7MBiO70xYPQvPez6po,4522
145
151
  openepd/patch_pydantic.py,sha256=bO7U5HqthFol0vfycb0a42UAGL3KOQ8-9MW4yCWOFP0,4150
146
152
  openepd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
147
- openepd-6.19.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
148
- openepd-6.19.0.dist-info/METADATA,sha256=odv3HP47b4WVhRfBXhEgRjAEsAwEuLo6OkpjYvWKgoY,9067
149
- openepd-6.19.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
150
- openepd-6.19.0.dist-info/RECORD,,
153
+ openepd-6.21.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
154
+ openepd-6.21.0.dist-info/METADATA,sha256=5zzo-Y9jYVGStwGi6qPaqnxdc1eXKibPA8la_MuXInc,9827
155
+ openepd-6.21.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
156
+ openepd-6.21.0.dist-info/RECORD,,