pglift-cli 1.8.0__tar.gz → 2.0.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 (46) hide show
  1. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/PKG-INFO +8 -9
  2. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/pyproject.toml +5 -8
  3. pglift_cli-2.0.0/pytest.ini +10 -0
  4. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/_settings.py +14 -20
  5. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/_site.py +2 -3
  6. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/database.py +23 -18
  7. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/instance.py +93 -64
  8. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/main.py +23 -30
  9. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/model.py +6 -7
  10. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/patroni.py +3 -3
  11. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/pgbackrest/__init__.py +6 -7
  12. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/pgbackrest/repo_path.py +3 -3
  13. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/pgconf.py +30 -19
  14. pglift_cli-2.0.0/src/pglift_cli/pghba.py +93 -0
  15. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/postgres.py +9 -8
  16. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/prometheus.py +14 -4
  17. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/role.py +41 -32
  18. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/util.py +108 -78
  19. pglift_cli-2.0.0/tests/expect/.gitignore +5 -0
  20. pglift_cli-2.0.0/tests/expect/test-base.t +33 -0
  21. pglift_cli-2.0.0/tests/expect/test-cli-walkthrough.t +1135 -0
  22. pglift_cli-2.0.0/tests/expect/test-help.t +999 -0
  23. pglift_cli-2.0.0/tests/expect/test-port-validation.t +153 -0
  24. pglift_cli-2.0.0/tests/expect/test-prometheus.t +103 -0
  25. pglift_cli-2.0.0/tests/expect/test-standby-pgbackrest.t +371 -0
  26. pglift_cli-2.0.0/tests/expect/test-upgrade.t +74 -0
  27. pglift_cli-2.0.0/tests/unit/__init__.py +15 -0
  28. pglift_cli-2.0.0/tests/unit/conftest.py +29 -0
  29. pglift_cli-2.0.0/tests/unit/test__site.py +45 -0
  30. pglift_cli-2.0.0/tests/unit/test_audit.py +58 -0
  31. pglift_cli-2.0.0/tests/unit/test_cli.py +581 -0
  32. pglift_cli-2.0.0/tests/unit/test_main.py +87 -0
  33. pglift_cli-2.0.0/tests/unit/test_model.py +357 -0
  34. pglift_cli-2.0.0/tests/unit/test_pm.py +62 -0
  35. pglift_cli-2.0.0/tests/unit/test_util.py +372 -0
  36. pglift_cli-1.8.0/src/pglift_cli/pghba.py +0 -57
  37. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/.gitignore +0 -0
  38. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/README.md +0 -0
  39. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/hatch.toml +0 -0
  40. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/__init__.py +0 -0
  41. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/__main__.py +0 -0
  42. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/base.py +0 -0
  43. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/console.py +0 -0
  44. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/hookspecs.py +0 -0
  45. {pglift_cli-1.8.0 → pglift_cli-2.0.0}/src/pglift_cli/pm.py +0 -0
  46. {pglift_cli-1.8.0 → pglift_cli-2.0.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.8.0
3
+ Version: 2.0.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,24 +15,24 @@ 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
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
21
22
  Classifier: Topic :: Database
22
23
  Classifier: Topic :: System :: Systems Administration
23
24
  Classifier: Typing :: Typed
24
- Requires-Python: <4,>=3.9
25
+ Requires-Python: <4,>=3.10
25
26
  Requires-Dist: click!=8.1.0,!=8.1.4,>=8.0.0
26
27
  Requires-Dist: filelock!=3.12.1,>=3.9.0
27
28
  Requires-Dist: pluggy
28
29
  Requires-Dist: psycopg>=3.1
29
- Requires-Dist: pydantic>=2.5.0
30
+ Requires-Dist: pydantic>=2.10.2
30
31
  Requires-Dist: pyyaml>=6.0.1
31
32
  Requires-Dist: rich!=13.9.0,>=11.0.0
32
33
  Provides-Extra: dev
33
34
  Requires-Dist: anyio; 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
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
36
36
  Requires-Dist: patroni[etcd]>=2.1.5; extra == 'dev'
37
37
  Requires-Dist: port-for; extra == 'dev'
38
38
  Requires-Dist: prysk[pytest-plugin]>=0.14.0; extra == 'dev'
@@ -49,8 +49,7 @@ Requires-Dist: pytest-cov; extra == 'test'
49
49
  Requires-Dist: pytest>=8; extra == 'test'
50
50
  Requires-Dist: trustme; extra == 'test'
51
51
  Provides-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
+ Requires-Dist: mypy>=1.10.0; extra == 'typing'
54
53
  Requires-Dist: types-pyyaml>=6.0.12.10; extra == 'typing'
55
54
  Description-Content-Type: text/markdown
56
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,9 +28,10 @@ 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",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
34
35
  "Programming Language :: Python :: 3 :: Only",
35
36
  "Typing :: Typed",
36
37
  ]
@@ -42,7 +43,7 @@ dependencies = [
42
43
  "filelock >= 3.9.0, != 3.12.1",
43
44
  "pluggy",
44
45
  "psycopg >= 3.1",
45
- "pydantic >= 2.5.0",
46
+ "pydantic >= 2.10.2",
46
47
  "rich >= 11.0.0, != 13.9.0",
47
48
  ]
48
49
 
@@ -57,8 +58,7 @@ test = [
57
58
  "trustme",
58
59
  ]
59
60
  typing = [
60
- "mypy >= 1.10.0 ; python_version >= '3.10'",
61
- "mypy >= 1.10.0, != 1.11.*, != 1.12.* ; python_version < '3.10'",
61
+ "mypy >= 1.10.0",
62
62
  "types-PyYAML >= 6.0.12.10",
63
63
  ]
64
64
  dev = [
@@ -75,6 +75,3 @@ 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"
@@ -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
 
@@ -20,7 +20,8 @@ from pglift import exceptions, plugin_manager
20
20
  from pglift.models import interface
21
21
 
22
22
  from . import hookspecs
23
- from ._settings import Settings, SiteSettings
23
+ from ._settings import Settings
24
+ from ._settings import SiteSettings as SiteSettings
24
25
  from .pm import PluginManager
25
26
 
26
27
 
@@ -30,8 +31,6 @@ def _settings() -> Settings:
30
31
  return SiteSettings()
31
32
  except (exceptions.SettingsError, pydantic.ValidationError) as e:
32
33
  raise click.ClickException(f"invalid site settings\n{e}") from e
33
- except exceptions.UnsupportedError as e:
34
- raise click.ClickException(f"unsupported operation: {e}") from None
35
34
 
36
35
 
37
36
  _default_settings = cache(Settings)
@@ -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,6 +34,7 @@ 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,
38
40
  )
@@ -67,7 +69,7 @@ def cli(**kwargs: Any) -> None:
67
69
  @click.pass_obj
68
70
  @async_command
69
71
  async def create(
70
- obj: Obj, instance: system.PostgreSQLInstance, database: interface.Database
72
+ obj: Obj, instance: PostgreSQLInstance, database: interface.Database
71
73
  ) -> None:
72
74
  """Create a database in a PostgreSQL instance"""
73
75
  with obj.lock, audit():
@@ -78,14 +80,14 @@ async def create(
78
80
  await databases.apply(instance, database)
79
81
 
80
82
 
81
- @cli.command("alter") # type: ignore[arg-type]
83
+ @cli.command("alter")
82
84
  @model.as_parameters(interface.Database, "update")
83
85
  @click.argument("dbname")
84
86
  @pass_postgresql_instance
85
- @click.pass_obj
87
+ @click.pass_obj # type: ignore[arg-type]
86
88
  @async_command
87
89
  async def alter(
88
- obj: Obj, instance: system.PostgreSQLInstance, dbname: str, **changes: Any
90
+ obj: Obj, instance: PostgreSQLInstance, dbname: str, **changes: Any
89
91
  ) -> None:
90
92
  """Alter a database in a PostgreSQL instance"""
91
93
  with obj.lock, audit():
@@ -99,27 +101,32 @@ async def alter(
99
101
  @cli.command("apply", hidden=True)
100
102
  @manifest_option
101
103
  @output_format_option
104
+ @diff_options["unified"]
105
+ @diff_options["ansible"]
102
106
  @dry_run_option
103
107
  @pass_postgresql_instance
104
108
  @click.pass_obj
105
109
  @async_command
106
110
  async def apply(
107
111
  obj: Obj,
108
- instance: system.PostgreSQLInstance,
112
+ instance: PostgreSQLInstance,
109
113
  data: ManifestData,
110
114
  output_format: OutputFormat | None,
111
115
  dry_run: bool,
116
+ diff_format: diff.Format | None,
112
117
  ) -> None:
113
118
  """Apply manifest as a database"""
114
119
  database = interface.Database.model_validate(data)
115
120
  if dry_run:
116
121
  ret = interface.ApplyResult(change_state=None)
117
122
  else:
118
- with obj.lock, audit():
123
+ with obj.lock, audit(dry_run=dry_run), diff.enabled(diff_format):
119
124
  async with postgresql.running(instance):
120
125
  ret = await databases.apply(instance, database)
121
126
  if output_format == "json":
122
127
  print_json_for(ret)
128
+ else:
129
+ print_result_diff(ret)
123
130
 
124
131
 
125
132
  @cli.command("get")
@@ -128,7 +135,7 @@ async def apply(
128
135
  @pass_postgresql_instance
129
136
  @async_command
130
137
  async def get(
131
- instance: system.PostgreSQLInstance, name: str, output_format: OutputFormat | None
138
+ instance: PostgreSQLInstance, name: str, output_format: OutputFormat | None
132
139
  ) -> None:
133
140
  """Get the description of a database"""
134
141
  async with postgresql.running(instance):
@@ -152,7 +159,7 @@ async def get(
152
159
  @pass_postgresql_instance
153
160
  @async_command
154
161
  async def ls(
155
- instance: system.PostgreSQLInstance,
162
+ instance: PostgreSQLInstance,
156
163
  dbname: Sequence[str],
157
164
  exclude_dbnames: Sequence[str],
158
165
  output_format: OutputFormat | None,
@@ -179,7 +186,7 @@ async def ls(
179
186
  @async_command
180
187
  async def drop(
181
188
  obj: Obj,
182
- instance: system.PostgreSQLInstance,
189
+ instance: PostgreSQLInstance,
183
190
  databasedropped: interface.DatabaseDropped,
184
191
  ) -> None:
185
192
  """Drop a database"""
@@ -196,7 +203,7 @@ async def drop(
196
203
  @pass_postgresql_instance
197
204
  @async_command
198
205
  async def list_privileges(
199
- instance: system.PostgreSQLInstance,
206
+ instance: PostgreSQLInstance,
200
207
  name: str,
201
208
  roles: Sequence[str],
202
209
  defaults: bool,
@@ -233,7 +240,7 @@ async def list_privileges(
233
240
  @pass_postgresql_instance
234
241
  @async_command
235
242
  async def run(
236
- instance: system.PostgreSQLInstance,
243
+ instance: PostgreSQLInstance,
237
244
  sql_command: str,
238
245
  dbnames: Sequence[str],
239
246
  exclude_dbnames: Sequence[str],
@@ -268,9 +275,7 @@ async def run(
268
275
  @click.argument("dbname")
269
276
  @pass_postgresql_instance
270
277
  @async_command
271
- async def dump(
272
- instance: system.PostgreSQLInstance, dbname: str, output: Path | None
273
- ) -> None:
278
+ async def dump(instance: PostgreSQLInstance, dbname: str, output: Path | None) -> None:
274
279
  """Dump a database"""
275
280
  async with postgresql.running(instance):
276
281
  await databases.dump(instance, dbname, output)
@@ -282,7 +287,7 @@ async def dump(
282
287
  @pass_postgresql_instance
283
288
  @async_command
284
289
  async def dumps(
285
- instance: system.PostgreSQLInstance,
290
+ instance: PostgreSQLInstance,
286
291
  dbname: Sequence[str],
287
292
  output_format: OutputFormat | None,
288
293
  ) -> None:
@@ -304,7 +309,7 @@ async def dumps(
304
309
  @pass_postgresql_instance
305
310
  @async_command
306
311
  async def restore(
307
- instance: system.PostgreSQLInstance, dump_id: str, targetdbname: str | None
312
+ instance: PostgreSQLInstance, dump_id: str, targetdbname: str | None
308
313
  ) -> None:
309
314
  """Restore a database dump
310
315