cmem-client 0.5.0__py3-none-any.whl → 0.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cmem_client/client.py +16 -0
- cmem_client/components/marketplace.py +9 -0
- cmem_client/exceptions.py +12 -0
- cmem_client/models/package.py +21 -1
- cmem_client/models/resource.py +25 -0
- cmem_client/repositories/files.py +181 -0
- cmem_client/repositories/graphs.py +1 -0
- cmem_client/repositories/marketplace_packages.py +122 -14
- cmem_client/repositories/protocols/delete_item.py +1 -2
- cmem_client/repositories/python_packages.py +11 -1
- {cmem_client-0.5.0.dist-info → cmem_client-0.7.1.dist-info}/METADATA +2 -2
- {cmem_client-0.5.0.dist-info → cmem_client-0.7.1.dist-info}/RECORD +14 -12
- {cmem_client-0.5.0.dist-info → cmem_client-0.7.1.dist-info}/WHEEL +0 -0
- {cmem_client-0.5.0.dist-info → cmem_client-0.7.1.dist-info}/licenses/LICENSE +0 -0
cmem_client/client.py
CHANGED
|
@@ -40,6 +40,7 @@ from cmem_client.config import Config
|
|
|
40
40
|
from cmem_client.exceptions import ClientNoAuthProviderError
|
|
41
41
|
from cmem_client.logging_utils import install_trace_logger
|
|
42
42
|
from cmem_client.models.logging_config import LoggingConfig
|
|
43
|
+
from cmem_client.repositories.files import FilesRepository
|
|
43
44
|
from cmem_client.repositories.graph_imports import GraphImportsRepository
|
|
44
45
|
from cmem_client.repositories.graphs import GraphsRepository
|
|
45
46
|
from cmem_client.repositories.marketplace_packages import MarketplacePackagesRepository
|
|
@@ -113,6 +114,9 @@ class Client:
|
|
|
113
114
|
_workflows: WorkflowsRepository
|
|
114
115
|
"""WorkflowsRepository object for workflow operations."""
|
|
115
116
|
|
|
117
|
+
_files: FilesRepository
|
|
118
|
+
"""FilesRepository object for file operations."""
|
|
119
|
+
|
|
116
120
|
def __init__(
|
|
117
121
|
self,
|
|
118
122
|
config: Config,
|
|
@@ -483,3 +487,15 @@ class Client:
|
|
|
483
487
|
except AttributeError:
|
|
484
488
|
self._workflows = WorkflowsRepository(client=self)
|
|
485
489
|
return self._workflows
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def files(self) -> FilesRepository:
|
|
493
|
+
"""Get the files repository for managing files
|
|
494
|
+
|
|
495
|
+
Returns: The files repository instance, created lazy on first access.
|
|
496
|
+
"""
|
|
497
|
+
try:
|
|
498
|
+
return self._files
|
|
499
|
+
except AttributeError:
|
|
500
|
+
self._files = FilesRepository(client=self)
|
|
501
|
+
return self._files
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import logging
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
16
16
|
|
|
17
|
+
from eccenca_marketplace_client.models.responses import PackageMetadata
|
|
17
18
|
from httpx import HTTPError
|
|
18
19
|
from xdg_base_dirs import xdg_cache_home
|
|
19
20
|
|
|
@@ -158,6 +159,14 @@ class Marketplace:
|
|
|
158
159
|
|
|
159
160
|
return sorted([version.get("package_version") for version in versions])
|
|
160
161
|
|
|
162
|
+
def get_available_packages(self) -> list[PackageMetadata]:
|
|
163
|
+
"""Get the available packages from the marketplace server."""
|
|
164
|
+
url = self.marketplace_url / "api/packages"
|
|
165
|
+
response = self._client.http.get(url=url)
|
|
166
|
+
response.raise_for_status()
|
|
167
|
+
packages_data = response.json()
|
|
168
|
+
return [PackageMetadata.model_validate(item) for item in packages_data]
|
|
169
|
+
|
|
161
170
|
@property
|
|
162
171
|
def marketplace_url(self) -> HttpUrl:
|
|
163
172
|
"""Get the marketplace server URL."""
|
cmem_client/exceptions.py
CHANGED
|
@@ -80,3 +80,15 @@ class MarketplacePackagesDeleteError(RepositoryModificationError):
|
|
|
80
80
|
|
|
81
81
|
class MarketplacePackagesExportError(BaseError):
|
|
82
82
|
"""Exception raised when a marketplace packages export fails."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class FilesImportError(RepositoryModificationError):
|
|
86
|
+
"""Exception raised when a file import fails."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class FilesDeleteError(RepositoryModificationError):
|
|
90
|
+
"""Exception raised when a file import fails."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class FilesExportError(RepositoryModificationError):
|
|
94
|
+
"""Exception raised when a file export fails."""
|
cmem_client/models/package.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Marketplace package models."""
|
|
2
2
|
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
3
5
|
from eccenca_marketplace_client.package_version import PackageVersion
|
|
4
|
-
from pydantic import ConfigDict
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
7
|
|
|
6
8
|
from cmem_client.models.base import Model, ReadRepositoryItem
|
|
7
9
|
|
|
@@ -33,3 +35,21 @@ class Package(ReadRepositoryItem):
|
|
|
33
35
|
The package_id which uniquely identifies this package.
|
|
34
36
|
"""
|
|
35
37
|
return str(self.package_version.manifest.package_id)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PackageInstallationMetadata(BaseModel):
|
|
41
|
+
"""Metadata about how and when a marketplace package was installed.
|
|
42
|
+
|
|
43
|
+
This metadata is stored as JSON in the RDF graph and used to determine
|
|
44
|
+
whether packages can be automatically removed when they are dependencies.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
dependency_level: int = Field(
|
|
48
|
+
ge=0, description="Dependency depth (0 for direct installs, >0 for dependencies", default=0
|
|
49
|
+
)
|
|
50
|
+
installed_at: datetime = Field(description="Timestamp when the package was installed", default=datetime.now(tz=UTC))
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def is_direct_installed(self) -> bool:
|
|
54
|
+
"""Indicates whether this package was installed directly"""
|
|
55
|
+
return self.dependency_level == 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""A file resource model"""
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from cmem_client.models.base import Model, ReadRepositoryItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResourceResponse(Model):
|
|
9
|
+
"""API response model for a file resource"""
|
|
10
|
+
|
|
11
|
+
name: str = Field(description="Resource name")
|
|
12
|
+
full_path: str = Field(description="Path of the resource", alias="fullPath")
|
|
13
|
+
modified: str = Field(description="Resource last modified time")
|
|
14
|
+
size: int = Field(description="Resource size")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Resource(Model, ReadRepositoryItem):
|
|
18
|
+
"""A file resource."""
|
|
19
|
+
|
|
20
|
+
file_id: str
|
|
21
|
+
project_id: str
|
|
22
|
+
|
|
23
|
+
def get_id(self) -> str:
|
|
24
|
+
"""Get the resource ID in format 'project_id:file_id'"""
|
|
25
|
+
return f"{self.project_id}:{self.file_id}"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Files Repository."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
7
|
+
|
|
8
|
+
from httpx import HTTPError
|
|
9
|
+
from pydantic import TypeAdapter
|
|
10
|
+
|
|
11
|
+
from cmem_client.repositories.protocols.export_item import ExportConfig, ExportItemProtocol
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
from cmem_client.client import Client
|
|
17
|
+
|
|
18
|
+
from cmem_client.exceptions import FilesDeleteError, FilesExportError, FilesImportError
|
|
19
|
+
from cmem_client.models.item import FileImportItem, ImportItem
|
|
20
|
+
from cmem_client.models.resource import Resource, ResourceResponse
|
|
21
|
+
from cmem_client.repositories.base.plain_list import PlainListRepository
|
|
22
|
+
from cmem_client.repositories.protocols.delete_item import DeleteConfig, DeleteItemProtocol
|
|
23
|
+
from cmem_client.repositories.protocols.import_item import ImportConfig, ImportItemProtocol
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FilesImportConfig(ImportConfig):
|
|
27
|
+
"""Files Import Configuration."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FilesExportConfig(ExportConfig):
|
|
31
|
+
"""Files Export Configuration."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FilesDeleteConfig(DeleteConfig):
|
|
35
|
+
"""Files Delete Configuration."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FilesRepository(PlainListRepository, ImportItemProtocol, DeleteItemProtocol, ExportItemProtocol):
|
|
39
|
+
"""Repository for files"""
|
|
40
|
+
|
|
41
|
+
_client: Client
|
|
42
|
+
_dict: dict[str, Resource]
|
|
43
|
+
_allowed_import_items: ClassVar[Sequence[type[ImportItem]]] = [FileImportItem]
|
|
44
|
+
|
|
45
|
+
def fetch_data(self) -> None:
|
|
46
|
+
"""Fetch all file resources from all projects."""
|
|
47
|
+
self._dict = {}
|
|
48
|
+
for project_id in self._client.projects:
|
|
49
|
+
for resource_data in self._get_resources(project_id):
|
|
50
|
+
resource = Resource(file_id=resource_data.full_path, project_id=project_id)
|
|
51
|
+
self._dict[resource.get_id()] = resource
|
|
52
|
+
|
|
53
|
+
def _get_resources(self, project_id: str) -> list[ResourceResponse]:
|
|
54
|
+
"""GET retrieve list of resources."""
|
|
55
|
+
url = self._client.config.url_build_api / "workspace/projects" / project_id / "resources"
|
|
56
|
+
response = self._client.http.get(url=url)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
adapter = TypeAdapter(list[ResourceResponse])
|
|
59
|
+
return adapter.validate_json(response.content)
|
|
60
|
+
|
|
61
|
+
def _import_item(
|
|
62
|
+
self,
|
|
63
|
+
path: Path | None = None,
|
|
64
|
+
replace: bool = False,
|
|
65
|
+
key: str | None = None,
|
|
66
|
+
configuration: FilesImportConfig | None = None,
|
|
67
|
+
) -> str:
|
|
68
|
+
"""Import a file to a specific project.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
path: Local file path to upload
|
|
72
|
+
replace: Whether to replace existing file
|
|
73
|
+
key: Composite key in format 'project_id:file_path'
|
|
74
|
+
configuration: Import configuration
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Composite key in format 'project_id:file_path'
|
|
78
|
+
"""
|
|
79
|
+
_ = configuration
|
|
80
|
+
|
|
81
|
+
if path is None:
|
|
82
|
+
raise FilesImportError("Path to file necessary.")
|
|
83
|
+
|
|
84
|
+
if key is None:
|
|
85
|
+
raise FilesImportError("Key in format 'project_id:file_path' is required.")
|
|
86
|
+
|
|
87
|
+
if ":" not in key:
|
|
88
|
+
raise FilesImportError(f"Invalid key format. Expected 'project_id:file_path', got '{key}'")
|
|
89
|
+
|
|
90
|
+
project_id, resource_name = key.split(":", 1)
|
|
91
|
+
|
|
92
|
+
if not replace and key in self._dict:
|
|
93
|
+
raise FilesImportError(f"File {key} already exists. Try replace.")
|
|
94
|
+
|
|
95
|
+
url = self._client.config.url_build_api / "workspace/projects" / project_id / "files"
|
|
96
|
+
|
|
97
|
+
with path.open("rb") as file:
|
|
98
|
+
try:
|
|
99
|
+
response = self._client.http.put(url, params={"path": resource_name}, content=file)
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
except HTTPError as e:
|
|
102
|
+
raise FilesImportError(f"Could not upload file '{resource_name}' to project '{project_id}'.") from e
|
|
103
|
+
|
|
104
|
+
self.fetch_data()
|
|
105
|
+
return key
|
|
106
|
+
|
|
107
|
+
def _delete_item(self, key: str, configuration: FilesDeleteConfig | None = None) -> None:
|
|
108
|
+
"""Delete a file.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
key: Composite key in format 'project_id:file_path'
|
|
112
|
+
configuration: Delete configuration
|
|
113
|
+
"""
|
|
114
|
+
_ = configuration
|
|
115
|
+
|
|
116
|
+
if ":" not in key:
|
|
117
|
+
raise FilesDeleteError(f"Invalid key format. Expected 'project_id:file_path', got '{key}'")
|
|
118
|
+
|
|
119
|
+
project_id, file_path = key.split(":", 1)
|
|
120
|
+
|
|
121
|
+
url = self._client.config.url_build_api / "workspace/projects" / project_id / "files"
|
|
122
|
+
try:
|
|
123
|
+
response = self._client.http.delete(url=url, params={"path": file_path})
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
except HTTPError as e:
|
|
126
|
+
raise FilesDeleteError(f"Could not delete file '{file_path}' from project '{project_id}'.") from e
|
|
127
|
+
|
|
128
|
+
def _export_item(
|
|
129
|
+
self,
|
|
130
|
+
key: str,
|
|
131
|
+
path: Path | None,
|
|
132
|
+
replace: bool = False,
|
|
133
|
+
configuration: FilesExportConfig | None = None,
|
|
134
|
+
) -> Path:
|
|
135
|
+
"""Export a file from a specific project.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
key: Composite key in format 'project_id:file_path'
|
|
139
|
+
path: Target export path
|
|
140
|
+
replace: Whether to replace existing file
|
|
141
|
+
configuration: Export configuration
|
|
142
|
+
"""
|
|
143
|
+
_ = configuration
|
|
144
|
+
|
|
145
|
+
if key is None:
|
|
146
|
+
raise FilesExportError("No resource key specified.")
|
|
147
|
+
|
|
148
|
+
if ":" not in key:
|
|
149
|
+
raise FilesExportError(f"Invalid key format. Expected 'project_id:file_path', got '{key}'")
|
|
150
|
+
|
|
151
|
+
project_id, file_path = key.split(":", 1)
|
|
152
|
+
|
|
153
|
+
if path is None:
|
|
154
|
+
target_path = Path(file_path)
|
|
155
|
+
elif path.is_dir():
|
|
156
|
+
target_path = path / file_path
|
|
157
|
+
else:
|
|
158
|
+
target_path = path
|
|
159
|
+
|
|
160
|
+
if target_path.exists() and not replace:
|
|
161
|
+
raise FilesExportError(f"File '{target_path}' already exists. Try replace=True.")
|
|
162
|
+
|
|
163
|
+
url = self._client.config.url_build_api / "workspace/projects" / project_id / "files"
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
with self._client.http.stream(
|
|
167
|
+
"GET",
|
|
168
|
+
url,
|
|
169
|
+
params={"path": file_path},
|
|
170
|
+
) as response:
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
|
|
173
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
with target_path.open("wb") as file:
|
|
175
|
+
for chunk in response.iter_bytes():
|
|
176
|
+
file.write(chunk)
|
|
177
|
+
|
|
178
|
+
except HTTPError as e:
|
|
179
|
+
raise FilesExportError(f"Could not export file '{file_path}' from project '{project_id}'.") from e
|
|
180
|
+
|
|
181
|
+
return target_path
|
|
@@ -209,6 +209,7 @@ class GraphsRepository(PlainListRepository, DeleteItemProtocol, ImportItemProtoc
|
|
|
209
209
|
params = {"graph": key}
|
|
210
210
|
response = self._client.http.delete(url=url, params=params)
|
|
211
211
|
response.raise_for_status()
|
|
212
|
+
self._reload_vocabularies(key)
|
|
212
213
|
|
|
213
214
|
def guess_file_type(self, path: Path) -> GraphFileSerialization:
|
|
214
215
|
"""Guess the RDF serialization format from a file path.
|
|
@@ -8,14 +8,16 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import shutil
|
|
10
10
|
import tempfile
|
|
11
|
+
from datetime import UTC, datetime
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import TYPE_CHECKING, ClassVar
|
|
13
14
|
from zipfile import BadZipFile, ZipFile
|
|
14
15
|
|
|
15
|
-
from eccenca_marketplace_client.dependencies import
|
|
16
|
-
from eccenca_marketplace_client.
|
|
16
|
+
from eccenca_marketplace_client.models.dependencies import MarketplacePackageDependency, PythonPackageDependency
|
|
17
|
+
from eccenca_marketplace_client.models.files import GraphFileSpec, ImageFileSpec, ProjectFileSpec, TextFileSpec
|
|
17
18
|
from eccenca_marketplace_client.ontology import (
|
|
18
19
|
NS_IRI,
|
|
20
|
+
NS_PREFIX,
|
|
19
21
|
get_data_graph_iri,
|
|
20
22
|
get_delete_query,
|
|
21
23
|
get_fetch_query,
|
|
@@ -23,22 +25,28 @@ from eccenca_marketplace_client.ontology import (
|
|
|
23
25
|
)
|
|
24
26
|
from eccenca_marketplace_client.package_graph import PackageGraph
|
|
25
27
|
from eccenca_marketplace_client.package_version import PackageVersion
|
|
28
|
+
from rdflib import Literal, Namespace, Variable
|
|
26
29
|
|
|
27
30
|
from cmem_client.exceptions import (
|
|
28
31
|
BaseError,
|
|
29
32
|
MarketplacePackagesExportError,
|
|
30
33
|
MarketplacePackagesImportError,
|
|
31
34
|
)
|
|
35
|
+
from cmem_client.models.project import Project, ProjectMetaData
|
|
32
36
|
from cmem_client.models.python_package import PythonPackage
|
|
37
|
+
from cmem_client.repositories.files import FilesImportConfig
|
|
33
38
|
from cmem_client.repositories.graph_imports import GraphImport
|
|
34
39
|
|
|
35
40
|
if TYPE_CHECKING:
|
|
36
41
|
from collections.abc import Sequence
|
|
37
42
|
|
|
38
|
-
from eccenca_marketplace_client.fields import
|
|
43
|
+
from eccenca_marketplace_client.fields import ( # Pydantic needs this at runtime
|
|
44
|
+
MANIFEST_NAME,
|
|
45
|
+
PackageVersionIdentifier,
|
|
46
|
+
)
|
|
39
47
|
|
|
40
48
|
from cmem_client.models.item import DirectoryImportItem, FileImportItem, ImportItem, ZipImportItem
|
|
41
|
-
from cmem_client.models.package import Package
|
|
49
|
+
from cmem_client.models.package import Package, PackageInstallationMetadata
|
|
42
50
|
from cmem_client.repositories.base.abc import Repository
|
|
43
51
|
from cmem_client.repositories.graphs import GraphImportConfig
|
|
44
52
|
from cmem_client.repositories.protocols.delete_item import DeleteConfig, DeleteItemProtocol
|
|
@@ -46,6 +54,19 @@ from cmem_client.repositories.protocols.export_item import ExportConfig, ExportI
|
|
|
46
54
|
from cmem_client.repositories.protocols.import_item import ImportConfig, ImportItemProtocol
|
|
47
55
|
|
|
48
56
|
MAX_DEPENDENCY_DEPTH = 5
|
|
57
|
+
MARKETPLACE_PROJECT_ID = "marketplace-packages"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_installation_metadata_query(package_iri: str) -> str:
|
|
61
|
+
"""Get the query for the installation metadata of the package."""
|
|
62
|
+
return f"""
|
|
63
|
+
PREFIX {NS_PREFIX}: <{NS_IRI}>
|
|
64
|
+
|
|
65
|
+
SELECT ?installation_metadata
|
|
66
|
+
WHERE {{
|
|
67
|
+
<{get_data_graph_iri()}{package_iri}> {NS_PREFIX}:installationMetadata ?installation_metadata .
|
|
68
|
+
}}
|
|
69
|
+
"""
|
|
49
70
|
|
|
50
71
|
|
|
51
72
|
class MarketplacePackagesImportConfig(ImportConfig):
|
|
@@ -123,13 +144,16 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
123
144
|
tmp_file.write(ontology_graph.serialize(format="turtle").encode("utf-8"))
|
|
124
145
|
|
|
125
146
|
try:
|
|
147
|
+
get_ontology_graph()
|
|
126
148
|
self._client.graphs.import_item(
|
|
127
|
-
path=tmp_path,
|
|
149
|
+
path=tmp_path,
|
|
150
|
+
replace=False,
|
|
151
|
+
key=NS_IRI,
|
|
128
152
|
)
|
|
129
153
|
finally:
|
|
130
154
|
tmp_path.unlink(missing_ok=True)
|
|
131
155
|
|
|
132
|
-
def _import_item( # noqa: C901, PLR0912
|
|
156
|
+
def _import_item( # noqa: C901, PLR0912, PLR0915
|
|
133
157
|
self,
|
|
134
158
|
path: Path | None = None,
|
|
135
159
|
replace: bool = False,
|
|
@@ -176,6 +200,7 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
176
200
|
imported_imports: list[str] = []
|
|
177
201
|
imported_python_packages: list[str] = []
|
|
178
202
|
imported_vocabulary_packages: list[str] = []
|
|
203
|
+
imported_files: list[str] = []
|
|
179
204
|
|
|
180
205
|
if manifest.package_id in self._dict:
|
|
181
206
|
if replace:
|
|
@@ -183,6 +208,8 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
183
208
|
else:
|
|
184
209
|
raise MarketplacePackagesImportError("Package already imported. Try replace.")
|
|
185
210
|
|
|
211
|
+
self._create_assets_project()
|
|
212
|
+
|
|
186
213
|
if not configuration.ignore_dependencies:
|
|
187
214
|
# import python package dependencies first
|
|
188
215
|
for dependency in manifest.dependencies:
|
|
@@ -193,7 +220,7 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
193
220
|
imported_python_packages.append(dependency.pypi_id)
|
|
194
221
|
|
|
195
222
|
for dependency in manifest.dependencies:
|
|
196
|
-
if isinstance(dependency,
|
|
223
|
+
if isinstance(dependency, MarketplacePackageDependency) and dependency.package_id not in self._dict:
|
|
197
224
|
if configuration.dependency_level >= MAX_DEPENDENCY_DEPTH:
|
|
198
225
|
self.logger.warning(
|
|
199
226
|
"Skipping dependency '%s' because the max depth of '%s' was reached.",
|
|
@@ -238,7 +265,18 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
238
265
|
self._client.graph_imports.create_item(item=new_import, skip_if_existing=True)
|
|
239
266
|
imported_imports.append(new_import.get_id())
|
|
240
267
|
|
|
241
|
-
|
|
268
|
+
for file in manifest.files:
|
|
269
|
+
file_resource_path = f"{manifest.package_id}/{file.file_path}"
|
|
270
|
+
composite_key = f"{MARKETPLACE_PROJECT_ID}:{file_resource_path}"
|
|
271
|
+
self._client.files.import_item(
|
|
272
|
+
path=package_version.get_file_path(file.file_path),
|
|
273
|
+
key=composite_key,
|
|
274
|
+
replace=True,
|
|
275
|
+
configuration=FilesImportConfig(use_archive_handler=False),
|
|
276
|
+
)
|
|
277
|
+
imported_files.append(composite_key)
|
|
278
|
+
|
|
279
|
+
self._add_package_triples(package_version, configuration)
|
|
242
280
|
|
|
243
281
|
# Rollback graphs + imports, projects, python packages
|
|
244
282
|
except (BaseError, BadZipFile) as error:
|
|
@@ -254,6 +292,10 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
254
292
|
self._client.python_packages.delete_item(key=python_package_id, skip_if_missing=True)
|
|
255
293
|
for vocabulary_id in imported_vocabulary_packages:
|
|
256
294
|
self._client.marketplace_packages.delete_item(key=vocabulary_id, skip_if_missing=True)
|
|
295
|
+
for file in imported_files:
|
|
296
|
+
self._client.files.delete_item(key=file, skip_if_missing=True)
|
|
297
|
+
if len(self._dict) == 0:
|
|
298
|
+
self._client.projects.delete_item(MARKETPLACE_PROJECT_ID, skip_if_missing=True)
|
|
257
299
|
raise MarketplacePackagesImportError(f"Failed to import package ({error!s})") from error
|
|
258
300
|
|
|
259
301
|
self.fetch_data()
|
|
@@ -311,7 +353,15 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
311
353
|
)
|
|
312
354
|
exported_files.append((exported_path, file_spec.file_path))
|
|
313
355
|
|
|
314
|
-
|
|
356
|
+
if isinstance(file_spec, (ImageFileSpec, TextFileSpec)):
|
|
357
|
+
file_path = tmp_path / file_spec.file_path
|
|
358
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
359
|
+
file_resource_path = f"{manifest.package_id}/{file_spec.file_path}"
|
|
360
|
+
composite_key = f"{MARKETPLACE_PROJECT_ID}:{file_resource_path}"
|
|
361
|
+
self._client.files.export_item(key=composite_key, path=file_path)
|
|
362
|
+
exported_files.append((file_path, file_spec.file_path))
|
|
363
|
+
|
|
364
|
+
manifest_path = tmp_path / MANIFEST_NAME
|
|
315
365
|
manifest_path.write_text(manifest_json_str, encoding="utf-8")
|
|
316
366
|
|
|
317
367
|
if path is None:
|
|
@@ -327,18 +377,18 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
327
377
|
with ZipFile(path, mode="w") as zipf:
|
|
328
378
|
for file_path, file_name in exported_files:
|
|
329
379
|
zipf.write(file_path, file_name)
|
|
330
|
-
zipf.write(manifest_path,
|
|
380
|
+
zipf.write(manifest_path, MANIFEST_NAME)
|
|
331
381
|
else:
|
|
332
382
|
path.mkdir(parents=True, exist_ok=True)
|
|
333
383
|
for file_path, file_name in exported_files:
|
|
334
384
|
file_dest = path / file_name
|
|
335
385
|
file_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
336
386
|
shutil.copy(file_path, file_dest)
|
|
337
|
-
shutil.copy(manifest_path, path /
|
|
387
|
+
shutil.copy(manifest_path, path / MANIFEST_NAME)
|
|
338
388
|
|
|
339
389
|
return path
|
|
340
390
|
|
|
341
|
-
def _delete_item(self, key: str, configuration: MarketplacePackagesDeleteConfig | None = None) -> None:
|
|
391
|
+
def _delete_item(self, key: str, configuration: MarketplacePackagesDeleteConfig | None = None) -> None: # noqa: C901, PLR0912
|
|
342
392
|
"""Delete a package by its package_id.
|
|
343
393
|
|
|
344
394
|
This method need be extended for new FileSpecs.
|
|
@@ -369,7 +419,7 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
369
419
|
key=dependency.pypi_id, skip_if_missing=configuration.skip_missing_dependencies
|
|
370
420
|
)
|
|
371
421
|
|
|
372
|
-
if isinstance(dependency,
|
|
422
|
+
if isinstance(dependency, MarketplacePackageDependency):
|
|
373
423
|
dependants = self._graph.get_package_dependants(dependency.package_id)
|
|
374
424
|
dependants.remove(package_id)
|
|
375
425
|
if len(dependants) > 0:
|
|
@@ -378,6 +428,21 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
378
428
|
f"use by other packages: {', '.join(dependants)}"
|
|
379
429
|
)
|
|
380
430
|
continue
|
|
431
|
+
|
|
432
|
+
# check if to be deleted package was directly installed prior
|
|
433
|
+
install_metadata_bindings = self._client.store.sparql.query(
|
|
434
|
+
get_installation_metadata_query(dependency.package_id)
|
|
435
|
+
).bindings
|
|
436
|
+
if install_metadata_bindings:
|
|
437
|
+
metadata_json_str = str(install_metadata_bindings[0][Variable("installation_metadata")])
|
|
438
|
+
metadata = PackageInstallationMetadata.model_validate_json(metadata_json_str)
|
|
439
|
+
if metadata.is_direct_installed:
|
|
440
|
+
self._logger.warning(
|
|
441
|
+
f"Marketplace package '{dependency.package_id}' can not be removed since "
|
|
442
|
+
f"it was installed directly, not just via dependencies'"
|
|
443
|
+
)
|
|
444
|
+
continue
|
|
445
|
+
|
|
381
446
|
self._client.marketplace_packages.delete_item(
|
|
382
447
|
key=dependency.package_id, skip_if_missing=configuration.skip_missing_dependencies
|
|
383
448
|
)
|
|
@@ -393,9 +458,19 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
393
458
|
deleted_import = GraphImport(from_graph=str(from_graph), to_graph=str(graph.graph_iri))
|
|
394
459
|
self._client.graph_imports.delete_item(key=deleted_import.get_id(), skip_if_missing=True)
|
|
395
460
|
|
|
461
|
+
for file in manifest.files:
|
|
462
|
+
file_resource_path = f"{manifest.package_id}/{file.file_path}"
|
|
463
|
+
composite_key = f"{MARKETPLACE_PROJECT_ID}:{file_resource_path}"
|
|
464
|
+
self._client.files.delete_item(key=composite_key, skip_if_missing=True)
|
|
465
|
+
|
|
396
466
|
self._client.store.sparql.update(get_delete_query(package_iri))
|
|
397
467
|
|
|
398
|
-
|
|
468
|
+
# Remaining package is removed by the protocol, marketplace vocabulary and project can be deleted safely
|
|
469
|
+
if len(self._dict) == 1 and key in self._dict:
|
|
470
|
+
self._client.graphs.delete_item(NS_IRI, skip_if_missing=True)
|
|
471
|
+
self._client.projects.delete_item(MARKETPLACE_PROJECT_ID, skip_if_missing=True)
|
|
472
|
+
|
|
473
|
+
def _add_package_triples(self, package: PackageVersion, config: MarketplacePackagesImportConfig) -> None:
|
|
399
474
|
"""Add a package to the data config graph in the Corporate Memory instance.
|
|
400
475
|
|
|
401
476
|
Converts the package metadata to RDF triples and inserts them into the
|
|
@@ -404,10 +479,22 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
404
479
|
|
|
405
480
|
Args:
|
|
406
481
|
package: The package metadata to add to the catalog.
|
|
482
|
+
config: The config object to determine if package was installed directly or
|
|
483
|
+
as a dependency and more metadata.
|
|
407
484
|
"""
|
|
408
485
|
self._add_marketplace_vocabulary()
|
|
409
486
|
g = package.to_rdf_graph()
|
|
410
487
|
|
|
488
|
+
eccm = Namespace(NS_IRI)
|
|
489
|
+
|
|
490
|
+
metadata = PackageInstallationMetadata(
|
|
491
|
+
dependency_level=config.dependency_level,
|
|
492
|
+
installed_at=datetime.now(tz=UTC),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
json_literal = Literal(metadata.model_dump_json())
|
|
496
|
+
g.add((package.iri(), eccm.installationMetadata, json_literal))
|
|
497
|
+
|
|
411
498
|
triples = g.serialize(format="nt")
|
|
412
499
|
|
|
413
500
|
sparql_update = f"""
|
|
@@ -484,3 +571,24 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
|
|
|
484
571
|
)
|
|
485
572
|
|
|
486
573
|
return package_version
|
|
574
|
+
|
|
575
|
+
def _create_assets_project(self) -> None:
|
|
576
|
+
"""Create the assets project for the marketplace."""
|
|
577
|
+
if MARKETPLACE_PROJECT_ID in self._client.projects:
|
|
578
|
+
return
|
|
579
|
+
mp_assets = Project(
|
|
580
|
+
name=MARKETPLACE_PROJECT_ID,
|
|
581
|
+
metaData=ProjectMetaData(
|
|
582
|
+
label="Marketplace Packages",
|
|
583
|
+
description="""This project contains all files that were installed via Marketplace packages.
|
|
584
|
+
|
|
585
|
+
This project was created when you installed your first package.
|
|
586
|
+
It will be deleted after the last package is uninstalled.
|
|
587
|
+
|
|
588
|
+
For more information about marketplace packages, have a look at
|
|
589
|
+
[documentation.eccenca.com](https://go.eccenca.com/feature/marketplace-packages).
|
|
590
|
+
""",
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
self._client.projects.create_item(mp_assets)
|
|
594
|
+
self._client.projects.fetch_data()
|
|
@@ -67,14 +67,13 @@ class DeleteItemProtocol(Protocol[ItemType, DeleteItemConfig_contra]):
|
|
|
67
67
|
RepositoryModificationError: if an error occurs while creating the item
|
|
68
68
|
HTTPError: for any other http error
|
|
69
69
|
"""
|
|
70
|
-
_ = configuration
|
|
71
70
|
if key not in self._dict:
|
|
72
71
|
if not skip_if_missing:
|
|
73
72
|
raise RepositoryModificationError(f"Repository item '{key}' does not exists.")
|
|
74
73
|
self.logger.info("Item '%s' does not exists, therefore not deleting.", key)
|
|
75
74
|
return
|
|
76
75
|
try:
|
|
77
|
-
self._delete_item(key=key)
|
|
76
|
+
self._delete_item(key=key, configuration=configuration)
|
|
78
77
|
except HTTPError as error:
|
|
79
78
|
raise RepositoryModificationError(f"Error on deleting repository item '{key}'.") from error
|
|
80
79
|
|
|
@@ -38,7 +38,10 @@ class PythonPackagesRepository(PlainListRepository, DeleteItemProtocol, CreateIt
|
|
|
38
38
|
"""Install a Python package."""
|
|
39
39
|
_ = configuration
|
|
40
40
|
url = self._url("/api/python/installPackageByName")
|
|
41
|
-
|
|
41
|
+
response = self._client.http.post(url, params={"name": item.name})
|
|
42
|
+
response.raise_for_status()
|
|
43
|
+
self._update_plugins()
|
|
44
|
+
return response
|
|
42
45
|
|
|
43
46
|
def _delete_item(self, key: str, configuration: PythonPackagesDeleteConfig | None = None) -> None:
|
|
44
47
|
"""Delete item from repository."""
|
|
@@ -46,6 +49,7 @@ class PythonPackagesRepository(PlainListRepository, DeleteItemProtocol, CreateIt
|
|
|
46
49
|
url = self._url("/api/python/uninstallPackage")
|
|
47
50
|
response = self._client.http.post(url, params={"name": key})
|
|
48
51
|
response.raise_for_status()
|
|
52
|
+
self._update_plugins()
|
|
49
53
|
|
|
50
54
|
def delete_all(self) -> None:
|
|
51
55
|
"""Delete all items from the repository
|
|
@@ -56,3 +60,9 @@ class PythonPackagesRepository(PlainListRepository, DeleteItemProtocol, CreateIt
|
|
|
56
60
|
self._delete_item("--all")
|
|
57
61
|
if hasattr(self, "fetch_data"):
|
|
58
62
|
self.fetch_data()
|
|
63
|
+
|
|
64
|
+
def _update_plugins(self) -> None:
|
|
65
|
+
"""Update the python packages in CMEM"""
|
|
66
|
+
url = self._url("/api/python/updatePlugins")
|
|
67
|
+
response = self._client.http.get(url)
|
|
68
|
+
response.raise_for_status()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmem-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Next generation eccenca Corporate Memory client library.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
-
Requires-Dist: eccenca-marketplace-client (>=0.
|
|
17
|
+
Requires-Dist: eccenca-marketplace-client (>=0.7.0,<0.8.0)
|
|
18
18
|
Requires-Dist: httpx (>=0.27.0,<0.28.0)
|
|
19
19
|
Requires-Dist: pydantic (>=2.8.2,<3.0.0)
|
|
20
20
|
Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
|
|
@@ -4,14 +4,14 @@ cmem_client/auth_provider/abc.py,sha256=ILt0G4ggscPU6m1ZtXdHeoK8t7zNyglaJ4Z9hAOP
|
|
|
4
4
|
cmem_client/auth_provider/client_credentials.py,sha256=iYzlRBJGWyLrX4k-Jj84zvY95p1pHFVxI_cb3CQG37k,9329
|
|
5
5
|
cmem_client/auth_provider/password.py,sha256=FzIt1RAvm33oDRyOdzXi3QGMfgtduE0QgerYsQI4LBI,11745
|
|
6
6
|
cmem_client/auth_provider/prefetched_token.py,sha256=pwT_gPtUaebMqpYnv1sKodHhAusGotXy4cGmVGWZvp0,6666
|
|
7
|
-
cmem_client/client.py,sha256=
|
|
7
|
+
cmem_client/client.py,sha256=qugUUEdRFhF39ib48uubCyjJmqxPzhbr4GDjrT-_95c,19287
|
|
8
8
|
cmem_client/components/__init__.py,sha256=DjuLm0R-UNidKROXBZFRa2SkTElqcFpFTkodLrGsJqk,448
|
|
9
9
|
cmem_client/components/graph_store.py,sha256=N3cJXNbebt6ov8UVs368gg8DfOE_62cJtoQw4nLLNBU,13905
|
|
10
|
-
cmem_client/components/marketplace.py,sha256=
|
|
10
|
+
cmem_client/components/marketplace.py,sha256=t82BmICtJuJfF1EveukMyezPwgAVUmdn7rTcwrLdIHg,7122
|
|
11
11
|
cmem_client/components/sparql_wrapper.py,sha256=75fo52S3S_XQ8z8ShSwHaFoHtKCLJxE-H-xNLFJnWLo,1630
|
|
12
12
|
cmem_client/components/workspace.py,sha256=L5DtVU-tf4EL56Uu3UCWBJBHODfu4ptDrZ6ZobPS_Z0,8866
|
|
13
13
|
cmem_client/config.py,sha256=YgciVNv4eP7jlGXT4wGM5FPkx9oIfPuf7jvh56PQEJ0,14108
|
|
14
|
-
cmem_client/exceptions.py,sha256=
|
|
14
|
+
cmem_client/exceptions.py,sha256=iaLODfx1vQeROeoXQBcVkIoyHOC-eRKZjUCZ-NH8Ihk,2883
|
|
15
15
|
cmem_client/logging_utils.py,sha256=aFsvsx6bao3-4rUoh3xows7OmN2bGBnN2obz4VenbSc,1756
|
|
16
16
|
cmem_client/models/__init__.py,sha256=__1KnflCo_HP7G-73oaZ091ENkJK30sxfm3l1nSOR_8,701
|
|
17
17
|
cmem_client/models/access_condition.py,sha256=1_BOUlhl8YiPRlFUTm25Q3dz2QVJoVd8JMHiDelVjVc,5625
|
|
@@ -21,9 +21,10 @@ cmem_client/models/error.py,sha256=id-xyQGSlGqI9ePgVdUdB7SJmwBDORdCbeYugSa7RkI,1
|
|
|
21
21
|
cmem_client/models/graph.py,sha256=5IzQWuCd45U2j81kyl935cQLSjeBTZRVM9QcUCUAhL8,785
|
|
22
22
|
cmem_client/models/item.py,sha256=gXp1Yh_hsgZKy-sbkGt78tTvV_2ODXT_PfbMS1OWh34,4108
|
|
23
23
|
cmem_client/models/logging_config.py,sha256=_pzI545AjyPf2LGaq_bqa9XbkFQa27R4bFjjdPQqRgs,1283
|
|
24
|
-
cmem_client/models/package.py,sha256=
|
|
24
|
+
cmem_client/models/package.py,sha256=kFE9U_2ZA-IBfc-c_UIc4ffqyTAHQaNBWsyy7kTR65w,1721
|
|
25
25
|
cmem_client/models/project.py,sha256=jfOZzw5ESqkgyn2InpiVDKdyzh-G5YQpc_zI6PJJ3gA,1457
|
|
26
26
|
cmem_client/models/python_package.py,sha256=4grVO5y57X0xwHhyRp51KYDzzjpX-IIn1fj20PEPlDw,689
|
|
27
|
+
cmem_client/models/resource.py,sha256=LR5Y6ptJdh6jY6yugtp83XH5TX8eJSNVu9UMPB6htrc,710
|
|
27
28
|
cmem_client/models/token.py,sha256=L7lc3xATw0A_-FSoEyqOam8e9rbPwGMm1wm0xP9-Ixc,1314
|
|
28
29
|
cmem_client/models/url.py,sha256=9Ao1afKdoJP8aQ4QNhhi7p6P0RJPNvJOtFAarmDfThM,1180
|
|
29
30
|
cmem_client/models/workflow.py,sha256=9ZzlwoU5ZTmZW1w8KBb8NRFw0xb0d3wqgrIUp9S3U8w,3749
|
|
@@ -35,18 +36,19 @@ cmem_client/repositories/base/paged_list.py,sha256=MwypWtYSCkNsaR-U3KvEnOh3yff9l
|
|
|
35
36
|
cmem_client/repositories/base/plain_list.py,sha256=qF-HhiEIHsMXIW1dKdTlk9Tq4bRxuV-xrFYXpMXYy5U,1433
|
|
36
37
|
cmem_client/repositories/base/task_search.py,sha256=zeDFQLTn8Sbsvjjo0ntge8tUfbGjKtxklWYypboq61M,2305
|
|
37
38
|
cmem_client/repositories/datasets.py,sha256=y5dq665rqA_Sm4Zpgu90ojZjy-KkqCG1vBlmegEMT9A,1351
|
|
39
|
+
cmem_client/repositories/files.py,sha256=n8wCiwAEQLpQYAplHlCWboaDL-2vIUPqigdNV9TMp0A,6404
|
|
38
40
|
cmem_client/repositories/graph_imports.py,sha256=NTVR6lAgUpGkKJsxbOWswG9AS2FwzkIJUX73C-vAJb8,3070
|
|
39
|
-
cmem_client/repositories/graphs.py,sha256=
|
|
40
|
-
cmem_client/repositories/marketplace_packages.py,sha256=
|
|
41
|
+
cmem_client/repositories/graphs.py,sha256=BjeC-BW3jzJSO-zNYo_b9a1S0Xi6EaNzVSlpYyzEppo,17886
|
|
42
|
+
cmem_client/repositories/marketplace_packages.py,sha256=fzbuw4aZH69Y0togyzWIPjUvJqcxepkeLFFDd_rzbx4,26620
|
|
41
43
|
cmem_client/repositories/projects.py,sha256=aY35uw1wp_ocV-WKh3ouFBClkBB5MTsQaKC1cZ95RY8,8642
|
|
42
44
|
cmem_client/repositories/protocols/__init__.py,sha256=qXSmMthpY6Tp9FpLZP32GM4cERDflTZoWpSCrEQHNcc,656
|
|
43
45
|
cmem_client/repositories/protocols/create_item.py,sha256=9ZFOcfFc9yqtNnclk7LpqMBiE7iGKASxpx49YYLCzbw,4771
|
|
44
|
-
cmem_client/repositories/protocols/delete_item.py,sha256=
|
|
46
|
+
cmem_client/repositories/protocols/delete_item.py,sha256=T8Odl7x5ZTZyuHMLfkXzRiBZDNa7yTPLi-TYYEfARKE,3431
|
|
45
47
|
cmem_client/repositories/protocols/export_item.py,sha256=wI0UDF8zpeEeUl5T-DzNGExlAf9csh_gqstKQI9TWN0,4193
|
|
46
48
|
cmem_client/repositories/protocols/import_item.py,sha256=k4Z0XjDEC2KEIcCSN9-fBqarxAAoEFltoJnQXo2Rylk,5708
|
|
47
|
-
cmem_client/repositories/python_packages.py,sha256=
|
|
49
|
+
cmem_client/repositories/python_packages.py,sha256=Hs5ydf2nb4JW8j_jtjju4VgyUidBrlIZkJBzHVrQQFY,2470
|
|
48
50
|
cmem_client/repositories/workflows.py,sha256=ZTlKIwQXGgad96UKa6Ic1wh3Xhpy6KIOR2LolGW519U,4893
|
|
49
|
-
cmem_client-0.
|
|
50
|
-
cmem_client-0.
|
|
51
|
-
cmem_client-0.
|
|
52
|
-
cmem_client-0.
|
|
51
|
+
cmem_client-0.7.1.dist-info/METADATA,sha256=wYM1tOyAiw4ESG6gQHIqJ4ov0iJTDH1zxVUVI2eV2Rk,2916
|
|
52
|
+
cmem_client-0.7.1.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
53
|
+
cmem_client-0.7.1.dist-info/licenses/LICENSE,sha256=5t6lcWcFU3TBO5wwq9PYNbgzfVfFUuL-80v5BTGuuMQ,11334
|
|
54
|
+
cmem_client-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|