cmem-cmemc 24.2.0rc2__py3-none-any.whl → 24.3.0rc2__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 (51) hide show
  1. cmem_cmemc/__init__.py +7 -12
  2. cmem_cmemc/command.py +20 -0
  3. cmem_cmemc/command_group.py +70 -0
  4. cmem_cmemc/commands/__init__.py +0 -81
  5. cmem_cmemc/commands/acl.py +118 -62
  6. cmem_cmemc/commands/admin.py +46 -35
  7. cmem_cmemc/commands/client.py +2 -1
  8. cmem_cmemc/commands/config.py +3 -1
  9. cmem_cmemc/commands/dataset.py +27 -24
  10. cmem_cmemc/commands/graph.py +160 -19
  11. cmem_cmemc/commands/metrics.py +195 -79
  12. cmem_cmemc/commands/migration.py +267 -0
  13. cmem_cmemc/commands/project.py +62 -17
  14. cmem_cmemc/commands/python.py +56 -25
  15. cmem_cmemc/commands/query.py +23 -14
  16. cmem_cmemc/commands/resource.py +10 -2
  17. cmem_cmemc/commands/scheduler.py +10 -2
  18. cmem_cmemc/commands/store.py +118 -14
  19. cmem_cmemc/commands/user.py +8 -2
  20. cmem_cmemc/commands/validation.py +165 -78
  21. cmem_cmemc/commands/variable.py +10 -2
  22. cmem_cmemc/commands/vocabulary.py +48 -29
  23. cmem_cmemc/commands/workflow.py +86 -59
  24. cmem_cmemc/commands/workspace.py +27 -8
  25. cmem_cmemc/completion.py +190 -140
  26. cmem_cmemc/constants.py +2 -0
  27. cmem_cmemc/context.py +88 -42
  28. cmem_cmemc/manual_helper/graph.py +1 -0
  29. cmem_cmemc/manual_helper/multi_page.py +3 -1
  30. cmem_cmemc/migrations/__init__.py +1 -0
  31. cmem_cmemc/migrations/abc.py +84 -0
  32. cmem_cmemc/migrations/access_conditions_243.py +122 -0
  33. cmem_cmemc/migrations/bootstrap_data.py +28 -0
  34. cmem_cmemc/migrations/shapes_widget_integrations_243.py +274 -0
  35. cmem_cmemc/migrations/workspace_configurations.py +28 -0
  36. cmem_cmemc/object_list.py +53 -22
  37. cmem_cmemc/parameter_types/__init__.py +1 -0
  38. cmem_cmemc/parameter_types/path.py +69 -0
  39. cmem_cmemc/smart_path/__init__.py +94 -0
  40. cmem_cmemc/smart_path/clients/__init__.py +63 -0
  41. cmem_cmemc/smart_path/clients/http.py +65 -0
  42. cmem_cmemc/string_processor.py +83 -0
  43. cmem_cmemc/title_helper.py +41 -0
  44. cmem_cmemc/utils.py +100 -45
  45. {cmem_cmemc-24.2.0rc2.dist-info → cmem_cmemc-24.3.0rc2.dist-info}/LICENSE +1 -1
  46. cmem_cmemc-24.3.0rc2.dist-info/METADATA +89 -0
  47. cmem_cmemc-24.3.0rc2.dist-info/RECORD +53 -0
  48. {cmem_cmemc-24.2.0rc2.dist-info → cmem_cmemc-24.3.0rc2.dist-info}/WHEEL +1 -1
  49. cmem_cmemc-24.2.0rc2.dist-info/METADATA +0 -69
  50. cmem_cmemc-24.2.0rc2.dist-info/RECORD +0 -37
  51. {cmem_cmemc-24.2.0rc2.dist-info → cmem_cmemc-24.3.0rc2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,274 @@
1
+ """Migration: Chart and Workflow Property Shapes to Widget Integration"""
2
+
3
+ from typing import ClassVar
4
+
5
+ from cmem_cmemc.migrations.abc import MigrationRecipe, components
6
+
7
+
8
+ class ChartsOnNodeShapesToToWidgetIntegrations(MigrationRecipe):
9
+ """24.3 Migrate Chart on NodeShapes to Widget Integrations"""
10
+
11
+ id = "charts-on-nshapes-24.3"
12
+ description = "Migrate Charts on Node Shapes to Widget Integrations"
13
+ component: components = "explore"
14
+ first_version = "24.3"
15
+ tags: ClassVar[list[str]] = ["shapes", "user"]
16
+ check_query = """{{DEFAULT_PREFIXES}}
17
+ SELECT *
18
+ WHERE {
19
+ GRAPH ?maybeOtherGraph {
20
+ ?maybeOtherGraph a shui:ShapeCatalog .
21
+ ?nodeShape a sh:NodeShape .
22
+ }
23
+ GRAPH ?shapeCatalog {
24
+ ?shapeCatalog a shui:ShapeCatalog .
25
+ ?nodeShape shui:provideChartVisualization ?shuiChart .
26
+ }
27
+ }
28
+ """
29
+ move_query = """{{DEFAULT_PREFIXES}}
30
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
31
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
32
+ PREFIX shui: <https://vocab.eccenca.com/shui/>
33
+ PREFIX sh: <http://www.w3.org/ns/shacl#>
34
+
35
+ DELETE {
36
+ GRAPH ?shapeCatalog {
37
+ ?nodeShape shui:provideChartVisualization ?shuiChart .
38
+ }
39
+ }
40
+ INSERT {
41
+ GRAPH ?shapeCatalog {
42
+ ?newWidgetIntegration a shui:WidgetIntegration ;
43
+ shui:WidgetIntegration_widget ?shuiChart ;
44
+ rdfs:label ?name ;
45
+ rdfs:comment ?description ;
46
+ sh:order -1000 .
47
+ ?nodeShape shui:WidgetIntegration_integrate ?newWidgetIntegration .
48
+ }
49
+ }
50
+ WHERE {
51
+ GRAPH ?maybeOtherGraph {
52
+ ?maybeOtherGraph a shui:ShapeCatalog .
53
+ ?nodeShape a sh:NodeShape .
54
+ }
55
+ GRAPH ?shapeCatalog {
56
+ ?shapeCatalog a shui:ShapeCatalog .
57
+ ?nodeShape shui:provideChartVisualization ?shuiChart .
58
+ }
59
+ OPTIONAL { ?shuiChart rdfs:label ?name }
60
+ OPTIONAL { ?shuiChart rdfs:comment ?description }
61
+ BIND (IRI(CONCAT(STR(?shuiChart), "_WidgetIntegration")) as ?newWidgetIntegration)
62
+ }
63
+ """
64
+
65
+ def is_applicable(self) -> bool:
66
+ """Test if the recipe can be applied."""
67
+ node_shapes_with_charts = self._select(self.check_query)
68
+ return len(node_shapes_with_charts) > 0
69
+
70
+ def apply(self) -> None:
71
+ """Apply the recipe to the current version."""
72
+ self._update(self.move_query)
73
+
74
+
75
+ class ChartsOnPropertyShapesToWidgetIntegrations(MigrationRecipe):
76
+ """24.3 Migrate Chart Property Shapes to Widget Integrations"""
77
+
78
+ id = "charts-on-pshapes-24.3"
79
+ description = "Migrate Charts on Property Shapes to Widget Integrations"
80
+ component: components = "explore"
81
+ first_version = "24.3"
82
+ tags: ClassVar[list[str]] = ["shapes", "user"]
83
+ check_query = """{{DEFAULT_PREFIXES}}
84
+ SELECT *
85
+ WHERE {
86
+ GRAPH ?shapeCatalog {
87
+ ?shapeCatalog a shui:ShapeCatalog .
88
+ ?propertyShape a sh:PropertyShape ;
89
+ shui:provideChartVisualization ?shuiChart .
90
+ }
91
+ }
92
+ """
93
+ move_query = """{{DEFAULT_PREFIXES}}
94
+ DELETE {
95
+ GRAPH ?shapeCatalog {
96
+ ?propertyShape ?p ?o .
97
+ ?nodeShape sh:property ?propertyShape .
98
+ }
99
+ }
100
+ INSERT {
101
+ GRAPH ?shapeCatalog {
102
+ ?propertyShape a shui:WidgetIntegration ;
103
+ shui:WidgetIntegration_widget ?shuiChart ;
104
+ rdfs:label ?name ;
105
+ rdfs:comment ?description ;
106
+ sh:order ?order ;
107
+ shui:WidgetIntegration_group ?group .
108
+ ?nodeShape shui:WidgetIntegration_integrate ?propertyShape .
109
+ }
110
+ }
111
+ WHERE {
112
+ GRAPH ?shapeCatalog {
113
+ ?shapeCatalog a shui:ShapeCatalog .
114
+ ?propertyShape a sh:PropertyShape ;
115
+ shui:provideChartVisualization ?shuiChart .
116
+ ?propertyShape ?p ?o .
117
+ OPTIONAL {?propertyShape sh:name ?name }
118
+ OPTIONAL {?propertyShape sh:description ?description }
119
+ OPTIONAL {?propertyShape sh:order ?order }
120
+ OPTIONAL {?propertyShape sh:group ?group }
121
+ OPTIONAL {?nodeShape sh:property ?propertyShape }
122
+ }
123
+ }
124
+ """
125
+
126
+ def is_applicable(self) -> bool:
127
+ """Test if the recipe can be applied."""
128
+ chart_property_shapes = self._select(self.check_query)
129
+ return len(chart_property_shapes) > 0
130
+
131
+ def apply(self) -> None:
132
+ """Apply the recipe to the current version."""
133
+ self._update(self.move_query)
134
+
135
+
136
+ class WorkflowTriggerPropertyShapesToWidgetIntegrations(MigrationRecipe):
137
+ """24.3 Migrate Workflow Trigger Property Shapes to Widget Integrations"""
138
+
139
+ id = "trigger-on-pshapes-24.3"
140
+ description = "Migrate Workflow Trigger on Property Shapes to Widget Integrations"
141
+ component: components = "explore"
142
+ first_version = "24.3"
143
+ tags: ClassVar[list[str]] = ["shapes", "user"]
144
+ check_query = """{{DEFAULT_PREFIXES}}
145
+ SELECT *
146
+ WHERE {
147
+ GRAPH ?shapeCatalog {
148
+ ?shapeCatalog a shui:ShapeCatalog .
149
+ ?propertyShape a sh:PropertyShape ;
150
+ shui:provideWorkflowTrigger ?shuiChart .
151
+ }
152
+ }
153
+ """
154
+ move_query = """{{DEFAULT_PREFIXES}}
155
+ DELETE {
156
+ GRAPH ?shapeCatalog {
157
+ ?propertyShape ?p ?o .
158
+ ?nodeShape sh:property ?propertyShape .
159
+ }
160
+ }
161
+ INSERT {
162
+ GRAPH ?shapeCatalog {
163
+ ?propertyShape a shui:WidgetIntegration ;
164
+ shui:WidgetIntegration_widget ?workflowTrigger ;
165
+ rdfs:label ?name ;
166
+ rdfs:comment ?description ;
167
+ sh:order ?order ;
168
+ shui:WidgetIntegration_group ?group .
169
+ ?nodeShape shui:WidgetIntegration_integrate ?propertyShape .
170
+ }
171
+ }
172
+ WHERE {
173
+ GRAPH ?shapeCatalog {
174
+ ?shapeCatalog a shui:ShapeCatalog .
175
+ ?propertyShape a sh:PropertyShape ;
176
+ shui:provideWorkflowTrigger ?workflowTrigger .
177
+ ?propertyShape ?p ?o .
178
+ OPTIONAL {?propertyShape sh:name ?name }
179
+ OPTIONAL {?propertyShape sh:description ?description }
180
+ OPTIONAL {?propertyShape sh:order ?order }
181
+ OPTIONAL {?propertyShape sh:group ?group }
182
+ OPTIONAL {?nodeShape sh:property ?propertyShape }
183
+ }
184
+ }
185
+ """
186
+
187
+ def is_applicable(self) -> bool:
188
+ """Test if the recipe can be applied."""
189
+ chart_property_shapes = self._select(self.check_query)
190
+ return len(chart_property_shapes) > 0
191
+
192
+ def apply(self) -> None:
193
+ """Apply the recipe to the current version."""
194
+ self._update(self.move_query)
195
+
196
+
197
+ class TableReportPropertyShapesToWidgetIntegrations(MigrationRecipe):
198
+ """24.3 Migrate Table Report Property Shapes to Widget Integrations"""
199
+
200
+ id = "table-reports-on-pshapes-24.3"
201
+ description = (
202
+ "Migrate Table Reports (which use shui:null) on Property Shapes to Widget Integrations"
203
+ )
204
+ component: components = "explore"
205
+ first_version = "24.3"
206
+ tags: ClassVar[list[str]] = ["shapes", "user"]
207
+ check_query = """{{DEFAULT_PREFIXES}}
208
+ SELECT ?propertyShape
209
+ WHERE {
210
+ GRAPH ?shapeCatalog {
211
+ ?shapeCatalog a shui:ShapeCatalog .
212
+ ?propertyShape a sh:PropertyShape ;
213
+ shui:valueQuery ?valueQuery ;
214
+ sh:path shui:null .
215
+ FILTER NOT EXISTS { ?propertyShape shui:isSystemResource true }
216
+ }
217
+ }
218
+ """
219
+ move_query = """{{DEFAULT_PREFIXES}}
220
+ DELETE {
221
+ GRAPH ?shapeCatalog {
222
+ ?propertyShape ?p ?o .
223
+ ?nodeShape sh:property ?propertyShape .
224
+ }
225
+ }
226
+ INSERT {
227
+ GRAPH ?shapeCatalog {
228
+ ?propertyShape a shui:WidgetIntegration ;
229
+ shui:WidgetIntegration_widget ?newTableReport ;
230
+ rdfs:label ?name ;
231
+ rdfs:comment ?description ;
232
+ sh:order ?order ;
233
+ shui:WidgetIntegration_group ?group .
234
+ ?nodeShape shui:WidgetIntegration_integrate ?propertyShape .
235
+
236
+ ?newTableReport a shui:TableReport ;
237
+ rdfs:label ?newTableReportLabel ;
238
+ shui:TableReport_hideFooter ?hideFooter ;
239
+ shui:TableReport_hideHeader ?hideHeader ;
240
+ shui:TableReport_query ?valueQuery .
241
+ }
242
+ }
243
+ WHERE {
244
+ GRAPH ?shapeCatalog {
245
+ ?shapeCatalog a shui:ShapeCatalog .
246
+ ?propertyShape a sh:PropertyShape ;
247
+ sh:path shui:null ;
248
+ shui:valueQuery ?valueQuery .
249
+ FILTER NOT EXISTS { ?propertyShape shui:isSystemResource true }
250
+ ?propertyShape ?p ?o .
251
+ OPTIONAL {?propertyShape sh:name ?name }
252
+ OPTIONAL {?propertyShape sh:description ?description }
253
+ OPTIONAL {?propertyShape sh:order ?order }
254
+ OPTIONAL {?propertyShape sh:group ?group }
255
+ OPTIONAL {?propertyShape sh:name ?name }
256
+ OPTIONAL {?propertyShape shui:valueQueryHideHeader ?hideHeaderValue }
257
+ OPTIONAL {?propertyShape shui:valueQueryHideFooter ?hideFooterValue }
258
+ OPTIONAL {?nodeShape sh:property ?propertyShape }
259
+ BIND ( IRI(CONCAT(STR(?propertyShape), "-TableReport")) AS ?newTableReport )
260
+ BIND ( STRLANG(CONCAT("Table Report: ", ?name), lang(?name)) AS ?newTableReportLabel )
261
+ BIND ( COALESCE(?hideHeaderValue, false) as ?hideHeader)
262
+ BIND ( COALESCE(?hideFooterValue, false) as ?hideFooter)
263
+ }
264
+ }
265
+ """
266
+
267
+ def is_applicable(self) -> bool:
268
+ """Test if the recipe can be applied."""
269
+ table_report_property_shapes = self._select(self.check_query)
270
+ return len(table_report_property_shapes) > 0
271
+
272
+ def apply(self) -> None:
273
+ """Apply the recipe to the current version."""
274
+ self._update(self.move_query)
@@ -0,0 +1,28 @@
1
+ """Workspace Configuration migration recipe"""
2
+
3
+ from typing import ClassVar
4
+
5
+ from cmem.cmempy.dp.workspace import migrate_workspaces
6
+ from cmem.cmempy.health import get_complete_status_info
7
+
8
+ from cmem_cmemc.migrations.abc import MigrationRecipe, components
9
+
10
+
11
+ class MigrateWorkspaceConfiguration(MigrationRecipe):
12
+ """Update Workspace Configuration to current version"""
13
+
14
+ id = "workspace-configurations"
15
+ description = "Migrate explore workspace configurations to the current version"
16
+ component: components = "explore"
17
+ first_version = "24.2"
18
+ tags: ClassVar[list[str]] = ["system", "config"]
19
+
20
+ def is_applicable(self) -> bool:
21
+ """Test if the recipe can be applied."""
22
+ status_info = get_complete_status_info()
23
+ config = status_info["explore"]["info"].get("workspaceConfiguration", {})
24
+ return bool(config.get("workspacesToMigrate"))
25
+
26
+ def apply(self) -> None:
27
+ """Apply the recipe to the current version."""
28
+ migrate_workspaces()
cmem_cmemc/object_list.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Filterable object list."""
2
+
2
3
  import re
3
4
  from abc import ABC, abstractmethod
4
5
  from collections.abc import Callable
@@ -8,8 +9,9 @@ from typing import Literal
8
9
  from click import Argument, Context, UsageError
9
10
  from click.shell_completion import CompletionItem
10
11
 
11
- from cmem_cmemc.completion import _finalize_completion, _get_completion_args
12
+ from cmem_cmemc.completion import finalize_completion, get_completion_args
12
13
  from cmem_cmemc.context import CONTEXT
14
+ from cmem_cmemc.title_helper import TitleHelper
13
15
 
14
16
 
15
17
  class Filter(ABC):
@@ -67,9 +69,7 @@ def compare_regex(ctx: Filter, object_value: str, filter_value: str) -> bool:
67
69
  f"Invalid filter value '{filter_value}' - "
68
70
  f"need a valid regular expression for filter '{ctx.name}'."
69
71
  ) from error
70
- if re.search(pattern, object_value):
71
- return True
72
- return False
72
+ return bool(re.search(pattern, object_value))
73
73
 
74
74
 
75
75
  def transform_none(ctx: Filter, value: str) -> str: # noqa: ARG001
@@ -98,6 +98,7 @@ class DirectValuePropertyFilter(Filter):
98
98
  completion_method: Literal["values", "none", "fixed"]
99
99
  fixed_completion: list[CompletionItem]
100
100
  fixed_completion_only: bool
101
+ title_helper: TitleHelper | None
101
102
 
102
103
  def __init__( # noqa: PLR0913
103
104
  self,
@@ -110,6 +111,7 @@ class DirectValuePropertyFilter(Filter):
110
111
  completion_method: Literal["values", "none", "fixed"] = "values",
111
112
  fixed_completion: list[CompletionItem] | None = None,
112
113
  fixed_completion_only: bool = False,
114
+ title_helper: TitleHelper | None = None,
113
115
  ):
114
116
  """Create the new filter
115
117
 
@@ -132,6 +134,9 @@ class DirectValuePropertyFilter(Filter):
132
134
  Initialization with fixed_completion will set completion_method="fixed"
133
135
  fixed_completion_only:
134
136
  Raise an UsageError if a value is not from fixed completion.
137
+ title_helper:
138
+ (Optional) TitleHelper instance which will be used to provide
139
+ resource titles as descriptions of completions candidates
135
140
  """
136
141
  self.name = name
137
142
  self.description = description
@@ -146,6 +151,7 @@ class DirectValuePropertyFilter(Filter):
146
151
  else:
147
152
  self.fixed_completion = []
148
153
  self.fixed_completion_only = fixed_completion_only
154
+ self.title_helper = title_helper
149
155
 
150
156
  def is_filtered(self, object_: dict, value: str) -> bool:
151
157
  """Return True if the object is filtered (stays in list).
@@ -163,13 +169,9 @@ class DirectValuePropertyFilter(Filter):
163
169
  if self.property_key not in object_ or object_[self.property_key] is None:
164
170
  if self.default_value is None:
165
171
  return False
166
- if self.compare(self, self.default_value, filter_value):
167
- return True
168
- return False
172
+ return bool(self.compare(self, self.default_value, filter_value))
169
173
  object_value = self.transform(self, str(object_[self.property_key]))
170
- if self.compare(self, object_value, filter_value):
171
- return True
172
- return False
174
+ return bool(self.compare(self, object_value, filter_value))
173
175
 
174
176
  def complete_values(self, objects: list[dict], incomplete: str) -> list[CompletionItem]:
175
177
  """Provide completion items for filter values"""
@@ -178,16 +180,22 @@ class DirectValuePropertyFilter(Filter):
178
180
  if self.completion_method == "fixed":
179
181
  return self.fixed_completion
180
182
  if self.completion_method == "values":
181
- candidates = []
183
+ candidates: list = []
182
184
  for _ in objects:
183
185
  if self.property_key not in _ or _[self.property_key] is None:
184
186
  if self.default_value is None:
187
+ # without actual values and no defaults, we can not complete something
185
188
  continue
189
+ # without actual values but at least a default value
186
190
  candidate = self.transform(self, str(self.default_value))
187
191
  else:
192
+ # a normal candidate value
188
193
  candidate = self.transform(self, str(_[self.property_key]))
189
194
  candidates.append(candidate)
190
- return _finalize_completion(
195
+ if self.title_helper:
196
+ self.title_helper.get(list(set(candidates)))
197
+ candidates = [(str(_), self.title_helper.get(_)) for _ in candidates]
198
+ return finalize_completion(
191
199
  candidates=candidates,
192
200
  incomplete=incomplete,
193
201
  )
@@ -195,7 +203,7 @@ class DirectValuePropertyFilter(Filter):
195
203
 
196
204
 
197
205
  class DirectListPropertyFilter(Filter):
198
- """Class to create filter based on direct list properties of an object
206
+ """Class to create filter based on direct list properties of an object
199
207
 
200
208
  False in case of missing key, key is None, or value is not in list
201
209
  True in case value is in the list
@@ -204,11 +212,31 @@ class DirectListPropertyFilter(Filter):
204
212
  name: str
205
213
  description: str
206
214
  property_key: str
215
+ title_helper: TitleHelper | None
216
+
217
+ def __init__(
218
+ self,
219
+ name: str,
220
+ description: str,
221
+ property_key: str,
222
+ title_helper: TitleHelper | None = None,
223
+ ):
224
+ """Create the new filter
207
225
 
208
- def __init__(self, name: str, description: str, property_key: str):
226
+ name:
227
+ The name of the filter (used as identifier)
228
+ description:
229
+ The description of the filter (used in filter name completion)
230
+ property_key:
231
+ The key of the property which is compared and completed
232
+ title_helper:
233
+ (Optional) TitleHelper instance which will be used to provide
234
+ resource titles as descriptions of completions candidates
235
+ """
209
236
  self.name = name
210
237
  self.description = description
211
238
  self.property_key = property_key
239
+ self.title_helper = title_helper
212
240
 
213
241
  def is_filtered(self, object_: dict, value: str) -> bool:
214
242
  """Return True if the object is filtered (stays in list)."""
@@ -218,13 +246,11 @@ class DirectListPropertyFilter(Filter):
218
246
  return False # key value is None
219
247
  if not isinstance(object_[self.property_key], list):
220
248
  return False # key value is not a list
221
- if value in [str(_) for _ in object_[self.property_key]]:
222
- return True
223
- return False
249
+ return value in [str(_) for _ in object_[self.property_key]]
224
250
 
225
251
  def complete_values(self, objects: list[dict], incomplete: str) -> list[CompletionItem]:
226
252
  """Provide completion items for filter values"""
227
- candidates = []
253
+ candidates: list = []
228
254
  for object_ in objects:
229
255
  if self.property_key not in object_:
230
256
  continue # key is not in object
@@ -233,7 +259,10 @@ class DirectListPropertyFilter(Filter):
233
259
  if not isinstance(object_[self.property_key], list):
234
260
  continue # key value is not a list
235
261
  candidates.extend([str(_) for _ in object_[self.property_key]])
236
- return _finalize_completion(candidates=candidates, incomplete=incomplete)
262
+ if self.title_helper:
263
+ self.title_helper.get(list(set(candidates)))
264
+ candidates = [(_, self.title_helper.get(_)) for _ in candidates]
265
+ return finalize_completion(candidates=candidates, incomplete=incomplete)
237
266
 
238
267
 
239
268
  class ObjectList:
@@ -295,11 +324,13 @@ class ObjectList:
295
324
  def apply_filters(
296
325
  self,
297
326
  ctx: Context,
298
- filter_: tuple[tuple[str, str]] | list[tuple[str, str]],
327
+ filter_: tuple[tuple[str, str]] | list[tuple[str, str]] | None = None,
299
328
  objects: list[dict] | None = None,
300
329
  ) -> list[dict]:
301
330
  """Filter a given object list"""
302
331
  filtered = list(self.get_objects(ctx)) if objects is None else list(objects)
332
+ if not filter_:
333
+ return filtered
303
334
  for filter_name, filter_value in filter_:
304
335
  the_filter = self.get_filter(filter_name)
305
336
  filtered = the_filter.filter_list(value=filter_value, objects=filtered)
@@ -315,7 +346,7 @@ class ObjectList:
315
346
  previous_filter = ctx.params.get("filter_") # tuple of (name, value) pairs or NONE
316
347
  previous_filter = previous_filter if previous_filter is not None else []
317
348
  previous_filter_names = [_[0] for _ in previous_filter]
318
- args = _get_completion_args(incomplete)
349
+ args = get_completion_args(incomplete)
319
350
  last_argument = args[len(args) - 1]
320
351
 
321
352
  if last_argument == "--filter":
@@ -325,7 +356,7 @@ class ObjectList:
325
356
  for name in self.get_filter_names()
326
357
  if name not in previous_filter_names # do not show already used filters
327
358
  ]
328
- return _finalize_completion(
359
+ return finalize_completion(
329
360
  candidates=candidates,
330
361
  incomplete=incomplete,
331
362
  )
@@ -0,0 +1 @@
1
+ """cmemc custom parameter types."""
@@ -0,0 +1,69 @@
1
+ """Custom Click smart_path ParamType"""
2
+
3
+ import os
4
+ from typing import IO, Any
5
+
6
+ import click
7
+ import smart_open
8
+ from click.core import Context, Parameter
9
+ from smart_open import compression
10
+
11
+
12
+ class ClickSmartPath(click.Path):
13
+ """Custom Click smart_path ParamType"""
14
+
15
+ name = "click-smart-path"
16
+
17
+ def __init__( # noqa: PLR0913
18
+ self,
19
+ exists: bool = False,
20
+ file_okay: bool = True,
21
+ dir_okay: bool = True,
22
+ writable: bool = False,
23
+ readable: bool = True,
24
+ resolve_path: bool = False,
25
+ allow_dash: bool = False,
26
+ remote_okay: bool = False,
27
+ ):
28
+ super().__init__(
29
+ exists=exists,
30
+ file_okay=file_okay,
31
+ dir_okay=dir_okay,
32
+ writable=writable,
33
+ readable=readable,
34
+ resolve_path=resolve_path,
35
+ allow_dash=allow_dash,
36
+ )
37
+ self.remote_okay = remote_okay
38
+
39
+ def convert(
40
+ self,
41
+ value: str | os.PathLike[str],
42
+ param: Parameter | None,
43
+ ctx: Context | None,
44
+ ) -> str | bytes | os.PathLike[str]:
45
+ """Convert the given value"""
46
+ try:
47
+ parsed_path = smart_open.parse_uri(value)
48
+ except NotImplementedError as exe:
49
+ self.fail(f"{exe}", param, ctx)
50
+ if parsed_path.scheme == "file":
51
+ return super().convert(parsed_path.uri_path, param, ctx)
52
+ if not self.remote_okay:
53
+ self.fail("Remote path not supported", param, ctx)
54
+
55
+ return value
56
+
57
+ @staticmethod
58
+ def open(
59
+ file_path: str, mode: str = "rb", transport_params: dict[str, Any] | None = None
60
+ ) -> IO:
61
+ """Open the file and return the file handle."""
62
+ if file_path == "-":
63
+ return click.open_file(file_path, mode=mode)
64
+ return smart_open.open( # type: ignore[no-any-return]
65
+ file_path,
66
+ mode,
67
+ transport_params=transport_params,
68
+ compression=compression.NO_COMPRESSION,
69
+ )
@@ -0,0 +1,94 @@
1
+ """Provides client classes for interacting with different storage systems."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import urllib.parse
7
+ from pathlib import Path
8
+ from typing import IO, TYPE_CHECKING, ClassVar
9
+
10
+ import smart_open
11
+
12
+ from cmem_cmemc.smart_path.clients.http import HttpPath
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Generator
16
+
17
+ from cmem_cmemc.smart_path.clients import StoragePath
18
+
19
+
20
+ class SmartPath:
21
+ """Smart path"""
22
+
23
+ SUPPORTED_SCHEMAS: ClassVar = {
24
+ "file": Path,
25
+ "http": HttpPath,
26
+ "https": HttpPath,
27
+ }
28
+
29
+ def __init__(self, path: str):
30
+ self.path = path
31
+ self.schema = self._sniff_schema(self.path)
32
+ if self.schema not in self.SUPPORTED_SCHEMAS:
33
+ raise NotImplementedError(f"Schema '{self.schema}' not supported")
34
+ self._client: StoragePath = self.SUPPORTED_SCHEMAS.get(self.schema)(self.path)
35
+
36
+ @staticmethod
37
+ def _sniff_schema(path: str) -> str:
38
+ """Return the scheme of the URL only, as a string."""
39
+ #
40
+ # urlsplit doesn't work on Windows -- it parses the drive as the scheme...
41
+ # no protocol given => assume a local file
42
+ #
43
+ if os.name == "nt" and "://" not in path:
44
+ path = "file://" + path
45
+ schema = urllib.parse.urlsplit(path).scheme
46
+ return schema if schema else "file"
47
+
48
+ def is_dir(self) -> bool:
49
+ """Determine if path is a directory or not."""
50
+ return self._client.is_dir()
51
+
52
+ def is_file(self) -> bool:
53
+ """Return the suffix of the path."""
54
+ return self._client.is_file()
55
+
56
+ def exists(self) -> bool:
57
+ """Determine if path exists or not."""
58
+ return self._client.exists()
59
+
60
+ @property
61
+ def suffix(self) -> str:
62
+ """Return the suffix of the path."""
63
+ return self._client.suffix
64
+
65
+ @property
66
+ def parent(self) -> StoragePath:
67
+ """The logical parent of the path."""
68
+ return self._client.parent
69
+
70
+ @property
71
+ def name(self) -> str:
72
+ """Determine the name of the path."""
73
+ return self._client.name
74
+
75
+ def open(self, mode: str = "r", encoding: str | None = None) -> IO:
76
+ """Open the file pointed by this path."""
77
+ file: IO = smart_open.open(self.path, mode=mode, encoding=encoding)
78
+ return file
79
+
80
+ def mkdir(self, parents: bool = False, exist_ok: bool = False) -> None:
81
+ """Return the suffix of the path."""
82
+ self._client.mkdir(parents=parents, exist_ok=exist_ok)
83
+
84
+ def glob(self, pattern: str) -> Generator[StoragePath, StoragePath, StoragePath]:
85
+ """Iterate over this subtree and yield all existing files"""
86
+ return self._client.glob(pattern=pattern)
87
+
88
+ def resolve(self) -> StoragePath:
89
+ """Iterate over this subtree and yield all existing files"""
90
+ return self._client.resolve()
91
+
92
+ def __truediv__(self, key: str) -> StoragePath:
93
+ """Return StoragePath with appending the key to the exising path"""
94
+ return self._client.__truediv__(key)