pglift-cli 1.3.1__tar.gz → 1.4.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 (25) hide show
  1. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/PKG-INFO +2 -2
  2. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/database.py +6 -4
  3. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/instance.py +13 -6
  4. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/model.py +13 -5
  5. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/pgbackrest/__init__.py +3 -2
  6. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/prometheus.py +16 -11
  7. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/role.py +6 -4
  8. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/util.py +38 -3
  9. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/.gitignore +0 -0
  10. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/README.md +0 -0
  11. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/hatch.toml +0 -0
  12. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/pyproject.toml +0 -0
  13. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/__init__.py +0 -0
  14. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/__main__.py +0 -0
  15. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/_settings.py +0 -0
  16. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/_site.py +0 -0
  17. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/base.py +0 -0
  18. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/console.py +0 -0
  19. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/hookspecs.py +0 -0
  20. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/main.py +0 -0
  21. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/patroni.py +0 -0
  22. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/pgbackrest/repo_path.py +0 -0
  23. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/pgconf.py +0 -0
  24. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/pm.py +0 -0
  25. {pglift_cli-1.3.1 → pglift_cli-1.4.0}/src/pglift_cli/postgres.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pglift_cli
3
- Version: 1.3.1
3
+ Version: 1.4.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/
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
  import functools
8
8
  from collections.abc import Sequence
9
9
  from pathlib import Path
10
- from typing import IO, Any
10
+ from typing import Any
11
11
 
12
12
  import click
13
13
  import psycopg
@@ -19,11 +19,13 @@ from pglift.models import interface, system
19
19
  from . import model
20
20
  from .util import (
21
21
  Group,
22
+ ManifestData,
22
23
  Obj,
23
24
  OutputFormat,
24
25
  async_command,
25
26
  dry_run_option,
26
27
  instance_identifier_option,
28
+ manifest_option,
27
29
  model_dump,
28
30
  output_format_option,
29
31
  pass_instance,
@@ -93,7 +95,7 @@ async def alter(
93
95
 
94
96
 
95
97
  @cli.command("apply", hidden=True)
96
- @click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
98
+ @manifest_option
97
99
  @output_format_option
98
100
  @dry_run_option
99
101
  @pass_instance
@@ -102,12 +104,12 @@ async def alter(
102
104
  async def apply(
103
105
  obj: Obj,
104
106
  instance: system.Instance,
105
- file: IO[str],
107
+ data: ManifestData,
106
108
  output_format: OutputFormat,
107
109
  dry_run: bool,
108
110
  ) -> None:
109
111
  """Apply manifest as a database"""
110
- database = interface.Database.parse_yaml(file)
112
+ database = interface.Database.model_validate(data)
111
113
  if dry_run:
112
114
  ret = interface.ApplyResult(change_state=None)
113
115
  else:
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
  import os
8
8
  from collections.abc import Sequence
9
9
  from functools import partial
10
- from typing import IO, Any
10
+ from typing import Any
11
11
 
12
12
  import click
13
13
  from pydantic.v1.utils import deep_update
@@ -23,12 +23,13 @@ from pglift import (
23
23
  )
24
24
  from pglift.models import interface, system
25
25
  from pglift.settings._postgresql import PostgreSQLVersion
26
- from pglift.types import Status
26
+ from pglift.types import Status, validation_context
27
27
 
28
28
  from . import _site
29
29
  from . import hookspecs as h
30
30
  from . import model
31
31
  from .util import (
32
+ ManifestData,
32
33
  Obj,
33
34
  OutputFormat,
34
35
  PluggableCommandGroup,
@@ -36,6 +37,7 @@ from .util import (
36
37
  dry_run_option,
37
38
  foreground_option,
38
39
  instance_identifier,
40
+ manifest_option,
39
41
  model_dump,
40
42
  output_format_option,
41
43
  print_argspec,
@@ -121,23 +123,28 @@ async def alter(obj: Obj, instance: system.Instance, **changes: Any) -> None:
121
123
  manifest = await instances._get(instance, status)
122
124
  values = manifest.model_dump(by_alias=True, exclude={"settings"})
123
125
  values = deep_update(values, changes)
124
- altered = _site.INSTANCE_MODEL.model_validate(values)
126
+ with validation_context({"operation": "update"}):
127
+ altered = _site.INSTANCE_MODEL.model_validate(values)
125
128
  await instances.apply(
126
129
  _site.SETTINGS, altered, _is_running=status == Status.running
127
130
  )
128
131
 
129
132
 
130
133
  @cli.command("apply", hidden=True)
131
- @click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
134
+ @manifest_option
132
135
  @output_format_option
133
136
  @dry_run_option
134
137
  @click.pass_obj
135
138
  @async_command
136
139
  async def apply(
137
- obj: Obj, file: IO[str], output_format: OutputFormat, dry_run: bool
140
+ obj: Obj, data: ManifestData, output_format: OutputFormat, dry_run: bool
138
141
  ) -> None:
139
142
  """Apply manifest as a PostgreSQL instance"""
140
- instance = _site.INSTANCE_MODEL.parse_yaml(file)
143
+ name, version = data["name"], data.get("version")
144
+ assert isinstance(version, (str, type(None)))
145
+ op = "update" if instances.exists(name, version, _site.SETTINGS) else "create"
146
+ with validation_context({"operation": op}):
147
+ instance = _site.INSTANCE_MODEL.model_validate(data)
141
148
  if dry_run:
142
149
  ret = interface.InstanceApplyResult(change_state=None)
143
150
  else:
@@ -20,10 +20,15 @@ import pydantic
20
20
  from pydantic.fields import FieldInfo
21
21
  from pydantic.v1.utils import deep_update, lenient_issubclass
22
22
 
23
- from pglift import exceptions
24
23
  from pglift._compat import zip
25
- from pglift.models.helpers import Operation, is_optional, optional_type
26
- from pglift.types import CLIConfig, StrEnum, field_annotation
24
+ from pglift.models.helpers import is_optional, optional_type
25
+ from pglift.types import (
26
+ CLIConfig,
27
+ Operation,
28
+ StrEnum,
29
+ field_annotation,
30
+ validation_context,
31
+ )
27
32
 
28
33
  logger = logging.getLogger(__name__)
29
34
 
@@ -94,7 +99,10 @@ def as_parameters(
94
99
  @functools.wraps(f)
95
100
  def callback(**kwargs: Any) -> Any:
96
101
  args = params_to_modelargs(kwargs)
97
- with catch_validationerror(*paramspecs):
102
+ with (
103
+ catch_validationerror(*paramspecs),
104
+ validation_context({"operation": operation}),
105
+ ):
98
106
  model = parse_params_as(model_type, args)
99
107
  kwargs[model_argname] = model
100
108
  return f(**kwargs)
@@ -336,7 +344,7 @@ def choices_from_enum(e: type[enum.Enum]) -> list[Any]:
336
344
  def catch_validationerror(*paramspec: ParamSpec) -> Iterator[None]:
337
345
  try:
338
346
  yield None
339
- except (exceptions.ValidationError, pydantic.ValidationError) as e:
347
+ except pydantic.ValidationError as e:
340
348
  errors = e.errors()
341
349
  for pspec in paramspec:
342
350
  for err in errors:
@@ -12,8 +12,9 @@ import click
12
12
 
13
13
  from pglift import pgbackrest, postgresql, types
14
14
  from pglift.models import system
15
- from pglift.pgbackrest import base, models
15
+ from pglift.pgbackrest import base
16
16
  from pglift.pgbackrest import register_if as register_if # noqa: F401
17
+ from pglift.pgbackrest.models.system import Service
17
18
 
18
19
  from .. import _site, hookimpl
19
20
  from ..util import (
@@ -43,7 +44,7 @@ def pgbackrest_proxy(
43
44
  context: click.Context, /, command: tuple[str, ...], **kwargs: Any
44
45
  ) -> None:
45
46
  """Proxy to pgbackrest operations on an instance"""
46
- s = context.obj.instance.service(models.Service)
47
+ s = context.obj.instance.service(Service)
47
48
  settings = pgbackrest.get_settings(_site.SETTINGS)
48
49
  cmd_args = base.make_cmd(s.stanza, settings, *command)
49
50
  try:
@@ -5,23 +5,25 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  from functools import partial
8
- from typing import IO
9
8
 
10
9
  import click
11
10
 
12
- from pglift import exceptions, prometheus, task
11
+ from pglift import exceptions, prometheus, task, types
13
12
  from pglift.models import interface
14
- from pglift.prometheus import impl, models
13
+ from pglift.prometheus import impl
15
14
  from pglift.prometheus import register_if as register_if # noqa: F401
15
+ from pglift.prometheus.models.interface import PostgresExporter
16
16
 
17
17
  from . import _site, hookimpl, model
18
18
  from .util import (
19
19
  Group,
20
+ ManifestData,
20
21
  Obj,
21
22
  OutputFormat,
22
23
  async_command,
23
24
  dry_run_option,
24
25
  foreground_option,
26
+ manifest_option,
25
27
  output_format_option,
26
28
  print_argspec,
27
29
  print_json_for,
@@ -33,7 +35,7 @@ from .util import (
33
35
  @click.option(
34
36
  "--schema",
35
37
  is_flag=True,
36
- callback=partial(print_schema, model=models.PostgresExporter),
38
+ callback=partial(print_schema, model=PostgresExporter),
37
39
  expose_value=False,
38
40
  is_eager=True,
39
41
  help="Print the JSON schema of postgres_exporter model and exit.",
@@ -41,7 +43,7 @@ from .util import (
41
43
  @click.option(
42
44
  "--ansible-argspec",
43
45
  is_flag=True,
44
- callback=partial(print_argspec, model=models.PostgresExporter),
46
+ callback=partial(print_argspec, model=PostgresExporter),
45
47
  expose_value=False,
46
48
  is_eager=True,
47
49
  hidden=True,
@@ -52,20 +54,23 @@ def cli() -> None:
52
54
 
53
55
 
54
56
  @cli.command("apply")
55
- @click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
57
+ @manifest_option
56
58
  @output_format_option
57
59
  @dry_run_option
58
60
  @click.pass_obj
59
61
  @async_command
60
62
  async def apply(
61
- obj: Obj, file: IO[str], output_format: OutputFormat, dry_run: bool
63
+ obj: Obj, data: ManifestData, output_format: OutputFormat, dry_run: bool
62
64
  ) -> None:
63
65
  """Apply manifest as a Prometheus postgres_exporter."""
64
- exporter = models.PostgresExporter.parse_yaml(file)
66
+ settings = prometheus.get_settings(_site.SETTINGS)
67
+ name = data["name"]
68
+ op = "update" if impl.exists(name, settings) else "create"
69
+ with types.validation_context({"operation": op}):
70
+ exporter = PostgresExporter.model_validate(data)
65
71
  if dry_run:
66
72
  ret = interface.ApplyResult(change_state=None)
67
73
  else:
68
- settings = prometheus.get_settings(_site.SETTINGS)
69
74
  with obj.lock:
70
75
  ret = await impl.apply(exporter, _site.SETTINGS, settings)
71
76
  if output_format == OutputFormat.json:
@@ -73,10 +78,10 @@ async def apply(
73
78
 
74
79
 
75
80
  @cli.command("install")
76
- @model.as_parameters(models.PostgresExporter, "create")
81
+ @model.as_parameters(PostgresExporter, "create")
77
82
  @click.pass_obj
78
83
  @async_command
79
- async def install(obj: Obj, postgresexporter: models.PostgresExporter) -> None:
84
+ async def install(obj: Obj, postgresexporter: PostgresExporter) -> None:
80
85
  """Install the service for a (non-local) instance."""
81
86
  settings = prometheus.get_settings(_site.SETTINGS)
82
87
  with obj.lock:
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  from collections.abc import Sequence
8
8
  from functools import partial
9
- from typing import IO, Any
9
+ from typing import Any
10
10
 
11
11
  import click
12
12
  from pydantic.v1.utils import deep_update
@@ -17,11 +17,13 @@ from pglift.models import interface, system
17
17
  from . import _site, model
18
18
  from .util import (
19
19
  Group,
20
+ ManifestData,
20
21
  Obj,
21
22
  OutputFormat,
22
23
  async_command,
23
24
  dry_run_option,
24
25
  instance_identifier_option,
26
+ manifest_option,
25
27
  model_dump,
26
28
  output_format_option,
27
29
  pass_instance,
@@ -100,7 +102,7 @@ async def alter(
100
102
 
101
103
 
102
104
  @cli.command("apply", hidden=True)
103
- @click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
105
+ @manifest_option
104
106
  @output_format_option
105
107
  @dry_run_option
106
108
  @pass_instance
@@ -109,12 +111,12 @@ async def alter(
109
111
  async def apply(
110
112
  obj: Obj,
111
113
  instance: system.Instance,
112
- file: IO[str],
114
+ data: ManifestData,
113
115
  output_format: OutputFormat,
114
116
  dry_run: bool,
115
117
  ) -> None:
116
118
  """Apply manifest as a role"""
117
- role = _site.ROLE_MODEL.parse_yaml(file)
119
+ role = _site.ROLE_MODEL.model_validate(data)
118
120
  if dry_run:
119
121
  ret = interface.ApplyResult(change_state=None)
120
122
  else:
@@ -17,7 +17,7 @@ import typing
17
17
  from collections.abc import Coroutine, Iterable, Iterator, Sequence
18
18
  from contextlib import contextmanager
19
19
  from functools import cache, cached_property, singledispatch, wraps
20
- from typing import Any, Callable, TypeVar
20
+ from typing import IO, Any, Callable, TypedDict, TypeVar
21
21
 
22
22
  import click
23
23
  import filelock
@@ -26,6 +26,7 @@ import pydantic
26
26
  import pydantic_core
27
27
  import rich
28
28
  import rich.prompt
29
+ import yaml
29
30
  from click.shell_completion import CompletionItem
30
31
  from rich.console import Console
31
32
  from rich.table import Table
@@ -364,6 +365,35 @@ instance_identifier_option = click.option(
364
365
  )
365
366
 
366
367
 
368
+ def yaml_load(
369
+ ctx: click.Context, param: click.Parameter, value: IO[str]
370
+ ) -> dict[str, Any]:
371
+ try:
372
+ data = yaml.safe_load(value)
373
+ except yaml.YAMLError as e:
374
+ raise click.BadParameter(f"invalid YAML: {e}") from e
375
+ if not isinstance(data, dict):
376
+ raise click.BadParameter(f"invalid YAML: expecting an object, got {type(data)}")
377
+ if "name" not in data:
378
+ raise click.BadParameter("invalid YAML: missing required 'name' field")
379
+ return data
380
+
381
+
382
+ manifest_option = click.option(
383
+ "-f",
384
+ "--file",
385
+ "data",
386
+ type=click.File("r"),
387
+ metavar="MANIFEST",
388
+ required=True,
389
+ callback=yaml_load,
390
+ )
391
+
392
+
393
+ class ManifestData(TypedDict, total=False):
394
+ name: str
395
+
396
+
367
397
  class OutputFormat(AutoStrEnum):
368
398
  """Output format"""
369
399
 
@@ -517,7 +547,7 @@ class Command(click.Command):
517
547
  except exceptions.Cancelled as e:
518
548
  logger.warning(str(e))
519
549
  raise click.Abort from None
520
- except (exceptions.ValidationError, pydantic.ValidationError) as e:
550
+ except pydantic.ValidationError as e:
521
551
  raise click.ClickException(str(e)) from None
522
552
  except exceptions.Error as e:
523
553
  logger.debug("an internal error occurred", exc_info=obj.debug)
@@ -529,7 +559,12 @@ class Command(click.Command):
529
559
  msg += f"\n{e.stdout}"
530
560
  raise click.ClickException(msg) from None
531
561
  except psycopg.DatabaseError as e:
532
- logger.debug("a database error occurred", exc_info=True)
562
+ logger.debug(
563
+ "a database error occurred: %s (SQLSTATE=%s)",
564
+ e,
565
+ e.sqlstate,
566
+ exc_info=obj.debug,
567
+ )
533
568
  raise click.ClickException(str(e).strip()) from None
534
569
 
535
570
 
File without changes
File without changes
File without changes
File without changes