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.
- cmem_client/__init__.py +13 -0
- cmem_client/auth_provider/__init__.py +14 -0
- cmem_client/auth_provider/abc.py +124 -0
- cmem_client/auth_provider/client_credentials.py +207 -0
- cmem_client/auth_provider/password.py +252 -0
- cmem_client/auth_provider/prefetched_token.py +153 -0
- cmem_client/client.py +485 -0
- cmem_client/components/__init__.py +10 -0
- cmem_client/components/graph_store.py +316 -0
- cmem_client/components/marketplace.py +179 -0
- cmem_client/components/sparql_wrapper.py +53 -0
- cmem_client/components/workspace.py +194 -0
- cmem_client/config.py +364 -0
- cmem_client/exceptions.py +82 -0
- cmem_client/logging_utils.py +49 -0
- cmem_client/models/__init__.py +16 -0
- cmem_client/models/access_condition.py +147 -0
- cmem_client/models/base.py +30 -0
- cmem_client/models/dataset.py +32 -0
- cmem_client/models/error.py +67 -0
- cmem_client/models/graph.py +26 -0
- cmem_client/models/item.py +143 -0
- cmem_client/models/logging_config.py +51 -0
- cmem_client/models/package.py +35 -0
- cmem_client/models/project.py +46 -0
- cmem_client/models/python_package.py +26 -0
- cmem_client/models/token.py +40 -0
- cmem_client/models/url.py +34 -0
- cmem_client/models/workflow.py +80 -0
- cmem_client/repositories/__init__.py +15 -0
- cmem_client/repositories/access_conditions.py +62 -0
- cmem_client/repositories/base/__init__.py +12 -0
- cmem_client/repositories/base/abc.py +138 -0
- cmem_client/repositories/base/paged_list.py +63 -0
- cmem_client/repositories/base/plain_list.py +39 -0
- cmem_client/repositories/base/task_search.py +70 -0
- cmem_client/repositories/datasets.py +36 -0
- cmem_client/repositories/graph_imports.py +93 -0
- cmem_client/repositories/graphs.py +458 -0
- cmem_client/repositories/marketplace_packages.py +486 -0
- cmem_client/repositories/projects.py +214 -0
- cmem_client/repositories/protocols/__init__.py +15 -0
- cmem_client/repositories/protocols/create_item.py +125 -0
- cmem_client/repositories/protocols/delete_item.py +95 -0
- cmem_client/repositories/protocols/export_item.py +114 -0
- cmem_client/repositories/protocols/import_item.py +141 -0
- cmem_client/repositories/python_packages.py +58 -0
- cmem_client/repositories/workflows.py +143 -0
- cmem_client-0.5.0.dist-info/METADATA +64 -0
- cmem_client-0.5.0.dist-info/RECORD +52 -0
- cmem_client-0.5.0.dist-info/WHEEL +4 -0
- 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
|
+
"""
|