cmem-client 0.6.0__py3-none-any.whl → 0.8.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.
@@ -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."""
@@ -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
@@ -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,6 +8,7 @@ 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
@@ -16,6 +17,7 @@ from eccenca_marketplace_client.models.dependencies import MarketplacePackageDep
16
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,6 +25,7 @@ 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,
@@ -37,10 +40,13 @@ from cmem_client.repositories.graph_imports import GraphImport
37
40
  if TYPE_CHECKING:
38
41
  from collections.abc import Sequence
39
42
 
40
- from eccenca_marketplace_client.fields import PackageVersionIdentifier # noqa: TC002 # Pydantic needs this at runtime
43
+ from eccenca_marketplace_client.fields import ( # Pydantic needs this at runtime
44
+ MANIFEST_NAME,
45
+ PackageVersionIdentifier,
46
+ )
41
47
 
42
48
  from cmem_client.models.item import DirectoryImportItem, FileImportItem, ImportItem, ZipImportItem
43
- from cmem_client.models.package import Package
49
+ from cmem_client.models.package import Package, PackageInstallationMetadata
44
50
  from cmem_client.repositories.base.abc import Repository
45
51
  from cmem_client.repositories.graphs import GraphImportConfig
46
52
  from cmem_client.repositories.protocols.delete_item import DeleteConfig, DeleteItemProtocol
@@ -51,6 +57,18 @@ MAX_DEPENDENCY_DEPTH = 5
51
57
  MARKETPLACE_PROJECT_ID = "marketplace-packages"
52
58
 
53
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
+ """
70
+
71
+
54
72
  class MarketplacePackagesImportConfig(ImportConfig):
55
73
  """Configuration for marketplace package import operations.
56
74
 
@@ -258,7 +276,7 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
258
276
  )
259
277
  imported_files.append(composite_key)
260
278
 
261
- self._add_package_triples(package_version)
279
+ self._add_package_triples(package_version, configuration)
262
280
 
263
281
  # Rollback graphs + imports, projects, python packages
264
282
  except (BaseError, BadZipFile) as error:
@@ -276,6 +294,8 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
276
294
  self._client.marketplace_packages.delete_item(key=vocabulary_id, skip_if_missing=True)
277
295
  for file in imported_files:
278
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)
279
299
  raise MarketplacePackagesImportError(f"Failed to import package ({error!s})") from error
280
300
 
281
301
  self.fetch_data()
@@ -341,7 +361,7 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
341
361
  self._client.files.export_item(key=composite_key, path=file_path)
342
362
  exported_files.append((file_path, file_spec.file_path))
343
363
 
344
- manifest_path = tmp_path / "manifest.json"
364
+ manifest_path = tmp_path / MANIFEST_NAME
345
365
  manifest_path.write_text(manifest_json_str, encoding="utf-8")
346
366
 
347
367
  if path is None:
@@ -357,18 +377,18 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
357
377
  with ZipFile(path, mode="w") as zipf:
358
378
  for file_path, file_name in exported_files:
359
379
  zipf.write(file_path, file_name)
360
- zipf.write(manifest_path, "manifest.json")
380
+ zipf.write(manifest_path, MANIFEST_NAME)
361
381
  else:
362
382
  path.mkdir(parents=True, exist_ok=True)
363
383
  for file_path, file_name in exported_files:
364
384
  file_dest = path / file_name
365
385
  file_dest.parent.mkdir(parents=True, exist_ok=True)
366
386
  shutil.copy(file_path, file_dest)
367
- shutil.copy(manifest_path, path / "manifest.json")
387
+ shutil.copy(manifest_path, path / MANIFEST_NAME)
368
388
 
369
389
  return path
370
390
 
371
- def _delete_item(self, key: str, configuration: MarketplacePackagesDeleteConfig | None = None) -> None: # noqa: C901
391
+ def _delete_item(self, key: str, configuration: MarketplacePackagesDeleteConfig | None = None) -> None: # noqa: C901, PLR0912
372
392
  """Delete a package by its package_id.
373
393
 
374
394
  This method need be extended for new FileSpecs.
@@ -408,6 +428,21 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
408
428
  f"use by other packages: {', '.join(dependants)}"
409
429
  )
410
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
+
411
446
  self._client.marketplace_packages.delete_item(
412
447
  key=dependency.package_id, skip_if_missing=configuration.skip_missing_dependencies
413
448
  )
@@ -435,7 +470,7 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
435
470
  self._client.graphs.delete_item(NS_IRI, skip_if_missing=True)
436
471
  self._client.projects.delete_item(MARKETPLACE_PROJECT_ID, skip_if_missing=True)
437
472
 
438
- def _add_package_triples(self, package: PackageVersion) -> None:
473
+ def _add_package_triples(self, package: PackageVersion, config: MarketplacePackagesImportConfig) -> None:
439
474
  """Add a package to the data config graph in the Corporate Memory instance.
440
475
 
441
476
  Converts the package metadata to RDF triples and inserts them into the
@@ -444,10 +479,22 @@ class MarketplacePackagesRepository(Repository, ImportItemProtocol, ExportItemPr
444
479
 
445
480
  Args:
446
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.
447
484
  """
448
485
  self._add_marketplace_vocabulary()
449
486
  g = package.to_rdf_graph()
450
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
+
451
498
  triples = g.serialize(format="nt")
452
499
 
453
500
  sparql_update = f"""
@@ -81,6 +81,7 @@ class CreateItemProtocol(Protocol[ItemType, CreateItemConfig_contra]):
81
81
  response = self._create_item(item)
82
82
  if isinstance(response, Response):
83
83
  self.raise_modification_error(response)
84
+ self.logger.info("Repository item '%s' created.", item.get_id())
84
85
  self.fetch_data()
85
86
 
86
87
  def raise_modification_error(self, response: Response) -> None:
@@ -76,7 +76,7 @@ class DeleteItemProtocol(Protocol[ItemType, DeleteItemConfig_contra]):
76
76
  self._delete_item(key=key, configuration=configuration)
77
77
  except HTTPError as error:
78
78
  raise RepositoryModificationError(f"Error on deleting repository item '{key}'.") from error
79
-
79
+ self.logger.info("Repository item '%s' deleted.", key)
80
80
  del self._dict[key]
81
81
 
82
82
  @abstractmethod
@@ -86,6 +86,7 @@ class ExportItemProtocol(Protocol[ItemType, ExportItemConfig_contra]):
86
86
  raise RepositoryReadError(
87
87
  f"Repository export returned different path than requested: '{path}' != `{new_path}`"
88
88
  )
89
+ self.logger.info("Repository item '%s' exported.", key)
89
90
  if path:
90
91
  return path
91
92
 
@@ -122,6 +122,7 @@ class ImportItemProtocol(Protocol[ItemType, ImportItemConfig_contra]):
122
122
  )
123
123
  if key and key not in self._dict:
124
124
  raise RepositoryModificationError(f"Repository item '{key}' not there after import.")
125
+ self.logger.info("Repository item '%s' imported.", key)
125
126
  if key:
126
127
  return key
127
128
  return new_key
@@ -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
- return self._client.http.post(url, params={"name": item.name})
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.6.0
3
+ Version: 0.8.0
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.6.0,<0.7.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)
@@ -7,7 +7,7 @@ cmem_client/auth_provider/prefetched_token.py,sha256=pwT_gPtUaebMqpYnv1sKodHhAus
7
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=aPswFFdyV9yibDDqQ5cOCsOhzrG9kEgM5Q6Hu0DbOac,6658
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
@@ -21,7 +21,7 @@ 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=I8cL4pSNtVw1sZM2x7s5h6GfR-IpGXHc75MytrDe_ww,954
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
27
  cmem_client/models/resource.py,sha256=LR5Y6ptJdh6jY6yugtp83XH5TX8eJSNVu9UMPB6htrc,710
@@ -38,17 +38,17 @@ cmem_client/repositories/base/task_search.py,sha256=zeDFQLTn8Sbsvjjo0ntge8tUfbGj
38
38
  cmem_client/repositories/datasets.py,sha256=y5dq665rqA_Sm4Zpgu90ojZjy-KkqCG1vBlmegEMT9A,1351
39
39
  cmem_client/repositories/files.py,sha256=n8wCiwAEQLpQYAplHlCWboaDL-2vIUPqigdNV9TMp0A,6404
40
40
  cmem_client/repositories/graph_imports.py,sha256=NTVR6lAgUpGkKJsxbOWswG9AS2FwzkIJUX73C-vAJb8,3070
41
- cmem_client/repositories/graphs.py,sha256=Y7jz0DCUj1y55lO2egSc9Ys607le1vg4TmVLdINju40,17847
42
- cmem_client/repositories/marketplace_packages.py,sha256=mLpVlG_HZFneyCLlDiR_EkmbxJEFAmShwJeQFwILFAo,24605
41
+ cmem_client/repositories/graphs.py,sha256=BjeC-BW3jzJSO-zNYo_b9a1S0Xi6EaNzVSlpYyzEppo,17886
42
+ cmem_client/repositories/marketplace_packages.py,sha256=fzbuw4aZH69Y0togyzWIPjUvJqcxepkeLFFDd_rzbx4,26620
43
43
  cmem_client/repositories/projects.py,sha256=aY35uw1wp_ocV-WKh3ouFBClkBB5MTsQaKC1cZ95RY8,8642
44
44
  cmem_client/repositories/protocols/__init__.py,sha256=qXSmMthpY6Tp9FpLZP32GM4cERDflTZoWpSCrEQHNcc,656
45
- cmem_client/repositories/protocols/create_item.py,sha256=9ZFOcfFc9yqtNnclk7LpqMBiE7iGKASxpx49YYLCzbw,4771
46
- cmem_client/repositories/protocols/delete_item.py,sha256=T8Odl7x5ZTZyuHMLfkXzRiBZDNa7yTPLi-TYYEfARKE,3431
47
- cmem_client/repositories/protocols/export_item.py,sha256=wI0UDF8zpeEeUl5T-DzNGExlAf9csh_gqstKQI9TWN0,4193
48
- cmem_client/repositories/protocols/import_item.py,sha256=k4Z0XjDEC2KEIcCSN9-fBqarxAAoEFltoJnQXo2Rylk,5708
49
- cmem_client/repositories/python_packages.py,sha256=_XlMzvXhzt-33QTuiJnxWSVSeBUYOxufFaGAq0UoLm4,2120
45
+ cmem_client/repositories/protocols/create_item.py,sha256=mwQNnxZbRv0XkzKJ7gKEuetqTHoQdaa6AGsG9rgTCQc,4844
46
+ cmem_client/repositories/protocols/delete_item.py,sha256=QymEckCgCSbLXmVA_-tBOGWt6iXmyALxn66BXa-baKc,3493
47
+ cmem_client/repositories/protocols/export_item.py,sha256=k7k6N-P-CETkyihk5f_2MQoZm2Hr3FLtYz1oJYNm5nI,4257
48
+ cmem_client/repositories/protocols/import_item.py,sha256=vFDRkH_P_GtMx8u99NToRbOGyJMJd8nE_oGB9F5Dt1A,5772
49
+ cmem_client/repositories/python_packages.py,sha256=Hs5ydf2nb4JW8j_jtjju4VgyUidBrlIZkJBzHVrQQFY,2470
50
50
  cmem_client/repositories/workflows.py,sha256=ZTlKIwQXGgad96UKa6Ic1wh3Xhpy6KIOR2LolGW519U,4893
51
- cmem_client-0.6.0.dist-info/METADATA,sha256=maaDIfIO2A8pE7LydnN6Xe59c00wg4Z3YP4N2U25WuQ,2916
52
- cmem_client-0.6.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
53
- cmem_client-0.6.0.dist-info/licenses/LICENSE,sha256=5t6lcWcFU3TBO5wwq9PYNbgzfVfFUuL-80v5BTGuuMQ,11334
54
- cmem_client-0.6.0.dist-info/RECORD,,
51
+ cmem_client-0.8.0.dist-info/METADATA,sha256=vtDdWgswKBNhp1xxW3zzxXiucoQTon5RLSm1co63gj4,2916
52
+ cmem_client-0.8.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
53
+ cmem_client-0.8.0.dist-info/licenses/LICENSE,sha256=5t6lcWcFU3TBO5wwq9PYNbgzfVfFUuL-80v5BTGuuMQ,11334
54
+ cmem_client-0.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.3.0
2
+ Generator: poetry-core 2.3.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any