cmem-client 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. cmem_client/__init__.py +13 -0
  2. cmem_client/auth_provider/__init__.py +14 -0
  3. cmem_client/auth_provider/abc.py +124 -0
  4. cmem_client/auth_provider/client_credentials.py +207 -0
  5. cmem_client/auth_provider/password.py +252 -0
  6. cmem_client/auth_provider/prefetched_token.py +153 -0
  7. cmem_client/client.py +485 -0
  8. cmem_client/components/__init__.py +10 -0
  9. cmem_client/components/graph_store.py +316 -0
  10. cmem_client/components/marketplace.py +179 -0
  11. cmem_client/components/sparql_wrapper.py +53 -0
  12. cmem_client/components/workspace.py +194 -0
  13. cmem_client/config.py +364 -0
  14. cmem_client/exceptions.py +82 -0
  15. cmem_client/logging_utils.py +49 -0
  16. cmem_client/models/__init__.py +16 -0
  17. cmem_client/models/access_condition.py +147 -0
  18. cmem_client/models/base.py +30 -0
  19. cmem_client/models/dataset.py +32 -0
  20. cmem_client/models/error.py +67 -0
  21. cmem_client/models/graph.py +26 -0
  22. cmem_client/models/item.py +143 -0
  23. cmem_client/models/logging_config.py +51 -0
  24. cmem_client/models/package.py +35 -0
  25. cmem_client/models/project.py +46 -0
  26. cmem_client/models/python_package.py +26 -0
  27. cmem_client/models/token.py +40 -0
  28. cmem_client/models/url.py +34 -0
  29. cmem_client/models/workflow.py +80 -0
  30. cmem_client/repositories/__init__.py +15 -0
  31. cmem_client/repositories/access_conditions.py +62 -0
  32. cmem_client/repositories/base/__init__.py +12 -0
  33. cmem_client/repositories/base/abc.py +138 -0
  34. cmem_client/repositories/base/paged_list.py +63 -0
  35. cmem_client/repositories/base/plain_list.py +39 -0
  36. cmem_client/repositories/base/task_search.py +70 -0
  37. cmem_client/repositories/datasets.py +36 -0
  38. cmem_client/repositories/graph_imports.py +93 -0
  39. cmem_client/repositories/graphs.py +458 -0
  40. cmem_client/repositories/marketplace_packages.py +486 -0
  41. cmem_client/repositories/projects.py +214 -0
  42. cmem_client/repositories/protocols/__init__.py +15 -0
  43. cmem_client/repositories/protocols/create_item.py +125 -0
  44. cmem_client/repositories/protocols/delete_item.py +95 -0
  45. cmem_client/repositories/protocols/export_item.py +114 -0
  46. cmem_client/repositories/protocols/import_item.py +141 -0
  47. cmem_client/repositories/python_packages.py +58 -0
  48. cmem_client/repositories/workflows.py +143 -0
  49. cmem_client-0.5.0.dist-info/METADATA +64 -0
  50. cmem_client-0.5.0.dist-info/RECORD +52 -0
  51. cmem_client-0.5.0.dist-info/WHEEL +4 -0
  52. cmem_client-0.5.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,214 @@
1
+ """Repository for managing Corporate Memory Build (DataIntegration) projects.
2
+
3
+ Provides ProjectRepository class with CRUD operations including create, delete, and import
4
+ functionality for projects from ZIP archives.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import shutil
10
+ import tempfile
11
+ from io import BytesIO
12
+ from pathlib import Path
13
+ from shutil import rmtree
14
+ from typing import TYPE_CHECKING, ClassVar, Literal
15
+ from zipfile import ZipFile
16
+
17
+ from pydantic import Field, TypeAdapter
18
+
19
+ from cmem_client.exceptions import ProjectExportError, ProjectImportError, RepositoryModificationError
20
+ from cmem_client.models.base import Model
21
+ from cmem_client.models.item import ImportItem, ZipImportItem
22
+ from cmem_client.models.project import Project
23
+ from cmem_client.repositories.base.abc import RepositoryConfig
24
+ from cmem_client.repositories.base.plain_list import PlainListRepository
25
+ from cmem_client.repositories.protocols.create_item import CreateConfig, CreateItemProtocol
26
+ from cmem_client.repositories.protocols.delete_item import DeleteConfig, DeleteItemProtocol
27
+ from cmem_client.repositories.protocols.export_item import ExportConfig, ExportItemProtocol
28
+ from cmem_client.repositories.protocols.import_item import ImportConfig, ImportItemProtocol
29
+
30
+ if TYPE_CHECKING:
31
+ from httpx import Response
32
+
33
+
34
+ class ProjectImportStatus(Model):
35
+ """Response of the project import status endpoint."""
36
+
37
+ project_id: str = Field(alias="projectId")
38
+ success: bool | None = None
39
+ failure_message: str | None = Field(alias="failureMessage", default=None)
40
+
41
+
42
+ class ProjectsImportConfig(ImportConfig):
43
+ """Dataset Import Configuration."""
44
+
45
+ use_archive_handler: bool = False
46
+
47
+
48
+ class ProjectsExportConfig(ExportConfig):
49
+ """Dataset Export Configuration."""
50
+
51
+ marshalling_plugin: Literal["xmlZip", "xmlZipWithoutResources"] = "xmlZip"
52
+ extract_project_zip: bool = False
53
+
54
+
55
+ class ProjectsCreateConfig(CreateConfig):
56
+ """Dataset Create Configuration."""
57
+
58
+
59
+ class ProjectsDeleteConfig(DeleteConfig):
60
+ """Dataset Delete Configuration."""
61
+
62
+
63
+ class ProjectsRepository(
64
+ PlainListRepository, DeleteItemProtocol, CreateItemProtocol, ImportItemProtocol, ExportItemProtocol
65
+ ):
66
+ """Repository for Build (DataIntegration) projects.
67
+
68
+ This repository manages Build (DataIntegration) projects which are described with
69
+ the [Project model][cmem_client.models.project.Project].
70
+ """
71
+
72
+ _dict: dict[str, Project]
73
+ _allowed_import_items: ClassVar[list[type[ImportItem]]] = [ZipImportItem]
74
+ _allowed_marshalling_plugins: ClassVar[list[str]] = ["xmlZip", "xmlZipWithoutResources"]
75
+ _default_import_config: ProjectsImportConfig = ProjectsImportConfig()
76
+
77
+ _config = RepositoryConfig(
78
+ component="build",
79
+ fetch_data_path="/workspace/projects",
80
+ fetch_data_adapter=TypeAdapter(list[Project]),
81
+ )
82
+
83
+ def _delete_item(self, key: str, configuration: ProjectsDeleteConfig | None = None) -> None:
84
+ """Delete an item."""
85
+ _ = configuration
86
+ url = self._url(f"/workspace/projects/{key}")
87
+ response = self._client.http.delete(url)
88
+ response.raise_for_status()
89
+
90
+ def _create_item(self, item: Project, configuration: ProjectsCreateConfig | None = None) -> Response:
91
+ """Create a new project
92
+
93
+ Note: the payload of this request needs to have an 'id' field (different from model)
94
+ """
95
+ _ = configuration
96
+ url = self._url("/api/workspace/projects")
97
+ data = {"id": item.get_id(), "metaData": item.meta_data.model_dump()}
98
+ return self._client.http.post(url, json=data)
99
+
100
+ def _import_item_get_status(self, import_id: str) -> ProjectImportStatus:
101
+ """Get the status of an import item process"""
102
+ url = self._url(f"/api/workspace/projectImport/{import_id}/status")
103
+ params = {"timeout": "1000"}
104
+ response = self._client.http.get(url, params=params).raise_for_status()
105
+ return ProjectImportStatus(**response.json())
106
+
107
+ def _import_item(
108
+ self,
109
+ path: Path | None = Path(),
110
+ replace: bool = False,
111
+ key: str | None = None,
112
+ configuration: ProjectsImportConfig | None = None,
113
+ ) -> str:
114
+ """Import project ZIP archive to the repository (concrete implementation)
115
+
116
+ This method is responsible for importing the item from the server.
117
+ It needs to return the key of the imported item.
118
+ """
119
+ if path is None:
120
+ raise ProjectImportError("Path must be specified.")
121
+
122
+ if configuration is None:
123
+ configuration = ProjectsImportConfig()
124
+
125
+ # use a temporary zip file always to ensure zip and directory functionality
126
+ with tempfile.NamedTemporaryFile(suffix=".zip" if path.is_dir() else path.suffix, delete=False) as tmp:
127
+ if path.is_dir():
128
+ shutil.make_archive(
129
+ tmp.name.removesuffix(".zip"),
130
+ "zip",
131
+ base_dir=path.name,
132
+ root_dir=str(path.parent.absolute()),
133
+ )
134
+ uploaded_file = Path(tmp.name)
135
+ else:
136
+ shutil.copyfile(path, tmp.name)
137
+ uploaded_file = Path(tmp.name)
138
+
139
+ # 1. the project file upload
140
+ upload_url = self._url("/api/workspace/projectImport")
141
+ with uploaded_file.open(mode="rb") as file:
142
+ upload_response = self._client.http.post(upload_url, files={"file": file}).raise_for_status()
143
+ project_import_id = upload_response.json()["projectImportId"]
144
+
145
+ # unlink the temporary file again
146
+ Path.unlink(Path(uploaded_file))
147
+
148
+ # 2. the validation of the uploaded file
149
+ validation_url = self._url(f"/api/workspace/projectImport/{project_import_id}")
150
+ # projectId, label, marshallerId, projectAlreadyExists
151
+ self._client.http.get(validation_url).raise_for_status()
152
+
153
+ # 3. the asynchronous execution of the project import
154
+ import_url = self._url(f"/api/workspace/projectImport/{project_import_id}")
155
+ params = {
156
+ "overwriteExisting": "true" if replace else "false",
157
+ "generateNewId": "true" if not key else "false",
158
+ "newProjectId": key if key else None,
159
+ }
160
+ self._client.http.post(import_url, params=params).raise_for_status()
161
+
162
+ # 4. wait until finished
163
+ import_status = self._import_item_get_status(import_id=project_import_id)
164
+ while not import_status.success:
165
+ import_status = self._import_item_get_status(import_id=project_import_id)
166
+ if import_status.success:
167
+ self.fetch_data()
168
+ return import_status.project_id
169
+ raise RepositoryModificationError(import_status.failure_message)
170
+
171
+ def _export_item(
172
+ self, key: str, path: Path | None, replace: bool = False, configuration: ProjectsExportConfig | None = None
173
+ ) -> Path:
174
+ """Export a project to a specified file path
175
+
176
+ Args:
177
+ key: The key of the project to export
178
+ path: The path to which the project gets exported to
179
+ replace: If True, replace the existing project with a new one.
180
+ configuration: Optional configuration for export
181
+
182
+ Returns:
183
+ Path to the exported file
184
+ """
185
+ if path is None:
186
+ raise ProjectExportError("Path must be specified.")
187
+
188
+ if configuration is None:
189
+ configuration = ProjectsExportConfig()
190
+
191
+ if configuration.marshalling_plugin not in self._allowed_marshalling_plugins:
192
+ raise ProjectExportError(f"Invalid marshalling plugin '{configuration.marshalling_plugin}'")
193
+
194
+ url = self._url(f"/workspace/projects/{key}/export/{configuration.marshalling_plugin}")
195
+ extracted_project = self._client.http.get(url).raise_for_status()
196
+
197
+ if not extracted_project.content:
198
+ raise ProjectExportError(f"Export returned empty content for project '{key}'")
199
+
200
+ if configuration.extract_project_zip:
201
+ if path.exists() and not replace:
202
+ raise ProjectExportError(f"Directory '{path}' already exists. Use replace=True to overwrite.")
203
+ if path.exists():
204
+ rmtree(path)
205
+ path.mkdir(parents=True)
206
+ with ZipFile(BytesIO(extracted_project.content), "r") as zip_file:
207
+ zip_file.extractall(path)
208
+ else:
209
+ if path.exists() and not replace:
210
+ raise ProjectExportError(f"File '{path}' already exists. Use replace=True to overwrite.")
211
+ with path.open(mode="wb") as file:
212
+ file.write(extracted_project.content)
213
+
214
+ return path
@@ -0,0 +1,15 @@
1
+ """Protocol interfaces for repository operations.
2
+
3
+ This package defines protocol interfaces that repositories can implement to
4
+ provide consistent operations. Protocols
5
+ allow for type-safe composition of repository functionality without requiring
6
+ inheritance from concrete base classes.
7
+
8
+ Available protocols:
9
+ - CreateItemProtocol: For repositories that can create new items
10
+ - DeleteItemProtocol: For repositories that can delete existing items
11
+ - ImportItemProtocol: For repositories that can import items from files
12
+
13
+ Repositories mix and match these protocols based on their capabilities,
14
+ ensuring consistent interfaces across different resource types.
15
+ """
@@ -0,0 +1,125 @@
1
+ """Protocol interface for repository item creation operations.
2
+
3
+ This module defines the CreateItemProtocol that repositories can implement
4
+ to provide item creation capabilities. It includes comprehensive error handling
5
+ for different API response formats and automatic repository refresh after
6
+ successful creation.
7
+
8
+ The protocol handles both DataIntegration (build) and DataPlatform (explore)
9
+ API error formats, providing consistent error reporting across different
10
+ Corporate Memory components.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from abc import ABC, abstractmethod
17
+ from http import HTTPStatus
18
+ from typing import TYPE_CHECKING, Protocol, TypeVar, runtime_checkable
19
+
20
+ from httpx import HTTPError, Response
21
+ from pydantic import ValidationError
22
+
23
+ from cmem_client.exceptions import RepositoryModificationError
24
+ from cmem_client.logging_utils import log_method
25
+ from cmem_client.models.base import Model
26
+ from cmem_client.models.error import ErrorResult, Problem
27
+ from cmem_client.repositories.base.abc import ItemType, RepositoryConfig
28
+
29
+ if TYPE_CHECKING:
30
+ from cmem_client.client import Client
31
+
32
+
33
+ class CreateConfig(Model, ABC):
34
+ """Abstract base class for repository item creation configurations."""
35
+
36
+
37
+ CreateItemConfig_contra = TypeVar("CreateItemConfig_contra", bound=CreateConfig, contravariant=True)
38
+
39
+
40
+ @runtime_checkable
41
+ class CreateItemProtocol(Protocol[ItemType, CreateItemConfig_contra]):
42
+ """Protocol which allows for creation of new items"""
43
+
44
+ _client: Client
45
+ _dict: dict[str, ItemType]
46
+ _config: RepositoryConfig
47
+ _logger: logging.Logger
48
+
49
+ @property
50
+ def logger(self) -> logging.Logger:
51
+ """Gets the client logger"""
52
+ if not hasattr(self, "_logger"):
53
+ self._logger = logging.getLogger(f"{self._client.logger.name}.{self.__class__.__name__}")
54
+ return self._logger
55
+
56
+ @abstractmethod
57
+ def fetch_data(self) -> None:
58
+ """Fetch new data and update the repository"""
59
+
60
+ @log_method
61
+ def create_item(
62
+ self, item: ItemType, skip_if_existing: bool = False, configuration: CreateItemConfig_contra | None = None
63
+ ) -> None:
64
+ """Create (add) a new item to the repository
65
+
66
+ Args:
67
+ item (ItemType): The item to add to the repository
68
+ skip_if_existing (bool, optional): If true, creating already existing items will be ignored
69
+ configuration (CreateItemConfig_contra | None): Optional configuration
70
+
71
+ Raises:
72
+ RepositoryModificationError: if an error occurs while creating the item
73
+ HTTPError: for any other http error
74
+ """
75
+ _ = configuration
76
+ if item.get_id() in self._dict:
77
+ if not skip_if_existing:
78
+ raise RepositoryModificationError(f"Item with id {item.get_id()} already exists.")
79
+ self.logger.info("Item '%s' already exists. Not creating new item.", item.get_id())
80
+ return
81
+ response = self._create_item(item)
82
+ if isinstance(response, Response):
83
+ self.raise_modification_error(response)
84
+ self.fetch_data()
85
+
86
+ def raise_modification_error(self, response: Response) -> None:
87
+ """Raise an exception if needed"""
88
+ if response.status_code == HTTPStatus.CREATED:
89
+ return
90
+
91
+ # some endpoint return OK instead of CREATED
92
+ # - /api/python/installPackageByName
93
+ if response.status_code == HTTPStatus.OK:
94
+ return
95
+
96
+ if self._config.component == "explore":
97
+ try:
98
+ problem = Problem.model_validate_json(response.content)
99
+ raise RepositoryModificationError(problem.get_exception_message())
100
+ except ValidationError as error:
101
+ raise RepositoryModificationError(response.content) from error
102
+
103
+ if self._config.component == "build":
104
+ try:
105
+ error_result = ErrorResult.model_validate_json(response.content)
106
+ raise RepositoryModificationError(f"{error_result.title}: {error_result.detail}")
107
+ except ValidationError as error:
108
+ raise RepositoryModificationError(response.content) from error
109
+
110
+ try:
111
+ response.raise_for_status()
112
+ except HTTPError as error:
113
+ raise RepositoryModificationError(str(error)) from error
114
+
115
+ @abstractmethod
116
+ def _create_item(self, item: ItemType, configuration: CreateItemConfig_contra | None = None) -> Response | None:
117
+ """Create (add) a new item to the repository (concrete implementation)
118
+
119
+ Args:
120
+ item (ItemType): The item to add to the repository
121
+ configuration (CreateItemConfig_contra | None): Optional configuration
122
+
123
+ Return:
124
+ Response: The response from the update request
125
+ """
@@ -0,0 +1,95 @@
1
+ """Protocol interface for repository item deletion operations.
2
+
3
+ This module defines the DeleteItemProtocol that repositories can implement
4
+ to provide item deletion capabilities. It includes validation to ensure items
5
+ exist before deletion and provides both individual and bulk deletion methods.
6
+
7
+ The protocol implements the Python __delitem__ method to support standard
8
+ dictionary-style deletion syntax while providing comprehensive error handling
9
+ for HTTP communication failures.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from abc import ABC, abstractmethod
16
+ from typing import TYPE_CHECKING, Protocol, TypeVar, runtime_checkable
17
+
18
+ from httpx import HTTPError
19
+
20
+ from cmem_client.exceptions import RepositoryModificationError
21
+ from cmem_client.logging_utils import log_method
22
+ from cmem_client.models.base import Model
23
+ from cmem_client.repositories.base.abc import ItemType
24
+
25
+ if TYPE_CHECKING:
26
+ from cmem_client.client import Client
27
+
28
+
29
+ class DeleteConfig(Model, ABC):
30
+ """Abstract base class for repository item deletion configurations."""
31
+
32
+
33
+ DeleteItemConfig_contra = TypeVar("DeleteItemConfig_contra", bound=DeleteConfig, contravariant=True)
34
+
35
+
36
+ @runtime_checkable
37
+ class DeleteItemProtocol(Protocol[ItemType, DeleteItemConfig_contra]):
38
+ """Protocol which allows for deletion of items"""
39
+
40
+ _client: Client
41
+ _dict: dict[str, ItemType]
42
+ _logger: logging.Logger
43
+
44
+ @property
45
+ def logger(self) -> logging.Logger:
46
+ """Gets the client logger"""
47
+ if not hasattr(self, "_logger"):
48
+ self._logger = logging.getLogger(f"{self._client.logger.name}.{self.__class__.__name__}")
49
+ return self._logger
50
+
51
+ def __delitem__(self, key: str) -> None:
52
+ """Delete an item from the repository"""
53
+ self.delete_item(key)
54
+
55
+ @log_method
56
+ def delete_item(
57
+ self, key: str, skip_if_missing: bool = False, configuration: DeleteItemConfig_contra | None = None
58
+ ) -> None:
59
+ """Delete an item from the repository
60
+
61
+ Args:
62
+ key (str): The key of the item to delete
63
+ skip_if_missing (bool, optional): If True, it is ignored if the deleted item even exists
64
+ configuration (DeleteItemConfig, optional): Optional configuration for deletion
65
+
66
+ Raises:
67
+ RepositoryModificationError: if an error occurs while creating the item
68
+ HTTPError: for any other http error
69
+ """
70
+ _ = configuration
71
+ if key not in self._dict:
72
+ if not skip_if_missing:
73
+ raise RepositoryModificationError(f"Repository item '{key}' does not exists.")
74
+ self.logger.info("Item '%s' does not exists, therefore not deleting.", key)
75
+ return
76
+ try:
77
+ self._delete_item(key=key)
78
+ except HTTPError as error:
79
+ raise RepositoryModificationError(f"Error on deleting repository item '{key}'.") from error
80
+
81
+ del self._dict[key]
82
+
83
+ @abstractmethod
84
+ def _delete_item(self, key: str, configuration: DeleteItemConfig_contra | None = None) -> None:
85
+ """Delete an item from the repository (concrete implementation)
86
+
87
+ This method is responsible for deleting the item from the server.
88
+ """
89
+
90
+ def delete_all(self) -> None:
91
+ """Delete all items from the repository"""
92
+ if hasattr(self, "fetch_data"):
93
+ self.fetch_data()
94
+ for key in list(self._dict):
95
+ self.delete_item(key=key)
@@ -0,0 +1,114 @@
1
+ """Protocol interface for repository item export operations.
2
+
3
+ This module defines the ExportItemProtocol that repositories can implement
4
+ to support exporting items to files.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from abc import ABC, abstractmethod
11
+ from typing import TYPE_CHECKING, Protocol, TypeVar, runtime_checkable
12
+
13
+ from httpx import HTTPError
14
+
15
+ from cmem_client.exceptions import RepositoryItemNotFoundError, RepositoryReadError
16
+ from cmem_client.logging_utils import log_method
17
+ from cmem_client.models.base import Model
18
+ from cmem_client.repositories.base.abc import ItemType
19
+
20
+ if TYPE_CHECKING:
21
+ from pathlib import Path
22
+
23
+ from cmem_client.client import Client
24
+
25
+
26
+ class ExportConfig(Model, ABC):
27
+ """Abstract base class for Export Item Configuration Objects"""
28
+
29
+
30
+ ExportItemConfig_contra = TypeVar("ExportItemConfig_contra", bound=ExportConfig, contravariant=True)
31
+
32
+
33
+ @runtime_checkable
34
+ class ExportItemProtocol(Protocol[ItemType, ExportItemConfig_contra]):
35
+ """Protocol which allows for exporting of items to a file path.
36
+
37
+ This protocol defines the interface that repositories must implement to support
38
+ exporting items to files. It provides both a public interface method and requires
39
+ implementation of a concrete export method.
40
+
41
+ Attributes:
42
+ _dict: Dictionary mapping item keys to repository items.
43
+ """
44
+
45
+ _client: Client
46
+ _dict: dict[str, ItemType]
47
+ _logger: logging.Logger
48
+
49
+ @property
50
+ def logger(self) -> logging.Logger:
51
+ """Gets the client logger"""
52
+ if not hasattr(self, "_logger"):
53
+ self._logger = logging.getLogger(f"{self._client.logger.name}.{self.__class__.__name__}")
54
+ return self._logger
55
+
56
+ @log_method
57
+ def export_item(
58
+ self,
59
+ key: str,
60
+ path: Path | None = None,
61
+ replace: bool = False,
62
+ configuration: ExportItemConfig_contra | None = None,
63
+ ) -> Path:
64
+ """Export an item from the repository to a file path.
65
+
66
+ Args:
67
+ key: The key identifying the item to export.
68
+ path: The target file path for export. If None, a path will be generated.
69
+ replace: Whether to replace existing files at the target path.
70
+ configuration: Optional configuration for export behavior.
71
+
72
+ Returns:
73
+ The actual path where the item was exported.
74
+
75
+ Raises:
76
+ RepositoryItemNotFoundError: If the specified item key is not found.
77
+ RepositoryReadError: If there's an error during export or path mismatch.
78
+ """
79
+ if key not in self._dict:
80
+ raise RepositoryItemNotFoundError(f"Repository item '{key}' not found.")
81
+ try:
82
+ new_path = self._export_item(path=path, key=key, replace=replace, configuration=configuration)
83
+ except HTTPError as error:
84
+ raise RepositoryReadError(f"Error on exporting repository item '{key}'.") from error
85
+ if path and path != new_path:
86
+ raise RepositoryReadError(
87
+ f"Repository export returned different path than requested: '{path}' != `{new_path}`"
88
+ )
89
+ if path:
90
+ return path
91
+
92
+ return new_path
93
+
94
+ @abstractmethod
95
+ def _export_item(
96
+ self, key: str, path: Path | None, replace: bool = False, configuration: ExportItemConfig_contra | None = None
97
+ ) -> Path:
98
+ """Export an item from the repository (concrete implementation).
99
+
100
+ This method must be implemented by concrete repository classes to handle
101
+ the actual export operation. It performs the low-level work of writing
102
+ the item data to the specified file path.
103
+
104
+ Args:
105
+ key: The key identifying the item to export.
106
+ path: The file path where the item should be exported. If None, a path
107
+ should be generated by the implementation.
108
+ replace: Whether to replace existing files at the target path.
109
+ configuration: Optional configuration for export behavior.
110
+
111
+ Returns:
112
+ The actual path where the item was exported. This may differ from the
113
+ input path if the implementation generates a different filename.
114
+ """