pglift-cli 1.9.0__tar.gz → 2.1.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 (48) hide show
  1. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/PKG-INFO +7 -10
  2. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/pyproject.toml +4 -6
  3. pglift_cli-2.1.0/pytest.ini +10 -0
  4. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/_settings.py +14 -20
  5. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/database.py +49 -26
  6. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/instance.py +130 -66
  7. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/main.py +20 -27
  8. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/model.py +6 -7
  9. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/patroni.py +3 -3
  10. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/pgbackrest/__init__.py +9 -10
  11. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/pgbackrest/repo_path.py +3 -3
  12. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/pgconf.py +30 -19
  13. pglift_cli-2.1.0/src/pglift_cli/pghba.py +93 -0
  14. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/postgres.py +9 -8
  15. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/prometheus.py +14 -4
  16. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/role.py +53 -44
  17. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/util.py +101 -77
  18. pglift_cli-2.1.0/tests/expect/.gitignore +5 -0
  19. pglift_cli-2.1.0/tests/expect/test-base.t +33 -0
  20. pglift_cli-2.1.0/tests/expect/test-cli-walkthrough.t +1225 -0
  21. pglift_cli-2.1.0/tests/expect/test-demote.t +231 -0
  22. pglift_cli-2.1.0/tests/expect/test-help.t +1031 -0
  23. pglift_cli-2.1.0/tests/expect/test-port-validation.t +153 -0
  24. pglift_cli-2.1.0/tests/expect/test-prometheus.t +103 -0
  25. pglift_cli-2.1.0/tests/expect/test-standby-pgbackrest.t +371 -0
  26. pglift_cli-2.1.0/tests/expect/test-transactions.t +70 -0
  27. pglift_cli-2.1.0/tests/expect/test-upgrade.t +74 -0
  28. pglift_cli-2.1.0/tests/unit/__init__.py +15 -0
  29. pglift_cli-2.1.0/tests/unit/conftest.py +29 -0
  30. pglift_cli-2.1.0/tests/unit/test__site.py +41 -0
  31. pglift_cli-2.1.0/tests/unit/test_audit.py +58 -0
  32. pglift_cli-2.1.0/tests/unit/test_cli.py +577 -0
  33. pglift_cli-2.1.0/tests/unit/test_main.py +85 -0
  34. pglift_cli-2.1.0/tests/unit/test_model.py +357 -0
  35. pglift_cli-2.1.0/tests/unit/test_pm.py +62 -0
  36. pglift_cli-2.1.0/tests/unit/test_util.py +372 -0
  37. pglift_cli-1.9.0/src/pglift_cli/pghba.py +0 -57
  38. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/.gitignore +0 -0
  39. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/README.md +0 -0
  40. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/hatch.toml +0 -0
  41. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/__init__.py +0 -0
  42. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/__main__.py +0 -0
  43. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/_site.py +0 -0
  44. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/base.py +0 -0
  45. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/console.py +0 -0
  46. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/hookspecs.py +0 -0
  47. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/pm.py +0 -0
  48. {pglift_cli-1.9.0 → pglift_cli-2.1.0}/src/pglift_cli/py.typed +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pglift_cli
3
- Version: 1.9.0
3
+ Version: 2.1.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/
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: System Administrators
15
15
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
@@ -23,18 +22,17 @@ Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Topic :: Database
24
23
  Classifier: Topic :: System :: Systems Administration
25
24
  Classifier: Typing :: Typed
26
- Requires-Python: <4,>=3.9
27
- Requires-Dist: click!=8.1.0,!=8.1.4,>=8.0.0
25
+ Requires-Python: <4,>=3.10
26
+ Requires-Dist: click!=8.1.0,!=8.1.4,<8.2.0,>=8.0.0
28
27
  Requires-Dist: filelock!=3.12.1,>=3.9.0
29
28
  Requires-Dist: pluggy
30
29
  Requires-Dist: psycopg>=3.1
31
- Requires-Dist: pydantic!=2.10.0,!=2.10.1,>=2.5.0
30
+ Requires-Dist: pydantic>=2.10.2
32
31
  Requires-Dist: pyyaml>=6.0.1
33
32
  Requires-Dist: rich!=13.9.0,>=11.0.0
34
33
  Provides-Extra: dev
35
34
  Requires-Dist: anyio; extra == 'dev'
36
- Requires-Dist: mypy!=1.11.*,!=1.12.*,>=1.10.0; (python_version < '3.10') and extra == 'dev'
37
- Requires-Dist: mypy>=1.10.0; (python_version >= '3.10') and extra == 'dev'
35
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
38
36
  Requires-Dist: patroni[etcd]>=2.1.5; extra == 'dev'
39
37
  Requires-Dist: port-for; extra == 'dev'
40
38
  Requires-Dist: prysk[pytest-plugin]>=0.14.0; extra == 'dev'
@@ -51,8 +49,7 @@ Requires-Dist: pytest-cov; extra == 'test'
51
49
  Requires-Dist: pytest>=8; extra == 'test'
52
50
  Requires-Dist: trustme; extra == 'test'
53
51
  Provides-Extra: typing
54
- Requires-Dist: mypy!=1.11.*,!=1.12.*,>=1.10.0; (python_version < '3.10') and extra == 'typing'
55
- Requires-Dist: mypy>=1.10.0; (python_version >= '3.10') and extra == 'typing'
52
+ Requires-Dist: mypy>=1.10.0; extra == 'typing'
56
53
  Requires-Dist: types-pyyaml>=6.0.12.10; extra == 'typing'
57
54
  Description-Content-Type: text/markdown
58
55
 
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
10
10
  name = "pglift_cli"
11
11
  description = "Command-line interface for pglift"
12
12
  readme = "README.md"
13
- requires-python = ">=3.9, <4"
13
+ requires-python = ">=3.10, <4"
14
14
  license = { text = "GPLv3" }
15
15
  authors = [{ name = "Dalibo SCOP", email = "contact@dalibo.com" }]
16
16
  keywords = [
@@ -28,7 +28,6 @@ classifiers = [
28
28
  "Topic :: Database",
29
29
  "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
30
30
  "Programming Language :: Python :: 3",
31
- "Programming Language :: Python :: 3.9",
32
31
  "Programming Language :: Python :: 3.10",
33
32
  "Programming Language :: Python :: 3.11",
34
33
  "Programming Language :: Python :: 3.12",
@@ -40,11 +39,11 @@ dynamic = ["version"]
40
39
 
41
40
  dependencies = [
42
41
  "PyYAML >= 6.0.1",
43
- "click >= 8.0.0, != 8.1.0, != 8.1.4",
42
+ "click >= 8.0.0, != 8.1.0, != 8.1.4, < 8.2.0",
44
43
  "filelock >= 3.9.0, != 3.12.1",
45
44
  "pluggy",
46
45
  "psycopg >= 3.1",
47
- "pydantic >= 2.5.0, != 2.10.0, != 2.10.1",
46
+ "pydantic >= 2.10.2",
48
47
  "rich >= 11.0.0, != 13.9.0",
49
48
  ]
50
49
 
@@ -59,8 +58,7 @@ test = [
59
58
  "trustme",
60
59
  ]
61
60
  typing = [
62
- "mypy >= 1.10.0 ; python_version >= '3.10'",
63
- "mypy >= 1.10.0, != 1.11.*, != 1.12.* ; python_version < '3.10'",
61
+ "mypy >= 1.10.0",
64
62
  "types-PyYAML >= 6.0.12.10",
65
63
  ]
66
64
  dev = [
@@ -0,0 +1,10 @@
1
+ # SPDX-FileCopyrightText: 2025 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ [pytest]
6
+ addopts = --strict-config --strict-markers
7
+ filterwarnings =
8
+ error
9
+ ignore:cannot guess 'postgresql.versions' setting as 'bindir' is unset:RuntimeWarning
10
+ testpaths = tests/unit
@@ -2,23 +2,15 @@
2
2
  #
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- import warnings
6
5
  from pathlib import Path
7
- from typing import Annotated, Any, Optional
6
+ from typing import Annotated, Any
8
7
 
9
- from pydantic import AfterValidator, Field, ValidationInfo
8
+ from pydantic import BeforeValidator, Field
10
9
 
11
10
  from pglift.settings import Settings as BaseSettings
12
11
  from pglift.settings import SiteSettings as BaseSiteSettings
13
12
  from pglift.settings.base import BaseModel, LogPath, RunPath
14
-
15
-
16
- def deprecated(value: Any, info: ValidationInfo) -> Any:
17
- if value is not None:
18
- warnings.warn(
19
- f"{info.field_name!r} setting is deprecated", FutureWarning, stacklevel=2
20
- )
21
- return value
13
+ from pglift.types import LogLevel
22
14
 
23
15
 
24
16
  class AuditSettings(BaseModel):
@@ -38,21 +30,23 @@ class AuditSettings(BaseModel):
38
30
  ] = "%Y-%m-%d %H:%M:%S"
39
31
 
40
32
 
33
+ def _upper(value: Any) -> Any:
34
+ if isinstance(value, str):
35
+ return value.upper()
36
+ return value
37
+
38
+
41
39
  class CLISettings(BaseModel):
42
40
  """Settings for pglift's command-line interface."""
43
41
 
44
- logpath: Annotated[
45
- Annotated[Optional[Path], LogPath],
46
- Field(
47
- description="Directory where temporary debug files from command executions will be stored (DEPRECATED).",
48
- ),
49
- AfterValidator(deprecated),
50
- ] = None
51
-
52
42
  log_format: Annotated[
53
43
  str, Field(description="Format for log messages when written to a file")
54
44
  ] = "%(asctime)s %(levelname)-8s %(name)s - %(message)s"
55
45
 
46
+ log_level: Annotated[
47
+ LogLevel | None, Field(description="Log level"), BeforeValidator(_upper)
48
+ ] = None
49
+
56
50
  date_format: Annotated[
57
51
  str, Field(description="Date format in log messages when written to a file")
58
52
  ] = "%Y-%m-%d %H:%M:%S"
@@ -62,7 +56,7 @@ class CLISettings(BaseModel):
62
56
  ] = Path(".pglift.lock")
63
57
 
64
58
  audit: Annotated[
65
- Optional[AuditSettings],
59
+ AuditSettings | None,
66
60
  Field(description="Settings for change operations auditing"),
67
61
  ] = None
68
62
 
@@ -14,8 +14,8 @@ import psycopg
14
14
  from attrs import asdict
15
15
  from pydantic.v1.utils import deep_update
16
16
 
17
- from pglift import databases, postgresql, privileges, task
18
- from pglift.models import interface, system
17
+ from pglift import databases, diff, postgresql, privileges, task
18
+ from pglift.models import PostgreSQLInstance, interface
19
19
 
20
20
  from . import model
21
21
  from .util import (
@@ -25,6 +25,7 @@ from .util import (
25
25
  OutputFormat,
26
26
  async_command,
27
27
  audit,
28
+ diff_options,
28
29
  dry_run_option,
29
30
  instance_identifier_option,
30
31
  manifest_option,
@@ -33,8 +34,10 @@ from .util import (
33
34
  pass_postgresql_instance,
34
35
  print_argspec,
35
36
  print_json_for,
37
+ print_result_diff,
36
38
  print_schema,
37
39
  print_table_for,
40
+ system_configure,
38
41
  )
39
42
 
40
43
 
@@ -63,14 +66,18 @@ def cli(**kwargs: Any) -> None:
63
66
 
64
67
  @cli.command("create")
65
68
  @model.as_parameters(interface.Database, "create")
69
+ @dry_run_option
66
70
  @pass_postgresql_instance
67
71
  @click.pass_obj
68
72
  @async_command
69
73
  async def create(
70
- obj: Obj, instance: system.PostgreSQLInstance, database: interface.Database
74
+ obj: Obj,
75
+ instance: PostgreSQLInstance,
76
+ database: interface.Database,
77
+ dry_run: bool,
71
78
  ) -> None:
72
79
  """Create a database in a PostgreSQL instance"""
73
- with obj.lock, audit():
80
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
74
81
  async with postgresql.running(instance):
75
82
  if await databases.exists(instance, database.name):
76
83
  raise click.ClickException("database already exists")
@@ -78,17 +85,22 @@ async def create(
78
85
  await databases.apply(instance, database)
79
86
 
80
87
 
81
- @cli.command("alter") # type: ignore[arg-type]
88
+ @cli.command("alter")
82
89
  @model.as_parameters(interface.Database, "update")
83
90
  @click.argument("dbname")
91
+ @dry_run_option
84
92
  @pass_postgresql_instance
85
- @click.pass_obj
93
+ @click.pass_obj # type: ignore[arg-type]
86
94
  @async_command
87
95
  async def alter(
88
- obj: Obj, instance: system.PostgreSQLInstance, dbname: str, **changes: Any
96
+ obj: Obj,
97
+ instance: PostgreSQLInstance,
98
+ dbname: str,
99
+ dry_run: bool,
100
+ **changes: Any,
89
101
  ) -> None:
90
102
  """Alter a database in a PostgreSQL instance"""
91
- with obj.lock, audit():
103
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
92
104
  async with postgresql.running(instance):
93
105
  values = (await databases.get(instance, dbname)).model_dump(by_alias=True)
94
106
  values = deep_update(values, changes)
@@ -99,27 +111,34 @@ async def alter(
99
111
  @cli.command("apply", hidden=True)
100
112
  @manifest_option
101
113
  @output_format_option
114
+ @diff_options["unified"]
115
+ @diff_options["ansible"]
102
116
  @dry_run_option
103
117
  @pass_postgresql_instance
104
118
  @click.pass_obj
105
119
  @async_command
106
120
  async def apply(
107
121
  obj: Obj,
108
- instance: system.PostgreSQLInstance,
122
+ instance: PostgreSQLInstance,
109
123
  data: ManifestData,
110
124
  output_format: OutputFormat | None,
111
125
  dry_run: bool,
126
+ diff_format: diff.Format | None,
112
127
  ) -> None:
113
128
  """Apply manifest as a database"""
114
129
  database = interface.Database.model_validate(data)
115
- if dry_run:
116
- ret = interface.ApplyResult(change_state=None)
117
- else:
118
- with obj.lock, audit():
119
- async with postgresql.running(instance):
120
- 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)
121
138
  if output_format == "json":
122
139
  print_json_for(ret)
140
+ else:
141
+ print_result_diff(ret)
123
142
 
124
143
 
125
144
  @cli.command("get")
@@ -128,7 +147,7 @@ async def apply(
128
147
  @pass_postgresql_instance
129
148
  @async_command
130
149
  async def get(
131
- instance: system.PostgreSQLInstance, name: str, output_format: OutputFormat | None
150
+ instance: PostgreSQLInstance, name: str, output_format: OutputFormat | None
132
151
  ) -> None:
133
152
  """Get the description of a database"""
134
153
  async with postgresql.running(instance):
@@ -152,7 +171,7 @@ async def get(
152
171
  @pass_postgresql_instance
153
172
  @async_command
154
173
  async def ls(
155
- instance: system.PostgreSQLInstance,
174
+ instance: PostgreSQLInstance,
156
175
  dbname: Sequence[str],
157
176
  exclude_dbnames: Sequence[str],
158
177
  output_format: OutputFormat | None,
@@ -174,16 +193,18 @@ async def ls(
174
193
 
175
194
  @cli.command("drop")
176
195
  @model.as_parameters(interface.DatabaseDropped, "create")
196
+ @dry_run_option
177
197
  @pass_postgresql_instance
178
198
  @click.pass_obj
179
199
  @async_command
180
200
  async def drop(
181
201
  obj: Obj,
182
- instance: system.PostgreSQLInstance,
202
+ instance: PostgreSQLInstance,
183
203
  databasedropped: interface.DatabaseDropped,
204
+ dry_run: bool,
184
205
  ) -> None:
185
206
  """Drop a database"""
186
- with obj.lock, audit():
207
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
187
208
  async with postgresql.running(instance):
188
209
  await databases.drop(instance, databasedropped)
189
210
 
@@ -196,7 +217,7 @@ async def drop(
196
217
  @pass_postgresql_instance
197
218
  @async_command
198
219
  async def list_privileges(
199
- instance: system.PostgreSQLInstance,
220
+ instance: PostgreSQLInstance,
200
221
  name: str,
201
222
  roles: Sequence[str],
202
223
  defaults: bool,
@@ -233,7 +254,7 @@ async def list_privileges(
233
254
  @pass_postgresql_instance
234
255
  @async_command
235
256
  async def run(
236
- instance: system.PostgreSQLInstance,
257
+ instance: PostgreSQLInstance,
237
258
  sql_command: str,
238
259
  dbnames: Sequence[str],
239
260
  exclude_dbnames: Sequence[str],
@@ -267,13 +288,15 @@ async def run(
267
288
  )
268
289
  @click.argument("dbname")
269
290
  @pass_postgresql_instance
291
+ @dry_run_option
270
292
  @async_command
271
293
  async def dump(
272
- instance: system.PostgreSQLInstance, dbname: str, output: Path | None
294
+ instance: PostgreSQLInstance, dbname: str, output: Path | None, dry_run: bool
273
295
  ) -> None:
274
296
  """Dump a database"""
275
- async with postgresql.running(instance):
276
- 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)
277
300
 
278
301
 
279
302
  @cli.command("dumps")
@@ -282,7 +305,7 @@ async def dump(
282
305
  @pass_postgresql_instance
283
306
  @async_command
284
307
  async def dumps(
285
- instance: system.PostgreSQLInstance,
308
+ instance: PostgreSQLInstance,
286
309
  dbname: Sequence[str],
287
310
  output_format: OutputFormat | None,
288
311
  ) -> None:
@@ -304,7 +327,7 @@ async def dumps(
304
327
  @pass_postgresql_instance
305
328
  @async_command
306
329
  async def restore(
307
- instance: system.PostgreSQLInstance, dump_id: str, targetdbname: str | None
330
+ instance: PostgreSQLInstance, dump_id: str, targetdbname: str | None
308
331
  ) -> None:
309
332
  """Restore a database dump
310
333