cmem-cmemc 26.1.0rc1__py3-none-any.whl → 26.1.0rc3__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.
@@ -33,6 +33,7 @@ from cmem_cmemc.smart_path import SmartPath as Path
33
33
  from cmem_cmemc.utils import (
34
34
  convert_iri_to_qname,
35
35
  convert_qname_to_iri,
36
+ get_objects_to_delete,
36
37
  get_query_text,
37
38
  struct_to_table,
38
39
  )
@@ -172,53 +173,6 @@ acl_list = ObjectList(
172
173
  )
173
174
 
174
175
 
175
- def _validate_acl_ids(access_condition_ids: tuple[str, ...]) -> None:
176
- """Validate that all provided access condition IDs exist."""
177
- if not access_condition_ids:
178
- return
179
- all_acls = fetch_all_acls()
180
- all_iris = {acl["iri"] for acl in all_acls}
181
- for acl_id in access_condition_ids:
182
- iri = convert_qname_to_iri(qname=acl_id, default_ns=NS_ACL)
183
- if iri not in all_iris:
184
- raise click.ClickException(
185
- f"Access condition {acl_id} not available. Use the 'admin acl list' "
186
- "command to get a list of existing access conditions."
187
- )
188
-
189
-
190
- def _get_acls_to_delete(
191
- ctx: Context,
192
- access_condition_ids: tuple[str, ...],
193
- all_: bool,
194
- filter_: tuple[tuple[str, str], ...],
195
- ) -> list[dict]:
196
- """Get the list of access conditions to delete based on selection method."""
197
- if all_:
198
- # Get all access conditions
199
- return fetch_all_acls() # type: ignore[no-any-return]
200
-
201
- # Validate provided IDs exist before proceeding
202
- _validate_acl_ids(access_condition_ids)
203
-
204
- # Build filter list
205
- filter_to_apply = list(filter_) if filter_ else []
206
-
207
- # Add IDs if provided (using internal multi-value filter)
208
- if access_condition_ids:
209
- iris = [convert_qname_to_iri(qname=_, default_ns=NS_ACL) for _ in access_condition_ids]
210
- filter_to_apply.append(("ids", ",".join(iris)))
211
-
212
- # Apply filters
213
- acls = acl_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
214
-
215
- # Validation: ensure we found access conditions
216
- if not acls and not access_condition_ids:
217
- raise click.ClickException("No access conditions found matching the provided filters.")
218
-
219
- return acls
220
-
221
-
222
176
  @click.command(cls=CmemcCommand, name="list")
223
177
  @click.option("--raw", is_flag=True, help="Outputs raw JSON.")
224
178
  @click.option(
@@ -645,8 +599,23 @@ def delete_command(
645
599
  "Either specify access condition IDs OR use a --filter or the --all option."
646
600
  )
647
601
 
602
+ # Convert qnames to IRIs for filtering
603
+ iris = [convert_qname_to_iri(qname=_, default_ns=NS_ACL) for _ in access_condition_ids]
604
+
648
605
  # Get access conditions to delete based on selection method
649
- acls_to_delete = _get_acls_to_delete(ctx, access_condition_ids, all_, filter_)
606
+ acls_to_delete = get_objects_to_delete(
607
+ ctx=ctx,
608
+ ids=tuple(iris),
609
+ all_=all_,
610
+ filter_=filter_,
611
+ object_list=acl_list,
612
+ get_all_objects=fetch_all_acls,
613
+ extract_id=lambda x: x["iri"],
614
+ error_message_template=(
615
+ "Access condition {} not available. "
616
+ "Use the 'admin acl list' command to get a list of existing access conditions."
617
+ ),
618
+ )
650
619
 
651
620
  # Avoid double removal as well as sort IRIs
652
621
  iris_to_delete = sorted({acl["iri"] for acl in acls_to_delete}, key=lambda v: v.lower())
@@ -38,18 +38,26 @@ from cmem_cmemc.parameter_types.path import ClickSmartPath
38
38
  from cmem_cmemc.smart_path import SmartPath as Path
39
39
  from cmem_cmemc.string_processor import DatasetLink, DatasetTypeLink
40
40
  from cmem_cmemc.title_helper import DatasetTypeTitleHelper, ProjectTitleHelper
41
- from cmem_cmemc.utils import check_or_select_project, struct_to_table
41
+ from cmem_cmemc.utils import check_or_select_project, get_objects_to_delete, struct_to_table
42
42
 
43
43
 
44
- def get_datasets(ctx: Context) -> list[dict]:
45
- """Get datasets for object list."""
44
+ def get_datasets_for_list(ctx: Context) -> list[dict]:
45
+ """Get datasets for object list, transforming structure for filtering."""
46
46
  _ = ctx
47
- return list_items(item_type="dataset")["results"] # type: ignore[no-any-return]
47
+ # Transform the dataset structure to add combined ID for easier filtering
48
+ transformed = []
49
+ for dataset in list_items(item_type="dataset")["results"]:
50
+ transformed_dataset = {
51
+ **dataset, # Keep all original fields
52
+ "combinedId": f"{dataset['projectId']}:{dataset['id']}", # Add combined ID
53
+ }
54
+ transformed.append(transformed_dataset)
55
+ return transformed
48
56
 
49
57
 
50
58
  dataset_list = ObjectList(
51
59
  name="datasets",
52
- get_objects=get_datasets,
60
+ get_objects=get_datasets_for_list,
53
61
  filters=[
54
62
  DirectValuePropertyFilter(
55
63
  name="project",
@@ -84,6 +92,11 @@ dataset_list = ObjectList(
84
92
  description="Internal filter for multiple dataset IDs.",
85
93
  property_key="id",
86
94
  ),
95
+ DirectMultiValuePropertyFilter(
96
+ name="combinedIds",
97
+ description="Internal filter for multiple combined dataset IDs (projectId:datasetId).",
98
+ property_key="combinedId",
99
+ ),
87
100
  ],
88
101
  )
89
102
 
@@ -111,57 +124,6 @@ def _validate_and_split_dataset_id(dataset_id: str) -> tuple[str, str]:
111
124
  return project_part, dataset_part
112
125
 
113
126
 
114
- def _validate_dataset_ids(dataset_ids: tuple[str, ...]) -> None:
115
- """Validate that all provided dataset IDs exist."""
116
- if not dataset_ids:
117
- return
118
- all_datasets = list_items(item_type="dataset")["results"]
119
- all_dataset_ids = [_["projectId"] + ":" + _["id"] for _ in all_datasets]
120
- for dataset_id in dataset_ids:
121
- if dataset_id not in all_dataset_ids:
122
- raise CmemcError(
123
- f"Dataset {dataset_id} not available. Use the 'dataset list' "
124
- "command to get a list of existing datasets."
125
- )
126
-
127
-
128
- def _get_datasets_to_delete(
129
- ctx: Context,
130
- dataset_ids: tuple[str, ...],
131
- all_: bool,
132
- filter_: tuple[tuple[str, str], ...],
133
- ) -> list[str]:
134
- """Get the list of dataset IDs to delete based on selection method."""
135
- if all_:
136
- # Get all datasets
137
- datasets = list_items(item_type="dataset")["results"]
138
- return [_["projectId"] + ":" + _["id"] for _ in datasets]
139
-
140
- # Validate provided IDs exist before proceeding
141
- _validate_dataset_ids(dataset_ids)
142
-
143
- # Build filter list
144
- filter_to_apply = list(filter_) if filter_ else []
145
-
146
- # Add IDs if provided (using internal multi-value filter)
147
- if dataset_ids:
148
- # Extract just the dataset ID part (after the colon) for filtering
149
- dataset_id_parts = [_.split(":")[1] for _ in dataset_ids]
150
- filter_to_apply.append(("ids", ",".join(dataset_id_parts)))
151
-
152
- # Apply filters
153
- datasets = dataset_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
154
-
155
- # Build full dataset IDs
156
- result = [_["projectId"] + ":" + _["id"] for _ in datasets]
157
-
158
- # Validation: ensure we found datasets
159
- if not result and not dataset_ids:
160
- raise CmemcError("No datasets found matching the provided filters.")
161
-
162
- return result
163
-
164
-
165
127
  def _post_file_resource(
166
128
  app: ApplicationContext,
167
129
  project_id: str,
@@ -500,10 +462,23 @@ def delete_command(
500
462
  raise UsageError("Either specify a dataset ID OR" " use a --filter or the --all option.")
501
463
 
502
464
  # Get datasets to delete based on selection method
503
- datasets_to_delete = _get_datasets_to_delete(ctx, dataset_ids, all_, filter_)
465
+ datasets_data = get_objects_to_delete(
466
+ ctx=ctx,
467
+ ids=dataset_ids,
468
+ all_=all_,
469
+ filter_=filter_,
470
+ object_list=dataset_list,
471
+ get_all_objects=lambda: get_datasets_for_list(ctx),
472
+ extract_id=lambda x: x["combinedId"],
473
+ error_message_template=(
474
+ "Dataset {} not available. "
475
+ "Use the 'dataset list' command to get a list of existing datasets."
476
+ ),
477
+ id_filter_name="combinedIds",
478
+ )
504
479
 
505
480
  # Avoid double removal as well as sort IDs
506
- processed_ids = sorted(set(datasets_to_delete), key=lambda v: v.lower())
481
+ processed_ids = sorted({_["combinedId"] for _ in datasets_data}, key=lambda v: v.lower())
507
482
  count = len(processed_ids)
508
483
 
509
484
  # Delete each dataset
@@ -27,7 +27,12 @@ from cmem_cmemc.object_list import (
27
27
  from cmem_cmemc.parameter_types.path import ClickSmartPath
28
28
  from cmem_cmemc.smart_path import SmartPath as Path
29
29
  from cmem_cmemc.string_processor import FileSize, TimeAgo
30
- from cmem_cmemc.utils import check_or_select_project, split_task_id, struct_to_table
30
+ from cmem_cmemc.utils import (
31
+ check_or_select_project,
32
+ get_objects_to_delete,
33
+ split_task_id,
34
+ struct_to_table,
35
+ )
31
36
 
32
37
 
33
38
  def get_resources(ctx: Context) -> list[dict]: # noqa: ARG001
@@ -120,48 +125,6 @@ def _upload_file_resource(
120
125
  app.echo_success("done")
121
126
 
122
127
 
123
- def _validate_resource_ids(resource_ids: tuple[str, ...]) -> None:
124
- """Validate that all provided resource IDs exist."""
125
- if not resource_ids:
126
- return
127
- all_resources = get_all_resources()
128
- all_resource_ids = [_["id"] for _ in all_resources]
129
- for resource_id in resource_ids:
130
- if resource_id not in all_resource_ids:
131
- raise CmemcError(f"Resource {resource_id} not available.")
132
-
133
-
134
- def _get_resources_to_delete(
135
- ctx: Context,
136
- resource_ids: tuple[str, ...],
137
- all_: bool,
138
- filter_: tuple[tuple[str, str], ...],
139
- ) -> list[dict]:
140
- """Get the list of resources to delete based on selection method."""
141
- if all_:
142
- _: list[dict] = get_all_resources()
143
- return _
144
-
145
- # Validate provided IDs exist before proceeding
146
- _validate_resource_ids(resource_ids)
147
-
148
- # Build filter list
149
- filter_to_apply = list(filter_) if filter_ else []
150
-
151
- # Add IDs if provided (using internal multi-value filter)
152
- if resource_ids:
153
- filter_to_apply.append(("ids", ",".join(resource_ids)))
154
-
155
- # Apply filters
156
- resources = resource_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
157
-
158
- # Validation: ensure we found resources
159
- if not resources and not resource_ids:
160
- raise CmemcError("No resources found matching the provided filters.")
161
-
162
- return resources
163
-
164
-
165
128
  @click.command(cls=CmemcCommand, name="list")
166
129
  @click.option("--raw", is_flag=True, help="Outputs raw JSON.")
167
130
  @click.option(
@@ -261,8 +224,16 @@ def delete_command(
261
224
  "--filter options to specify resources for deletion."
262
225
  )
263
226
 
264
- # Get resources to delete based on selection method
265
- resources_to_delete = _get_resources_to_delete(ctx, resource_ids, all_, filter_)
227
+ resources_to_delete = get_objects_to_delete(
228
+ ctx=ctx,
229
+ ids=resource_ids,
230
+ all_=all_,
231
+ filter_=filter_,
232
+ object_list=resource_list,
233
+ get_all_objects=get_all_resources,
234
+ extract_id=lambda x: x["id"],
235
+ error_message_template="Resource {} not available.",
236
+ )
266
237
 
267
238
  # Avoid double removal as well as sort IDs
268
239
  processed_ids = sorted({_["id"] for _ in resources_to_delete}, key=lambda v: v.lower())
@@ -47,6 +47,7 @@ from cmem_cmemc.utils import (
47
47
  convert_uri_to_filename,
48
48
  get_graphs,
49
49
  get_graphs_as_dict,
50
+ get_objects_to_delete,
50
51
  iri_to_qname,
51
52
  read_rdf_graph_files,
52
53
  tuple_to_list,
@@ -231,49 +232,6 @@ def _add_imported_graphs(iris: list[str], all_graphs: dict) -> list[str]:
231
232
  return list(set(extended_list))
232
233
 
233
234
 
234
- def _validate_graph_iris(iris: tuple[str, ...]) -> None:
235
- """Validate that all provided graph IRIs exist."""
236
- if not iris:
237
- return
238
- all_graphs = get_graphs_as_dict()
239
- for iri in iris:
240
- if iri not in all_graphs:
241
- raise CmemcError(UNKNOWN_GRAPH_ERROR.format(iri))
242
-
243
-
244
- def _get_graphs_to_delete(
245
- ctx: Context,
246
- iris: tuple[str, ...],
247
- all_: bool,
248
- filter_: tuple[tuple[str, str], ...],
249
- ) -> list[dict]:
250
- """Get the list of graphs to delete based on selection method."""
251
- if all_:
252
- return get_graphs(writeable=True, readonly=False)
253
-
254
- # Validate provided IRIs exist before proceeding
255
- _validate_graph_iris(iris)
256
-
257
- # Build filter list
258
- filter_to_apply = list(filter_) if filter_ else []
259
-
260
- # Add IRIs if provided (using internal multi-value filter)
261
- if iris:
262
- filter_to_apply.append(("iris", ",".join(iris)))
263
-
264
- # Apply filters to writeable graphs only
265
- writeable_graphs = get_graphs(writeable=True, readonly=False)
266
- graphs = graph_delete_obj.apply_filters(
267
- ctx=ctx, filter_=filter_to_apply, objects=writeable_graphs
268
- )
269
-
270
- # Validation: ensure we found graphs
271
- if not graphs:
272
- raise CmemcError("No graphs found matching the provided criteria.")
273
-
274
- return graphs
275
-
276
-
277
235
  def _check_and_extend_exported_graphs(
278
236
  iris: list[str], all_flag: bool, imported_flag: bool, all_graphs: dict
279
237
  ) -> list[str]:
@@ -919,8 +877,18 @@ def delete_command( # noqa: PLR0913
919
877
  "--filter options to specify graphs for deletion."
920
878
  )
921
879
 
922
- # Get base list of graphs to delete using ObjectList filtering
923
- graphs_to_delete = _get_graphs_to_delete(ctx, iris, all_, filter_)
880
+ # Get base list of graphs to delete (only writeable graphs)
881
+ graphs_to_delete = get_objects_to_delete(
882
+ ctx=ctx,
883
+ ids=iris,
884
+ all_=all_,
885
+ filter_=filter_,
886
+ object_list=graph_delete_obj,
887
+ get_all_objects=lambda: get_graphs(writeable=True, readonly=False),
888
+ extract_id=lambda x: x["iri"],
889
+ error_message_template=UNKNOWN_GRAPH_ERROR,
890
+ id_filter_name="iris",
891
+ )
924
892
  iris_to_delete = [g["iri"] for g in graphs_to_delete]
925
893
 
926
894
  # Handle --include-imports flag
@@ -27,7 +27,11 @@ from cmem_cmemc.object_list import (
27
27
  transform_lower,
28
28
  )
29
29
  from cmem_cmemc.string_processor import GraphLink, TimeAgo
30
- from cmem_cmemc.utils import get_graphs_as_dict, struct_to_table
30
+ from cmem_cmemc.utils import (
31
+ get_graphs_as_dict,
32
+ get_objects_to_delete,
33
+ struct_to_table,
34
+ )
31
35
 
32
36
 
33
37
  def get_api_url(path: str = "") -> str:
@@ -242,6 +246,7 @@ def delete_command(
242
246
  if snapshot_ids and (all_ or filter_):
243
247
  raise click.UsageError("Either specify snapshot IDs OR use a --filter or the --all option.")
244
248
 
249
+ # Special case: --all uses bulk DELETE endpoint
245
250
  if all_:
246
251
  app.echo_info("Deleting all snapshots ... ", nl=False)
247
252
  request(method="DELETE", uri=get_api_url("/snapshot"))
@@ -249,20 +254,19 @@ def delete_command(
249
254
  return
250
255
 
251
256
  # Get snapshots to delete based on selection method
252
- filter_to_apply = list(filter_) if filter_ else []
253
- if snapshot_ids:
254
- # Use internal multi-value filter for multiple IDs
255
- filter_to_apply.append(("ids", ",".join(snapshot_ids)))
256
- snapshots_to_delete = snapshot_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
257
-
258
- if not snapshots_to_delete and snapshot_ids:
259
- raise CmemcError(
260
- f"Snapshot ID(s) {', '.join(snapshot_ids)} not found. "
257
+ snapshots_to_delete = get_objects_to_delete(
258
+ ctx=ctx,
259
+ ids=snapshot_ids,
260
+ all_=False, # Already handled above
261
+ filter_=filter_,
262
+ object_list=snapshot_list,
263
+ get_all_objects=lambda: get_snapshots(ctx),
264
+ extract_id=lambda x: x["databaseId"],
265
+ error_message_template=(
266
+ "Snapshot {} not found. "
261
267
  "Use the 'graph insights list' command to get a list of existing snapshots."
262
- )
263
-
264
- if not snapshots_to_delete and not snapshot_ids:
265
- raise CmemcError("No snapshots found to delete.")
268
+ ),
269
+ )
266
270
 
267
271
  # Avoid double removal as well as sort IDs
268
272
  ids_to_delete = sorted({_["databaseId"] for _ in snapshots_to_delete}, key=lambda v: v.lower())
@@ -8,12 +8,12 @@ import click
8
8
  import requests
9
9
  from click.shell_completion import CompletionItem
10
10
  from cmem_client.client import Client
11
+ from cmem_client.components.marketplace import Marketplace
11
12
  from cmem_client.repositories.marketplace_packages import (
12
13
  MarketplacePackagesExportConfig,
13
14
  MarketplacePackagesImportConfig,
14
15
  MarketplacePackagesRepository,
15
16
  )
16
- from eccenca_marketplace_client.manifests import AbstractPackageManifest, ManifestMetadata
17
17
  from eccenca_marketplace_client.package_version import PackageVersion
18
18
  from pydantic_extra_types.semantic_version import SemanticVersion
19
19
 
@@ -93,53 +93,18 @@ def _complete_installed_package_ids(
93
93
  return completion.finalize_completion(candidates=candidates, incomplete=incomplete)
94
94
 
95
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")
96
+ @suppress_completion_errors
97
+ def _complete_marketplace_package_ids(
98
+ ctx: click.Context,
99
+ param: click.Argument, # noqa: ARG001
100
+ incomplete: str,
101
+ ) -> list[CompletionItem]:
102
+ """Prepare a list of IDs of available packages."""
103
+ ApplicationContext.set_connection_from_params(ctx.find_root().params)
104
+ client = Client.from_cmempy()
105
+ marketplace = Marketplace(client=client)
106
+ candidates = [(_.id, f"{_.name} ({_.type})") for _ in marketplace.get_available_packages()]
107
+ return completion.finalize_completion(candidates=candidates, incomplete=incomplete)
143
108
 
144
109
 
145
110
  @click.command(cls=CmemcCommand, name="inspect")
@@ -165,7 +130,9 @@ def inspect_command(app: ApplicationContext, package_path: Path, key: str, raw:
165
130
  """Inspect the manifest of a package."""
166
131
  path = Path(package_path)
167
132
  package_version = (
168
- PackageVersion.from_directory(path) if path.is_dir() else PackageVersion.from_archive(path)
133
+ PackageVersion.from_directory(path, validate_files=False)
134
+ if path.is_dir()
135
+ else PackageVersion.from_archive(path)
169
136
  )
170
137
  manifest = package_version.manifest
171
138
  manifest_data = json.loads(manifest.model_dump_json(indent=2))
@@ -247,6 +214,7 @@ def list_command(
247
214
  "PACKAGE_ID",
248
215
  required=False,
249
216
  type=click.STRING,
217
+ shell_complete=_complete_marketplace_package_ids,
250
218
  )
251
219
  @click.option(
252
220
  "--input",
@@ -281,7 +249,7 @@ def install_command(
281
249
  if input_path:
282
250
  package_path = Path(input_path)
283
251
  package_version = (
284
- PackageVersion.from_directory(package_path)
252
+ PackageVersion.from_directory(package_path, validate_files=False)
285
253
  if package_path.is_dir()
286
254
  else PackageVersion.from_archive(package_path)
287
255
  )
@@ -447,7 +415,7 @@ def build_command(
447
415
  Package archives can be published to the marketplace using the `package publish` command.
448
416
  """
449
417
  package_path = Path(package_directory)
450
- package_version = PackageVersion.from_directory(package_path)
418
+ package_version = PackageVersion.from_directory(package_path, validate_files=False)
451
419
  if version:
452
420
  if version.startswith("v"):
453
421
  version = version[1:]
@@ -466,25 +434,32 @@ def build_command(
466
434
 
467
435
  if version_str.endswith("dirty"):
468
436
  app.echo_warning(
469
- "Dirty Repository: Your version strings ends with 'dirty'."
437
+ "Dirty Repository: Your version string ends with 'dirty'. "
470
438
  "This indicates an unclean repository."
471
439
  )
440
+ if version_str == "0.0.0":
441
+ raise CmemcError(
442
+ "Invalid Version 0.0.0: "
443
+ "Use the --version option to override this, or change the manifest."
444
+ )
472
445
  if cpa_file.exists() and not replace:
473
446
  raise CmemcError(
474
447
  f"Package archive `{cpa_file}` already exists. Use `--replace` to overwrite."
475
448
  )
476
449
  app.echo_info(f"Building package archive `{cpa_file.name}` ... ", nl=False)
450
+ package_version.validate_text_files(package_version.manifest, package_path)
451
+ package_version.validate_image_files(package_version.manifest, package_path)
477
452
  package_version.build_archive(archive=cpa_file)
478
453
  app.echo_success("done")
479
454
 
480
455
 
481
456
  @click.command(cls=CmemcCommand, name="publish")
482
457
  @click.argument(
483
- "PACKAGE",
458
+ "PACKAGE_ARCHIVE",
484
459
  required=True,
485
460
  type=ClickSmartPath(
486
461
  allow_dash=False,
487
- dir_okay=True,
462
+ dir_okay=False,
488
463
  readable=True,
489
464
  exists=True,
490
465
  remote_okay=True,
@@ -497,25 +472,16 @@ def build_command(
497
472
  default="https://marketplace.eccenca.dev/",
498
473
  )
499
474
  @click.pass_obj
500
- def publish_command(app: ApplicationContext, package: str, marketplace_url: str) -> None:
475
+ def publish_command(app: ApplicationContext, package_archive: str, marketplace_url: str) -> None:
501
476
  """Publish a package archive to the marketplace server."""
502
- package_path = Path(package)
477
+ package_path = Path(package_archive)
503
478
 
504
- package_version = (
505
- PackageVersion.from_directory(package_path)
506
- if package_path.is_dir()
507
- else PackageVersion.from_archive(package_path)
508
- )
479
+ package_version = PackageVersion.from_archive(package_path)
509
480
  package_id = package_version.manifest.package_id
510
481
 
511
482
  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
483
+ package_data = package_path.read_bytes()
484
+ filename = package_path.name
519
485
 
520
486
  if marketplace_url.endswith("/"):
521
487
  marketplace_url = marketplace_url[:-1]
@@ -538,7 +504,6 @@ def package_group() -> CmemcGroup: # type: ignore[empty-body]
538
504
  """List, (un)install, export, create, or inspect packages."""
539
505
 
540
506
 
541
- package_group.add_command(create_command)
542
507
  package_group.add_command(inspect_command)
543
508
  package_group.add_command(list_command)
544
509
  package_group.add_command(install_command)
@@ -38,7 +38,7 @@ from cmem_cmemc.context import ApplicationContext, build_caption
38
38
  from cmem_cmemc.exceptions import CmemcError
39
39
  from cmem_cmemc.object_list import (
40
40
  DirectListPropertyFilter,
41
- DirectValuePropertyFilter,
41
+ DirectMultiValuePropertyFilter,
42
42
  Filter,
43
43
  MultiFieldPropertyFilter,
44
44
  ObjectList,
@@ -46,6 +46,7 @@ from cmem_cmemc.object_list import (
46
46
  from cmem_cmemc.parameter_types.path import ClickSmartPath
47
47
  from cmem_cmemc.smart_path import SmartPath as Path
48
48
  from cmem_cmemc.string_processor import ProjectLink, TimeAgo
49
+ from cmem_cmemc.utils import get_objects_to_delete
49
50
 
50
51
 
51
52
  def get_projects_for_list(ctx: Context) -> list[dict]:
@@ -90,11 +91,10 @@ project_list = ObjectList(
90
91
  name="projects",
91
92
  get_objects=get_projects_for_list,
92
93
  filters=[
93
- DirectValuePropertyFilter(
94
- name="id",
95
- description="Filter by project ID (name).",
94
+ DirectMultiValuePropertyFilter(
95
+ name="ids",
96
+ description="Internal filter for multiple projects.",
96
97
  property_key="name",
97
- completion_method="values",
98
98
  ),
99
99
  MultiFieldPropertyFilter(
100
100
  name="regex",
@@ -296,15 +296,20 @@ def delete_command(
296
296
  if project_ids and (all_ or filter_):
297
297
  raise UsageError("Either specify a project ID OR use a --filter or the --all option.")
298
298
 
299
- if all_ or filter_:
300
- # in case --all or --filter is given, a list of projects is fetched
301
- project_ids = []
302
- filtered_projects = project_list.apply_filters(ctx=ctx, filter_=filter_)
303
- for _ in filtered_projects:
304
- project_ids.append(_["name"])
299
+ # Get projects to delete based on selection method
300
+ projects_data = get_objects_to_delete(
301
+ ctx=ctx,
302
+ ids=project_ids,
303
+ all_=all_,
304
+ filter_=filter_,
305
+ object_list=project_list,
306
+ get_all_objects=get_projects,
307
+ extract_id=lambda x: x["name"],
308
+ error_message_template="Project {} not available.",
309
+ )
305
310
 
306
311
  # Avoid double removal as well as sort project IDs
307
- projects_to_delete = sorted(set(project_ids), key=lambda v: v.lower())
312
+ projects_to_delete = sorted({_["name"] for _ in projects_data}, key=lambda v: v.lower())
308
313
  count = len(projects_to_delete)
309
314
  for current, project_id in enumerate(projects_to_delete, start=1):
310
315
  current_string = str(current).zfill(len(str(count)))
@@ -31,6 +31,7 @@ from cmem_cmemc.object_list import (
31
31
  compare_regex,
32
32
  transform_lower,
33
33
  )
34
+ from cmem_cmemc.utils import get_objects_to_delete
34
35
 
35
36
  NO_USER_ERROR = (
36
37
  "{} is not a valid user account. Use the 'admin user list' command "
@@ -81,52 +82,6 @@ user_list = ObjectList(
81
82
  )
82
83
 
83
84
 
84
- def _validate_usernames(usernames: tuple[str, ...]) -> None:
85
- """Validate that all provided usernames exist."""
86
- if not usernames:
87
- return
88
- all_users = list_users()
89
- all_usernames = [user["username"] for user in all_users]
90
- for username in usernames:
91
- if username not in all_usernames:
92
- raise CmemcError(NO_USER_ERROR.format(username))
93
-
94
-
95
- def _get_users_to_delete(
96
- ctx: click.Context,
97
- usernames: tuple[str, ...],
98
- all_: bool,
99
- filter_: tuple[tuple[str, str], ...],
100
- ) -> list[str]:
101
- """Get the list of usernames to delete based on selection method."""
102
- if all_:
103
- # Get all users
104
- users = list_users()
105
- return [user["username"] for user in users]
106
-
107
- # Validate provided usernames exist before proceeding
108
- _validate_usernames(usernames)
109
-
110
- # Build filter list
111
- filter_to_apply = list(filter_) if filter_ else []
112
-
113
- # Add usernames if provided (using internal multi-value filter)
114
- if usernames:
115
- filter_to_apply.append(("usernames", ",".join(usernames)))
116
-
117
- # Apply filters
118
- users = user_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
119
-
120
- # Build list of usernames
121
- result = [user["username"] for user in users]
122
-
123
- # Validation: ensure we found users
124
- if not result and not usernames:
125
- raise CmemcError("No user accounts found matching the provided filters.")
126
-
127
- return result
128
-
129
-
130
85
  @click.command(cls=CmemcCommand, name="list")
131
86
  @click.option("--raw", is_flag=True, help="Outputs raw JSON.")
132
87
  @click.option(
@@ -228,11 +183,20 @@ def delete_command(
228
183
  if usernames and (all_ or filter_):
229
184
  raise click.UsageError("Either specify a username OR use a --filter or the --all option.")
230
185
 
231
- # Get users to delete based on selection method
232
- users_to_delete = _get_users_to_delete(ctx, usernames, all_, filter_)
186
+ users_data = get_objects_to_delete(
187
+ ctx=ctx,
188
+ ids=usernames,
189
+ all_=all_,
190
+ filter_=filter_,
191
+ object_list=user_list,
192
+ get_all_objects=list_users,
193
+ extract_id=lambda x: x["username"],
194
+ error_message_template=NO_USER_ERROR,
195
+ id_filter_name="usernames",
196
+ )
233
197
 
234
198
  # Avoid double removal as well as sort usernames
235
- processed_usernames = sorted(set(users_to_delete), key=lambda v: v.lower())
199
+ processed_usernames = sorted({user["username"] for user in users_data}, key=lambda v: v.lower())
236
200
  count = len(processed_usernames)
237
201
 
238
202
  # Delete each user
@@ -17,7 +17,6 @@ from cmem_cmemc import completion
17
17
  from cmem_cmemc.command import CmemcCommand
18
18
  from cmem_cmemc.command_group import CmemcGroup
19
19
  from cmem_cmemc.context import ApplicationContext, build_caption
20
- from cmem_cmemc.exceptions import CmemcError
21
20
  from cmem_cmemc.object_list import (
22
21
  DirectMultiValuePropertyFilter,
23
22
  DirectValuePropertyFilter,
@@ -25,7 +24,11 @@ from cmem_cmemc.object_list import (
25
24
  ObjectList,
26
25
  compare_regex,
27
26
  )
28
- from cmem_cmemc.utils import check_or_select_project, split_task_id
27
+ from cmem_cmemc.utils import (
28
+ check_or_select_project,
29
+ get_objects_to_delete,
30
+ split_task_id,
31
+ )
29
32
 
30
33
 
31
34
  def get_variables(ctx: Context) -> list[dict]: # noqa: ARG001
@@ -172,41 +175,6 @@ def _sort_variables_by_dependency(variables: list[dict]) -> list[str]:
172
175
  return result
173
176
 
174
177
 
175
- def _validate_variable_ids(variable_ids: tuple[str, ...]) -> None:
176
- """Validate that provided variable IDs exist."""
177
- all_variables = get_all_variables()
178
- all_variable_ids = [_["id"] for _ in all_variables]
179
- for variable_id in variable_ids:
180
- if variable_id not in all_variable_ids:
181
- raise CmemcError(f"Variable {variable_id} not available.")
182
-
183
-
184
- def _get_variables_to_delete(
185
- ctx: Context,
186
- variable_ids: tuple[str, ...],
187
- all_: bool,
188
- filter_: tuple[tuple[str, str], ...],
189
- ) -> list[dict]:
190
- """Get the list of variables to delete based on selection method."""
191
- if all_:
192
- _: list[dict] = get_all_variables()
193
- return _
194
-
195
- _validate_variable_ids(variable_ids)
196
-
197
- filter_to_apply = list(filter_) if filter_ else []
198
-
199
- if variable_ids:
200
- filter_to_apply.append(("ids", ",".join(variable_ids)))
201
-
202
- variables = variable_list_obj.apply_filters(ctx=ctx, filter_=filter_to_apply)
203
-
204
- if not variables and not variable_ids:
205
- raise CmemcError("No variables found matching the provided filters.")
206
-
207
- return variables
208
-
209
-
210
178
  @click.command(cls=CmemcCommand, name="list")
211
179
  @click.option("--raw", is_flag=True, help="Outputs raw JSON.")
212
180
  @click.option(
@@ -340,8 +308,16 @@ def delete_command(
340
308
  "--filter options to specify variables for deletion."
341
309
  )
342
310
 
343
- # Get variables to delete based on selection method
344
- variables_to_delete = _get_variables_to_delete(ctx, variable_ids, all_, filter_)
311
+ variables_to_delete = get_objects_to_delete(
312
+ ctx=ctx,
313
+ ids=variable_ids,
314
+ all_=all_,
315
+ filter_=filter_,
316
+ object_list=variable_list_obj,
317
+ get_all_objects=get_all_variables,
318
+ extract_id=lambda x: x["id"],
319
+ error_message_template="Variable {} not available.",
320
+ )
345
321
 
346
322
  # Remove duplicates while preserving variable objects for dependency analysis
347
323
  unique_variables = list({v["id"]: v for v in variables_to_delete}.values())
@@ -26,6 +26,7 @@ def get_icon_for_command_group(full_name: str) -> str:
26
26
  "admin workspace python": "material/language-python",
27
27
  "config": "material/cog-outline",
28
28
  "dataset": "eccenca/artefact-dataset",
29
+ "package": "material/shopping",
29
30
  "project": "eccenca/artefact-project",
30
31
  "project file": "eccenca/artefact-file",
31
32
  "project variable": "material/variable-box",
@@ -54,6 +55,7 @@ def get_tags_for_command_group(full_name: str) -> str:
54
55
  "admin workspace python": ["Python", "cmemc"],
55
56
  "config": ["Configuration", "cmemc"],
56
57
  "dataset": ["cmemc"],
58
+ "package": ["cmemc", "Package"],
57
59
  "project": ["Project", "cmemc"],
58
60
  "project file": ["Files", "cmemc"],
59
61
  "project variable": ["Variables", "cmemc"],
cmem_cmemc/utils.py CHANGED
@@ -6,11 +6,13 @@ import pathlib
6
6
  import re
7
7
  import sys
8
8
  import unicodedata
9
+ from collections.abc import Callable
9
10
  from dataclasses import dataclass
10
11
  from importlib.metadata import version as cmemc_version
11
12
  from typing import TYPE_CHECKING
12
13
  from zipfile import BadZipFile, ZipFile
13
14
 
15
+ import click
14
16
  import requests
15
17
  from click import Argument
16
18
  from cmem.cmempy.dp.proxy.graph import get_graphs_list
@@ -24,6 +26,7 @@ from cmem_cmemc.exceptions import CmemcError
24
26
  from cmem_cmemc.smart_path import SmartPath
25
27
 
26
28
  if TYPE_CHECKING:
29
+ from cmem_cmemc import object_list
27
30
  from cmem_cmemc.context import ApplicationContext
28
31
 
29
32
 
@@ -453,3 +456,113 @@ def tuple_to_list(ctx: type["ApplicationContext"], param: Argument, value: tuple
453
456
  Used as callback to have mutable values
454
457
  """
455
458
  return list(value)
459
+
460
+
461
+ def validate_ids(
462
+ ids: tuple[str, ...],
463
+ get_all_objects: Callable[[], list[dict]],
464
+ extract_id: Callable[[dict], str],
465
+ error_message_template: str,
466
+ ) -> None:
467
+ """Validate that all provided IDs exist.
468
+
469
+ This is a reusable validation function for delete commands.
470
+
471
+ Args:
472
+ ----
473
+ ids: Tuple of identifiers to validate
474
+ get_all_objects: Callable that returns list of all objects (e.g., list_items, get_graphs)
475
+ extract_id: Callable that extracts the ID from an object dict (e.g., lambda x: x["iri"])
476
+ error_message_template: Error message template with {} placeholder for the missing ID
477
+
478
+ Raises:
479
+ ------
480
+ CmemcError: If any ID is not found
481
+
482
+ Example:
483
+ -------
484
+ validate_ids(
485
+ ids=("graph1", "graph2"),
486
+ get_all_objects=lambda: get_graphs(),
487
+ extract_id=lambda x: x["iri"],
488
+ error_message_template="{} is not a valid graph IRI."
489
+ )
490
+
491
+ """
492
+ if not ids:
493
+ return
494
+ all_objects = get_all_objects()
495
+ all_ids = {extract_id(obj) for obj in all_objects}
496
+ for id_ in ids:
497
+ if id_ not in all_ids:
498
+ raise CmemcError(error_message_template.format(id_))
499
+
500
+
501
+ def get_objects_to_delete( # noqa: PLR0913
502
+ ctx: click.Context,
503
+ ids: tuple[str, ...],
504
+ all_: bool,
505
+ filter_: tuple[tuple[str, str], ...],
506
+ object_list: "object_list.ObjectList",
507
+ get_all_objects: Callable[[], list[dict]],
508
+ extract_id: Callable[[dict], str],
509
+ error_message_template: str,
510
+ id_filter_name: str = "ids",
511
+ ) -> list[dict]:
512
+ """Get the list of objects to delete based on selection method.
513
+
514
+ This is a reusable utility for delete commands that encapsulates the common pattern of:
515
+ 1. If --all flag is set, return all objects
516
+ 2. Validate provided IDs exist
517
+ 3. Build filter list with IDs (using internal multi-value filter)
518
+ 4. Apply filters using ObjectList
519
+ 5. Validate that objects were found
520
+
521
+ Args:
522
+ ----
523
+ ctx: Click context
524
+ ids: Tuple of specific identifiers to delete
525
+ all_: Flag to delete all objects
526
+ filter_: Tuple of filter name/value pairs
527
+ object_list: ObjectList instance for filtering
528
+ get_all_objects: Callable that returns list of all objects (for validation)
529
+ extract_id: Callable that extracts the ID from an object dict
530
+ error_message_template: Template string for error messages
531
+ id_filter_name: Name of the internal multi-value filter (default: "ids")
532
+
533
+ Returns:
534
+ -------
535
+ List of object dictionaries to delete
536
+
537
+ Raises:
538
+ ------
539
+ CmemcError: If no objects found matching the criteria
540
+
541
+ """
542
+ if all_:
543
+ return object_list.apply_filters(ctx=ctx, filter_=[])
544
+
545
+ # Validate provided IDs exist before proceeding
546
+ if ids:
547
+ validate_ids(
548
+ ids=ids,
549
+ get_all_objects=get_all_objects,
550
+ extract_id=extract_id,
551
+ error_message_template=error_message_template,
552
+ )
553
+
554
+ # Build filter list
555
+ filter_to_apply = list(filter_) if filter_ else []
556
+
557
+ # Add IDs if provided (using internal multi-value filter)
558
+ if ids:
559
+ filter_to_apply.append((id_filter_name, ",".join(ids)))
560
+
561
+ # Apply filters
562
+ objects = object_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
563
+
564
+ # Validation: ensure we found objects
565
+ if not objects:
566
+ raise CmemcError("No objects found matching the provided criteria.")
567
+
568
+ return objects
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmem-cmemc
3
- Version: 26.1.0rc1
3
+ Version: 26.1.0rc3
4
4
  Summary: Command line client for eccenca Corporate Memory
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -32,7 +32,7 @@ Requires-Dist: certifi (>=2024.2.2)
32
32
  Requires-Dist: click (>=8.3.0,<9.0.0)
33
33
  Requires-Dist: click-didyoumean (>=0.3.1,<0.4.0)
34
34
  Requires-Dist: click-help-colors (>=0.9.4,<0.10.0)
35
- Requires-Dist: cmem-client (>=0.5.0,<0.6.0)
35
+ Requires-Dist: cmem-client (==0.7.1)
36
36
  Requires-Dist: cmem-cmempy (==25.4.0)
37
37
  Requires-Dist: configparser (>=7.2.0,<8.0.0)
38
38
  Requires-Dist: humanize (>=4.14.0,<5.0.0)
@@ -4,27 +4,27 @@ cmem_cmemc/cli.py,sha256=VGfHqSIPPTvYpPHrjwbnLYLB6O0u8kwiJDXeXMlI1xU,4855
4
4
  cmem_cmemc/command.py,sha256=CuaskaqD12soZLhDP1prgXOT4cRFu1CzuJm6LBp4zLM,1949
5
5
  cmem_cmemc/command_group.py,sha256=0I2Jg1lCdoozcMg7d0g9sk0zVJ8_LiKvWWwqM6tZGI0,4793
6
6
  cmem_cmemc/commands/__init__.py,sha256=NaGM5jOzf0S_-4UIAwlVDOf2AZ3mliGPoRLXQJfTyZs,22
7
- cmem_cmemc/commands/acl.py,sha256=IHLEmrbz3r9SVEsaEWxpGVRjoT0oXWhYJ4d3y-_402Y,31421
7
+ cmem_cmemc/commands/acl.py,sha256=X7RMwEO66SvOY4gXrlBJvKTUYiUdURf0YLaZx1Feqx8,30238
8
8
  cmem_cmemc/commands/admin.py,sha256=XyJVk9CuxLh60GdWNtMkGPpZ3BOftDF4T04M8gRaOQ0,10193
9
9
  cmem_cmemc/commands/client.py,sha256=4Ke9Pl6-KQ-6_uSpRbVhB7spAi8tP0r39y99fwFRSWE,5304
10
10
  cmem_cmemc/commands/config.py,sha256=tRB7YK6vzkaasmyZVWGLOw1_x0SM2x-coYMBRIJwEJs,8392
11
- cmem_cmemc/commands/dataset.py,sha256=OCRBcDFVBBpap4bnjSIOfrWgFNRU9m0V_QlSobvObkY,30472
12
- cmem_cmemc/commands/file.py,sha256=FBmi0D8bZs5NoKcOWEYIuaJM0x9kWQAaIQ2IyL8sFiE,17038
13
- cmem_cmemc/commands/graph.py,sha256=HzddrKcoxq0w2xWMIhVft5p5Oacq0TSrmhebLngJdMs,36623
11
+ cmem_cmemc/commands/dataset.py,sha256=ZdiKLXWq9ItnJb4xGiP0qaby5W3Kvkyqg8KIaYsHelI,29715
12
+ cmem_cmemc/commands/file.py,sha256=Ds7vzaiWIfnOesxS9KQQuBinlkSjcSqV_02SX1ad_sE,15926
13
+ cmem_cmemc/commands/graph.py,sha256=oUInQQEuL-n6bFEscNUB7qfPUk0yNajEy0H5VFXFPoQ,35667
14
14
  cmem_cmemc/commands/graph_imports.py,sha256=VcOisHSvNYJPxMiRb6sHEswYIQHwlIamLwQD1jgr_e0,14368
15
- cmem_cmemc/commands/graph_insights.py,sha256=sx-Lvv4FWK9-eB8X4R5fDU63lrErAhc8W3a2fEQAjDI,14473
15
+ cmem_cmemc/commands/graph_insights.py,sha256=slQXBUy7azEG-w7M-2r9ybY4J3HgV9TTWbP5Ih3HDu0,14395
16
16
  cmem_cmemc/commands/manual.py,sha256=-sZWeFL92Kj8gL3VYsbpKh2ZaVTyM3LgKaUcpNn9u3A,2179
17
17
  cmem_cmemc/commands/metrics.py,sha256=t5I6VjBzjp_bQEoGkU9cdqNu_sa_WQwiIeJA3f9KNWc,12385
18
18
  cmem_cmemc/commands/migration.py,sha256=FibmYpvZD2mrutjyRBhs7xDAZ-sjPiH9Kn3CI-zUPm0,9861
19
- cmem_cmemc/commands/package.py,sha256=mJ9vDj34YdGySGK_Nh7Yl3O2tyuT-FhV8p4taRNLU_c,17762
20
- cmem_cmemc/commands/project.py,sha256=ovn9zre7L0EzthMITQHGF9dffiP44UQUWNjG8Is6l8k,24938
19
+ cmem_cmemc/commands/package.py,sha256=iY543hVwMqTA5t7mQAe2etz9n5sNgxx5y8Y-B53kJRY,16812
20
+ cmem_cmemc/commands/project.py,sha256=HRA4xpOENajyQub1iWLo5voAsXFUzFPt7kJq9sx4sx4,25066
21
21
  cmem_cmemc/commands/python.py,sha256=lcbBAYZN5NB37HLSmVPs0SXJV7Ey4xVMYQiSiuyGkvc,12225
22
22
  cmem_cmemc/commands/query.py,sha256=1cj1QbvwL98YbBGSCO0Zazbzscts_kiv0A7k75KwJXw,32231
23
23
  cmem_cmemc/commands/scheduler.py,sha256=3wk3BF6Z3uRb0e5pphOYBusbXgs7C6Lz-D9wi7Nlohc,8855
24
24
  cmem_cmemc/commands/store.py,sha256=zKz8FTtVSvFU6gMm6An7Jja9Bu9dZKbI1GW7UCq034s,10655
25
- cmem_cmemc/commands/user.py,sha256=F0JSRkrj274Hi0i4nBIUkWFm-ItC40VwuqvLYETJBdM,15844
25
+ cmem_cmemc/commands/user.py,sha256=Uqhqn5gb4Fy4_ZaJbAwVFTRERvRO7A0yYO2ic6Xz1Wc,14707
26
26
  cmem_cmemc/commands/validation.py,sha256=v9_cXGzaexemuz6xBA358XY1_vP42SBfOD3PEZLcqbw,29731
27
- cmem_cmemc/commands/variable.py,sha256=ZcvrQDb0CztWGkFIX-jnvqBTLp3qpQEZ7ZpBmKV16SI,18841
27
+ cmem_cmemc/commands/variable.py,sha256=LRgZ82lubDnY0yh67KBF0se2W1PeE58pYAJuwftbfyE,17926
28
28
  cmem_cmemc/commands/vocabulary.py,sha256=sv7hDZOeRPrPlc5RJfpAKzKH5JRyKjB93D-Jl4eLNqI,18359
29
29
  cmem_cmemc/commands/workflow.py,sha256=UdKAsY3chxfrIpkjL1K9MqLVyu7jTdFYkOXfGVvJISI,26369
30
30
  cmem_cmemc/commands/workspace.py,sha256=IcZgBsvtulLRFofS70qpln6oKQIZunrVLfSAUeiFhCA,4579
@@ -35,7 +35,7 @@ cmem_cmemc/context.py,sha256=oCcd6dFl6BdYqKsueVqzQhSEwTNW7b1MjrE4CRznxt8,23220
35
35
  cmem_cmemc/exceptions.py,sha256=c4Z6CKgymu0a7gD8MtHxzK_7WCsb9I2Zl-EgEkwu-YY,760
36
36
  cmem_cmemc/manual_helper/__init__.py,sha256=G3Lqw2aPxo8x63Tg7L0aa5VD9BMaRzZDmhrog7IuEPg,43
37
37
  cmem_cmemc/manual_helper/graph.py,sha256=dTkFXgU9fgySn54rE93t79v1MjWjQkprKRIfJhc7Jps,3655
38
- cmem_cmemc/manual_helper/multi_page.py,sha256=I1gTCDETlCli2k-G7Mkdpw_MCqey60HxFl35wTmvYFU,12279
38
+ cmem_cmemc/manual_helper/multi_page.py,sha256=vTW1xHeMqM0BCFYsftoirXZ6RwxEeV0LjiOPwbqBous,12360
39
39
  cmem_cmemc/manual_helper/single_page.py,sha256=0mMn_IJwFCe-WPKAmxGEStb8IINLpQRxAx_F1pIxg1E,1526
40
40
  cmem_cmemc/migrations/__init__.py,sha256=i6Ri7qN58ou_MwOzm2KibPkXOD7u-1ELky-nUE5LjAA,24
41
41
  cmem_cmemc/migrations/abc.py,sha256=UGJzrvMzUFdp2-sosp49ObRI-SrUSzLJqLEhvB4QTzg,3564
@@ -54,9 +54,9 @@ cmem_cmemc/smart_path/clients/__init__.py,sha256=YFOm69BfTCRvAcJjN_CoUmCv3kzEciy
54
54
  cmem_cmemc/smart_path/clients/http.py,sha256=3clZu2v4uuOvPY4MY_8SVSy7hIXJDNooahFRBRpy0ok,2347
55
55
  cmem_cmemc/string_processor.py,sha256=19YSLUF9PIbfTmsTm2bZslsNhFUAYx0MerWYwC3BVEo,8616
56
56
  cmem_cmemc/title_helper.py,sha256=8Cyes2U4lHTQbzYwBSYqCrZbq29_oBg6uibe7xZ6DEg,3486
57
- cmem_cmemc/utils.py,sha256=jmXjbEQ2MDIz031A81kivGnir7HG5XUkLbOd1OCoV6s,14662
58
- cmem_cmemc-26.1.0rc1.dist-info/METADATA,sha256=-8H1iGcueln3Y31sViwBpY-g2hBleljk2Vm-YSsRL2Y,5761
59
- cmem_cmemc-26.1.0rc1.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
60
- cmem_cmemc-26.1.0rc1.dist-info/entry_points.txt,sha256=2G0AWAyz501EHpFTjIxccdlCTsHt80NT0pdUGP1QkPA,45
61
- cmem_cmemc-26.1.0rc1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
62
- cmem_cmemc-26.1.0rc1.dist-info/RECORD,,
57
+ cmem_cmemc/utils.py,sha256=rs3qf5UZeiTQO0USUpFQq6upQnG_S43CW7YYUrCwmzk,18240
58
+ cmem_cmemc-26.1.0rc3.dist-info/METADATA,sha256=mB1KAv64UgOmwJ_4WbdqxeH7nI6MkVgq6d96hFrBPaE,5754
59
+ cmem_cmemc-26.1.0rc3.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
60
+ cmem_cmemc-26.1.0rc3.dist-info/entry_points.txt,sha256=2G0AWAyz501EHpFTjIxccdlCTsHt80NT0pdUGP1QkPA,45
61
+ cmem_cmemc-26.1.0rc3.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
62
+ cmem_cmemc-26.1.0rc3.dist-info/RECORD,,