cmem-cmemc 24.2.0rc1__py3-none-any.whl → 24.3.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.
- 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 +100 -16
- cmem_cmemc/commands/metrics.py +195 -79
- cmem_cmemc/commands/migration.py +265 -0
- cmem_cmemc/commands/project.py +62 -17
- cmem_cmemc/commands/python.py +57 -26
- 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 +185 -141
- 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 +118 -0
- cmem_cmemc/migrations/bootstrap_data.py +30 -0
- cmem_cmemc/migrations/shapes_widget_integrations_243.py +194 -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 +77 -0
- cmem_cmemc/title_helper.py +41 -0
- cmem_cmemc/utils.py +114 -47
- {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/LICENSE +1 -1
- cmem_cmemc-24.3.0rc1.dist-info/METADATA +89 -0
- cmem_cmemc-24.3.0rc1.dist-info/RECORD +53 -0
- {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0rc1.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.0rc1.dist-info}/entry_points.txt +0 -0
cmem_cmemc/commands/metrics.py
CHANGED
|
@@ -1,39 +1,155 @@
|
|
|
1
1
|
"""metrics commands for cmem command line interface."""
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
|
-
from
|
|
4
|
+
from click import Argument, Context
|
|
5
|
+
from click.shell_completion import CompletionItem
|
|
6
|
+
from cmem.cmempy.api import request
|
|
7
|
+
from cmem.cmempy.config import get_cmem_base_uri, get_di_api_endpoint, get_dp_api_endpoint
|
|
8
|
+
from prometheus_client.parser import text_string_to_metric_families
|
|
9
|
+
from requests import HTTPError
|
|
5
10
|
|
|
6
11
|
from cmem_cmemc import completion
|
|
7
|
-
from cmem_cmemc.
|
|
8
|
-
from cmem_cmemc.
|
|
12
|
+
from cmem_cmemc.command import CmemcCommand
|
|
13
|
+
from cmem_cmemc.command_group import CmemcGroup
|
|
14
|
+
from cmem_cmemc.context import CONTEXT, ApplicationContext
|
|
15
|
+
from cmem_cmemc.object_list import (
|
|
16
|
+
DirectValuePropertyFilter,
|
|
17
|
+
ObjectList,
|
|
18
|
+
compare_regex,
|
|
19
|
+
compare_str_equality,
|
|
20
|
+
)
|
|
9
21
|
from cmem_cmemc.utils import (
|
|
10
22
|
metric_get_labels,
|
|
11
|
-
metrics_get_dict,
|
|
12
|
-
metrics_get_family,
|
|
13
23
|
struct_to_table,
|
|
14
24
|
)
|
|
15
25
|
|
|
16
26
|
|
|
17
|
-
def
|
|
27
|
+
def get_all_metrics(ctx: click.Context) -> list[dict]:
|
|
28
|
+
"""Get metrics data for object list"""
|
|
29
|
+
known_metrics_urls: list[tuple[str, str]] = [
|
|
30
|
+
("explore", get_dp_api_endpoint() + "/actuator/prometheus"),
|
|
31
|
+
("build", get_di_api_endpoint() + "/metrics"),
|
|
32
|
+
("store", get_dp_api_endpoint() + "/actuator/proxy/graphdb/metrics/structures"),
|
|
33
|
+
("store", get_dp_api_endpoint() + "/actuator/proxy/graphdb/metrics/infrastructure"),
|
|
34
|
+
("store", get_dp_api_endpoint() + "/actuator/proxy/graphdb/metrics/cluster"),
|
|
35
|
+
("store", get_dp_api_endpoint() + "/actuator/proxy/graphdb/metrics/repository/cmem"),
|
|
36
|
+
]
|
|
37
|
+
app: ApplicationContext = ctx.obj
|
|
38
|
+
all_metrics: list[dict] = []
|
|
39
|
+
for job, url in known_metrics_urls:
|
|
40
|
+
try:
|
|
41
|
+
families = text_string_to_metric_families(request(url).text)
|
|
42
|
+
except HTTPError as error:
|
|
43
|
+
if app is not None:
|
|
44
|
+
# when in completion mode, obj is not set :-(
|
|
45
|
+
app.echo_debug(str(error))
|
|
46
|
+
continue
|
|
47
|
+
for family in families:
|
|
48
|
+
documentation = family.documentation
|
|
49
|
+
if documentation == "":
|
|
50
|
+
documentation = f"No documentation available for {job}:{family.name}"
|
|
51
|
+
new_metric = {
|
|
52
|
+
"id": f"{job}:{family.name}",
|
|
53
|
+
"job": job,
|
|
54
|
+
"name": family.name,
|
|
55
|
+
"type": family.type,
|
|
56
|
+
"documentation": documentation,
|
|
57
|
+
"labels": metric_get_labels(family),
|
|
58
|
+
"samples": family.samples,
|
|
59
|
+
# "object": family
|
|
60
|
+
}
|
|
61
|
+
if family.type not in ("unknown", "histogram"):
|
|
62
|
+
all_metrics.append(new_metric)
|
|
63
|
+
return all_metrics
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
metrics_list = ObjectList(
|
|
67
|
+
name="metrics",
|
|
68
|
+
get_objects=get_all_metrics,
|
|
69
|
+
filters=[
|
|
70
|
+
DirectValuePropertyFilter(
|
|
71
|
+
name="job",
|
|
72
|
+
description="Filter metrics by job ID.",
|
|
73
|
+
property_key="job",
|
|
74
|
+
),
|
|
75
|
+
DirectValuePropertyFilter(
|
|
76
|
+
name="name",
|
|
77
|
+
description="Filter metrics by regex matching the name.",
|
|
78
|
+
property_key="name",
|
|
79
|
+
compare=compare_regex,
|
|
80
|
+
fixed_completion=[],
|
|
81
|
+
),
|
|
82
|
+
DirectValuePropertyFilter(
|
|
83
|
+
name="type",
|
|
84
|
+
description="Filter metrics by type.",
|
|
85
|
+
property_key="type",
|
|
86
|
+
),
|
|
87
|
+
DirectValuePropertyFilter(
|
|
88
|
+
name="id",
|
|
89
|
+
description="Filter metrics by ID.",
|
|
90
|
+
property_key="id",
|
|
91
|
+
compare=compare_str_equality,
|
|
92
|
+
),
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _complete_metrics_id(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]: # noqa: ARG001
|
|
98
|
+
"""Prepare a list of metric identifier."""
|
|
99
|
+
CONTEXT.set_connection_from_params(ctx.find_root().params)
|
|
100
|
+
candidates = [(_["id"], _["documentation"]) for _ in metrics_list.apply_filters(ctx=ctx)]
|
|
101
|
+
return completion.finalize_completion(candidates=candidates, incomplete=incomplete)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _complete_metric_label_filter(
|
|
105
|
+
ctx: Context,
|
|
106
|
+
param: Argument, # noqa: ARG001
|
|
107
|
+
incomplete: str,
|
|
108
|
+
) -> list[CompletionItem]:
|
|
109
|
+
"""Prepare a list of label name or values"""
|
|
110
|
+
CONTEXT.set_connection_from_params(ctx.find_root().params)
|
|
111
|
+
args = completion.get_completion_args(incomplete)
|
|
112
|
+
incomplete = incomplete.lower()
|
|
113
|
+
options: list[str] = []
|
|
114
|
+
try:
|
|
115
|
+
metric_id = ctx.args[0]
|
|
116
|
+
metric = metrics_list.apply_filters(ctx=ctx, filter_=[("id", metric_id)])[0]
|
|
117
|
+
except IndexError:
|
|
118
|
+
# means: --filter is used before we have a metrics ID
|
|
119
|
+
return []
|
|
120
|
+
labels = metric["labels"]
|
|
121
|
+
if args[len(args) - 1] in "--filter":
|
|
122
|
+
# we are in the name position
|
|
123
|
+
options = list(labels.keys())
|
|
124
|
+
if args[len(args) - 2] in "--filter":
|
|
125
|
+
label_name = args[len(args) - 1]
|
|
126
|
+
# we are in the value position
|
|
127
|
+
options = labels[label_name]
|
|
128
|
+
return completion.finalize_completion(candidates=options, incomplete=incomplete)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _filter_samples(family: dict, label_filter: tuple[tuple[str, str], ...]) -> list:
|
|
18
132
|
"""Filter samples by labels."""
|
|
19
|
-
|
|
133
|
+
family_name = family.get("name")
|
|
134
|
+
all_samples: list[list] = family["samples"]
|
|
20
135
|
if not label_filter:
|
|
21
136
|
return all_samples
|
|
22
|
-
labels =
|
|
137
|
+
labels: dict[str, list[str]] = family["labels"]
|
|
23
138
|
samples = []
|
|
24
139
|
for sample in all_samples:
|
|
25
140
|
matching_labels = 0
|
|
141
|
+
sample_labels = sample[1]
|
|
26
142
|
for name, value in label_filter:
|
|
27
143
|
if name not in labels:
|
|
28
144
|
raise ValueError(
|
|
29
|
-
f"The metric '{
|
|
145
|
+
f"The metric '{family_name}' does " f"not have a label named '{name}'."
|
|
30
146
|
)
|
|
31
147
|
if value not in labels[name]:
|
|
32
148
|
raise ValueError(
|
|
33
|
-
f"The metric '{
|
|
149
|
+
f"The metric '{family_name}' does "
|
|
34
150
|
f"not have a label '{name}' with the value '{value}'."
|
|
35
151
|
)
|
|
36
|
-
if name in
|
|
152
|
+
if name in sample_labels and sample_labels[name] == value:
|
|
37
153
|
matching_labels += 1
|
|
38
154
|
if matching_labels == len(label_filter):
|
|
39
155
|
# all filter match
|
|
@@ -43,20 +159,12 @@ def _filter_samples(family: Metric, label_filter: tuple[tuple[str, str], ...]) -
|
|
|
43
159
|
|
|
44
160
|
# pylint: disable-msg=too-many-arguments
|
|
45
161
|
@click.command(cls=CmemcCommand, name="get")
|
|
46
|
-
@click.argument("metric_id", required=True, type=click.STRING, shell_complete=
|
|
47
|
-
@click.option(
|
|
48
|
-
"--job",
|
|
49
|
-
"job_id",
|
|
50
|
-
type=click.Choice(["DP"]),
|
|
51
|
-
default="DP",
|
|
52
|
-
show_default=True,
|
|
53
|
-
help="The job from which the metrics data is fetched.",
|
|
54
|
-
)
|
|
162
|
+
@click.argument("metric_id", required=True, type=click.STRING, shell_complete=_complete_metrics_id)
|
|
55
163
|
@click.option(
|
|
56
164
|
"--filter",
|
|
57
165
|
"label_filter",
|
|
58
166
|
type=(str, str),
|
|
59
|
-
shell_complete=
|
|
167
|
+
shell_complete=_complete_metric_label_filter,
|
|
60
168
|
multiple=True,
|
|
61
169
|
help="A set of label name/value pairs in order to filter the samples "
|
|
62
170
|
"of the requested metric family. Each metric has a different set "
|
|
@@ -74,11 +182,10 @@ def _filter_samples(family: Metric, label_filter: tuple[tuple[str, str], ...]) -
|
|
|
74
182
|
"even for single row tables.",
|
|
75
183
|
)
|
|
76
184
|
@click.option("--raw", is_flag=True, help="Outputs raw prometheus sample classes.")
|
|
77
|
-
@click.
|
|
78
|
-
def get_command(
|
|
79
|
-
|
|
185
|
+
@click.pass_context
|
|
186
|
+
def get_command(
|
|
187
|
+
ctx: click.Context,
|
|
80
188
|
metric_id: str,
|
|
81
|
-
job_id: str,
|
|
82
189
|
label_filter: tuple[tuple[str, str], ...],
|
|
83
190
|
raw: bool,
|
|
84
191
|
enforce_table: bool,
|
|
@@ -90,32 +197,33 @@ def get_command( # noqa: PLR0913
|
|
|
90
197
|
command. A metric can contain multiple samples.
|
|
91
198
|
These samples are distinguished by labels (name and value).
|
|
92
199
|
"""
|
|
93
|
-
app
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
200
|
+
app = ctx.obj
|
|
201
|
+
data = metrics_list.apply_filters(ctx=ctx, filter_=[("id", metric_id)])
|
|
202
|
+
if len(data) == 0:
|
|
203
|
+
raise click.UsageError(
|
|
204
|
+
f"No metric with ID '{metric_id}' found. "
|
|
205
|
+
"Use the `metrics list` command to list all metrics."
|
|
206
|
+
)
|
|
207
|
+
if len(data) > 1:
|
|
208
|
+
raise click.UsageError("Unknown Error - More than one metric with ID '{metric_id}' found.")
|
|
209
|
+
metric = data[0]
|
|
210
|
+
|
|
211
|
+
samples = _filter_samples(metric, label_filter)
|
|
212
|
+
if raw:
|
|
213
|
+
app.echo_info_json(samples)
|
|
214
|
+
return
|
|
215
|
+
|
|
97
216
|
if len(samples) == 0:
|
|
98
217
|
raise ValueError(
|
|
99
218
|
"No data - the given label combination filtered out "
|
|
100
219
|
f"all available samples of the metric {metric_id}."
|
|
101
220
|
)
|
|
102
221
|
|
|
103
|
-
if raw:
|
|
104
|
-
for sample in samples:
|
|
105
|
-
app.echo_info(str(sample))
|
|
106
|
-
return
|
|
107
|
-
|
|
108
222
|
if len(samples) == 1 and enforce_table is not True:
|
|
109
223
|
app.echo_info(str(samples[0].value))
|
|
110
224
|
return
|
|
111
225
|
|
|
112
|
-
|
|
113
|
-
raise NotImplementedError(
|
|
114
|
-
"Visualization of histogram metrics is not supported yet.\n"
|
|
115
|
-
"Please use the 'metrics inspect' command to get the raw data."
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
label_dict = metric_get_labels(family, clean=True)
|
|
226
|
+
label_dict = metric["labels"]
|
|
119
227
|
table = []
|
|
120
228
|
for sample in samples:
|
|
121
229
|
row = [sample.labels[key] for key in sorted(label_dict.keys())]
|
|
@@ -127,42 +235,39 @@ def get_command( # noqa: PLR0913
|
|
|
127
235
|
|
|
128
236
|
|
|
129
237
|
@click.command(cls=CmemcCommand, name="inspect")
|
|
130
|
-
@click.argument("metric_id", required=True, type=click.STRING, shell_complete=
|
|
131
|
-
@click.option(
|
|
132
|
-
"--job",
|
|
133
|
-
"job_id",
|
|
134
|
-
type=click.Choice(["DP"]),
|
|
135
|
-
default="DP",
|
|
136
|
-
show_default=True,
|
|
137
|
-
help="The job from which the metrics data is fetched.",
|
|
138
|
-
)
|
|
238
|
+
@click.argument("metric_id", required=True, type=click.STRING, shell_complete=_complete_metrics_id)
|
|
139
239
|
@click.option("--raw", is_flag=True, help="Outputs raw JSON of the table data.")
|
|
140
|
-
@click.
|
|
141
|
-
def inspect_command(
|
|
240
|
+
@click.pass_context
|
|
241
|
+
def inspect_command(ctx: Context, metric_id: str, raw: bool) -> None:
|
|
142
242
|
"""Inspect a metric.
|
|
143
243
|
|
|
144
244
|
This command outputs the data of a metric.
|
|
145
245
|
The first table includes basic metadata about the metric.
|
|
146
246
|
The second table includes sample labels and values.
|
|
147
247
|
"""
|
|
148
|
-
|
|
248
|
+
app = ctx.obj
|
|
249
|
+
data = metrics_list.apply_filters(ctx=ctx, filter_=[("id", metric_id)])
|
|
250
|
+
if len(data) == 0:
|
|
251
|
+
raise click.UsageError(
|
|
252
|
+
f"No metric with ID '{metric_id}' found. "
|
|
253
|
+
"Use the `metrics list` command to list all metrics."
|
|
254
|
+
)
|
|
255
|
+
if len(data) > 1:
|
|
256
|
+
raise click.UsageError("Unknown Error - More than one metric with ID '{metric_id}' found.")
|
|
149
257
|
if raw:
|
|
150
|
-
app.echo_info_json(
|
|
258
|
+
app.echo_info_json(data)
|
|
151
259
|
return
|
|
152
|
-
app.echo_info_table(struct_to_table(
|
|
153
|
-
app.echo_info_table(
|
|
154
|
-
struct_to_table(metric_get_labels(family)), headers=["Label", "Value"], sort_column=0
|
|
155
|
-
)
|
|
260
|
+
app.echo_info_table(struct_to_table(data), headers=["Key", "Value"], sort_column=0)
|
|
156
261
|
|
|
157
262
|
|
|
158
263
|
@click.command(cls=CmemcCommand, name="list")
|
|
159
264
|
@click.option(
|
|
160
|
-
"--
|
|
161
|
-
"
|
|
162
|
-
type=
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
265
|
+
"--filter",
|
|
266
|
+
"filter_",
|
|
267
|
+
type=(str, str),
|
|
268
|
+
multiple=True,
|
|
269
|
+
help=metrics_list.get_filter_help_text(),
|
|
270
|
+
shell_complete=metrics_list.complete_values,
|
|
166
271
|
)
|
|
167
272
|
@click.option(
|
|
168
273
|
"--id-only",
|
|
@@ -172,8 +277,10 @@ def inspect_command(app: ApplicationContext, metric_id: str, job_id: str, raw: b
|
|
|
172
277
|
@click.option(
|
|
173
278
|
"--raw", is_flag=True, help="Outputs (sorted) JSON dict, parsed from the metrics API output."
|
|
174
279
|
)
|
|
175
|
-
@click.
|
|
176
|
-
def list_command(
|
|
280
|
+
@click.pass_context
|
|
281
|
+
def list_command(
|
|
282
|
+
ctx: click.Context, filter_: tuple[tuple[str, str]], id_only: bool, raw: bool
|
|
283
|
+
) -> None:
|
|
177
284
|
"""List metrics for a specific job.
|
|
178
285
|
|
|
179
286
|
For each metric, the output table shows the metric ID,
|
|
@@ -181,24 +288,33 @@ def list_command(app: ApplicationContext, job_id: str, id_only: bool, raw: bool)
|
|
|
181
288
|
are describing the samples (L) and a count of how many samples are
|
|
182
289
|
currently available for a metric (S).
|
|
183
290
|
"""
|
|
184
|
-
|
|
291
|
+
app = ctx.obj
|
|
292
|
+
data = metrics_list.apply_filters(ctx=ctx, filter_=filter_)
|
|
185
293
|
if raw:
|
|
186
|
-
|
|
187
|
-
for key, family in sorted(data.items()):
|
|
188
|
-
output[key] = family.__dict__
|
|
189
|
-
app.echo_info_json(output)
|
|
294
|
+
app.echo_info_json(data)
|
|
190
295
|
return
|
|
191
296
|
if id_only:
|
|
192
|
-
for
|
|
193
|
-
app.echo_info(
|
|
297
|
+
for _ in sorted([_.get("id") for _ in data]):
|
|
298
|
+
app.echo_info(_)
|
|
194
299
|
return
|
|
195
300
|
|
|
196
|
-
table = [
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
301
|
+
table = [
|
|
302
|
+
[
|
|
303
|
+
_.get("id"),
|
|
304
|
+
_.get("type"),
|
|
305
|
+
len(_.get("labels")),
|
|
306
|
+
len(_.get("samples")),
|
|
307
|
+
_.get("documentation"),
|
|
308
|
+
]
|
|
309
|
+
for _ in data
|
|
310
|
+
]
|
|
311
|
+
app.echo_info_table(
|
|
312
|
+
table,
|
|
313
|
+
headers=["ID", "Type", "L", "S", "Documentation"],
|
|
314
|
+
sort_column=0,
|
|
315
|
+
caption=f"{len(table)} metrics families of {get_cmem_base_uri()}",
|
|
316
|
+
empty_table_message="No metrics families available.",
|
|
317
|
+
)
|
|
202
318
|
|
|
203
319
|
|
|
204
320
|
@click.group(cls=CmemcGroup)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""migrations command group"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click import Argument, Context
|
|
7
|
+
from click.shell_completion import CompletionItem
|
|
8
|
+
|
|
9
|
+
from cmem_cmemc.command import CmemcCommand
|
|
10
|
+
from cmem_cmemc.command_group import CmemcGroup
|
|
11
|
+
from cmem_cmemc.completion import check_option_in_params, finalize_completion
|
|
12
|
+
from cmem_cmemc.context import CONTEXT
|
|
13
|
+
from cmem_cmemc.migrations.access_conditions_243 import (
|
|
14
|
+
MoveAccessConditionsToNewGraph,
|
|
15
|
+
RenameAuthVocabularyResources,
|
|
16
|
+
)
|
|
17
|
+
from cmem_cmemc.migrations.bootstrap_data import MigrateBootstrapData
|
|
18
|
+
from cmem_cmemc.migrations.shapes_widget_integrations_243 import (
|
|
19
|
+
ChartsOnNodeShapesToToWidgetIntegrations,
|
|
20
|
+
ChartsOnPropertyShapesToWidgetIntegrations,
|
|
21
|
+
WorkflowTriggerPropertyShapesToWidgetIntegrations,
|
|
22
|
+
)
|
|
23
|
+
from cmem_cmemc.migrations.workspace_configurations import MigrateWorkspaceConfiguration
|
|
24
|
+
from cmem_cmemc.object_list import (
|
|
25
|
+
DirectListPropertyFilter,
|
|
26
|
+
DirectValuePropertyFilter,
|
|
27
|
+
ObjectList,
|
|
28
|
+
compare_regex,
|
|
29
|
+
transform_lower,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from cmem_cmemc.context import ApplicationContext
|
|
34
|
+
from cmem_cmemc.migrations.abc import MigrationRecipe
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_migrations(ctx: click.Context) -> list[dict]: # noqa: ARG001
|
|
38
|
+
"""Get users for object list"""
|
|
39
|
+
data = [
|
|
40
|
+
{
|
|
41
|
+
"id": _.id,
|
|
42
|
+
"description": _.description,
|
|
43
|
+
"component": _.component,
|
|
44
|
+
"first_version": _.first_version,
|
|
45
|
+
"last_version": _.last_version,
|
|
46
|
+
"tags": _.tags,
|
|
47
|
+
"object": _,
|
|
48
|
+
}
|
|
49
|
+
for _ in [
|
|
50
|
+
MoveAccessConditionsToNewGraph(),
|
|
51
|
+
RenameAuthVocabularyResources(),
|
|
52
|
+
MigrateBootstrapData(),
|
|
53
|
+
MigrateWorkspaceConfiguration(),
|
|
54
|
+
ChartsOnNodeShapesToToWidgetIntegrations(),
|
|
55
|
+
ChartsOnPropertyShapesToWidgetIntegrations(),
|
|
56
|
+
WorkflowTriggerPropertyShapesToWidgetIntegrations(),
|
|
57
|
+
]
|
|
58
|
+
]
|
|
59
|
+
data.sort(key=lambda x: x["first_version"])
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def complete_migration_ids(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]:
|
|
64
|
+
"""Prepare a list of migration recipe IDs"""
|
|
65
|
+
CONTEXT.set_connection_from_params(ctx.find_root().params)
|
|
66
|
+
options = []
|
|
67
|
+
for _ in get_migrations(ctx=ctx):
|
|
68
|
+
id_ = _["id"]
|
|
69
|
+
description = _["description"]
|
|
70
|
+
if check_option_in_params(id_, ctx.params.get(param.name)): # type: ignore[attr-defined, arg-type]
|
|
71
|
+
continue
|
|
72
|
+
options.append((id_, description))
|
|
73
|
+
return finalize_completion(candidates=options, incomplete=incomplete)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
migrations_list = ObjectList(
|
|
77
|
+
name="migration recipes",
|
|
78
|
+
get_objects=get_migrations,
|
|
79
|
+
filters=[
|
|
80
|
+
DirectValuePropertyFilter(
|
|
81
|
+
name="id",
|
|
82
|
+
description="Filter migrations by id.",
|
|
83
|
+
property_key="id",
|
|
84
|
+
transform=transform_lower,
|
|
85
|
+
),
|
|
86
|
+
DirectValuePropertyFilter(
|
|
87
|
+
name="description",
|
|
88
|
+
description="Filter migrations by regex over description.",
|
|
89
|
+
property_key="description",
|
|
90
|
+
compare=compare_regex,
|
|
91
|
+
fixed_completion=[],
|
|
92
|
+
),
|
|
93
|
+
DirectValuePropertyFilter(
|
|
94
|
+
name="first_version",
|
|
95
|
+
description="Filter migrations by first version which is target of this migration.",
|
|
96
|
+
property_key="first_version",
|
|
97
|
+
transform=transform_lower,
|
|
98
|
+
),
|
|
99
|
+
DirectListPropertyFilter(
|
|
100
|
+
name="tag",
|
|
101
|
+
description="Filter migrations by tags",
|
|
102
|
+
property_key="tags",
|
|
103
|
+
),
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@click.command(cls=CmemcCommand, name="list")
|
|
109
|
+
@click.option(
|
|
110
|
+
"--filter",
|
|
111
|
+
"filter_",
|
|
112
|
+
type=(str, str),
|
|
113
|
+
multiple=True,
|
|
114
|
+
help=migrations_list.get_filter_help_text(),
|
|
115
|
+
shell_complete=migrations_list.complete_values,
|
|
116
|
+
)
|
|
117
|
+
@click.option(
|
|
118
|
+
"--id-only",
|
|
119
|
+
is_flag=True,
|
|
120
|
+
help="Lists only IDs. This is useful for piping the IDs into other commands.",
|
|
121
|
+
)
|
|
122
|
+
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
123
|
+
@click.pass_context
|
|
124
|
+
def list_command(
|
|
125
|
+
ctx: click.Context, filter_: tuple[tuple[str, str]], id_only: bool, raw: bool
|
|
126
|
+
) -> None:
|
|
127
|
+
"""List migration recipies.
|
|
128
|
+
|
|
129
|
+
This command lists all available migration recipies
|
|
130
|
+
"""
|
|
131
|
+
app: ApplicationContext = ctx.obj
|
|
132
|
+
data = migrations_list.apply_filters(ctx=ctx, filter_=filter_)
|
|
133
|
+
if raw:
|
|
134
|
+
# https://stackoverflow.com/questions/17665809/
|
|
135
|
+
app.echo_info_json([(_, _.pop("object"))[0] for _ in data])
|
|
136
|
+
return
|
|
137
|
+
if id_only:
|
|
138
|
+
for _ in sorted([_.get("id") for _ in data]):
|
|
139
|
+
app.echo_info(_)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
table = [
|
|
143
|
+
[
|
|
144
|
+
_.get("id"),
|
|
145
|
+
_.get("description"),
|
|
146
|
+
", ".join(sorted(_.get("tags"))),
|
|
147
|
+
f"{_.get('first_version')} ({_.get('component')})",
|
|
148
|
+
]
|
|
149
|
+
for _ in data
|
|
150
|
+
]
|
|
151
|
+
app.echo_info_table(
|
|
152
|
+
table,
|
|
153
|
+
headers=["ID", "Description", "Tags", "First Version"],
|
|
154
|
+
sort_column=3,
|
|
155
|
+
caption=f"{len(table)} migration(s)",
|
|
156
|
+
empty_table_message="No migrations available.",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@click.command(cls=CmemcCommand, name="execute")
|
|
161
|
+
@click.argument(
|
|
162
|
+
"migration_id", type=click.STRING, required=False, shell_complete=complete_migration_ids
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--filter",
|
|
166
|
+
"filter_",
|
|
167
|
+
type=(str, str),
|
|
168
|
+
multiple=True,
|
|
169
|
+
help=migrations_list.get_filter_help_text(),
|
|
170
|
+
shell_complete=migrations_list.complete_values,
|
|
171
|
+
)
|
|
172
|
+
@click.option(
|
|
173
|
+
"-a",
|
|
174
|
+
"--all",
|
|
175
|
+
"all_",
|
|
176
|
+
is_flag=True,
|
|
177
|
+
help="Execute all needed migrations.",
|
|
178
|
+
)
|
|
179
|
+
@click.option("--test-only", is_flag=True, help="Only test, do not execute migrations.")
|
|
180
|
+
@click.option("--id-only", is_flag=True, help="Lists only recipe identifier. ")
|
|
181
|
+
@click.pass_context
|
|
182
|
+
def execute_command( # noqa: PLR0913
|
|
183
|
+
ctx: click.Context,
|
|
184
|
+
migration_id: str,
|
|
185
|
+
filter_: tuple[tuple[str, str]],
|
|
186
|
+
all_: bool,
|
|
187
|
+
test_only: bool,
|
|
188
|
+
id_only: bool,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Execute needed migration recipes.
|
|
191
|
+
|
|
192
|
+
This command executes one or more migration recipes.
|
|
193
|
+
Each recipe has a check method to determine if a migration is needed.
|
|
194
|
+
In addition to that, the current component version needs to match the specified
|
|
195
|
+
first-last-version range of the recipe.
|
|
196
|
+
|
|
197
|
+
Recipes are executed ordered by first_version.
|
|
198
|
+
|
|
199
|
+
Here are some argument examples, in order to see how to use this command:
|
|
200
|
+
`execute --all --test-only` will list all needed migrations (but not execute them),
|
|
201
|
+
`execute --filter tag system` will apply all migrations which target system data,
|
|
202
|
+
`execute bootstrap-data` will apply bootstrap-data migration if needed.
|
|
203
|
+
"""
|
|
204
|
+
app: ApplicationContext = ctx.obj
|
|
205
|
+
if not all_ and not migration_id and not filter_:
|
|
206
|
+
raise click.UsageError(
|
|
207
|
+
"You can execute a single recipe, some recipes based on filter, or all recipes. "
|
|
208
|
+
"See the documentation for more information."
|
|
209
|
+
)
|
|
210
|
+
data = migrations_list.apply_filters(ctx=ctx, filter_=filter_)
|
|
211
|
+
if migration_id:
|
|
212
|
+
data = migrations_list.apply_filters(ctx=ctx, filter_=[("id", migration_id)])
|
|
213
|
+
if not data:
|
|
214
|
+
raise click.UsageError(
|
|
215
|
+
f"Migration recipe '{migration_id}' not found. "
|
|
216
|
+
"Use the 'migration list' command to get available migration recipes."
|
|
217
|
+
)
|
|
218
|
+
applied_counter = 0
|
|
219
|
+
for _ in data:
|
|
220
|
+
recipe: MigrationRecipe = _.get("object")
|
|
221
|
+
if not recipe.version_matches():
|
|
222
|
+
app.echo_debug(f"Migration '{recipe.id}' does not match component version.")
|
|
223
|
+
continue
|
|
224
|
+
if not recipe.is_applicable():
|
|
225
|
+
app.echo_debug(f"Migration '{recipe.id}' is not applicable.")
|
|
226
|
+
continue
|
|
227
|
+
app.echo_debug(f"Migration '{recipe.id}' could be applied.")
|
|
228
|
+
applied_counter += 1
|
|
229
|
+
|
|
230
|
+
app.echo_info(recipe.id, condition=id_only)
|
|
231
|
+
app.echo_info(f"{recipe.description} ({recipe.id}) ... ", nl=False, condition=not id_only)
|
|
232
|
+
if test_only:
|
|
233
|
+
app.echo_warning("needed", condition=not id_only)
|
|
234
|
+
continue
|
|
235
|
+
recipe.apply()
|
|
236
|
+
app.echo_success("done", condition=not id_only)
|
|
237
|
+
app.echo_success("No migration needed.", condition=(not applied_counter and not id_only))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@click.group(cls=CmemcGroup)
|
|
241
|
+
def migration() -> CmemcGroup: # type: ignore[empty-body]
|
|
242
|
+
"""List and apply migration recipes.
|
|
243
|
+
|
|
244
|
+
With this command group, you can check your instance for needed
|
|
245
|
+
migration activities as well as apply them to your data.
|
|
246
|
+
|
|
247
|
+
Beside an ID and a description, migration recipes have the following
|
|
248
|
+
metadata: 'First Version' is the first Corporate Memory version,
|
|
249
|
+
where this recipe is maybe applicable. The recipe will never be applied
|
|
250
|
+
to a version below this version. 'Tags' is a classification of the recipe
|
|
251
|
+
with regard to the target data, it migrates.
|
|
252
|
+
|
|
253
|
+
The following tags are important:
|
|
254
|
+
'system' recipes target data structures
|
|
255
|
+
which are needed to run the most basic functionality properly. These recipes
|
|
256
|
+
can and should be applied after each version upgrade.
|
|
257
|
+
'user' recipes can change user and / or customizing data.
|
|
258
|
+
'acl' recipes migrate access condition data.
|
|
259
|
+
'shapes' recipes migrate shape data.
|
|
260
|
+
'config' recipes migrate configuration data.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
migration.add_command(list_command)
|
|
265
|
+
migration.add_command(execute_command)
|