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,10 +1,8 @@
1
1
  """Build project file commands for cmemc."""
2
2
 
3
- import re
4
-
5
3
  import click
6
- from click import ClickException, Context, UsageError
7
- from cmem.cmempy.config import get_cmem_base_uri
4
+ from click import Context, UsageError
5
+ from click.shell_completion import CompletionItem
8
6
  from cmem.cmempy.workspace.projects.resources import get_all_resources
9
7
  from cmem.cmempy.workspace.projects.resources.resource import (
10
8
  create_resource,
@@ -18,20 +16,56 @@ from cmem.cmempy.workspace.projects.resources.resource import (
18
16
  from cmem_cmemc import completion
19
17
  from cmem_cmemc.command import CmemcCommand
20
18
  from cmem_cmemc.command_group import CmemcGroup
21
- from cmem_cmemc.context import ApplicationContext
19
+ from cmem_cmemc.context import ApplicationContext, build_caption
22
20
  from cmem_cmemc.exceptions import CmemcError
21
+ from cmem_cmemc.object_list import (
22
+ DirectMultiValuePropertyFilter,
23
+ DirectValuePropertyFilter,
24
+ ObjectList,
25
+ compare_regex,
26
+ )
23
27
  from cmem_cmemc.parameter_types.path import ClickSmartPath
24
28
  from cmem_cmemc.smart_path import SmartPath as Path
25
29
  from cmem_cmemc.string_processor import FileSize, TimeAgo
26
30
  from cmem_cmemc.utils import check_or_select_project, split_task_id, struct_to_table
27
31
 
28
- RESOURCE_FILTER_TYPES = ["project", "regex"]
29
- RESOURCE_FILTER_TYPES_HIDDEN = ["ids"]
30
- RESOURCE_FILTER_TEXT = (
31
- "Filter file resources based on metadata. "
32
- f"First parameter CHOICE can be one of {RESOURCE_FILTER_TYPES!s}"
33
- ". The second parameter is based on CHOICE, e.g. a project "
34
- "ID or a regular expression string."
32
+
33
+ def get_resources(ctx: Context) -> list[dict]: # noqa: ARG001
34
+ """Get file resources for object list."""
35
+ _: list[dict] = get_all_resources()
36
+ return _
37
+
38
+
39
+ resource_list = ObjectList(
40
+ name="file resources",
41
+ get_objects=get_resources,
42
+ filters=[
43
+ DirectValuePropertyFilter(
44
+ name="project",
45
+ description="Filter file resources by project ID.",
46
+ property_key="project",
47
+ completion_method="values",
48
+ ),
49
+ DirectValuePropertyFilter(
50
+ name="regex",
51
+ description="Filter by regex matching the resource name.",
52
+ property_key="name",
53
+ compare=compare_regex,
54
+ fixed_completion=[
55
+ CompletionItem("csv$", help="File resources which name ends with .csv"),
56
+ CompletionItem(
57
+ "2021-10-[0-9][0-9]",
58
+ help="File resources which name has a date from 2021-10 in it",
59
+ ),
60
+ ],
61
+ fixed_completion_only=False,
62
+ ),
63
+ DirectMultiValuePropertyFilter(
64
+ name="ids",
65
+ description="Internal filter for multiple resource IDs.",
66
+ property_key="id",
67
+ ),
68
+ ],
35
69
  )
36
70
 
37
71
 
@@ -60,7 +94,6 @@ def _upload_file_resource(
60
94
  exist = resource_exist(project_name=project_id, resource_name=remote_file_name)
61
95
  if exist and not replace:
62
96
  raise CmemcError(
63
- app,
64
97
  f"A file resource with the name '{remote_file_name}' already "
65
98
  "exists in this project. \n"
66
99
  "Please rename the file or use the '--replace' "
@@ -87,25 +120,45 @@ def _upload_file_resource(
87
120
  app.echo_success("done")
88
121
 
89
122
 
90
- def _get_resources_filtered(
91
- resources: list[dict], filter_name: str, filter_value: str | tuple[str, ...]
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], ...],
92
139
  ) -> list[dict]:
93
- """Get file resources but filtered according to name and value."""
94
- # check for correct filter names (filter ids is used internally only)
95
- if filter_name not in RESOURCE_FILTER_TYPES + RESOURCE_FILTER_TYPES_HIDDEN:
96
- raise UsageError(
97
- f"{filter_name} is an unknown filter name. " f"Use one of {RESOURCE_FILTER_TYPES}."
98
- )
99
- # filter by ID list
100
- if filter_name == "ids":
101
- return [_ for _ in resources if _["id"] in filter_value]
102
- # filter by project
103
- if filter_name == "project":
104
- return [_ for _ in resources if _["project"] == str(filter_value)]
105
- # filter by regex
106
- if filter_name == "regex":
107
- return [_ for _ in resources if re.search(str(filter_value), _["name"])]
108
- # return unfiltered list
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
+
109
162
  return resources
110
163
 
111
164
 
@@ -119,24 +172,21 @@ def _get_resources_filtered(
119
172
  )
120
173
  @click.option(
121
174
  "--filter",
122
- "filters_",
175
+ "filter_",
123
176
  multiple=True,
124
177
  type=(str, str),
125
- shell_complete=completion.resource_list_filter,
126
- help=RESOURCE_FILTER_TEXT,
178
+ shell_complete=resource_list.complete_values,
179
+ help=resource_list.get_filter_help_text(),
127
180
  )
128
- @click.pass_obj
129
- def list_command(
130
- app: ApplicationContext, raw: bool, id_only: bool, filters_: tuple[tuple[str, str], ...]
131
- ) -> None:
181
+ @click.pass_context
182
+ def list_command(ctx: Context, raw: bool, id_only: bool, filter_: tuple[tuple[str, str]]) -> None:
132
183
  """List available file resources.
133
184
 
134
185
  Outputs a table or a list of file resources.
135
186
  """
136
- resources = get_all_resources()
137
- for _ in filters_:
138
- filter_name, filter_value = _
139
- resources = _get_resources_filtered(resources, filter_name, filter_value)
187
+ app: ApplicationContext = ctx.obj
188
+ resources = resource_list.apply_filters(ctx=ctx, filter_=filter_)
189
+
140
190
  if raw:
141
191
  app.echo_info_json(resources)
142
192
  return
@@ -155,21 +205,17 @@ def list_command(
155
205
  ]
156
206
  table.append(row)
157
207
 
158
- caption = f"{len(table)} files of {get_cmem_base_uri()}"
159
- empty_note = "No resources found."
160
- if len(filters_) > 0:
161
- caption += " (filtered)"
162
- empty_note = "No resources found for these filters."
163
-
208
+ filtered = len(filter_) > 0
164
209
  app.echo_info_table(
165
210
  table,
166
211
  headers=headers,
167
212
  sort_column=0,
168
213
  cell_processing={1: TimeAgo(), 2: FileSize()},
169
- caption=caption,
170
- empty_table_message=f"{empty_note} "
171
- "Use the `dataset create` command to create a new file-based dataset, or "
172
- "the `project file upload` command to create only a file resource.",
214
+ caption=build_caption(len(table), "file resource", filtered=filtered),
215
+ empty_table_message="No resources found for these filters."
216
+ if filtered
217
+ else "No resources found. Use the `dataset create` command to create a new "
218
+ "file-based dataset, or the `project file upload` command to create only a file resource.",
173
219
  )
174
220
 
175
221
 
@@ -181,23 +227,23 @@ def list_command(
181
227
  "--all",
182
228
  "all_",
183
229
  is_flag=True,
184
- help="Delete all resources. " "This is a dangerous option, so use it with care.",
230
+ help="Delete all resources. This is a dangerous option, so use it with care.",
185
231
  )
186
232
  @click.option(
187
233
  "--filter",
188
- "filters_",
234
+ "filter_",
189
235
  multiple=True,
190
236
  type=(str, str),
191
- shell_complete=completion.resource_list_filter,
192
- help=RESOURCE_FILTER_TEXT,
237
+ shell_complete=resource_list.complete_values,
238
+ help=resource_list.get_filter_help_text(),
193
239
  )
194
- @click.pass_obj
240
+ @click.pass_context
195
241
  def delete_command(
196
- app: ApplicationContext,
242
+ ctx: Context,
197
243
  resource_ids: tuple[str, ...],
198
244
  force: bool,
199
245
  all_: bool,
200
- filters_: tuple[tuple[str, str], ...],
246
+ filter_: tuple[tuple[str, str], ...],
201
247
  ) -> None:
202
248
  """Delete file resources.
203
249
 
@@ -206,25 +252,23 @@ def delete_command(
206
252
  on the filter type and value will be deleted; by using --all, which will
207
253
  delete all resources.
208
254
  """
209
- if resource_ids == () and not all_ and filters_ == ():
255
+ app: ApplicationContext = ctx.obj
256
+
257
+ # Validation: require at least one selection method
258
+ if not resource_ids and not all_ and not filter_:
210
259
  raise UsageError(
211
260
  "Either specify at least one resource ID or use the --all or "
212
261
  "--filter options to specify resources for deletion."
213
262
  )
214
263
 
215
- resources = get_all_resources()
216
- if len(resource_ids) > 0:
217
- for resource_id in resource_ids:
218
- if resource_id not in [_["id"] for _ in resources]:
219
- raise ClickException(f"Resource {resource_id} not available.")
220
- # "filter" by id
221
- resources = _get_resources_filtered(resources, "ids", resource_ids)
222
- for _ in filters_:
223
- resources = _get_resources_filtered(resources, _[0], _[1])
224
-
225
- # avoid double removal as well as sort IDs
226
- processed_ids = sorted({_["id"] for _ in resources}, key=lambda v: v.lower())
264
+ # Get resources to delete based on selection method
265
+ resources_to_delete = _get_resources_to_delete(ctx, resource_ids, all_, filter_)
266
+
267
+ # Avoid double removal as well as sort IDs
268
+ processed_ids = sorted({_["id"] for _ in resources_to_delete}, key=lambda v: v.lower())
227
269
  count = len(processed_ids)
270
+
271
+ # Delete each resource
228
272
  for current, resource_id in enumerate(processed_ids, start=1):
229
273
  current_string = str(current).zfill(len(str(count)))
230
274
  app.echo_info(f"Delete resource {current_string}/{count}: {resource_id} ... ", nl=False)
@@ -310,7 +354,7 @@ def download_command(
310
354
  for chunk in response.iter_content(chunk_size=8192):
311
355
  resource_file.write(chunk)
312
356
  app.echo_success("done")
313
- except (OSError, ClickException) as error:
357
+ except (OSError, CmemcError) as error:
314
358
  app.echo_error(f"failed: {error!s}")
315
359
  continue
316
360