cmem-cmemc 24.2.0rc1__py3-none-any.whl → 24.3.0__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.
- cmem_cmemc/__init__.py +7 -12
- cmem_cmemc/command.py +20 -0
- cmem_cmemc/command_group.py +70 -0
- cmem_cmemc/commands/__init__.py +0 -81
- cmem_cmemc/commands/acl.py +118 -62
- cmem_cmemc/commands/admin.py +46 -35
- cmem_cmemc/commands/client.py +2 -1
- cmem_cmemc/commands/config.py +3 -1
- cmem_cmemc/commands/dataset.py +27 -24
- cmem_cmemc/commands/graph.py +160 -19
- cmem_cmemc/commands/metrics.py +195 -79
- cmem_cmemc/commands/migration.py +267 -0
- cmem_cmemc/commands/project.py +62 -17
- cmem_cmemc/commands/python.py +56 -25
- cmem_cmemc/commands/query.py +23 -14
- cmem_cmemc/commands/resource.py +10 -2
- cmem_cmemc/commands/scheduler.py +10 -2
- cmem_cmemc/commands/store.py +118 -14
- cmem_cmemc/commands/user.py +8 -2
- cmem_cmemc/commands/validation.py +304 -113
- cmem_cmemc/commands/variable.py +10 -2
- cmem_cmemc/commands/vocabulary.py +48 -29
- cmem_cmemc/commands/workflow.py +86 -59
- cmem_cmemc/commands/workspace.py +27 -8
- cmem_cmemc/completion.py +190 -140
- cmem_cmemc/constants.py +2 -0
- cmem_cmemc/context.py +88 -42
- cmem_cmemc/manual_helper/graph.py +1 -0
- cmem_cmemc/manual_helper/multi_page.py +3 -1
- cmem_cmemc/migrations/__init__.py +1 -0
- cmem_cmemc/migrations/abc.py +84 -0
- cmem_cmemc/migrations/access_conditions_243.py +122 -0
- cmem_cmemc/migrations/bootstrap_data.py +28 -0
- cmem_cmemc/migrations/shapes_widget_integrations_243.py +274 -0
- cmem_cmemc/migrations/workspace_configurations.py +28 -0
- cmem_cmemc/object_list.py +53 -22
- cmem_cmemc/parameter_types/__init__.py +1 -0
- cmem_cmemc/parameter_types/path.py +69 -0
- cmem_cmemc/smart_path/__init__.py +94 -0
- cmem_cmemc/smart_path/clients/__init__.py +63 -0
- cmem_cmemc/smart_path/clients/http.py +65 -0
- cmem_cmemc/string_processor.py +83 -0
- cmem_cmemc/title_helper.py +41 -0
- cmem_cmemc/utils.py +100 -45
- {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0.dist-info}/LICENSE +1 -1
- cmem_cmemc-24.3.0.dist-info/METADATA +89 -0
- cmem_cmemc-24.3.0.dist-info/RECORD +53 -0
- {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0.dist-info}/WHEEL +1 -1
- cmem_cmemc-24.2.0rc1.dist-info/METADATA +0 -69
- cmem_cmemc-24.2.0rc1.dist-info/RECORD +0 -37
- {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0.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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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)
|