pglift-cli 1.6.0__tar.gz → 1.8.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 (27) hide show
  1. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/PKG-INFO +8 -6
  2. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/pyproject.toml +4 -3
  3. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/base.py +5 -1
  4. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/database.py +53 -39
  5. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/instance.py +56 -31
  6. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/main.py +2 -2
  7. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/model.py +151 -49
  8. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/pgbackrest/__init__.py +3 -3
  9. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/pgconf.py +19 -15
  10. pglift_cli-1.8.0/src/pglift_cli/pghba.py +57 -0
  11. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/postgres.py +1 -1
  12. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/prometheus.py +2 -2
  13. pglift_cli-1.8.0/src/pglift_cli/py.typed +0 -0
  14. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/role.py +69 -27
  15. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/util.py +100 -44
  16. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/.gitignore +0 -0
  17. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/README.md +0 -0
  18. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/hatch.toml +0 -0
  19. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/__init__.py +0 -0
  20. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/__main__.py +0 -0
  21. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/_settings.py +0 -0
  22. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/_site.py +0 -0
  23. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/console.py +0 -0
  24. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/hookspecs.py +0 -0
  25. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/patroni.py +0 -0
  26. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/pgbackrest/repo_path.py +0 -0
  27. {pglift_cli-1.6.0 → pglift_cli-1.8.0}/src/pglift_cli/pm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pglift_cli
3
- Version: 1.6.0
3
+ Version: 1.8.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/
@@ -28,15 +28,16 @@ Requires-Dist: pluggy
28
28
  Requires-Dist: psycopg>=3.1
29
29
  Requires-Dist: pydantic>=2.5.0
30
30
  Requires-Dist: pyyaml>=6.0.1
31
- Requires-Dist: rich>=11.0.0
31
+ Requires-Dist: rich!=13.9.0,>=11.0.0
32
32
  Provides-Extra: dev
33
33
  Requires-Dist: anyio; extra == 'dev'
34
- Requires-Dist: mypy>=1.10.0; extra == 'dev'
34
+ Requires-Dist: mypy!=1.11.*,!=1.12.*,>=1.10.0; (python_version < '3.10') and extra == 'dev'
35
+ Requires-Dist: mypy>=1.10.0; (python_version >= '3.10') and extra == 'dev'
35
36
  Requires-Dist: patroni[etcd]>=2.1.5; extra == 'dev'
36
37
  Requires-Dist: port-for; extra == 'dev'
37
38
  Requires-Dist: prysk[pytest-plugin]>=0.14.0; extra == 'dev'
38
- Requires-Dist: pytest; extra == 'dev'
39
39
  Requires-Dist: pytest-cov; extra == 'dev'
40
+ Requires-Dist: pytest>=8; extra == 'dev'
40
41
  Requires-Dist: trustme; extra == 'dev'
41
42
  Requires-Dist: types-pyyaml>=6.0.12.10; extra == 'dev'
42
43
  Provides-Extra: test
@@ -44,11 +45,12 @@ Requires-Dist: anyio; extra == 'test'
44
45
  Requires-Dist: patroni[etcd]>=2.1.5; extra == 'test'
45
46
  Requires-Dist: port-for; extra == 'test'
46
47
  Requires-Dist: prysk[pytest-plugin]>=0.14.0; extra == 'test'
47
- Requires-Dist: pytest; extra == 'test'
48
48
  Requires-Dist: pytest-cov; extra == 'test'
49
+ Requires-Dist: pytest>=8; extra == 'test'
49
50
  Requires-Dist: trustme; extra == 'test'
50
51
  Provides-Extra: typing
51
- Requires-Dist: mypy>=1.10.0; extra == 'typing'
52
+ Requires-Dist: mypy!=1.11.*,!=1.12.*,>=1.10.0; (python_version < '3.10') and extra == 'typing'
53
+ Requires-Dist: mypy>=1.10.0; (python_version >= '3.10') and extra == 'typing'
52
54
  Requires-Dist: types-pyyaml>=6.0.12.10; extra == 'typing'
53
55
  Description-Content-Type: text/markdown
54
56
 
@@ -43,7 +43,7 @@ dependencies = [
43
43
  "pluggy",
44
44
  "psycopg >= 3.1",
45
45
  "pydantic >= 2.5.0",
46
- "rich >= 11.0.0",
46
+ "rich >= 11.0.0, != 13.9.0",
47
47
  ]
48
48
 
49
49
  [project.optional-dependencies]
@@ -52,12 +52,13 @@ test = [
52
52
  "patroni[etcd] >= 2.1.5",
53
53
  "port-for",
54
54
  "prysk[pytest-plugin] >= 0.14.0",
55
- "pytest",
55
+ "pytest >= 8",
56
56
  "pytest-cov",
57
57
  "trustme",
58
58
  ]
59
59
  typing = [
60
- "mypy >= 1.10.0",
60
+ "mypy >= 1.10.0 ; python_version >= '3.10'",
61
+ "mypy >= 1.10.0, != 1.11.*, != 1.12.* ; python_version < '3.10'",
61
62
  "types-PyYAML >= 6.0.12.10",
62
63
  ]
63
64
  dev = [
@@ -26,6 +26,7 @@ class CLIGroup(click.Group):
26
26
  "role",
27
27
  "database",
28
28
  "postgres",
29
+ "pghba",
29
30
  ]
30
31
 
31
32
  @classmethod
@@ -38,7 +39,10 @@ class CLIGroup(click.Group):
38
39
 
39
40
  def list_commands(self, context: click.Context) -> list[str]:
40
41
  main_commands = super().list_commands(context)
41
- plugins_commands: list[str] = sorted(g.name for g in hooks(_site.PLUGIN_MANAGER, h.command)) # type: ignore[misc]
42
+ plugins_commands: list[str] = sorted(
43
+ g.name # type: ignore[misc]
44
+ for g in hooks(_site.PLUGIN_MANAGER, h.command)
45
+ )
42
46
  return main_commands + self.submodules + plugins_commands
43
47
 
44
48
  def get_command(self, context: click.Context, name: str) -> click.Command | None:
@@ -30,7 +30,7 @@ from .util import (
30
30
  manifest_option,
31
31
  model_dump,
32
32
  output_format_option,
33
- pass_instance,
33
+ pass_postgresql_instance,
34
34
  print_argspec,
35
35
  print_json_for,
36
36
  print_schema,
@@ -63,11 +63,11 @@ def cli(**kwargs: Any) -> None:
63
63
 
64
64
  @cli.command("create")
65
65
  @model.as_parameters(interface.Database, "create")
66
- @pass_instance
66
+ @pass_postgresql_instance
67
67
  @click.pass_obj
68
68
  @async_command
69
69
  async def create(
70
- obj: Obj, instance: system.Instance, database: interface.Database
70
+ obj: Obj, instance: system.PostgreSQLInstance, database: interface.Database
71
71
  ) -> None:
72
72
  """Create a database in a PostgreSQL instance"""
73
73
  with obj.lock, audit():
@@ -79,13 +79,13 @@ async def create(
79
79
 
80
80
 
81
81
  @cli.command("alter") # type: ignore[arg-type]
82
- @model.as_parameters(interface.Database, "update", parse_model=False)
82
+ @model.as_parameters(interface.Database, "update")
83
83
  @click.argument("dbname")
84
- @pass_instance
84
+ @pass_postgresql_instance
85
85
  @click.pass_obj
86
86
  @async_command
87
87
  async def alter(
88
- obj: Obj, instance: system.Instance, dbname: str, **changes: Any
88
+ obj: Obj, instance: system.PostgreSQLInstance, dbname: str, **changes: Any
89
89
  ) -> None:
90
90
  """Alter a database in a PostgreSQL instance"""
91
91
  with obj.lock, audit():
@@ -100,14 +100,14 @@ async def alter(
100
100
  @manifest_option
101
101
  @output_format_option
102
102
  @dry_run_option
103
- @pass_instance
103
+ @pass_postgresql_instance
104
104
  @click.pass_obj
105
105
  @async_command
106
106
  async def apply(
107
107
  obj: Obj,
108
- instance: system.Instance,
108
+ instance: system.PostgreSQLInstance,
109
109
  data: ManifestData,
110
- output_format: OutputFormat,
110
+ output_format: OutputFormat | None,
111
111
  dry_run: bool,
112
112
  ) -> None:
113
113
  """Apply manifest as a database"""
@@ -118,38 +118,44 @@ async def apply(
118
118
  with obj.lock, audit():
119
119
  async with postgresql.running(instance):
120
120
  ret = await databases.apply(instance, database)
121
- if output_format == OutputFormat.json:
121
+ if output_format == "json":
122
122
  print_json_for(ret)
123
123
 
124
124
 
125
125
  @cli.command("get")
126
126
  @output_format_option
127
127
  @click.argument("name")
128
- @pass_instance
128
+ @pass_postgresql_instance
129
129
  @async_command
130
130
  async def get(
131
- instance: system.Instance, name: str, output_format: OutputFormat
131
+ instance: system.PostgreSQLInstance, name: str, output_format: OutputFormat | None
132
132
  ) -> None:
133
133
  """Get the description of a database"""
134
134
  async with postgresql.running(instance):
135
135
  db = await databases.get(instance, name)
136
- if output_format == OutputFormat.json:
136
+ if output_format == "json":
137
137
  print_json_for(model_dump(db))
138
138
  else:
139
- print_table_for(
140
- [db],
141
- functools.partial(model_dump, exclude={"extensions", "schemas"}),
142
- box=None,
143
- )
139
+ print_table_for([db], functools.partial(model_dump, mode="pretty"), box=None)
144
140
 
145
141
 
146
142
  @cli.command("list")
147
143
  @output_format_option
144
+ @click.option(
145
+ "-x",
146
+ "--exclude-database",
147
+ "exclude_dbnames",
148
+ multiple=True,
149
+ help="Database to exclude from listing.",
150
+ )
148
151
  @click.argument("dbname", nargs=-1)
149
- @pass_instance
152
+ @pass_postgresql_instance
150
153
  @async_command
151
154
  async def ls(
152
- instance: system.Instance, dbname: Sequence[str], output_format: OutputFormat
155
+ instance: system.PostgreSQLInstance,
156
+ dbname: Sequence[str],
157
+ exclude_dbnames: Sequence[str],
158
+ output_format: OutputFormat | None,
153
159
  ) -> None:
154
160
  """List databases (all or specified ones)
155
161
 
@@ -157,8 +163,10 @@ async def ls(
157
163
  """
158
164
 
159
165
  async with postgresql.running(instance):
160
- dbs = await databases.ls(instance, dbnames=dbname)
161
- if output_format == OutputFormat.json:
166
+ dbs = await databases.ls(
167
+ instance, dbnames=dbname, exclude_dbnames=exclude_dbnames
168
+ )
169
+ if output_format == "json":
162
170
  print_json_for([model_dump(db) for db in dbs])
163
171
  else:
164
172
  print_table_for(dbs, model_dump)
@@ -166,11 +174,13 @@ async def ls(
166
174
 
167
175
  @cli.command("drop")
168
176
  @model.as_parameters(interface.DatabaseDropped, "create")
169
- @pass_instance
177
+ @pass_postgresql_instance
170
178
  @click.pass_obj
171
179
  @async_command
172
180
  async def drop(
173
- obj: Obj, instance: system.Instance, databasedropped: interface.DatabaseDropped
181
+ obj: Obj,
182
+ instance: system.PostgreSQLInstance,
183
+ databasedropped: interface.DatabaseDropped,
174
184
  ) -> None:
175
185
  """Drop a database"""
176
186
  with obj.lock, audit():
@@ -183,14 +193,14 @@ async def drop(
183
193
  @click.option("-r", "--role", "roles", multiple=True, help="Role to inspect")
184
194
  @click.option("--default", "defaults", is_flag=True, help="Display default privileges")
185
195
  @output_format_option
186
- @pass_instance
196
+ @pass_postgresql_instance
187
197
  @async_command
188
198
  async def list_privileges(
189
- instance: system.Instance,
199
+ instance: system.PostgreSQLInstance,
190
200
  name: str,
191
201
  roles: Sequence[str],
192
202
  defaults: bool,
193
- output_format: OutputFormat,
203
+ output_format: OutputFormat | None,
194
204
  ) -> None:
195
205
  """List privileges on a database."""
196
206
  async with postgresql.running(instance):
@@ -201,7 +211,7 @@ async def list_privileges(
201
211
  )
202
212
  except ValueError as e:
203
213
  raise click.ClickException(str(e)) from None
204
- if output_format == OutputFormat.json:
214
+ if output_format == "json":
205
215
  print_json_for([model_dump(p) for p in prvlgs])
206
216
  else:
207
217
  print_table_for(prvlgs, model_dump)
@@ -220,14 +230,14 @@ async def list_privileges(
220
230
  help="Database to not run command on",
221
231
  )
222
232
  @output_format_option
223
- @pass_instance
233
+ @pass_postgresql_instance
224
234
  @async_command
225
235
  async def run(
226
- instance: system.Instance,
236
+ instance: system.PostgreSQLInstance,
227
237
  sql_command: str,
228
238
  dbnames: Sequence[str],
229
239
  exclude_dbnames: Sequence[str],
230
- output_format: OutputFormat,
240
+ output_format: OutputFormat | None,
231
241
  ) -> None:
232
242
  """Run given command on databases of a PostgreSQL instance"""
233
243
  async with postgresql.running(instance):
@@ -240,7 +250,7 @@ async def run(
240
250
  )
241
251
  except psycopg.ProgrammingError as e:
242
252
  raise click.ClickException(str(e)) from None
243
- if output_format == OutputFormat.json:
253
+ if output_format == "json":
244
254
  print_json_for(result)
245
255
  else:
246
256
  for dbname, rows in result.items():
@@ -256,9 +266,11 @@ async def run(
256
266
  help="Write dump file(s) to DIRECTORY instead of default dumps directory.",
257
267
  )
258
268
  @click.argument("dbname")
259
- @pass_instance
269
+ @pass_postgresql_instance
260
270
  @async_command
261
- async def dump(instance: system.Instance, dbname: str, output: Path | None) -> None:
271
+ async def dump(
272
+ instance: system.PostgreSQLInstance, dbname: str, output: Path | None
273
+ ) -> None:
262
274
  """Dump a database"""
263
275
  async with postgresql.running(instance):
264
276
  await databases.dump(instance, dbname, output)
@@ -267,17 +279,19 @@ async def dump(instance: system.Instance, dbname: str, output: Path | None) -> N
267
279
  @cli.command("dumps")
268
280
  @click.argument("dbname", nargs=-1)
269
281
  @output_format_option
270
- @pass_instance
282
+ @pass_postgresql_instance
271
283
  @async_command
272
284
  async def dumps(
273
- instance: system.Instance, dbname: Sequence[str], output_format: OutputFormat
285
+ instance: system.PostgreSQLInstance,
286
+ dbname: Sequence[str],
287
+ output_format: OutputFormat | None,
274
288
  ) -> None:
275
289
  """List the database dumps
276
290
 
277
291
  Only dumps created in the default dumps directory are listed.
278
292
  """
279
293
  values = [asdict(dump) async for dump in databases.dumps(instance, dbnames=dbname)]
280
- if output_format == OutputFormat.json:
294
+ if output_format == "json":
281
295
  print_json_for(values)
282
296
  else:
283
297
  dbnames = ", ".join(dbname) if dbname else "all databases"
@@ -287,10 +301,10 @@ async def dumps(
287
301
  @cli.command("restore")
288
302
  @click.argument("dump_id")
289
303
  @click.argument("targetdbname", required=False)
290
- @pass_instance
304
+ @pass_postgresql_instance
291
305
  @async_command
292
306
  async def restore(
293
- instance: system.Instance, dump_id: str, targetdbname: str | None
307
+ instance: system.PostgreSQLInstance, dump_id: str, targetdbname: str | None
294
308
  ) -> None:
295
309
  """Restore a database dump
296
310
 
@@ -14,6 +14,7 @@ from pydantic.v1.utils import deep_update
14
14
 
15
15
  from pglift import (
16
16
  async_hooks,
17
+ exceptions,
17
18
  hooks,
18
19
  hookspecs,
19
20
  instances,
@@ -26,9 +27,8 @@ from pglift.settings import default_postgresql_version
26
27
  from pglift.settings._postgresql import PostgreSQLVersion
27
28
  from pglift.types import Operation, Status, validation_context
28
29
 
29
- from . import _site
30
+ from . import _site, model
30
31
  from . import hookspecs as h
31
- from . import model
32
32
  from .util import (
33
33
  ManifestData,
34
34
  Obj,
@@ -42,6 +42,7 @@ from .util import (
42
42
  manifest_option,
43
43
  model_dump,
44
44
  output_format_option,
45
+ postgresql_instance_identifier,
45
46
  print_argspec,
46
47
  print_json_for,
47
48
  print_schema,
@@ -115,19 +116,19 @@ async def create(obj: Obj, instance: interface.Instance, drop_on_error: bool) ->
115
116
 
116
117
  @cli.command("alter") # type: ignore[arg-type]
117
118
  @instance_identifier(nargs=1)
118
- @model.as_parameters(_site.INSTANCE_MODEL, "update", parse_model=False)
119
+ @model.as_parameters(_site.INSTANCE_MODEL, "update")
119
120
  @click.pass_obj
120
121
  @async_command
121
122
  async def alter(obj: Obj, instance: system.Instance, **changes: Any) -> None:
122
123
  """Alter PostgreSQL INSTANCE"""
123
124
  with obj.lock, audit():
124
- status = await postgresql.status(instance)
125
+ status = await postgresql.status(instance.postgresql)
125
126
  manifest = await instances._get(instance, status)
126
127
  values = manifest.model_dump(by_alias=True, exclude={"settings"})
127
128
  values = deep_update(values, changes)
128
129
  # No need for 'settings' in validation_context() as a 'version' key
129
130
  # must be present in 'values' when altering.
130
- with validation_context(operation="update"):
131
+ with validation_context(operation="update", instance=manifest):
131
132
  altered = _site.INSTANCE_MODEL.model_validate(values)
132
133
  await instances.apply(
133
134
  _site.SETTINGS, altered, _is_running=status == Status.running
@@ -141,7 +142,7 @@ async def alter(obj: Obj, instance: system.Instance, **changes: Any) -> None:
141
142
  @click.pass_obj
142
143
  @async_command
143
144
  async def apply(
144
- obj: Obj, data: ManifestData, output_format: OutputFormat, dry_run: bool
145
+ obj: Obj, data: ManifestData, output_format: OutputFormat | None, dry_run: bool
145
146
  ) -> None:
146
147
  """Apply manifest as a PostgreSQL instance"""
147
148
  name, version = data["name"], data.get("version")
@@ -149,17 +150,25 @@ async def apply(
149
150
  version = default_postgresql_version(_site.SETTINGS.postgresql)
150
151
  elif not isinstance(version, str):
151
152
  version = str(version)
152
- op: Operation = (
153
- "update" if instances.exists(name, version, _site.SETTINGS) else "create"
154
- )
155
- with validation_context(operation=op, settings=_site.SETTINGS):
153
+ op: Operation = "create"
154
+ actual: interface.Instance | None = None
155
+ if data.get("state") == "absent":
156
+ op = "update"
157
+ else:
158
+ try:
159
+ actual = await instances.get((name, version), settings=_site.SETTINGS)
160
+ except exceptions.InstanceNotFound:
161
+ pass
162
+ else:
163
+ op = "update"
164
+ with validation_context(operation=op, settings=_site.SETTINGS, instance=actual):
156
165
  instance = _site.INSTANCE_MODEL.model_validate(data)
157
166
  if dry_run:
158
167
  ret = interface.InstanceApplyResult(change_state=None)
159
168
  else:
160
169
  with obj.lock, audit():
161
170
  ret = await instances.apply(_site.SETTINGS, instance)
162
- if output_format == OutputFormat.json:
171
+ if output_format == "json":
163
172
  print_json_for(ret)
164
173
 
165
174
 
@@ -177,14 +186,14 @@ async def promote(obj: Obj, instance: system.Instance) -> None:
177
186
  @output_format_option
178
187
  @instance_identifier(nargs=1)
179
188
  @async_command
180
- async def get(instance: system.Instance, output_format: OutputFormat) -> None:
189
+ async def get(instance: system.Instance, output_format: OutputFormat | None) -> None:
181
190
  """Get the description of PostgreSQL INSTANCE.
182
191
 
183
192
  Unless --output-format is specified, 'settings' and 'state' fields are not
184
193
  shown as well as 'standby' information if INSTANCE is not a standby.
185
194
  """
186
195
  i = await instances.get(instance)
187
- if output_format == OutputFormat.json:
196
+ if output_format == "json":
188
197
  print_json_for(model_dump(i))
189
198
  else:
190
199
  exclude = {
@@ -194,9 +203,11 @@ async def get(instance: system.Instance, output_format: OutputFormat) -> None:
194
203
  "wal_directory",
195
204
  "powa",
196
205
  }
197
- if not instance.standby:
206
+ if not instance.postgresql.standby:
198
207
  exclude.add("standby")
199
- print_table_for([i], partial(model_dump, exclude=exclude), box=None)
208
+ print_table_for(
209
+ [i], partial(model_dump, exclude=exclude, mode="pretty"), box=None
210
+ )
200
211
 
201
212
 
202
213
  @cli.command("list")
@@ -207,10 +218,12 @@ async def get(instance: system.Instance, output_format: OutputFormat) -> None:
207
218
  )
208
219
  @output_format_option
209
220
  @async_command
210
- async def ls(version: PostgreSQLVersion | None, output_format: OutputFormat) -> None:
221
+ async def ls(
222
+ version: PostgreSQLVersion | None, output_format: OutputFormat | None
223
+ ) -> None:
211
224
  """List the available instances"""
212
225
  items = [i async for i in instances.ls(_site.SETTINGS, version=version)]
213
- if output_format == OutputFormat.json:
226
+ if output_format == "json":
214
227
  print_json_for([model_dump(m) for m in items])
215
228
  else:
216
229
  print_table_for(items, model_dump)
@@ -290,12 +303,12 @@ async def stop(
290
303
 
291
304
 
292
305
  @cli.command("reload")
293
- @instance_identifier(nargs=-1)
306
+ @postgresql_instance_identifier(nargs=-1)
294
307
  @click.option("--all", "all_instances", is_flag=True, help="Reload all instances.")
295
308
  @click.pass_obj
296
309
  @async_command
297
310
  async def reload(
298
- obj: Obj, instance: tuple[system.Instance, ...], all_instances: bool
311
+ obj: Obj, instance: tuple[system.PostgreSQLInstance, ...], all_instances: bool
299
312
  ) -> None:
300
313
  """Reload PostgreSQL INSTANCE"""
301
314
  with obj.lock, audit():
@@ -362,7 +375,7 @@ def shell(instance: system.Instance, shell: str) -> None:
362
375
  @cli.command("env")
363
376
  @instance_identifier(nargs=1)
364
377
  @output_format_option
365
- def env(instance: system.Instance, output_format: OutputFormat) -> None:
378
+ def env(instance: system.Instance, output_format: OutputFormat | None) -> None:
366
379
  """Output environment variables suitable to handle to PostgreSQL INSTANCE.
367
380
 
368
381
  This can be injected in shell using:
@@ -370,7 +383,7 @@ def env(instance: system.Instance, output_format: OutputFormat) -> None:
370
383
  export $(pglift instance env myinstance)
371
384
  """
372
385
  instance_env = instances.env_for(instance, path=True)
373
- if output_format == OutputFormat.json:
386
+ if output_format == "json":
374
387
  print_json_for(instance_env)
375
388
  else:
376
389
  for key, value in sorted(instance_env.items()):
@@ -379,8 +392,8 @@ def env(instance: system.Instance, output_format: OutputFormat) -> None:
379
392
 
380
393
  @cli.command("logs")
381
394
  @click.option("--follow/--no-follow", "-f/", default=False, help="Follow log output.")
382
- @instance_identifier(nargs=1)
383
- def logs(instance: system.Instance, follow: bool) -> None:
395
+ @postgresql_instance_identifier(nargs=1)
396
+ def logs(instance: system.PostgreSQLInstance, follow: bool) -> None:
384
397
  """Output PostgreSQL logs of INSTANCE.
385
398
 
386
399
  This assumes that the PostgreSQL instance is configured to use file-based
@@ -400,7 +413,7 @@ def logs(instance: system.Instance, follow: bool) -> None:
400
413
 
401
414
 
402
415
  @cli.command("privileges")
403
- @instance_identifier(nargs=1)
416
+ @postgresql_instance_identifier(nargs=1)
404
417
  @click.option(
405
418
  "-d",
406
419
  "--database",
@@ -413,11 +426,11 @@ def logs(instance: system.Instance, follow: bool) -> None:
413
426
  @output_format_option
414
427
  @async_command
415
428
  async def list_privileges(
416
- instance: system.Instance,
429
+ instance: system.PostgreSQLInstance,
417
430
  databases: Sequence[str],
418
431
  roles: Sequence[str],
419
432
  defaults: bool,
420
- output_format: OutputFormat,
433
+ output_format: OutputFormat | None,
421
434
  ) -> None:
422
435
  """List privileges on INSTANCE's databases."""
423
436
  async with postgresql.running(instance):
@@ -427,7 +440,7 @@ async def list_privileges(
427
440
  )
428
441
  except ValueError as e:
429
442
  raise click.ClickException(str(e)) from None
430
- if output_format == OutputFormat.json:
443
+ if output_format == "json":
431
444
  print_json_for([model_dump(p) for p in prvlgs])
432
445
  else:
433
446
  if defaults:
@@ -455,8 +468,9 @@ async def list_privileges(
455
468
  "--jobs",
456
469
  required=False,
457
470
  type=click.INT,
458
- help="Number of simultaneous processes or threads to use (from pg_upgrade).",
471
+ help="Number of simultaneous processes or threads to use (from pg_upgrade). Deprecated. Use extra options instead.",
459
472
  )
473
+ @click.argument("extra_opts", nargs=-1, type=click.UNPROCESSED)
460
474
  @click.pass_obj
461
475
  @async_command
462
476
  async def upgrade(
@@ -466,12 +480,23 @@ async def upgrade(
466
480
  newname: str | None,
467
481
  port: int | None,
468
482
  jobs: int | None,
483
+ extra_opts: tuple[str, ...],
469
484
  ) -> None:
470
- """Upgrade INSTANCE using pg_upgrade"""
485
+ """Upgrade INSTANCE using pg_upgrade
486
+
487
+ Extra options can be passed to the pg_upgrade command. They may need to be prefixed
488
+ with -- to separate them from the current command options when confusion arises. When using
489
+ extra options, providing the instance identifier is required.
490
+ """
471
491
  with obj.lock, audit():
472
- await postgresql.check_status(instance, Status.not_running)
492
+ await postgresql.check_status(instance.postgresql, Status.not_running)
473
493
  async with task.async_transaction():
474
494
  new_instance = await instances.upgrade(
475
- instance, version=newversion, name=newname, port=port, jobs=jobs
495
+ instance,
496
+ version=newversion,
497
+ name=newname,
498
+ port=port,
499
+ jobs=jobs,
500
+ extra_opts=extra_opts,
476
501
  )
477
502
  await instances.start(new_instance)
@@ -207,7 +207,7 @@ def site_settings(
207
207
  /,
208
208
  defaults: bool | None,
209
209
  schema: bool,
210
- output_format: OutputFormat,
210
+ output_format: OutputFormat | None,
211
211
  ) -> None:
212
212
  """Show site settings.
213
213
 
@@ -227,7 +227,7 @@ def site_settings(
227
227
  value = _site.SETTINGS.model_dump(
228
228
  mode="json", exclude_defaults=defaults is False
229
229
  )
230
- if output_format == OutputFormat.json:
230
+ if output_format == "json":
231
231
  console.print_json(data=value)
232
232
  else:
233
233
  assert output_format is None