pglift-cli 1.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.
@@ -0,0 +1,101 @@
1
+ # SPDX-FileCopyrightText: 2021 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ import subprocess
8
+ from datetime import datetime
9
+ from typing import Any
10
+
11
+ import click
12
+
13
+ from pglift import pgbackrest, postgresql, types
14
+ from pglift.models import system
15
+ from pglift.pgbackrest import base, models
16
+ from pglift.pgbackrest import register_if as register_if # noqa: F401
17
+
18
+ from .. import _site, hookimpl
19
+ from ..util import (
20
+ Command,
21
+ Obj,
22
+ OutputFormat,
23
+ async_command,
24
+ instance_identifier,
25
+ instance_identifier_option,
26
+ model_dump,
27
+ output_format_option,
28
+ print_json_for,
29
+ print_table_for,
30
+ )
31
+
32
+
33
+ @click.command(
34
+ "pgbackrest",
35
+ hidden=True,
36
+ cls=Command,
37
+ context_settings={"ignore_unknown_options": True},
38
+ )
39
+ @instance_identifier_option
40
+ @click.argument("command", nargs=-1, type=click.UNPROCESSED)
41
+ @click.pass_context
42
+ def pgbackrest_proxy(
43
+ context: click.Context, /, command: tuple[str, ...], **kwargs: Any
44
+ ) -> None:
45
+ """Proxy to pgbackrest operations on an instance"""
46
+ s = context.obj.instance.service(models.Service)
47
+ settings = pgbackrest.get_settings(_site.SETTINGS)
48
+ cmd_args = base.make_cmd(s.stanza, settings, *command)
49
+ try:
50
+ subprocess.run(cmd_args, capture_output=False, check=True) # nosec
51
+ except subprocess.CalledProcessError as e:
52
+ raise click.ClickException(str(e)) from e
53
+
54
+
55
+ @click.command("restore", cls=Command)
56
+ @instance_identifier(nargs=1)
57
+ @click.option("--label", help="Label of backup to restore")
58
+ @click.option("--date", type=click.DateTime(), help="Date of backup to restore")
59
+ @click.pass_obj
60
+ @async_command
61
+ async def instance_restore(
62
+ obj: Obj, instance: system.Instance, label: str | None, date: datetime | None
63
+ ) -> None:
64
+ """Restore PostgreSQL INSTANCE"""
65
+ await postgresql.check_status(instance, types.Status.not_running)
66
+ if label is not None and date is not None:
67
+ raise click.BadArgumentUsage(
68
+ "--label and --date arguments are mutually exclusive"
69
+ ) from None
70
+ settings = pgbackrest.get_settings(_site.SETTINGS)
71
+ with obj.lock:
72
+ await base.restore(instance, settings, label=label, date=date)
73
+
74
+
75
+ @click.command("backups", cls=Command)
76
+ @output_format_option
77
+ @instance_identifier(nargs=1)
78
+ @async_command
79
+ async def instance_backups(
80
+ instance: system.Instance, output_format: OutputFormat
81
+ ) -> None:
82
+ """List available backups for INSTANCE"""
83
+ settings = pgbackrest.get_settings(_site.SETTINGS)
84
+ backups = [b async for b in base.iter_backups(instance, settings)]
85
+ if output_format == OutputFormat.json:
86
+ print_json_for([model_dump(b) for b in backups])
87
+ else:
88
+ print_table_for(
89
+ backups, model_dump, title=f"Available backups for instance {instance}"
90
+ )
91
+
92
+
93
+ @hookimpl
94
+ def command() -> click.Command:
95
+ return pgbackrest_proxy
96
+
97
+
98
+ @hookimpl
99
+ def add_instance_commands(group: click.Group) -> None:
100
+ group.add_command(instance_backups)
101
+ group.add_command(instance_restore)
@@ -0,0 +1,40 @@
1
+ # SPDX-FileCopyrightText: 2024 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ import click
8
+
9
+ from pglift import pgbackrest, types
10
+ from pglift.models import system
11
+ from pglift.pgbackrest import repo_path
12
+ from pglift.pgbackrest.repo_path import register_if as register_if # noqa: F401
13
+
14
+ from .. import _site, hookimpl
15
+ from ..util import Command, Obj, async_command, instance_identifier
16
+
17
+
18
+ @click.command("backup", cls=Command)
19
+ @instance_identifier(nargs=1)
20
+ @click.option(
21
+ "--type",
22
+ "backup_type",
23
+ type=click.Choice(types.BACKUP_TYPES),
24
+ default=types.DEFAULT_BACKUP_TYPE,
25
+ help="Backup type",
26
+ )
27
+ @click.pass_obj
28
+ @async_command
29
+ async def instance_backup(
30
+ obj: Obj, instance: system.Instance, backup_type: types.BackupType
31
+ ) -> None:
32
+ """Back up PostgreSQL INSTANCE"""
33
+ settings = pgbackrest.get_settings(_site.SETTINGS)
34
+ with obj.lock:
35
+ await repo_path.backup(instance, settings, type=backup_type)
36
+
37
+
38
+ @hookimpl
39
+ def add_instance_commands(group: click.Group) -> None:
40
+ group.add_command(instance_backup)
pglift_cli/pgconf.py ADDED
@@ -0,0 +1,151 @@
1
+ # SPDX-FileCopyrightText: 2021 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Iterable
8
+ from typing import Any
9
+
10
+ import click
11
+ import pgtoolkit.conf
12
+
13
+ from pglift import h, hook, instances, postgresql
14
+ from pglift.models import system
15
+ from pglift.types import ConfigChanges, Status
16
+
17
+ from .util import Group, Obj, async_command, instance_identifier_option, pass_instance
18
+
19
+
20
+ @click.group(cls=Group)
21
+ @instance_identifier_option
22
+ def cli(**kwargs: Any) -> None:
23
+ """Manage configuration of a PostgreSQL instance."""
24
+
25
+
26
+ def show_configuration_changes(
27
+ changes: ConfigChanges, parameters: Iterable[str] | None = None
28
+ ) -> None:
29
+ for param, (old, new) in sorted(changes.items()):
30
+ click.secho(f"{param}: {old} -> {new}", err=True, fg="green")
31
+ if parameters is None:
32
+ return
33
+ if unchanged := set(parameters) - set(changes):
34
+ click.secho(
35
+ f"changes in {', '.join(map(repr, sorted(unchanged)))} not applied",
36
+ err=True,
37
+ fg="red",
38
+ )
39
+ click.secho(
40
+ " hint: either these changes have no effect (values already set) "
41
+ "or specified parameters are already defined in an un-managed file "
42
+ "(e.g. 'postgresql.conf')",
43
+ err=True,
44
+ fg="blue",
45
+ )
46
+
47
+
48
+ @cli.command("show")
49
+ @click.argument("parameter", nargs=-1)
50
+ @pass_instance
51
+ def show(instance: system.Instance, parameter: tuple[str]) -> None:
52
+ """Show configuration (all parameters or specified ones).
53
+
54
+ Only uncommented parameters are shown when no PARAMETER is specified. When
55
+ specific PARAMETERs are queried, commented values are also shown.
56
+ """
57
+ config = instance.config()
58
+ for entry in config.entries.values():
59
+ if parameter:
60
+ if entry.name in parameter:
61
+ if entry.commented:
62
+ click.echo(f"# {entry.name} = {entry.serialize()}")
63
+ else:
64
+ click.echo(f"{entry.name} = {entry.serialize()}")
65
+ elif not entry.commented:
66
+ click.echo(f"{entry.name} = {entry.serialize()}")
67
+
68
+
69
+ def validate_configuration_parameters(
70
+ context: click.Context, param: click.Parameter, value: tuple[str]
71
+ ) -> dict[str, str]:
72
+ items = {}
73
+ for v in value:
74
+ try:
75
+ key, val = v.split("=", 1)
76
+ except ValueError:
77
+ raise click.BadParameter(v) from None
78
+ items[key] = val
79
+ return items
80
+
81
+
82
+ @cli.command("set")
83
+ @click.argument(
84
+ "parameters",
85
+ metavar="<PARAMETER>=<VALUE>...",
86
+ nargs=-1,
87
+ callback=validate_configuration_parameters,
88
+ required=True,
89
+ )
90
+ @pass_instance
91
+ @click.pass_obj
92
+ @async_command
93
+ async def set_(obj: Obj, instance: system.Instance, parameters: dict[str, Any]) -> None:
94
+ """Set configuration items."""
95
+ with obj.lock:
96
+ status = await postgresql.status(instance)
97
+ manifest = await instances._get(instance, status)
98
+ manifest.settings.update(parameters)
99
+ changes = await instances.configure(
100
+ instance, manifest, _is_running=status == Status.running
101
+ )
102
+ show_configuration_changes(changes, parameters.keys())
103
+
104
+
105
+ @cli.command("remove")
106
+ @click.argument("parameters", nargs=-1, required=True)
107
+ @pass_instance
108
+ @click.pass_obj
109
+ @async_command
110
+ async def remove(obj: Obj, instance: system.Instance, parameters: tuple[str]) -> None:
111
+ """Remove configuration items."""
112
+ with obj.lock:
113
+ status = await postgresql.status(instance)
114
+ manifest = await instances._get(instance, status)
115
+ for p in parameters:
116
+ try:
117
+ del manifest.settings[p]
118
+ except KeyError:
119
+ raise click.ClickException(
120
+ f"{p!r} not found in managed configuration"
121
+ ) from None
122
+ changes = await instances.configure(
123
+ instance, manifest, _is_running=status == Status.running
124
+ )
125
+ show_configuration_changes(changes, parameters)
126
+
127
+
128
+ @cli.command("edit")
129
+ @pass_instance
130
+ @click.pass_obj
131
+ @async_command
132
+ async def edit(obj: Obj, instance: system.Instance) -> None:
133
+ """Edit managed configuration."""
134
+ with obj.lock:
135
+ actual_config = hook(
136
+ instance._settings, h.postgresql_editable_conf, instance=instance
137
+ )
138
+ edited = click.edit(text=actual_config)
139
+ if edited is None:
140
+ click.echo("no change", err=True)
141
+ return
142
+ config = pgtoolkit.conf.parse_string(edited)
143
+ values = config.as_dict()
144
+ status = await postgresql.status(instance)
145
+ manifest = await instances._get(instance, status)
146
+ manifest.settings.clear()
147
+ manifest.settings.update(values)
148
+ changes = await instances.configure(
149
+ instance, manifest, _is_running=status == Status.running
150
+ )
151
+ show_configuration_changes(changes)
pglift_cli/pm.py ADDED
@@ -0,0 +1,19 @@
1
+ # SPDX-FileCopyrightText: 2024 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ from pglift.pm import PluginManager as BasePluginManager
8
+
9
+ from . import __name__ as pkgname
10
+
11
+
12
+ class PluginManager(BasePluginManager):
13
+ ns = pkgname
14
+ modules = (
15
+ "patroni",
16
+ "pgbackrest",
17
+ "pgbackrest.repo_path",
18
+ "prometheus",
19
+ )
pglift_cli/postgres.py ADDED
@@ -0,0 +1,30 @@
1
+ # SPDX-FileCopyrightText: 2021 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ import click
8
+
9
+ from pglift.cmd import execute_program
10
+ from pglift.exceptions import InstanceNotFound
11
+ from pglift.models import system
12
+
13
+ from . import _site
14
+
15
+
16
+ def instance_from_qualname(
17
+ context: click.Context, param: click.Parameter, value: str
18
+ ) -> system.PostgreSQLInstance:
19
+ try:
20
+ return system.PostgreSQLInstance.from_qualname(value, _site.SETTINGS)
21
+ except (ValueError, InstanceNotFound) as e:
22
+ raise click.BadParameter(str(e), context) from None
23
+
24
+
25
+ @click.command("postgres", hidden=True)
26
+ @click.argument("instance", callback=instance_from_qualname)
27
+ def cli(instance: system.Instance) -> None:
28
+ """Start postgres for specified INSTANCE, identified as <version>-<name>."""
29
+ cmd = [str(instance.bindir / "postgres"), "-D", str(instance.datadir)]
30
+ execute_program(cmd)
@@ -0,0 +1,138 @@
1
+ # SPDX-FileCopyrightText: 2021 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ from functools import partial
8
+ from typing import IO
9
+
10
+ import click
11
+
12
+ from pglift import exceptions, prometheus, task
13
+ from pglift.models import interface
14
+ from pglift.prometheus import impl, models
15
+ from pglift.prometheus import register_if as register_if # noqa: F401
16
+
17
+ from . import _site, hookimpl, model
18
+ from .util import (
19
+ Group,
20
+ Obj,
21
+ OutputFormat,
22
+ async_command,
23
+ dry_run_option,
24
+ foreground_option,
25
+ output_format_option,
26
+ print_argspec,
27
+ print_json_for,
28
+ print_schema,
29
+ )
30
+
31
+
32
+ @click.group("postgres_exporter", cls=Group)
33
+ @click.option(
34
+ "--schema",
35
+ is_flag=True,
36
+ callback=partial(print_schema, model=models.PostgresExporter),
37
+ expose_value=False,
38
+ is_eager=True,
39
+ help="Print the JSON schema of postgres_exporter model and exit.",
40
+ )
41
+ @click.option(
42
+ "--ansible-argspec",
43
+ is_flag=True,
44
+ callback=partial(print_argspec, model=models.PostgresExporter),
45
+ expose_value=False,
46
+ is_eager=True,
47
+ hidden=True,
48
+ help="Print the Ansible argspec of postgres_exporter model and exit.",
49
+ )
50
+ def cli() -> None:
51
+ """Handle Prometheus postgres_exporter"""
52
+
53
+
54
+ @cli.command("apply")
55
+ @click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
56
+ @output_format_option
57
+ @dry_run_option
58
+ @click.pass_obj
59
+ @async_command
60
+ async def apply(
61
+ obj: Obj, file: IO[str], output_format: OutputFormat, dry_run: bool
62
+ ) -> None:
63
+ """Apply manifest as a Prometheus postgres_exporter."""
64
+ exporter = models.PostgresExporter.parse_yaml(file)
65
+ if dry_run:
66
+ ret = interface.ApplyResult(change_state=None)
67
+ else:
68
+ settings = prometheus.get_settings(_site.SETTINGS)
69
+ with obj.lock:
70
+ ret = await impl.apply(exporter, _site.SETTINGS, settings)
71
+ if output_format == OutputFormat.json:
72
+ print_json_for(ret)
73
+
74
+
75
+ @cli.command("install")
76
+ @model.as_parameters(models.PostgresExporter, "create")
77
+ @click.pass_obj
78
+ @async_command
79
+ async def install(obj: Obj, postgresexporter: models.PostgresExporter) -> None:
80
+ """Install the service for a (non-local) instance."""
81
+ settings = prometheus.get_settings(_site.SETTINGS)
82
+ with obj.lock:
83
+ async with task.async_transaction():
84
+ await impl.apply(postgresexporter, _site.SETTINGS, settings)
85
+
86
+
87
+ @cli.command("uninstall")
88
+ @click.argument("name")
89
+ @click.pass_obj
90
+ @async_command
91
+ async def uninstall(obj: Obj, name: str) -> None:
92
+ """Uninstall the service."""
93
+ with obj.lock:
94
+ await impl.drop(_site.SETTINGS, name)
95
+
96
+
97
+ @cli.command("start")
98
+ @click.argument("name")
99
+ @foreground_option
100
+ @click.pass_obj
101
+ @async_command
102
+ async def start(obj: Obj, name: str, foreground: bool) -> None:
103
+ """Start postgres_exporter service NAME.
104
+
105
+ The NAME argument is a local identifier for the postgres_exporter
106
+ service. If the service is bound to a local instance, it should be
107
+ <version>-<name>.
108
+ """
109
+ settings = prometheus.get_settings(_site.SETTINGS)
110
+ with obj.lock:
111
+ service = impl.system_lookup(name, settings)
112
+ if service is None:
113
+ raise exceptions.InstanceNotFound(name)
114
+ await impl.start(_site.SETTINGS, service, foreground=foreground)
115
+
116
+
117
+ @cli.command("stop")
118
+ @click.argument("name")
119
+ @click.pass_obj
120
+ @async_command
121
+ async def stop(obj: Obj, name: str) -> None:
122
+ """Stop postgres_exporter service NAME.
123
+
124
+ The NAME argument is a local identifier for the postgres_exporter
125
+ service. If the service is bound to a local instance, it should be
126
+ <version>-<name>.
127
+ """
128
+ settings = prometheus.get_settings(_site.SETTINGS)
129
+ with obj.lock:
130
+ service = impl.system_lookup(name, settings)
131
+ if service is None:
132
+ raise exceptions.InstanceNotFound(name)
133
+ await impl.stop(_site.SETTINGS, service)
134
+
135
+
136
+ @hookimpl
137
+ def command() -> click.Group:
138
+ return cli
pglift_cli/role.py ADDED
@@ -0,0 +1,197 @@
1
+ # SPDX-FileCopyrightText: 2021 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Sequence
8
+ from functools import partial
9
+ from typing import IO, Any
10
+
11
+ import click
12
+ from pydantic.v1.utils import deep_update
13
+
14
+ from pglift import postgresql, privileges, roles
15
+ from pglift.models import interface, system
16
+
17
+ from . import _site, model
18
+ from .util import (
19
+ Group,
20
+ Obj,
21
+ OutputFormat,
22
+ async_command,
23
+ dry_run_option,
24
+ instance_identifier_option,
25
+ model_dump,
26
+ output_format_option,
27
+ pass_instance,
28
+ print_argspec,
29
+ print_json_for,
30
+ print_schema,
31
+ print_table_for,
32
+ )
33
+
34
+
35
+ def print_role_schema(
36
+ context: click.Context, param: click.Parameter, value: bool
37
+ ) -> None:
38
+ return print_schema(context, param, value, model=_site.ROLE_MODEL)
39
+
40
+
41
+ def print_role_argspec(
42
+ context: click.Context, param: click.Parameter, value: bool
43
+ ) -> None:
44
+ print_argspec(context, param, value, model=_site.ROLE_MODEL)
45
+
46
+
47
+ @click.group("role", cls=Group)
48
+ @instance_identifier_option
49
+ @click.option(
50
+ "--schema",
51
+ is_flag=True,
52
+ callback=print_role_schema,
53
+ expose_value=False,
54
+ is_eager=True,
55
+ help="Print the JSON schema of role model and exit.",
56
+ )
57
+ @click.option(
58
+ "--ansible-argspec",
59
+ is_flag=True,
60
+ callback=print_role_argspec,
61
+ expose_value=False,
62
+ is_eager=True,
63
+ hidden=True,
64
+ help="Print the Ansible argspec of role model and exit.",
65
+ )
66
+ def cli(**kwargs: Any) -> None:
67
+ """Manage roles."""
68
+
69
+
70
+ @cli.command("create")
71
+ @model.as_parameters(_site.ROLE_MODEL, "create")
72
+ @pass_instance
73
+ @click.pass_obj
74
+ @async_command
75
+ async def create(obj: Obj, instance: system.Instance, role: interface.Role) -> None:
76
+ """Create a role in a PostgreSQL instance"""
77
+ with obj.lock:
78
+ async with postgresql.running(instance):
79
+ if await roles.exists(instance, role.name):
80
+ raise click.ClickException("role already exists")
81
+ await roles.apply(instance, role)
82
+
83
+
84
+ @cli.command("alter") # type: ignore[arg-type]
85
+ @model.as_parameters(_site.ROLE_MODEL, "update", parse_model=False)
86
+ @click.argument("rolname")
87
+ @pass_instance
88
+ @click.pass_obj
89
+ @async_command
90
+ async def alter(
91
+ obj: Obj, instance: system.Instance, rolname: str, **changes: Any
92
+ ) -> None:
93
+ """Alter a role in a PostgreSQL instance"""
94
+ with obj.lock:
95
+ async with postgresql.running(instance):
96
+ values = (await roles.get(instance, rolname)).model_dump(by_alias=True)
97
+ values = deep_update(values, changes)
98
+ altered = _site.ROLE_MODEL.model_validate(values)
99
+ await roles.apply(instance, altered)
100
+
101
+
102
+ @cli.command("apply", hidden=True)
103
+ @click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
104
+ @output_format_option
105
+ @dry_run_option
106
+ @pass_instance
107
+ @click.pass_obj
108
+ @async_command
109
+ async def apply(
110
+ obj: Obj,
111
+ instance: system.Instance,
112
+ file: IO[str],
113
+ output_format: OutputFormat,
114
+ dry_run: bool,
115
+ ) -> None:
116
+ """Apply manifest as a role"""
117
+ role = _site.ROLE_MODEL.parse_yaml(file)
118
+ if dry_run:
119
+ ret = interface.ApplyResult(change_state=None)
120
+ else:
121
+ with obj.lock:
122
+ async with postgresql.running(instance):
123
+ ret = await roles.apply(instance, role)
124
+ if output_format == OutputFormat.json:
125
+ print_json_for(ret)
126
+
127
+
128
+ @cli.command("list")
129
+ @output_format_option
130
+ @pass_instance
131
+ @async_command
132
+ async def ls(instance: system.Instance, output_format: OutputFormat) -> None:
133
+ """List roles in instance"""
134
+ async with postgresql.running(instance):
135
+ rls = await roles.ls(instance)
136
+ if output_format == OutputFormat.json:
137
+ print_json_for([model_dump(r) for r in rls])
138
+ else:
139
+ print_table_for(rls, partial(model_dump, exclude={"pgpass"}))
140
+
141
+
142
+ @cli.command("get")
143
+ @output_format_option
144
+ @click.argument("name")
145
+ @pass_instance
146
+ @async_command
147
+ async def get(
148
+ instance: system.Instance, name: str, output_format: OutputFormat
149
+ ) -> None:
150
+ """Get the description of a role"""
151
+ async with postgresql.running(instance):
152
+ r = await roles.get(instance, name)
153
+ if output_format == OutputFormat.json:
154
+ print_json_for(model_dump(r))
155
+ else:
156
+ print_table_for([r], model_dump, box=None)
157
+
158
+
159
+ @cli.command("drop")
160
+ @model.as_parameters(interface.RoleDropped, "create")
161
+ @pass_instance
162
+ @async_command
163
+ async def drop(instance: system.Instance, roledropped: interface.RoleDropped) -> None:
164
+ """Drop a role"""
165
+ async with postgresql.running(instance):
166
+ await roles.drop(instance, roledropped)
167
+
168
+
169
+ @cli.command("privileges")
170
+ @click.argument("name")
171
+ @click.option(
172
+ "-d", "--database", "databases", multiple=True, help="Database to inspect"
173
+ )
174
+ @click.option("--default", "defaults", is_flag=True, help="Display default privileges")
175
+ @output_format_option
176
+ @pass_instance
177
+ @async_command
178
+ async def list_privileges(
179
+ instance: system.Instance,
180
+ name: str,
181
+ databases: Sequence[str],
182
+ defaults: bool,
183
+ output_format: OutputFormat,
184
+ ) -> None:
185
+ """List privileges of a role."""
186
+ async with postgresql.running(instance):
187
+ await roles.get(instance, name) # check existence
188
+ try:
189
+ prvlgs = await privileges.get(
190
+ instance, databases=databases, roles=(name,), defaults=defaults
191
+ )
192
+ except ValueError as e:
193
+ raise click.ClickException(str(e)) from None
194
+ if output_format == OutputFormat.json:
195
+ print_json_for([model_dump(p) for p in prvlgs])
196
+ else:
197
+ print_table_for(prvlgs, model_dump)