pglift-cli 1.3.0__py3-none-any.whl

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.
pglift_cli/util.py ADDED
@@ -0,0 +1,576 @@
1
+ # SPDX-FileCopyrightText: 2021 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ import abc
8
+ import asyncio
9
+ import enum
10
+ import json
11
+ import logging
12
+ import os
13
+ import pathlib
14
+ import tempfile
15
+ import time
16
+ import typing
17
+ from collections.abc import Coroutine, Iterable, Iterator, Sequence
18
+ from contextlib import contextmanager
19
+ from functools import cache, cached_property, singledispatch, wraps
20
+ from typing import Any, Callable, TypeVar
21
+
22
+ import click
23
+ import filelock
24
+ import psycopg
25
+ import pydantic
26
+ import pydantic_core
27
+ import rich
28
+ import rich.prompt
29
+ from click.shell_completion import CompletionItem
30
+ from rich.console import Console
31
+ from rich.table import Table
32
+
33
+ from pglift import exceptions, install, instances, task
34
+ from pglift._compat import ParamSpec
35
+ from pglift.models import helpers, system
36
+ from pglift.settings import Settings
37
+ from pglift.settings._postgresql import PostgreSQLVersion
38
+ from pglift.task import Displayer
39
+ from pglift.types import AutoStrEnum, ByteSizeType
40
+
41
+ from . import _site, model
42
+ from .console import console
43
+
44
+ logger = logging.getLogger("pglift")
45
+
46
+
47
+ def model_dump(
48
+ m: pydantic.BaseModel, by_alias: bool = True, **kwargs: Any
49
+ ) -> dict[str, Any]:
50
+ return m.model_dump(by_alias=by_alias, **kwargs)
51
+
52
+
53
+ @singledispatch
54
+ def prettify(value: Any, annotations: Sequence[Any] = ()) -> str:
55
+ """Prettify a value."""
56
+ return str(value)
57
+
58
+
59
+ @prettify.register(int)
60
+ def _(value: int, annotations: Sequence[Any] = ()) -> str:
61
+ """Prettify an integer value"""
62
+ for a in annotations:
63
+ if isinstance(a, ByteSizeType):
64
+ return a.human_readable(value)
65
+ return str(value)
66
+
67
+
68
+ @prettify.register(list)
69
+ def _(value: list[Any], annotations: Sequence[Any] = ()) -> str:
70
+ """Prettify a List value"""
71
+ return ", ".join(str(x) for x in value)
72
+
73
+
74
+ @prettify.register(set)
75
+ def _(value: set[Any], annotations: Sequence[Any] = ()) -> str:
76
+ """Prettify a Set value"""
77
+ return prettify(sorted(value), annotations)
78
+
79
+
80
+ @prettify.register(type(None))
81
+ def _(value: None, annotations: Sequence[Any] = ()) -> str:
82
+ """Prettify a None value"""
83
+ return ""
84
+
85
+
86
+ @prettify.register(dict)
87
+ def _(value: dict[str, Any], annotations: Sequence[Any] = ()) -> str:
88
+ """Prettify a Dict value"""
89
+
90
+ def prettify_dict(
91
+ d: dict[str, Any], level: int = 0, indent: str = " "
92
+ ) -> Iterator[str]:
93
+ for key, value in d.items():
94
+ row = f"{indent * level}{key}:"
95
+ if isinstance(value, dict):
96
+ yield row
97
+ yield from prettify_dict(value, level + 1)
98
+ else:
99
+ yield row + " " + prettify(value)
100
+
101
+ return "\n".join(prettify_dict(value))
102
+
103
+
104
+ _I = TypeVar("_I")
105
+
106
+
107
+ def print_table_for(
108
+ items: Iterable[_I],
109
+ asdict: Callable[[_I], dict[str, Any]],
110
+ title: str | None = None,
111
+ *,
112
+ console: Console = console,
113
+ **kwargs: Any,
114
+ ) -> None:
115
+ """Render a list of items as a table."""
116
+ table = None
117
+ headers: list[str] = []
118
+ rows = []
119
+ for item in items:
120
+ row = []
121
+ hdr = []
122
+ annotations = typing.get_type_hints(item.__class__, include_extras=True)
123
+ for k, v in asdict(item).items():
124
+ f_annotations = []
125
+ try:
126
+ i_annotations = annotations[k]
127
+ except KeyError:
128
+ pass
129
+ else:
130
+ if args := typing.get_args(i_annotations):
131
+ _, *f_annotations = args
132
+ hdr.append(k)
133
+ row.append(prettify(v, f_annotations))
134
+ if not headers:
135
+ headers = hdr[:]
136
+ rows.append(row)
137
+ if not rows:
138
+ return
139
+ table = Table(*headers, title=title, **kwargs)
140
+ for row in rows:
141
+ table.add_row(*row)
142
+ console.print(table)
143
+
144
+
145
+ def print_json_for(data: Any, *, console: Console = console) -> None:
146
+ """Render `data` as JSON."""
147
+ console.print_json(data=pydantic_core.to_jsonable_python(data))
148
+
149
+
150
+ P = ParamSpec("P")
151
+
152
+
153
+ def print_schema(
154
+ context: click.Context,
155
+ param: click.Parameter,
156
+ value: bool,
157
+ *,
158
+ model: type[pydantic.BaseModel],
159
+ console: Console = console,
160
+ ) -> None:
161
+ """Callback for --schema flag."""
162
+ if value:
163
+ console.print_json(data=model.model_json_schema())
164
+ context.exit()
165
+
166
+
167
+ def print_argspec(
168
+ context: click.Context,
169
+ param: click.Parameter,
170
+ value: bool,
171
+ *,
172
+ model: type[pydantic.BaseModel],
173
+ ) -> None:
174
+ """Callback for --ansible-argspec flag."""
175
+ if value:
176
+ click.echo(
177
+ json.dumps(helpers.argspec_from_model(model), sort_keys=False, indent=2)
178
+ )
179
+ context.exit()
180
+
181
+
182
+ def pass_instance(f: Callable[P, None]) -> Callable[P, None]:
183
+ """Command decorator passing 'instance' bound to click.Context's object."""
184
+
185
+ @wraps(f)
186
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
187
+ context = click.get_current_context()
188
+ instance = context.obj.instance
189
+ assert isinstance(instance, system.Instance), instance
190
+ context.invoke(f, instance, *args, **kwargs)
191
+
192
+ return wrapper
193
+
194
+
195
+ def get_instance(name: str, version: str | None, settings: Settings) -> system.Instance:
196
+ """Return an Instance from name/version, possibly guessing version if unspecified."""
197
+ if version is None:
198
+ found = None
199
+ for version in PostgreSQLVersion:
200
+ try:
201
+ instance = system.Instance.system_lookup((name, version, settings))
202
+ except exceptions.InstanceNotFound:
203
+ logger.debug("instance '%s' not found in version %s", name, version)
204
+ else:
205
+ if found:
206
+ raise click.BadParameter(
207
+ f"instance {name!r} exists in several PostgreSQL versions;"
208
+ " please select version explicitly"
209
+ )
210
+ found = instance
211
+
212
+ if found:
213
+ return found
214
+
215
+ raise click.BadParameter(f"instance {name!r} not found")
216
+
217
+ try:
218
+ return system.Instance.system_lookup((name, version, settings))
219
+ except Exception as e:
220
+ raise click.BadParameter(str(e)) from None
221
+
222
+
223
+ def nameversion_from_id(instance_id: str) -> tuple[str, str | None]:
224
+ version = None
225
+ try:
226
+ version, name = instance_id.split("/", 1)
227
+ except ValueError:
228
+ name = instance_id
229
+ return name, version
230
+
231
+
232
+ def instance_lookup(
233
+ context: click.Context, param: click.Parameter, value: None | str | tuple[str]
234
+ ) -> system.Instance | tuple[system.Instance, ...]:
235
+ """Return one or more system.Instance, possibly guessed if there is only
236
+ one on system, depending on 'param' variadic flag (nargs).
237
+ """
238
+
239
+ settings = _site.SETTINGS
240
+
241
+ def guess() -> tuple[str, str | None]:
242
+ """Return (name, version) of the instance found on system, if there's
243
+ only one, or fail.
244
+ """
245
+ try:
246
+ (i,) = instances.system_list(settings)
247
+ except ValueError:
248
+ raise click.UsageError(
249
+ f"argument {param.get_error_hint(context)} is required."
250
+ ) from None
251
+ return i.name, i.version
252
+
253
+ if context.params.get("all_instances"):
254
+ return tuple(
255
+ get_instance(i.name, i.version, i._settings)
256
+ for i in instances.system_list(settings)
257
+ )
258
+
259
+ if param.nargs == 1:
260
+ if value is None:
261
+ name, version = guess()
262
+ else:
263
+ assert isinstance(value, str)
264
+ name, version = nameversion_from_id(value)
265
+ return get_instance(name, version, settings)
266
+
267
+ elif param.nargs == -1:
268
+ assert isinstance(value, tuple)
269
+ if value:
270
+ return tuple(
271
+ get_instance(*nameversion_from_id(item), settings) for item in value
272
+ )
273
+ else:
274
+ name, version = guess()
275
+ return (get_instance(name, version, settings),)
276
+
277
+ else:
278
+ raise AssertionError(f"unexpected nargs={param.nargs}")
279
+
280
+
281
+ def instance_bind_context(
282
+ context: click.Context, param: click.Parameter, value: str | None
283
+ ) -> None:
284
+ """Bind instance specified as -i/--instance to context's object, possibly
285
+ guessing from available instance if there is only one.
286
+ """
287
+ obj: Obj = context.obj
288
+ version: str | None
289
+ if value is None:
290
+ values = list(instances.system_list(_site.SETTINGS))
291
+ if not values:
292
+ obj._instance = "no instance found; create one first."
293
+ return
294
+ elif len(values) > 1:
295
+ option = param.get_error_hint(context)
296
+ obj._instance = f"several instances found; option {option} is required."
297
+ return
298
+ (i,) = values
299
+ name, version = i.name, i.version
300
+ else:
301
+ name, version = nameversion_from_id(value)
302
+ instance = get_instance(name, version, _site.SETTINGS)
303
+ obj._instance = instance
304
+
305
+
306
+ def _list_instances(
307
+ context: click.Context, param: click.Parameter, incomplete: str
308
+ ) -> list[CompletionItem]:
309
+ """Shell completion function for instance identifier <name> or <version>/<name>."""
310
+ out = []
311
+ iname, iversion = nameversion_from_id(incomplete)
312
+ for i in instances.system_list(_site.SETTINGS):
313
+ if iversion is not None and i.version.startswith(iversion):
314
+ if i.name.startswith(iname):
315
+ out.append(
316
+ CompletionItem(f"{i.version}/{i.name}", help=f"port={i.port}")
317
+ )
318
+ else:
319
+ out.append(CompletionItem(i.version))
320
+ else:
321
+ out.append(
322
+ CompletionItem(i.name, help=f"{i.version}/{i.name} port={i.port}")
323
+ )
324
+ return out
325
+
326
+
327
+ F = TypeVar("F", bound=Callable[..., Any])
328
+
329
+
330
+ def instance_identifier(nargs: int = 1, required: bool = False) -> Callable[[F], F]:
331
+ def decorator(fn: F) -> F:
332
+ command = click.argument(
333
+ "instance",
334
+ nargs=nargs,
335
+ required=required,
336
+ callback=instance_lookup,
337
+ shell_complete=_list_instances,
338
+ )(fn)
339
+ assert command.__doc__
340
+ command.__doc__ += (
341
+ "\n\n INSTANCE identifies target instance as <version>/<name> where the "
342
+ "<version>/ prefix may be omitted if there is only one instance "
343
+ "matching <name>."
344
+ )
345
+ if not required:
346
+ command.__doc__ += " Required if there is more than one instance on system."
347
+ return command
348
+
349
+ return decorator
350
+
351
+
352
+ instance_identifier_option = click.option(
353
+ "-i",
354
+ "--instance",
355
+ "instance",
356
+ metavar="<version>/<name>",
357
+ callback=instance_bind_context,
358
+ shell_complete=_list_instances,
359
+ help=(
360
+ "Instance identifier; the <version>/ prefix may be omitted if "
361
+ "there's only one instance matching <name>. "
362
+ "Required if there is more than one instance on system."
363
+ ),
364
+ )
365
+
366
+
367
+ class OutputFormat(AutoStrEnum):
368
+ """Output format"""
369
+
370
+ json = enum.auto()
371
+
372
+
373
+ output_format_option = click.option(
374
+ "-o",
375
+ "--output-format",
376
+ type=click.Choice(model.choices_from_enum(OutputFormat), case_sensitive=False),
377
+ help="Specify the output format.",
378
+ )
379
+
380
+ dry_run_option = click.option(
381
+ "--dry-run", is_flag=True, help="Only validate input data."
382
+ )
383
+
384
+
385
+ def validate_foreground(
386
+ context: click.Context, param: click.Parameter, value: bool
387
+ ) -> bool:
388
+ if _site.SETTINGS.service_manager == "systemd" and value:
389
+ raise click.BadParameter("cannot be used with systemd")
390
+ return value
391
+
392
+
393
+ foreground_option = click.option(
394
+ "--foreground",
395
+ is_flag=True,
396
+ help="Start the program in foreground.",
397
+ callback=validate_foreground,
398
+ )
399
+
400
+
401
+ @contextmanager
402
+ def command_logging(logdir: pathlib.Path) -> Iterator[None]:
403
+ logdir_created = False
404
+ logfilename = f"{time.time()}.log"
405
+ logfile = logdir / logfilename
406
+ try:
407
+ if not logdir.exists():
408
+ logdir.mkdir(parents=True)
409
+ logdir_created = True
410
+ except OSError:
411
+ # Might be, e.g. PermissionError, if log file path is not writable.
412
+ logfile = pathlib.Path(
413
+ tempfile.NamedTemporaryFile(prefix="pglift", suffix=logfilename).name
414
+ )
415
+ handler = logging.FileHandler(logfile)
416
+ formatter = logging.Formatter(
417
+ fmt="%(levelname)-8s - %(asctime)s - %(name)s:%(filename)s:%(lineno)d - %(message)s",
418
+ datefmt="%Y-%m-%d %H:%M:%S",
419
+ )
420
+ handler.setFormatter(formatter)
421
+ logger.addHandler(handler)
422
+ if logdir_created:
423
+ logger.debug("created log directory %s", logdir)
424
+ logger.debug("logging command at %s", logfile)
425
+ keep_logfile = False
426
+ try:
427
+ yield None
428
+ except (click.Abort, click.ClickException, click.exceptions.Exit):
429
+ raise
430
+ except Exception:
431
+ keep_logfile = True
432
+ logger.exception("an unexpected error occurred")
433
+ raise click.ClickException(
434
+ "an unexpected error occurred, this is probably a bug; "
435
+ f"details can be found at {logfile}"
436
+ ) from None
437
+ finally:
438
+ if not keep_logfile:
439
+ os.unlink(logfile)
440
+ if logdir_created and next(logdir.iterdir(), None) is None:
441
+ logger.debug("removing log directory %s", logdir)
442
+ logdir.rmdir()
443
+
444
+
445
+ class LogDisplayer:
446
+ def handle(self, msg: str) -> None:
447
+ logger.info(msg)
448
+
449
+
450
+ class InteractiveUserInterface:
451
+ """An interactive UI that prompts for confirmation."""
452
+
453
+ def confirm(self, message: str, default: bool) -> bool:
454
+ return rich.prompt.Confirm().ask(
455
+ f"[yellow]>[/yellow] {message}", default=default
456
+ )
457
+
458
+ @cache
459
+ def prompt(self, message: str, hide_input: bool = False) -> str:
460
+ return rich.prompt.Prompt().ask(
461
+ f"[yellow]>[/yellow] {message}", password=hide_input
462
+ )
463
+
464
+
465
+ class Obj:
466
+ """Object bound to click.Context"""
467
+
468
+ # Set in commands taking a -i/--instance option through
469
+ # instance_identifier_option decorator's callback.
470
+ _instance: str | system.Instance
471
+
472
+ def __init__(
473
+ self,
474
+ *,
475
+ displayer: Displayer | None = None,
476
+ debug: bool = False,
477
+ ) -> None:
478
+ self.displayer = displayer
479
+ self.debug = debug
480
+
481
+ @cached_property
482
+ def lock(self) -> filelock.FileLock:
483
+ """Lock to prevent concurrent execution."""
484
+ lockfile = _site.SETTINGS.cli.lock_file
485
+ lockfile.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
486
+ return filelock.FileLock(lockfile, timeout=0)
487
+
488
+ @cached_property
489
+ def instance(self) -> system.Instance:
490
+ if isinstance(self._instance, system.Instance):
491
+ return self._instance
492
+ else:
493
+ assert isinstance(self._instance, str)
494
+ raise click.UsageError(self._instance)
495
+
496
+
497
+ def async_command(
498
+ callback: Callable[P, Coroutine[None, None, None]]
499
+ ) -> Callable[P, None]:
500
+ @wraps(callback)
501
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
502
+ asyncio.run(callback(*args, **kwargs))
503
+
504
+ return wrapper
505
+
506
+
507
+ class Command(click.Command):
508
+ def invoke(self, context: click.Context) -> Any:
509
+ obj: Obj = context.obj
510
+ displayer = obj.displayer
511
+ with command_logging(_site.SETTINGS.cli.logpath):
512
+ try:
513
+ with task.displayer_installed(displayer):
514
+ return super().invoke(context)
515
+ except filelock.Timeout:
516
+ raise click.ClickException("another operation is in progress") from None
517
+ except exceptions.Cancelled as e:
518
+ logger.warning(str(e))
519
+ raise click.Abort from None
520
+ except (exceptions.ValidationError, pydantic.ValidationError) as e:
521
+ raise click.ClickException(str(e)) from None
522
+ except exceptions.Error as e:
523
+ logger.debug("an internal error occurred", exc_info=obj.debug)
524
+ msg = str(e)
525
+ if isinstance(e, exceptions.CommandError):
526
+ if e.stderr:
527
+ msg += f"\n{e.stderr}"
528
+ if e.stdout:
529
+ msg += f"\n{e.stdout}"
530
+ raise click.ClickException(msg) from None
531
+ except psycopg.DatabaseError as e:
532
+ logger.debug("a database error occurred", exc_info=True)
533
+ raise click.ClickException(str(e).strip()) from None
534
+
535
+
536
+ class Group(click.Group):
537
+ command_class = Command
538
+ group_class = type
539
+
540
+ def add_command(self, command: click.Command, name: str | None = None) -> None:
541
+ name = name or command.name
542
+ assert name not in self.commands, f"command {name!r} already registered"
543
+ super().add_command(command, name)
544
+
545
+ def invoke(self, ctx: click.Context) -> Any:
546
+ if not install.check(_site.SETTINGS):
547
+ raise click.ClickException(
548
+ "broken installation; did you run 'site-configure install'?",
549
+ )
550
+ return super().invoke(ctx)
551
+
552
+
553
+ class PluggableCommandGroup(abc.ABC, Group):
554
+ _plugin_commands_loaded: bool = False
555
+
556
+ @abc.abstractmethod
557
+ def register_plugin_commands(self, obj: Obj) -> None: ...
558
+
559
+ def _load_plugins_commands(self, context: click.Context) -> None:
560
+ if self._plugin_commands_loaded:
561
+ return
562
+ obj: Obj | None = context.obj
563
+ if obj is None:
564
+ obj = context.ensure_object(Obj)
565
+ if obj is None:
566
+ return
567
+ self.register_plugin_commands(obj)
568
+ self._plugin_commands_loaded = True
569
+
570
+ def list_commands(self, context: click.Context) -> list[str]:
571
+ self._load_plugins_commands(context)
572
+ return super().list_commands(context)
573
+
574
+ def get_command(self, context: click.Context, name: str) -> click.Command | None:
575
+ self._load_plugins_commands(context)
576
+ return super().get_command(context, name)
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.1
2
+ Name: pglift_cli
3
+ Version: 1.3.0
4
+ Summary: Command-line interface for pglift
5
+ Project-URL: Documentation, https://pglift.readthedocs.io/
6
+ Project-URL: Source, https://gitlab.com/dalibo/pglift/
7
+ Project-URL: Tracker, https://gitlab.com/dalibo/pglift/-/issues/
8
+ Author-email: Dalibo SCOP <contact@dalibo.com>
9
+ License: GPLv3
10
+ Keywords: administration,command-line,deployment,postgresql
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Topic :: Database
22
+ Classifier: Topic :: System :: Systems Administration
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: <4,>=3.9
25
+ Requires-Dist: click!=8.1.0,!=8.1.4,>=8.0.0
26
+ Requires-Dist: filelock!=3.12.1,>=3.9.0
27
+ Requires-Dist: pluggy
28
+ Requires-Dist: psycopg>=3.1
29
+ Requires-Dist: pydantic>=2.5.0
30
+ Requires-Dist: pyyaml>=6.0.1
31
+ Requires-Dist: rich>=11.0.0
32
+ Provides-Extra: dev
33
+ Requires-Dist: pglift-cli[test,typing]; extra == 'dev'
34
+ Provides-Extra: test
35
+ Requires-Dist: anyio; extra == 'test'
36
+ Requires-Dist: patroni[etcd]>=2.1.5; extra == 'test'
37
+ Requires-Dist: port-for; extra == 'test'
38
+ Requires-Dist: prysk[pytest-plugin]>=0.14.0; extra == 'test'
39
+ Requires-Dist: pytest; extra == 'test'
40
+ Requires-Dist: pytest-cov; extra == 'test'
41
+ Requires-Dist: trustme; extra == 'test'
42
+ Provides-Extra: typing
43
+ Requires-Dist: mypy>=1.8.0; extra == 'typing'
44
+ Requires-Dist: types-pyyaml>=6.0.12.10; extra == 'typing'
45
+ Description-Content-Type: text/markdown
46
+
47
+ <!--
48
+ SPDX-FileCopyrightText: 2024 Dalibo
49
+
50
+ SPDX-License-Identifier: GPL-3.0-or-later
51
+ -->
52
+
53
+ This package provides the command-line interface for [pglift][]. It is a
54
+ dependency package and should not be installed directly but rather through the
55
+ ``cli`` *optional dependency* (aka "extra") of the pglift package, e.g.:
56
+
57
+ $ pip install "pglift[cli]"
58
+
59
+ [pglift]: https://pglift.readthedocs.io/
@@ -0,0 +1,24 @@
1
+ pglift_cli/__init__.py,sha256=hgySzF5bjmvicFPVexksIJOMtm2Jlp3XKfZlNs11xs0,143
2
+ pglift_cli/__main__.py,sha256=BcvVDRUWCioA3XaySTcVAC9qP1dyT3nAigzmF4xuSiI,199
3
+ pglift_cli/_settings.py,sha256=ObWR7YyU45BMb6ahXLWbriGegguLND1K5v0QaE4YHLc,1281
4
+ pglift_cli/_site.py,sha256=vMjzi1syv18HDiKrHGrZjUk0Dvk2AB-Ti139SB28Akw,931
5
+ pglift_cli/base.py,sha256=SGawQThhNOmk2RnaCw3QRsusz8Mg0T-0JRjwch5saTQ,1628
6
+ pglift_cli/console.py,sha256=E7qUe4l4XmDSOSjn_0ximqu9vzHniQ6WNswPd0ZtV2w,172
7
+ pglift_cli/database.py,sha256=CEmhsx-K_SP28fZIijVpjLzufBfT91IOjcCXgjeF26M,7484
8
+ pglift_cli/hookspecs.py,sha256=KR3R7DQ5ZZ-9owcS2cGFpgyAXTjc0JZ1CCUI3E5L98o,563
9
+ pglift_cli/instance.py,sha256=lGoRfDtt_bvPPA1YA1jLAxA9ZvnQjA3oeHbwFLKWcb4,13822
10
+ pglift_cli/main.py,sha256=DpOEAJv5kOfhE2g3aBQX2pnsK0QpIRKnnVddvdSWWAo,7990
11
+ pglift_cli/model.py,sha256=al0Kx3b-Ilv9JPFE26vvhCGSR6BQ6KGl9oXgedyceWk,12344
12
+ pglift_cli/patroni.py,sha256=dra7cmlsR2_W2ku_KQXg0bdq5vsujX8vEH2baUAZXuI,878
13
+ pglift_cli/pgconf.py,sha256=_967q_fA5jh6w_hEEnyX3DvidhbNMoi2WUOzCu2WU0M,4879
14
+ pglift_cli/pm.py,sha256=Za8CYTMHHojojmfBUFjYZvC4eQVyxsgFE0zmv4d_Go4,389
15
+ pglift_cli/postgres.py,sha256=VUwjQkh-C1Zw3jg-Wt3J4oY_nGgoP-Q672bQvXB3Y3I,923
16
+ pglift_cli/prometheus.py,sha256=5fKtwwEjat87QDgVY5SzIHyHdDPbB-4VTN6BY1a0nRI,3922
17
+ pglift_cli/role.py,sha256=PtKxB_Sotwpkko7lgpD_pm4E-WrvnNu_-0z3QXZuCB8,5582
18
+ pglift_cli/util.py,sha256=uh-7axokFv1gLComjauqjTPbCjQYEi1lys3Y2Ctj8sA,17754
19
+ pglift_cli/pgbackrest/__init__.py,sha256=q9pSa4qChjo9VBDsRWNCKf9v7BsVgS-90BQcBvQSUl4,3016
20
+ pglift_cli/pgbackrest/repo_path.py,sha256=COoo_gvBQUNk7w23WF9Z5jb6bZQ-MOaY7m7AL03-lfc,1079
21
+ pglift_cli-1.3.0.dist-info/METADATA,sha256=hatNCFHx4DomAwn6AOVAPuy7F3dngyCn4wxjgIwtRxc,2249
22
+ pglift_cli-1.3.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
23
+ pglift_cli-1.3.0.dist-info/entry_points.txt,sha256=jk1c4HAZ3oKjEEwxeBKjrZzqrOZD6_yEEasUvYfJDW8,255
24
+ pglift_cli-1.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.21.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,8 @@
1
+ [console_scripts]
2
+ pglift = pglift_cli.main:cli
3
+
4
+ [pglift]
5
+ pglift_cli.patroni = pglift_cli.patroni
6
+ pglift_cli.pgbackrest = pglift_cli.pgbackrest
7
+ pglift_cli.pgbackrest.repo_path = pglift_cli.pgbackrest.repo_path
8
+ pglift_cli.prometheus = pglift_cli.prometheus