cmem-cmemc 25.6.0__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 (39) hide show
  1. cmem_cmemc/cli.py +11 -6
  2. cmem_cmemc/command.py +1 -1
  3. cmem_cmemc/command_group.py +27 -0
  4. cmem_cmemc/commands/acl.py +388 -20
  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 +162 -118
  9. cmem_cmemc/commands/file.py +117 -73
  10. cmem_cmemc/commands/graph.py +200 -72
  11. cmem_cmemc/commands/graph_imports.py +12 -5
  12. cmem_cmemc/commands/graph_insights.py +61 -25
  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 +155 -22
  17. cmem_cmemc/commands/python.py +8 -4
  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 +18 -13
  25. cmem_cmemc/commands/workflow.py +21 -11
  26. cmem_cmemc/completion.py +105 -105
  27. cmem_cmemc/context.py +38 -8
  28. cmem_cmemc/exceptions.py +8 -2
  29. cmem_cmemc/manual_helper/multi_page.py +0 -1
  30. cmem_cmemc/object_list.py +234 -7
  31. cmem_cmemc/string_processor.py +142 -5
  32. cmem_cmemc/title_helper.py +50 -0
  33. cmem_cmemc/utils.py +8 -7
  34. {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/METADATA +6 -6
  35. cmem_cmemc-26.1.0rc1.dist-info/RECORD +62 -0
  36. {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/WHEEL +1 -1
  37. cmem_cmemc-25.6.0.dist-info/RECORD +0 -61
  38. {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/entry_points.txt +0 -0
  39. {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,19 @@
1
1
  """metrics commands for cmem command line interface."""
2
2
 
3
3
  import click
4
- from click import Argument, ClickException, Context, UsageError
4
+ from click import Argument, Context, UsageError
5
5
  from click.shell_completion import CompletionItem
6
6
  from cmem.cmempy.api import request
7
- from cmem.cmempy.config import get_cmem_base_uri, get_di_api_endpoint, get_dp_api_endpoint
7
+ from cmem.cmempy.config import get_di_api_endpoint, get_dp_api_endpoint
8
8
  from prometheus_client.parser import text_string_to_metric_families
9
9
  from requests import HTTPError
10
10
 
11
11
  from cmem_cmemc import completion
12
12
  from cmem_cmemc.command import CmemcCommand
13
13
  from cmem_cmemc.command_group import CmemcGroup
14
- from cmem_cmemc.context import ApplicationContext
14
+ from cmem_cmemc.completion import suppress_completion_errors
15
+ from cmem_cmemc.context import ApplicationContext, build_caption
16
+ from cmem_cmemc.exceptions import CmemcError
15
17
  from cmem_cmemc.object_list import (
16
18
  DirectValuePropertyFilter,
17
19
  ObjectList,
@@ -94,6 +96,7 @@ metrics_list = ObjectList(
94
96
  )
95
97
 
96
98
 
99
+ @suppress_completion_errors
97
100
  def _complete_metrics_id(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]: # noqa: ARG001
98
101
  """Prepare a list of metric identifier."""
99
102
  ApplicationContext.set_connection_from_params(ctx.find_root().params)
@@ -101,6 +104,7 @@ def _complete_metrics_id(ctx: Context, param: Argument, incomplete: str) -> list
101
104
  return completion.finalize_completion(candidates=candidates, incomplete=incomplete)
102
105
 
103
106
 
107
+ @suppress_completion_errors
104
108
  def _complete_metric_label_filter(
105
109
  ctx: Context,
106
110
  param: Argument, # noqa: ARG001
@@ -141,11 +145,11 @@ def _filter_samples(family: dict, label_filter: tuple[tuple[str, str], ...]) ->
141
145
  sample_labels = sample[1]
142
146
  for name, value in label_filter:
143
147
  if name not in labels:
144
- raise ClickException(
148
+ raise CmemcError(
145
149
  f"The metric '{family_name}' does " f"not have a label named '{name}'."
146
150
  )
147
151
  if value not in labels[name]:
148
- raise ClickException(
152
+ raise CmemcError(
149
153
  f"The metric '{family_name}' does "
150
154
  f"not have a label '{name}' with the value '{value}'."
151
155
  )
@@ -157,7 +161,6 @@ def _filter_samples(family: dict, label_filter: tuple[tuple[str, str], ...]) ->
157
161
  return samples
158
162
 
159
163
 
160
- # pylint: disable-msg=too-many-arguments
161
164
  @click.command(cls=CmemcCommand, name="get")
162
165
  @click.argument("metric_id", required=True, type=click.STRING, shell_complete=_complete_metrics_id)
163
166
  @click.option(
@@ -214,7 +217,7 @@ def get_command(
214
217
  return
215
218
 
216
219
  if len(samples) == 0:
217
- raise ClickException(
220
+ raise CmemcError(
218
221
  "No data - the given label combination filtered out "
219
222
  f"all available samples of the metric {metric_id}."
220
223
  )
@@ -308,12 +311,15 @@ def list_command(
308
311
  ]
309
312
  for _ in data
310
313
  ]
314
+ filtered = len(filter_) > 0
311
315
  app.echo_info_table(
312
316
  table,
313
317
  headers=["ID", "Type", "L", "S", "Documentation"],
314
318
  sort_column=0,
315
- caption=f"{len(table)} metrics families of {get_cmem_base_uri()}",
316
- empty_table_message="No metrics families available.",
319
+ caption=build_caption(len(table), "metric", filtered=filtered),
320
+ empty_table_message="No metrics found for these filters."
321
+ if filtered
322
+ else "No metrics available.",
317
323
  )
318
324
 
319
325
 
@@ -8,8 +8,12 @@ from click.shell_completion import CompletionItem
8
8
 
9
9
  from cmem_cmemc.command import CmemcCommand
10
10
  from cmem_cmemc.command_group import CmemcGroup
11
- from cmem_cmemc.completion import check_option_in_params, finalize_completion
12
- from cmem_cmemc.context import ApplicationContext
11
+ from cmem_cmemc.completion import (
12
+ check_option_in_params,
13
+ finalize_completion,
14
+ suppress_completion_errors,
15
+ )
16
+ from cmem_cmemc.context import ApplicationContext, build_caption
13
17
  from cmem_cmemc.migrations.access_conditions_243 import (
14
18
  MoveAccessConditionsToNewGraph,
15
19
  RenameAuthVocabularyResources,
@@ -65,6 +69,7 @@ def get_migrations(ctx: click.Context) -> list[dict]: # noqa: ARG001
65
69
  return data
66
70
 
67
71
 
72
+ @suppress_completion_errors
68
73
  def complete_migration_ids(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]:
69
74
  """Prepare a list of migration recipe IDs"""
70
75
  ApplicationContext.set_connection_from_params(ctx.find_root().params)
@@ -153,12 +158,15 @@ def list_command(
153
158
  ]
154
159
  for _ in data
155
160
  ]
161
+ filtered = len(filter_) > 0
156
162
  app.echo_info_table(
157
163
  table,
158
164
  headers=["ID", "Description", "Tags", "First Version"],
159
165
  sort_column=3,
160
- caption=f"{len(table)} migration(s)",
161
- empty_table_message="No migrations available.",
166
+ caption=build_caption(len(table), "migration", filtered=filtered),
167
+ empty_table_message="No migrations found for these filters."
168
+ if filtered
169
+ else "No migrations available.",
162
170
  )
163
171
 
164
172
 
@@ -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)