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.
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 +100 -16
  11. cmem_cmemc/commands/metrics.py +195 -79
  12. cmem_cmemc/commands/migration.py +265 -0
  13. cmem_cmemc/commands/project.py +62 -17
  14. cmem_cmemc/commands/python.py +57 -26
  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 +304 -113
  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 +185 -141
  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 +118 -0
  33. cmem_cmemc/migrations/bootstrap_data.py +30 -0
  34. cmem_cmemc/migrations/shapes_widget_integrations_243.py +194 -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 +77 -0
  43. cmem_cmemc/title_helper.py +41 -0
  44. cmem_cmemc/utils.py +114 -47
  45. {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/LICENSE +1 -1
  46. cmem_cmemc-24.3.0rc1.dist-info/METADATA +89 -0
  47. cmem_cmemc-24.3.0rc1.dist-info/RECORD +53 -0
  48. {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/WHEEL +1 -1
  49. cmem_cmemc-24.2.0rc1.dist-info/METADATA +0 -69
  50. cmem_cmemc-24.2.0rc1.dist-info/RECORD +0 -37
  51. {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/entry_points.txt +0 -0
@@ -1,39 +1,155 @@
1
1
  """metrics commands for cmem command line interface."""
2
2
 
3
3
  import click
4
- from prometheus_client import Metric
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.commands import CmemcCommand, CmemcGroup
8
- from cmem_cmemc.context import ApplicationContext
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 _filter_samples(family: Metric, label_filter: tuple[tuple[str, str], ...]) -> list:
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
- all_samples = family.samples
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 = metric_get_labels(family)
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 '{family.name}' does " f"not have a label named '{name}'."
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 '{family.name}' does "
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 sample.labels and sample.labels[name] == value:
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=completion.metric_ids)
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=completion.metric_label_filter,
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.pass_obj
78
- def get_command( # noqa: PLR0913
79
- app: ApplicationContext,
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.echo_debug(f"Get metrics family '{metric_id}' for job '{job_id}'")
94
- family = metrics_get_family(job_id, metric_id)
95
- app.echo_debug(f"Filter family '{metric_id}' with filter '{label_filter}'")
96
- samples = _filter_samples(family, label_filter)
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
- if family.type == "histogram":
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=completion.metric_ids)
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.pass_obj
141
- def inspect_command(app: ApplicationContext, metric_id: str, job_id: str, raw: bool) -> None:
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
- family = metrics_get_family(job_id, metric_id)
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({"family": family.__dict__, "labels": metric_get_labels(family)})
258
+ app.echo_info_json(data)
151
259
  return
152
- app.echo_info_table(struct_to_table(family.__dict__), headers=["Key", "Value"], sort_column=0)
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
- "--job",
161
- "job_id",
162
- type=click.Choice(["DP"]),
163
- default="DP",
164
- show_default=True,
165
- help="The job from which the metrics data is fetched.",
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.pass_obj
176
- def list_command(app: ApplicationContext, job_id: str, id_only: bool, raw: bool) -> None:
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
- data = metrics_get_dict(job_id=job_id)
291
+ app = ctx.obj
292
+ data = metrics_list.apply_filters(ctx=ctx, filter_=filter_)
185
293
  if raw:
186
- output = {}
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 key, _ in sorted(data.items()):
193
- app.echo_info(key)
297
+ for _ in sorted([_.get("id") for _ in data]):
298
+ app.echo_info(_)
194
299
  return
195
300
 
196
- table = []
197
- for _, family in sorted(data.items()):
198
- labels = metric_get_labels(family)
199
- table.append([family.name, family.type, len(labels), len(family.samples)])
200
-
201
- app.echo_info_table(table, headers=["ID", "Type", "L", "S"], sort_column=0)
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)