cmem-cmemc 25.5.0rc1__py3-none-any.whl → 26.1.0rc1__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 (42) hide show
  1. cmem_cmemc/cli.py +11 -6
  2. cmem_cmemc/command.py +1 -1
  3. cmem_cmemc/command_group.py +59 -31
  4. cmem_cmemc/commands/acl.py +403 -26
  5. cmem_cmemc/commands/admin.py +10 -10
  6. cmem_cmemc/commands/client.py +12 -5
  7. cmem_cmemc/commands/config.py +106 -12
  8. cmem_cmemc/commands/dataset.py +163 -172
  9. cmem_cmemc/commands/file.py +509 -0
  10. cmem_cmemc/commands/graph.py +200 -72
  11. cmem_cmemc/commands/graph_imports.py +12 -5
  12. cmem_cmemc/commands/graph_insights.py +157 -53
  13. cmem_cmemc/commands/metrics.py +15 -9
  14. cmem_cmemc/commands/migration.py +12 -4
  15. cmem_cmemc/commands/package.py +548 -0
  16. cmem_cmemc/commands/project.py +157 -22
  17. cmem_cmemc/commands/python.py +9 -5
  18. cmem_cmemc/commands/query.py +119 -25
  19. cmem_cmemc/commands/scheduler.py +6 -4
  20. cmem_cmemc/commands/store.py +2 -1
  21. cmem_cmemc/commands/user.py +124 -24
  22. cmem_cmemc/commands/validation.py +15 -10
  23. cmem_cmemc/commands/variable.py +264 -61
  24. cmem_cmemc/commands/vocabulary.py +31 -17
  25. cmem_cmemc/commands/workflow.py +21 -11
  26. cmem_cmemc/completion.py +126 -109
  27. cmem_cmemc/context.py +40 -10
  28. cmem_cmemc/exceptions.py +8 -2
  29. cmem_cmemc/manual_helper/graph.py +2 -2
  30. cmem_cmemc/manual_helper/multi_page.py +5 -7
  31. cmem_cmemc/object_list.py +234 -7
  32. cmem_cmemc/placeholder.py +2 -2
  33. cmem_cmemc/string_processor.py +153 -4
  34. cmem_cmemc/title_helper.py +50 -0
  35. cmem_cmemc/utils.py +9 -8
  36. {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/METADATA +7 -6
  37. cmem_cmemc-26.1.0rc1.dist-info/RECORD +62 -0
  38. {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/WHEEL +1 -1
  39. cmem_cmemc/commands/resource.py +0 -220
  40. cmem_cmemc-25.5.0rc1.dist-info/RECORD +0 -61
  41. {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/entry_points.txt +0 -0
  42. {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,548 @@
1
+ """Package command group"""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import requests
9
+ from click.shell_completion import CompletionItem
10
+ from cmem_client.client import Client
11
+ from cmem_client.repositories.marketplace_packages import (
12
+ MarketplacePackagesExportConfig,
13
+ MarketplacePackagesImportConfig,
14
+ MarketplacePackagesRepository,
15
+ )
16
+ from eccenca_marketplace_client.manifests import AbstractPackageManifest, ManifestMetadata
17
+ from eccenca_marketplace_client.package_version import PackageVersion
18
+ from pydantic_extra_types.semantic_version import SemanticVersion
19
+
20
+ from cmem_cmemc import completion
21
+ from cmem_cmemc.command import CmemcCommand
22
+ from cmem_cmemc.command_group import CmemcGroup
23
+ from cmem_cmemc.completion import suppress_completion_errors
24
+ from cmem_cmemc.context import ApplicationContext
25
+ from cmem_cmemc.exceptions import CmemcError
26
+ from cmem_cmemc.object_list import (
27
+ DirectValuePropertyFilter,
28
+ ObjectList,
29
+ compare_regex,
30
+ compare_str_equality,
31
+ )
32
+ from cmem_cmemc.parameter_types.path import ClickSmartPath
33
+ from cmem_cmemc.utils import struct_to_table
34
+
35
+
36
+ def get_installed_packages_list(ctx: click.Context) -> list[dict]:
37
+ """Get the list of installed packages"""
38
+ ApplicationContext.set_connection_from_params(ctx.find_root().params)
39
+ client = Client.from_cmempy()
40
+ packages: MarketplacePackagesRepository = client.marketplace_packages
41
+ return [
42
+ {
43
+ "id": package_.package_version.manifest.package_id,
44
+ "type": str(package_.package_version.manifest.package_type).replace(
45
+ "PackageTypes.", ""
46
+ ),
47
+ "version": str(package_.package_version.manifest.package_version),
48
+ "name": package_.package_version.manifest.metadata.name,
49
+ "description": package_.package_version.manifest.metadata.description,
50
+ }
51
+ for package_ in packages.values()
52
+ ]
53
+
54
+
55
+ installed_packages_list = ObjectList(
56
+ name="installed packages",
57
+ get_objects=get_installed_packages_list,
58
+ filters=[
59
+ DirectValuePropertyFilter(
60
+ name="type",
61
+ description="Filter list by package type.",
62
+ property_key="type",
63
+ ),
64
+ DirectValuePropertyFilter(
65
+ name="name",
66
+ description="Filter list by regex matching the package name.",
67
+ property_key="name",
68
+ compare=compare_regex,
69
+ fixed_completion=[],
70
+ ),
71
+ DirectValuePropertyFilter(
72
+ name="id",
73
+ description="Filter list by package ID.",
74
+ property_key="id",
75
+ compare=compare_str_equality,
76
+ ),
77
+ ],
78
+ )
79
+
80
+
81
+ @suppress_completion_errors
82
+ def _complete_installed_package_ids(
83
+ ctx: click.Context,
84
+ param: click.Argument, # noqa: ARG001
85
+ incomplete: str,
86
+ ) -> list[CompletionItem]:
87
+ """Prepare a list of IDs of installed packages."""
88
+ ApplicationContext.set_connection_from_params(ctx.find_root().params)
89
+ candidates = [
90
+ (_["id"], f"{_['version']}: {_['name']}")
91
+ for _ in installed_packages_list.apply_filters(ctx=ctx)
92
+ ]
93
+ return completion.finalize_completion(candidates=candidates, incomplete=incomplete)
94
+
95
+
96
+ @click.command(cls=CmemcCommand, name="create")
97
+ @click.argument("package_id", required=True)
98
+ @click.option(
99
+ "--name",
100
+ type=click.STRING,
101
+ help=ManifestMetadata.model_fields.get("name").description + " Defaults to package ID.",
102
+ )
103
+ @click.option(
104
+ "--version",
105
+ type=click.STRING,
106
+ default="0.0.1",
107
+ show_default=True,
108
+ help=AbstractPackageManifest.model_fields.get("package_version").description,
109
+ )
110
+ @click.option(
111
+ "--description",
112
+ type=click.STRING,
113
+ help=ManifestMetadata.model_fields.get("description").description,
114
+ default="This is the first version of a wonderful eccenca Corporate Memory package 🤓",
115
+ show_default=True,
116
+ )
117
+ @click.pass_obj
118
+ def create_command(
119
+ app: ApplicationContext, package_id: str, version: str, name: str | None, description: str
120
+ ) -> None:
121
+ """Initialize an empty package directory with a minimal manifest."""
122
+ if Path(package_id).exists():
123
+ raise click.UsageError(f"Package directory '{package_id}' already exists.")
124
+ if name is None:
125
+ name = package_id
126
+ manifest_src = {
127
+ "package_id": package_id,
128
+ "package_type": "project",
129
+ "package_version": version,
130
+ "metadata": {
131
+ "name": name,
132
+ "description": description,
133
+ },
134
+ "files": [],
135
+ }
136
+ package_version = PackageVersion.from_json(json.dumps(manifest_src))
137
+ directory = Path(package_id)
138
+ app.echo_info(f"Initialize package directory '{directory}' ... ", nl=False)
139
+ directory.mkdir(parents=True, exist_ok=True)
140
+ manifest = directory / "manifest.json"
141
+ manifest.write_text(package_version.manifest.model_dump_json(indent=2))
142
+ app.echo_success("done")
143
+
144
+
145
+ @click.command(cls=CmemcCommand, name="inspect")
146
+ @click.argument(
147
+ "PACKAGE_PATH",
148
+ required=True,
149
+ type=ClickSmartPath(
150
+ allow_dash=False,
151
+ dir_okay=True,
152
+ readable=True,
153
+ exists=True,
154
+ remote_okay=True,
155
+ ),
156
+ )
157
+ @click.option(
158
+ "--key",
159
+ "key",
160
+ help="Get a specific key only from the manifest.",
161
+ )
162
+ @click.option("--raw", is_flag=True, help="Outputs raw JSON.")
163
+ @click.pass_obj
164
+ def inspect_command(app: ApplicationContext, package_path: Path, key: str, raw: str) -> None:
165
+ """Inspect the manifest of a package."""
166
+ path = Path(package_path)
167
+ package_version = (
168
+ PackageVersion.from_directory(path) if path.is_dir() else PackageVersion.from_archive(path)
169
+ )
170
+ manifest = package_version.manifest
171
+ manifest_data = json.loads(manifest.model_dump_json(indent=2))
172
+ if raw:
173
+ app.echo_info_json(manifest_data)
174
+ return
175
+ if key:
176
+ table = [
177
+ line
178
+ for line in struct_to_table(manifest_data)
179
+ if line[0].startswith(key) or key == "all"
180
+ ]
181
+ if len(table) == 1:
182
+ app.echo_info(table[0][1])
183
+ return
184
+ if len(table) == 0:
185
+ raise CmemcError(f"No values for key '{key}'.")
186
+ app.echo_info_table(table, headers=["Key", "Value"], sort_column=0)
187
+ return
188
+ table = struct_to_table(manifest_data)
189
+ app.echo_info_table(
190
+ table,
191
+ headers=["Key", "Value"],
192
+ sort_column=0,
193
+ caption=f"Manifest of package '{manifest.package_id}' in"
194
+ f" {path.name} (v{manifest.package_version})",
195
+ )
196
+
197
+
198
+ @click.command(cls=CmemcCommand, name="list")
199
+ @click.option(
200
+ "--filter",
201
+ "filter_",
202
+ multiple=True,
203
+ type=(str, str),
204
+ shell_complete=installed_packages_list.complete_values,
205
+ help=installed_packages_list.get_filter_help_text(),
206
+ )
207
+ @click.option(
208
+ "--id-only",
209
+ is_flag=True,
210
+ help="Lists only package IDs. This is useful for piping the IDs into other commands.",
211
+ )
212
+ @click.option("--raw", is_flag=True, help="Outputs raw JSON.")
213
+ @click.pass_context
214
+ def list_command(
215
+ ctx: click.Context, filter_: tuple[tuple[str, str]], id_only: bool, raw: bool
216
+ ) -> None:
217
+ """List installed packages."""
218
+ app: ApplicationContext = ctx.obj
219
+ data = installed_packages_list.apply_filters(ctx=ctx, filter_=filter_)
220
+ if id_only:
221
+ for _ in sorted(data, key=lambda _: _["id"]):
222
+ app.echo_info(_["id"])
223
+ return
224
+ if raw:
225
+ app.echo_info_json(data)
226
+ return
227
+ table = [
228
+ (
229
+ _["id"],
230
+ _["version"],
231
+ _["type"],
232
+ _["name"],
233
+ )
234
+ for _ in data
235
+ ]
236
+ app.echo_info_table(
237
+ table,
238
+ headers=["ID", "Version", "Type", "Name"],
239
+ sort_column=0,
240
+ empty_table_message="No installed packages found. "
241
+ "You can use the `package install` command to install packages.",
242
+ )
243
+
244
+
245
+ @click.command(cls=CmemcCommand, name="install")
246
+ @click.argument(
247
+ "PACKAGE_ID",
248
+ required=False,
249
+ type=click.STRING,
250
+ )
251
+ @click.option(
252
+ "--input",
253
+ "-i",
254
+ "input_path",
255
+ type=ClickSmartPath(allow_dash=False, dir_okay=True, file_okay=True, readable=True),
256
+ help="Install a package from a package archive (.cpa) or directory.",
257
+ )
258
+ @click.option(
259
+ "--replace", is_flag=True, help="Replace (overwrite) existing package version, if present."
260
+ )
261
+ @click.pass_obj
262
+ def install_command(
263
+ app: ApplicationContext, package_id: str, replace: bool, input_path: str
264
+ ) -> None:
265
+ """Install packages.
266
+
267
+ This command installs a package either from the marketplace or from local package
268
+ archives (.cpa) or directories.
269
+ """
270
+ if not package_id and not input_path:
271
+ raise CmemcError(
272
+ "Nothing to install. Either specify a package ID from the marketplace, "
273
+ "or use the `--input` option to install a local package."
274
+ )
275
+ if package_id and input_path:
276
+ raise CmemcError(
277
+ "You can not install from the marketplace and local files at the same time."
278
+ )
279
+
280
+ packages = app.client.marketplace_packages
281
+ if input_path:
282
+ package_path = Path(input_path)
283
+ package_version = (
284
+ PackageVersion.from_directory(package_path)
285
+ if package_path.is_dir()
286
+ else PackageVersion.from_archive(package_path)
287
+ )
288
+ package_id = package_version.manifest.package_id
289
+ app.echo_info(f"Installing package '{package_id}' from '{input_path}' ... ", nl=False)
290
+ packages.import_item(
291
+ path=package_path,
292
+ replace=replace,
293
+ configuration=MarketplacePackagesImportConfig(install_from_marketplace=False),
294
+ )
295
+ else:
296
+ app.echo_info(f"Installing package '{package_id}' from marketplace ... ", nl=False)
297
+ packages.import_item(key=package_id, replace=replace)
298
+ app.echo_success("done")
299
+
300
+
301
+ def _filter_installed_packages(
302
+ ctx: click.Context, package_id: str | None, filter_: tuple[tuple[str, str]], all_: bool
303
+ ) -> list[dict]:
304
+ """Filter installed packages."""
305
+ if package_id is None and not filter_ and not all_:
306
+ raise click.UsageError("Either provide a package ID or a filter, or use the --all flag.")
307
+
308
+ if all_:
309
+ packages_to_work_on = installed_packages_list.apply_filters(ctx=ctx)
310
+ else:
311
+ filter_to_apply = list(filter_) if filter_ else []
312
+ if package_id:
313
+ filter_to_apply.append(("id", package_id))
314
+ packages_to_work_on = installed_packages_list.apply_filters(
315
+ ctx=ctx, filter_=filter_to_apply
316
+ )
317
+
318
+ if not packages_to_work_on and package_id:
319
+ raise CmemcError(f"Package '{package_id}' is not installed.")
320
+ return packages_to_work_on
321
+
322
+
323
+ @click.command(cls=CmemcCommand, name="uninstall")
324
+ @click.argument(
325
+ "PACKAGE_ID",
326
+ type=click.STRING,
327
+ shell_complete=_complete_installed_package_ids,
328
+ required=False,
329
+ )
330
+ @click.option(
331
+ "--filter",
332
+ "filter_",
333
+ multiple=True,
334
+ type=(str, str),
335
+ shell_complete=installed_packages_list.complete_values,
336
+ help=installed_packages_list.get_filter_help_text(),
337
+ )
338
+ @click.option(
339
+ "-a",
340
+ "--all",
341
+ "all_",
342
+ is_flag=True,
343
+ help="Uninstall all packages. This is a dangerous option, so use it with care.",
344
+ )
345
+ @click.pass_context
346
+ def uninstall_command(
347
+ ctx: click.Context,
348
+ package_id: str | None,
349
+ filter_: tuple[tuple[str, str]],
350
+ all_: bool,
351
+ ) -> None:
352
+ """Uninstall installed packages."""
353
+ app: ApplicationContext = ctx.obj
354
+ packages_to_uninstall = _filter_installed_packages(
355
+ ctx=ctx, package_id=package_id, filter_=filter_, all_=all_
356
+ )
357
+ packages = app.client.marketplace_packages
358
+ for _ in packages_to_uninstall:
359
+ id_to_uninstall = _["id"]
360
+ app.echo_info(f"Uninstalling package {id_to_uninstall} ... ", nl=False)
361
+ packages.delete_item(id_to_uninstall)
362
+ app.echo_success("done")
363
+
364
+
365
+ @click.command(cls=CmemcCommand, name="export")
366
+ @click.argument(
367
+ "PACKAGE_ID",
368
+ type=click.STRING,
369
+ shell_complete=_complete_installed_package_ids,
370
+ required=False,
371
+ )
372
+ @click.option(
373
+ "--filter",
374
+ "filter_",
375
+ multiple=True,
376
+ type=(str, str),
377
+ shell_complete=installed_packages_list.complete_values,
378
+ help=installed_packages_list.get_filter_help_text(),
379
+ )
380
+ @click.option(
381
+ "-a",
382
+ "--all",
383
+ "all_",
384
+ is_flag=True,
385
+ help="Export all installed packages.",
386
+ )
387
+ @click.option("--replace", is_flag=True, help="Replace (overwrite) existing files, if present.")
388
+ @click.pass_context
389
+ def export_command(
390
+ ctx: click.Context,
391
+ package_id: str | None,
392
+ filter_: tuple[tuple[str, str]],
393
+ all_: bool,
394
+ replace: bool,
395
+ ) -> None:
396
+ """Export installed packages to package directories."""
397
+ app: ApplicationContext = ctx.obj
398
+ packages_to_export = _filter_installed_packages(
399
+ ctx=ctx, package_id=package_id, filter_=filter_, all_=all_
400
+ )
401
+ packages = app.client.marketplace_packages
402
+ for _ in packages_to_export:
403
+ id_to_export = _["id"]
404
+ app.echo_info(f"Exporting package `{id_to_export}` ... ", nl=False)
405
+ configuration = MarketplacePackagesExportConfig(export_as_zip=False)
406
+ packages.export_item(
407
+ id_to_export,
408
+ path=Path(id_to_export),
409
+ replace=replace,
410
+ configuration=configuration,
411
+ )
412
+ app.echo_success("done")
413
+
414
+
415
+ @click.command(cls=CmemcCommand, name="build")
416
+ @click.argument(
417
+ "PACKAGE_DIRECTORY",
418
+ required=True,
419
+ type=ClickSmartPath(
420
+ allow_dash=False,
421
+ dir_okay=True,
422
+ file_okay=False,
423
+ readable=True,
424
+ exists=True,
425
+ remote_okay=False,
426
+ ),
427
+ )
428
+ @click.option("--version", help="Set the package version.")
429
+ @click.option("--replace", is_flag=True, help="Replace package archive, if present.")
430
+ @click.option(
431
+ "--output-dir",
432
+ type=ClickSmartPath(writable=True, file_okay=False, dir_okay=True),
433
+ help="Create the package archive in a specific directory.",
434
+ default=".",
435
+ show_default=True,
436
+ )
437
+ @click.pass_obj
438
+ def build_command(
439
+ app: ApplicationContext, package_directory: str, version: str, replace: bool, output_dir: str
440
+ ) -> None:
441
+ """Build a package archive from a package directory.
442
+
443
+ This command processes a package directory, validates its content including the manifest,
444
+ and creates a versioned Corporate Memory package archive (.cpa) with the following naming
445
+ convention: {package_id}-v{version}.cpa
446
+
447
+ Package archives can be published to the marketplace using the `package publish` command.
448
+ """
449
+ package_path = Path(package_directory)
450
+ package_version = PackageVersion.from_directory(package_path)
451
+ if version:
452
+ if version.startswith("v"):
453
+ version = version[1:]
454
+ package_version.manifest.package_version = SemanticVersion.parse(version)
455
+ version_str = str(package_version.manifest.package_version)
456
+ package_id = package_version.manifest.package_id
457
+
458
+ if output_dir is not None:
459
+ cpa_file = Path(
460
+ os.path.normpath(str(Path(output_dir) / f"{package_id}-v{version_str}.cpa"))
461
+ )
462
+ else:
463
+ cpa_file = Path(f"{package_id}-v{version_str}.cpa")
464
+
465
+ Path(cpa_file).parent.mkdir(exist_ok=True, parents=True)
466
+
467
+ if version_str.endswith("dirty"):
468
+ app.echo_warning(
469
+ "Dirty Repository: Your version strings ends with 'dirty'."
470
+ "This indicates an unclean repository."
471
+ )
472
+ if cpa_file.exists() and not replace:
473
+ raise CmemcError(
474
+ f"Package archive `{cpa_file}` already exists. Use `--replace` to overwrite."
475
+ )
476
+ app.echo_info(f"Building package archive `{cpa_file.name}` ... ", nl=False)
477
+ package_version.build_archive(archive=cpa_file)
478
+ app.echo_success("done")
479
+
480
+
481
+ @click.command(cls=CmemcCommand, name="publish")
482
+ @click.argument(
483
+ "PACKAGE",
484
+ required=True,
485
+ type=ClickSmartPath(
486
+ allow_dash=False,
487
+ dir_okay=True,
488
+ readable=True,
489
+ exists=True,
490
+ remote_okay=True,
491
+ ),
492
+ )
493
+ @click.option(
494
+ "--marketplace-url",
495
+ type=str,
496
+ help="Alternative Marketplace URL.",
497
+ default="https://marketplace.eccenca.dev/",
498
+ )
499
+ @click.pass_obj
500
+ def publish_command(app: ApplicationContext, package: str, marketplace_url: str) -> None:
501
+ """Publish a package archive to the marketplace server."""
502
+ package_path = Path(package)
503
+
504
+ package_version = (
505
+ PackageVersion.from_directory(package_path)
506
+ if package_path.is_dir()
507
+ else PackageVersion.from_archive(package_path)
508
+ )
509
+ package_id = package_version.manifest.package_id
510
+
511
+ app.echo_info(f"Publishing package `{package_id}` ... ", nl=False)
512
+
513
+ if package_path.is_dir():
514
+ package_data = b"".join(PackageVersion.create_archive(package_path))
515
+ filename = f"{package_id}.cpa"
516
+ else:
517
+ package_data = package_path.read_bytes()
518
+ filename = package_path.name
519
+
520
+ if marketplace_url.endswith("/"):
521
+ marketplace_url = marketplace_url[:-1]
522
+
523
+ files = {"archive": (filename, package_data, "application/octet-stream")}
524
+ url = f"{marketplace_url}/api/packages/{package_id}/versions"
525
+ response = requests.post(
526
+ url=url,
527
+ timeout=30,
528
+ files=files,
529
+ headers={"accept": "application/json"},
530
+ )
531
+
532
+ response.raise_for_status()
533
+ app.echo_success("done")
534
+
535
+
536
+ @click.group(cls=CmemcGroup)
537
+ def package_group() -> CmemcGroup: # type: ignore[empty-body]
538
+ """List, (un)install, export, create, or inspect packages."""
539
+
540
+
541
+ package_group.add_command(create_command)
542
+ package_group.add_command(inspect_command)
543
+ package_group.add_command(list_command)
544
+ package_group.add_command(install_command)
545
+ package_group.add_command(uninstall_command)
546
+ package_group.add_command(export_command)
547
+ package_group.add_command(build_command)
548
+ package_group.add_command(publish_command)