pglift-cli 2.0.0__tar.gz → 2.2.0__tar.gz

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 (47) hide show
  1. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/PKG-INFO +2 -2
  2. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/pyproject.toml +4 -1
  3. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/database.py +37 -17
  4. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/instance.py +47 -9
  5. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/model.py +2 -22
  6. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/pgbackrest/__init__.py +4 -4
  7. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/role.py +24 -15
  8. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/util.py +22 -14
  9. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/expect/test-cli-walkthrough.t +119 -7
  10. pglift_cli-2.2.0/tests/expect/test-demote.t +231 -0
  11. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/expect/test-help.t +97 -36
  12. pglift_cli-2.2.0/tests/expect/test-transactions.t +70 -0
  13. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/conftest.py +1 -1
  14. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/test__site.py +9 -13
  15. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/test_cli.py +21 -18
  16. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/test_main.py +9 -11
  17. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/test_model.py +1 -11
  18. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/test_util.py +2 -2
  19. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/.gitignore +0 -0
  20. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/README.md +0 -0
  21. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/hatch.toml +0 -0
  22. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/pytest.ini +0 -0
  23. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/__init__.py +0 -0
  24. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/__main__.py +0 -0
  25. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/_settings.py +0 -0
  26. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/_site.py +0 -0
  27. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/base.py +0 -0
  28. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/console.py +0 -0
  29. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/hookspecs.py +0 -0
  30. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/main.py +0 -0
  31. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/patroni.py +0 -0
  32. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/pgbackrest/repo_path.py +0 -0
  33. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/pgconf.py +0 -0
  34. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/pghba.py +0 -0
  35. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/pm.py +0 -0
  36. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/postgres.py +0 -0
  37. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/prometheus.py +0 -0
  38. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/src/pglift_cli/py.typed +0 -0
  39. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/expect/.gitignore +0 -0
  40. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/expect/test-base.t +0 -0
  41. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/expect/test-port-validation.t +0 -0
  42. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/expect/test-prometheus.t +0 -0
  43. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/expect/test-standby-pgbackrest.t +0 -0
  44. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/expect/test-upgrade.t +0 -0
  45. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/__init__.py +0 -0
  46. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/test_audit.py +0 -0
  47. {pglift_cli-2.0.0 → pglift_cli-2.2.0}/tests/unit/test_pm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pglift_cli
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Command-line interface for pglift
5
5
  Project-URL: Documentation, https://pglift.readthedocs.io/
6
6
  Project-URL: Source, https://gitlab.com/dalibo/pglift/
@@ -23,7 +23,7 @@ Classifier: Topic :: Database
23
23
  Classifier: Topic :: System :: Systems Administration
24
24
  Classifier: Typing :: Typed
25
25
  Requires-Python: <4,>=3.10
26
- Requires-Dist: click!=8.1.0,!=8.1.4,>=8.0.0
26
+ Requires-Dist: click>=8.2.0
27
27
  Requires-Dist: filelock!=3.12.1,>=3.9.0
28
28
  Requires-Dist: pluggy
29
29
  Requires-Dist: psycopg>=3.1
@@ -39,7 +39,7 @@ dynamic = ["version"]
39
39
 
40
40
  dependencies = [
41
41
  "PyYAML >= 6.0.1",
42
- "click >= 8.0.0, != 8.1.0, != 8.1.4",
42
+ "click >= 8.2.0",
43
43
  "filelock >= 3.9.0, != 3.12.1",
44
44
  "pluggy",
45
45
  "psycopg >= 3.1",
@@ -75,3 +75,6 @@ Tracker = "https://gitlab.com/dalibo/pglift/-/issues/"
75
75
  "pglift_cli.pgbackrest" = "pglift_cli.pgbackrest"
76
76
  "pglift_cli.pgbackrest.repo_path" = "pglift_cli.pgbackrest.repo_path"
77
77
  "pglift_cli.prometheus" = "pglift_cli.prometheus"
78
+
79
+ [project.scripts]
80
+ pglift = "pglift_cli.main:cli"
@@ -37,6 +37,7 @@ from .util import (
37
37
  print_result_diff,
38
38
  print_schema,
39
39
  print_table_for,
40
+ system_configure,
40
41
  )
41
42
 
42
43
 
@@ -65,14 +66,18 @@ def cli(**kwargs: Any) -> None:
65
66
 
66
67
  @cli.command("create")
67
68
  @model.as_parameters(interface.Database, "create")
69
+ @dry_run_option
68
70
  @pass_postgresql_instance
69
71
  @click.pass_obj
70
72
  @async_command
71
73
  async def create(
72
- obj: Obj, instance: PostgreSQLInstance, database: interface.Database
74
+ obj: Obj,
75
+ instance: PostgreSQLInstance,
76
+ database: interface.Database,
77
+ dry_run: bool,
73
78
  ) -> None:
74
79
  """Create a database in a PostgreSQL instance"""
75
- with obj.lock, audit():
80
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
76
81
  async with postgresql.running(instance):
77
82
  if await databases.exists(instance, database.name):
78
83
  raise click.ClickException("database already exists")
@@ -83,14 +88,19 @@ async def create(
83
88
  @cli.command("alter")
84
89
  @model.as_parameters(interface.Database, "update")
85
90
  @click.argument("dbname")
91
+ @dry_run_option
86
92
  @pass_postgresql_instance
87
93
  @click.pass_obj # type: ignore[arg-type]
88
94
  @async_command
89
95
  async def alter(
90
- obj: Obj, instance: PostgreSQLInstance, dbname: str, **changes: Any
96
+ obj: Obj,
97
+ instance: PostgreSQLInstance,
98
+ dbname: str,
99
+ dry_run: bool,
100
+ **changes: Any,
91
101
  ) -> None:
92
102
  """Alter a database in a PostgreSQL instance"""
93
- with obj.lock, audit():
103
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
94
104
  async with postgresql.running(instance):
95
105
  values = (await databases.get(instance, dbname)).model_dump(by_alias=True)
96
106
  values = deep_update(values, changes)
@@ -117,12 +127,14 @@ async def apply(
117
127
  ) -> None:
118
128
  """Apply manifest as a database"""
119
129
  database = interface.Database.model_validate(data)
120
- if dry_run:
121
- ret = interface.ApplyResult(change_state=None)
122
- else:
123
- with obj.lock, audit(dry_run=dry_run), diff.enabled(diff_format):
124
- async with postgresql.running(instance):
125
- ret = await databases.apply(instance, database)
130
+ with (
131
+ obj.lock,
132
+ audit(dry_run=dry_run),
133
+ diff.enabled(diff_format),
134
+ system_configure(dry_run=dry_run),
135
+ ):
136
+ async with postgresql.running(instance):
137
+ ret = await databases.apply(instance, database)
126
138
  if output_format == "json":
127
139
  print_json_for(ret)
128
140
  else:
@@ -181,6 +193,7 @@ async def ls(
181
193
 
182
194
  @cli.command("drop")
183
195
  @model.as_parameters(interface.DatabaseDropped, "create")
196
+ @dry_run_option
184
197
  @pass_postgresql_instance
185
198
  @click.pass_obj
186
199
  @async_command
@@ -188,9 +201,10 @@ async def drop(
188
201
  obj: Obj,
189
202
  instance: PostgreSQLInstance,
190
203
  databasedropped: interface.DatabaseDropped,
204
+ dry_run: bool,
191
205
  ) -> None:
192
206
  """Drop a database"""
193
- with obj.lock, audit():
207
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
194
208
  async with postgresql.running(instance):
195
209
  await databases.drop(instance, databasedropped)
196
210
 
@@ -274,11 +288,15 @@ async def run(
274
288
  )
275
289
  @click.argument("dbname")
276
290
  @pass_postgresql_instance
291
+ @dry_run_option
277
292
  @async_command
278
- async def dump(instance: PostgreSQLInstance, dbname: str, output: Path | None) -> None:
293
+ async def dump(
294
+ instance: PostgreSQLInstance, dbname: str, output: Path | None, dry_run: bool
295
+ ) -> None:
279
296
  """Dump a database"""
280
- async with postgresql.running(instance):
281
- await databases.dump(instance, dbname, output)
297
+ with system_configure(dry_run=dry_run):
298
+ async with postgresql.running(instance):
299
+ await databases.dump(instance, dbname, output)
282
300
 
283
301
 
284
302
  @cli.command("dumps")
@@ -307,9 +325,10 @@ async def dumps(
307
325
  @click.argument("dump_id")
308
326
  @click.argument("targetdbname", required=False)
309
327
  @pass_postgresql_instance
328
+ @dry_run_option
310
329
  @async_command
311
330
  async def restore(
312
- instance: PostgreSQLInstance, dump_id: str, targetdbname: str | None
331
+ instance: PostgreSQLInstance, dump_id: str, targetdbname: str | None, dry_run: bool
313
332
  ) -> None:
314
333
  """Restore a database dump
315
334
 
@@ -322,5 +341,6 @@ async def restore(
322
341
  name that appears in the dump. In this case, the restore command will
323
342
  create the database so it needs to be dropped before running the command.
324
343
  """
325
- async with postgresql.running(instance):
326
- await databases.restore(instance, dump_id, targetdbname)
344
+ with system_configure(dry_run=dry_run):
345
+ async with postgresql.running(instance):
346
+ await databases.restore(instance, dump_id, targetdbname)
@@ -25,8 +25,11 @@ from pglift import (
25
25
  task,
26
26
  )
27
27
  from pglift.models import Instance, PostgreSQLInstance, interface
28
- from pglift.settings import default_postgresql_version
29
- from pglift.settings._postgresql import PostgreSQLVersion
28
+ from pglift.settings import (
29
+ POSTGRESQL_VERSIONS,
30
+ PostgreSQLVersion,
31
+ default_postgresql_version,
32
+ )
30
33
  from pglift.types import Operation, Status, validation_context
31
34
 
32
35
  from . import _site, model
@@ -114,8 +117,8 @@ async def create(obj: Obj, instance: interface.Instance, drop_on_error: bool) ->
114
117
  with obj.lock, audit():
115
118
  if instances.exists(instance.name, instance.version, _site.SETTINGS):
116
119
  raise click.ClickException("instance already exists")
117
- async with task.async_transaction(drop_on_error):
118
- with manager.from_manifest(instance, settings=_site.SETTINGS):
120
+ with manager.from_manifest(instance, settings=_site.SETTINGS):
121
+ async with task.async_transaction(drop_on_error):
119
122
  await instances.apply(_site.SETTINGS, instance)
120
123
 
121
124
 
@@ -213,6 +216,41 @@ async def promote(obj: Obj, instance: Instance) -> None:
213
216
  await instances.promote(instance)
214
217
 
215
218
 
219
+ @cli.command("demote")
220
+ @instance_identifier(nargs=1)
221
+ @model.as_parameters(postgresql.RewindSource, "create")
222
+ @click.option(
223
+ "--start/--no-start",
224
+ help="Start the instance at the end of the demotion process",
225
+ default=True,
226
+ show_default=True,
227
+ )
228
+ @click.argument("rewind_opts", nargs=-1, type=click.UNPROCESSED)
229
+ @click.pass_obj
230
+ @async_command
231
+ async def demote(
232
+ obj: Obj,
233
+ instance: Instance,
234
+ rewindsource: postgresql.RewindSource,
235
+ start: bool,
236
+ rewind_opts: tuple[str, ...],
237
+ ) -> None:
238
+ """Demote PostgreSQL INSTANCE as standby of specified source server using pg_rewind.
239
+
240
+ The instance must not be running and it may be started at the end of the
241
+ "demotion" process.
242
+
243
+ Extra options can be passed to the pg_rewind command. They may need to
244
+ be prefixed with -- to separate them from the current command options
245
+ when confusion arises. When using extra options, providing the instance
246
+ identifier is required.
247
+ """
248
+ with obj.lock, audit(), manager.from_instance(instance.postgresql):
249
+ await instances.demote(instance, rewindsource, rewind_opts=rewind_opts)
250
+ if start:
251
+ await instances.start(instance)
252
+
253
+
216
254
  @cli.command("get")
217
255
  @output_format_option
218
256
  @instance_identifier(nargs=1)
@@ -244,7 +282,7 @@ async def get(instance: Instance, output_format: OutputFormat | None) -> None:
244
282
  @cli.command("list")
245
283
  @click.option(
246
284
  "--version",
247
- type=click.Choice(list(PostgreSQLVersion)),
285
+ type=click.Choice(POSTGRESQL_VERSIONS),
248
286
  help="Only list instances of specified version.",
249
287
  )
250
288
  @output_format_option
@@ -368,7 +406,7 @@ async def restart(
368
406
  @instance_identifier(nargs=1, required=True)
369
407
  @click.argument("command", required=True, nargs=-1, type=click.UNPROCESSED)
370
408
  def exec(instance: Instance, command: tuple[str, ...]) -> None:
371
- """Execute command in the libpq environment for PostgreSQL INSTANCE.
409
+ """Execute COMMAND in the libpq environment for PostgreSQL INSTANCE.
372
410
 
373
411
  COMMAND parts may need to be prefixed with -- to separate them from
374
412
  options when confusion arises.
@@ -491,7 +529,7 @@ async def list_privileges(
491
529
  @click.option(
492
530
  "--version",
493
531
  "newversion",
494
- type=click.Choice(list(PostgreSQLVersion)),
532
+ type=click.Choice(POSTGRESQL_VERSIONS),
495
533
  help="PostgreSQL version of the new instance (default to site-configured value).",
496
534
  )
497
535
  @click.option(
@@ -519,8 +557,8 @@ async def upgrade(
519
557
  """
520
558
  with obj.lock, audit():
521
559
  await postgresql.check_status(instance.postgresql, Status.not_running)
522
- async with task.async_transaction():
523
- with manager.from_instance(instance.postgresql):
560
+ with manager.from_instance(instance.postgresql):
561
+ async with task.async_transaction():
524
562
  new_instance = await instances.upgrade(
525
563
  instance,
526
564
  version=newversion,
@@ -4,7 +4,6 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- import enum
8
7
  import functools
9
8
  import inspect
10
9
  import typing
@@ -24,7 +23,6 @@ from pglift.models.helpers import is_optional, optional_type
24
23
  from pglift.models.interface import PresenceState
25
24
  from pglift.types import (
26
25
  Operation,
27
- StrEnum,
28
26
  field_annotation,
29
27
  validation_context,
30
28
  )
@@ -106,7 +104,7 @@ def as_parameters(model_type: ModelType, operation: Operation) -> ClickDecorator
106
104
  )
107
105
 
108
106
  @functools.wraps(f)
109
- def callback(**kwargs: Any) -> Any: # type: ignore[misc]
107
+ def callback(**kwargs: Any) -> Any:
110
108
  args = params_to_modelargs(kwargs)
111
109
  with (
112
110
  catch_validationerror(*paramspecs),
@@ -358,14 +356,6 @@ def _paramspecs_from_model(
358
356
  choices = config.choices
359
357
  attrs["type"] = click.Choice(choices)
360
358
 
361
- elif lenient_issubclass(ftype, enum.Enum):
362
- if config:
363
- assert isinstance(config, cli.Choices)
364
- choices = config.choices
365
- else:
366
- choices = choices_from_enum(ftype)
367
- attrs["type"] = click.Choice(choices)
368
-
369
359
  elif lenient_issubclass(origin_type or ftype, list):
370
360
  attrs["multiple"] = True
371
361
  try:
@@ -373,10 +363,7 @@ def _paramspecs_from_model(
373
363
  except ValueError:
374
364
  pass
375
365
  else:
376
- if lenient_issubclass(itemtype, enum.Enum):
377
- attrs["type"] = click.Choice(choices_from_enum(itemtype))
378
- else:
379
- attrs["metavar"] = metavar
366
+ attrs["metavar"] = metavar
380
367
  if not _parents and operation == "update" and is_editable(itemtype):
381
368
  # List fields for the "update" operation are mapped to
382
369
  # --add-<fname>, --remove-<fname> options; built and yield
@@ -460,13 +447,6 @@ def _paramspecs_from_model(
460
447
  )
461
448
 
462
449
 
463
- def choices_from_enum(e: type[enum.Enum]) -> list[Any]:
464
- if lenient_issubclass(e, StrEnum):
465
- return list(e)
466
- else:
467
- return [v.value for v in e]
468
-
469
-
470
450
  @contextmanager
471
451
  def catch_validationerror(*paramspec: ParamSpec) -> Iterator[None]:
472
452
  try:
@@ -12,7 +12,7 @@ import click
12
12
 
13
13
  from pglift import pgbackrest, postgresql, types
14
14
  from pglift.models import Instance
15
- from pglift.pgbackrest import base, models
15
+ from pglift.pgbackrest import models
16
16
  from pglift.pgbackrest import register_if as register_if
17
17
 
18
18
  from .. import _site, hookimpl
@@ -46,7 +46,7 @@ def pgbackrest_proxy(
46
46
  """Proxy to pgbackrest operations on an instance"""
47
47
  s = context.obj.instance.service(models.Service)
48
48
  settings = pgbackrest.get_settings(_site.SETTINGS)
49
- cmd_args = base.make_cmd(s.stanza, settings, *command)
49
+ cmd_args = pgbackrest.make_cmd(s.stanza, settings, *command)
50
50
  try:
51
51
  subprocess.run(cmd_args, capture_output=False, check=True) # nosec
52
52
  except subprocess.CalledProcessError as e:
@@ -70,7 +70,7 @@ async def instance_restore(
70
70
  ) from None
71
71
  settings = pgbackrest.get_settings(_site.SETTINGS)
72
72
  with obj.lock, audit():
73
- await base.restore(instance, settings, label=label, date=date)
73
+ await pgbackrest.restore(instance, settings, label=label, date=date)
74
74
 
75
75
 
76
76
  @click.command("backups", cls=Command)
@@ -82,7 +82,7 @@ async def instance_backups(
82
82
  ) -> None:
83
83
  """List available backups for INSTANCE"""
84
84
  settings = pgbackrest.get_settings(_site.SETTINGS)
85
- backups = [b async for b in base.iter_backups(instance, settings)]
85
+ backups = [b async for b in pgbackrest.iter_backups(instance, settings)]
86
86
  if output_format == "json":
87
87
  print_json_for([model_dump(b) for b in backups])
88
88
  else:
@@ -35,6 +35,7 @@ from .util import (
35
35
  print_result_diff,
36
36
  print_schema,
37
37
  print_table_for,
38
+ system_configure,
38
39
  )
39
40
 
40
41
 
@@ -76,11 +77,14 @@ def cli(**kwargs: Any) -> None:
76
77
  @cli.command("create")
77
78
  @model.as_parameters(_site.ROLE_MODEL, "create")
78
79
  @pass_postgresql_instance
80
+ @dry_run_option
79
81
  @click.pass_obj
80
82
  @async_command
81
- async def create(obj: Obj, instance: PostgreSQLInstance, role: interface.Role) -> None:
83
+ async def create(
84
+ obj: Obj, instance: PostgreSQLInstance, role: interface.Role, dry_run: bool
85
+ ) -> None:
82
86
  """Create a role in a PostgreSQL instance"""
83
- with obj.lock, audit():
87
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
84
88
  async with postgresql.running(instance):
85
89
  if await roles.exists(instance, role.name):
86
90
  raise click.ClickException("role already exists")
@@ -92,13 +96,14 @@ async def create(obj: Obj, instance: PostgreSQLInstance, role: interface.Role) -
92
96
  @model.as_parameters(_site.ROLE_MODEL, "update")
93
97
  @click.argument("rolname")
94
98
  @pass_postgresql_instance
99
+ @dry_run_option
95
100
  @click.pass_obj # type: ignore[arg-type]
96
101
  @async_command
97
102
  async def alter(
98
- obj: Obj, instance: PostgreSQLInstance, rolname: str, **changes: Any
103
+ obj: Obj, instance: PostgreSQLInstance, rolname: str, dry_run: bool, **changes: Any
99
104
  ) -> None:
100
105
  """Alter a role in a PostgreSQL instance"""
101
- with obj.lock, audit():
106
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
102
107
  async with postgresql.running(instance):
103
108
  with manager.from_instance(instance):
104
109
  values = model_dump(await roles.get(instance, rolname))
@@ -136,13 +141,15 @@ async def apply(
136
141
  )
137
142
  with validation_context(operation=op, settings=_site.SETTINGS):
138
143
  role = _site.ROLE_MODEL.model_validate(data)
139
- if dry_run:
140
- ret = interface.ApplyResult(change_state=None)
141
- else:
142
- with obj.lock, audit(dry_run=dry_run), diff.enabled(diff_format):
143
- async with postgresql.running(instance):
144
- with manager.from_instance(instance):
145
- ret = await roles.apply(instance, role)
144
+ with (
145
+ obj.lock,
146
+ audit(dry_run=dry_run),
147
+ diff.enabled(diff_format),
148
+ system_configure(dry_run=dry_run),
149
+ ):
150
+ async with postgresql.running(instance):
151
+ with manager.from_instance(instance):
152
+ ret = await roles.apply(instance, role)
146
153
  if output_format == "json":
147
154
  print_json_for(ret)
148
155
  else:
@@ -192,14 +199,16 @@ async def get(
192
199
  @cli.command("drop")
193
200
  @model.as_parameters(interface.RoleDropped, "create")
194
201
  @pass_postgresql_instance
202
+ @dry_run_option
195
203
  @async_command
196
204
  async def drop(
197
- instance: PostgreSQLInstance, roledropped: interface.RoleDropped
205
+ instance: PostgreSQLInstance, roledropped: interface.RoleDropped, dry_run: bool
198
206
  ) -> None:
199
207
  """Drop a role"""
200
- async with postgresql.running(instance):
201
- with manager.from_instance(instance):
202
- await roles.drop(instance, roledropped)
208
+ with audit(dry_run=dry_run), system_configure(dry_run=dry_run):
209
+ async with postgresql.running(instance):
210
+ with manager.from_instance(instance):
211
+ await roles.drop(instance, roledropped)
203
212
 
204
213
 
205
214
  @cli.command("privileges")
@@ -37,7 +37,7 @@ from rich.table import Table
37
37
 
38
38
  from pglift import exceptions, instances, system
39
39
  from pglift.models import Instance, PostgreSQLInstance, helpers, interface
40
- from pglift.settings import Settings
40
+ from pglift.settings import PostgreSQLVersion, Settings
41
41
  from pglift.system import install
42
42
  from pglift.types import ByteSizeType
43
43
 
@@ -116,7 +116,6 @@ def print_table_for(
116
116
  **kwargs: Any,
117
117
  ) -> None:
118
118
  """Render a list of items as a table."""
119
- table = None
120
119
  headers: list[str] = []
121
120
  rows = []
122
121
  for item in items:
@@ -139,7 +138,13 @@ def print_table_for(
139
138
  rows.append(row)
140
139
  if not rows:
141
140
  return
142
- table = Table(*headers, title=title, **kwargs)
141
+ table = Table(title=title, **kwargs)
142
+ # https://github.com/Textualize/rich/issues/3761
143
+ overflow: Literal["ellipsis", "fold"] = (
144
+ "fold" if console.options.ascii_only else "ellipsis"
145
+ )
146
+ for header in headers:
147
+ table.add_column(header, overflow=overflow)
143
148
  for row in rows:
144
149
  table.add_row(*row)
145
150
  console.print(table)
@@ -219,13 +224,13 @@ def pass_postgresql_instance(f: Callable[P, None]) -> Callable[P, None]:
219
224
 
220
225
 
221
226
  def get_postgresql_instance(
222
- name: str, version: str | None, settings: Settings
227
+ name: str, version: PostgreSQLVersion | None, settings: Settings
223
228
  ) -> PostgreSQLInstance:
224
229
  """Return a PostgreSQLInstance from name/version, possibly guessing version if unspecified."""
225
230
  if version is None:
226
231
  found = None
227
232
  for v in settings.postgresql.versions:
228
- version = v.version.value
233
+ version = v.version
229
234
  try:
230
235
  instance = PostgreSQLInstance.system_lookup(name, version, settings)
231
236
  except exceptions.InstanceNotFound:
@@ -249,19 +254,21 @@ def get_postgresql_instance(
249
254
  raise click.BadParameter(str(e)) from None
250
255
 
251
256
 
252
- def get_instance(name: str, version: str | None, settings: Settings) -> Instance:
257
+ def get_instance(
258
+ name: str, version: PostgreSQLVersion | None, settings: Settings
259
+ ) -> Instance:
253
260
  """Return an Instance from name/version, possibly guessing version if unspecified."""
254
261
  pg_instance = get_postgresql_instance(name, version, settings)
255
262
  return Instance.from_postgresql(pg_instance)
256
263
 
257
264
 
258
- def nameversion_from_id(instance_id: str) -> tuple[str, str | None]:
265
+ def nameversion_from_id(instance_id: str) -> tuple[str, PostgreSQLVersion | None]:
259
266
  version = None
260
267
  try:
261
268
  version, name = instance_id.split("/", 1)
262
269
  except ValueError:
263
270
  name = instance_id
264
- return name, version
271
+ return name, typing.cast(PostgreSQLVersion, version)
265
272
 
266
273
 
267
274
  def postgresql_instance_lookup(
@@ -732,12 +739,13 @@ class Group(click.Group):
732
739
  super().add_command(command, name)
733
740
 
734
741
  def invoke(self, ctx: click.Context) -> Any:
735
- if is_root():
736
- raise click.ClickException("pglift cannot be used as root")
737
- if not install.check(_site.SETTINGS):
738
- raise click.ClickException(
739
- "broken installation; did you run 'site-configure install'?",
740
- )
742
+ if set(ctx.help_option_names) - set(ctx.args):
743
+ if is_root():
744
+ raise click.ClickException("pglift cannot be used as root")
745
+ if not install.check(_site.SETTINGS):
746
+ raise click.ClickException(
747
+ "broken installation; did you run 'site-configure install'?",
748
+ )
741
749
  return super().invoke(ctx)
742
750
 
743
751