pglift-cli 2.2.0__tar.gz → 2.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.
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/PKG-INFO +1 -1
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/database.py +3 -1
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/instance.py +3 -1
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/model.py +105 -20
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/pgconf.py +3 -4
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/pghba.py +27 -4
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/role.py +10 -2
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/util.py +6 -3
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-cli-walkthrough.t +54 -19
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-help.t +8 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-port-validation.t +6 -6
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-prometheus.t +1 -1
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-standby-pgbackrest.t +2 -2
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/test_audit.py +3 -3
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/test_cli.py +27 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/test_model.py +18 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/.gitignore +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/README.md +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/hatch.toml +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/pyproject.toml +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/pytest.ini +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/__init__.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/__main__.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/_settings.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/_site.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/base.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/console.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/hookspecs.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/main.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/patroni.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/pgbackrest/__init__.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/pgbackrest/repo_path.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/pm.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/postgres.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/prometheus.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/src/pglift_cli/py.typed +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/.gitignore +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-base.t +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-demote.t +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-transactions.t +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/expect/test-upgrade.t +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/__init__.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/conftest.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/test__site.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/test_main.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/test_pm.py +0 -0
- {pglift_cli-2.2.0 → pglift_cli-2.4.0}/tests/unit/test_util.py +0 -0
|
@@ -155,7 +155,9 @@ async def get(
|
|
|
155
155
|
if output_format == "json":
|
|
156
156
|
print_json_for(model_dump(db))
|
|
157
157
|
else:
|
|
158
|
-
print_table_for(
|
|
158
|
+
print_table_for(
|
|
159
|
+
[db], functools.partial(model_dump, context={"pretty": True}), box=None
|
|
160
|
+
)
|
|
159
161
|
|
|
160
162
|
|
|
161
163
|
@cli.command("list")
|
|
@@ -275,7 +275,9 @@ async def get(instance: Instance, output_format: OutputFormat | None) -> None:
|
|
|
275
275
|
if not instance.postgresql.standby:
|
|
276
276
|
exclude.add("standby")
|
|
277
277
|
print_table_for(
|
|
278
|
-
[i],
|
|
278
|
+
[i],
|
|
279
|
+
partial(model_dump, exclude=exclude, context={"pretty": True}),
|
|
280
|
+
box=None,
|
|
279
281
|
)
|
|
280
282
|
|
|
281
283
|
|
|
@@ -17,8 +17,10 @@ import click
|
|
|
17
17
|
import pydantic
|
|
18
18
|
from pydantic.fields import FieldInfo
|
|
19
19
|
from pydantic.v1.utils import deep_update, lenient_issubclass
|
|
20
|
+
from pydantic_core import ErrorDetails
|
|
20
21
|
|
|
21
22
|
from pglift.annotations import cli
|
|
23
|
+
from pglift.exceptions import MutuallyExclusiveError
|
|
22
24
|
from pglift.models.helpers import is_optional, optional_type
|
|
23
25
|
from pglift.models.interface import PresenceState
|
|
24
26
|
from pglift.types import (
|
|
@@ -54,7 +56,7 @@ def as_parameters(model_type: ModelType, operation: Operation) -> ClickDecorator
|
|
|
54
56
|
for modelname, argname in modelnames_and_argnames:
|
|
55
57
|
value = kwargs.pop(argname)
|
|
56
58
|
if value is DEFAULT:
|
|
57
|
-
continue
|
|
59
|
+
continue # ignore unset or set with default value parameters
|
|
58
60
|
args[modelname] = value
|
|
59
61
|
return args
|
|
60
62
|
|
|
@@ -165,7 +167,11 @@ class ParamSpec(ABC):
|
|
|
165
167
|
loc: tuple[str, ...]
|
|
166
168
|
description: str | None = None
|
|
167
169
|
|
|
168
|
-
objtype: ClassVar = click.Parameter
|
|
170
|
+
objtype: ClassVar[type[click.Parameter]] = click.Parameter
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def param(self) -> click.Parameter:
|
|
174
|
+
return self.objtype(self.param_decls, **self.attrs)
|
|
169
175
|
|
|
170
176
|
@property
|
|
171
177
|
@abstractmethod
|
|
@@ -179,9 +185,7 @@ class ParamSpec(ABC):
|
|
|
179
185
|
return self.loc == loc
|
|
180
186
|
|
|
181
187
|
def badparameter_exception(self, message: str) -> click.BadParameter:
|
|
182
|
-
return click.BadParameter(
|
|
183
|
-
message, None, param=self.objtype(self.param_decls, **self.attrs)
|
|
184
|
-
)
|
|
188
|
+
return click.BadParameter(message, None, param=self.param)
|
|
185
189
|
|
|
186
190
|
|
|
187
191
|
class ArgumentSpec(ParamSpec):
|
|
@@ -226,6 +230,50 @@ class _Parent:
|
|
|
226
230
|
required: bool
|
|
227
231
|
|
|
228
232
|
|
|
233
|
+
def is_editable(ftype: Any) -> bool:
|
|
234
|
+
"""Determine whether a given type is considered "editable".
|
|
235
|
+
|
|
236
|
+
A type is considered editable if:
|
|
237
|
+
|
|
238
|
+
- It is a subclass of pydantic.BaseModel.
|
|
239
|
+
- It has a type hint for a field named "state" of type PresenceState.
|
|
240
|
+
|
|
241
|
+
This function also handles types wrapped with typing.Annotated,
|
|
242
|
+
automatically extracting the underlying type for the check.
|
|
243
|
+
|
|
244
|
+
>>> class Editable(pydantic.BaseModel):
|
|
245
|
+
... state: typing.Annotated[PresenceState, object()]
|
|
246
|
+
>>>
|
|
247
|
+
>>> is_editable(Editable)
|
|
248
|
+
True
|
|
249
|
+
>>> class AlsoEditable(pydantic.BaseModel):
|
|
250
|
+
... state: typing.Annotated[typing.Annotated[PresenceState, object()], object()]
|
|
251
|
+
>>>
|
|
252
|
+
>>> is_editable(AlsoEditable)
|
|
253
|
+
True
|
|
254
|
+
>>>
|
|
255
|
+
>>> class EditableNotAnnotated(pydantic.BaseModel):
|
|
256
|
+
... state: PresenceState
|
|
257
|
+
>>>
|
|
258
|
+
>>> is_editable(EditableNotAnnotated)
|
|
259
|
+
True
|
|
260
|
+
>>> class NotEditable(pydantic.BaseModel):
|
|
261
|
+
... state: typing.Annotated[typing.Annotated[str, object()], object()]
|
|
262
|
+
>>>
|
|
263
|
+
>>> is_editable(NotEditable)
|
|
264
|
+
False
|
|
265
|
+
"""
|
|
266
|
+
# Check if there's a "state" field (PresenceState type)
|
|
267
|
+
if typing.get_origin(ftype) is typing.Annotated:
|
|
268
|
+
ftype = typing.get_args(ftype)[0]
|
|
269
|
+
assert ftype is not None
|
|
270
|
+
hints = typing.get_type_hints(ftype)
|
|
271
|
+
return (
|
|
272
|
+
lenient_issubclass(ftype, pydantic.BaseModel)
|
|
273
|
+
and hints.get("state") is PresenceState
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
229
277
|
def _paramspecs_from_model(
|
|
230
278
|
model_type: ModelType,
|
|
231
279
|
operation: Operation,
|
|
@@ -237,11 +285,18 @@ def _paramspecs_from_model(
|
|
|
237
285
|
"""
|
|
238
286
|
|
|
239
287
|
def default(_ctx: click.Context, param: click.Argument, value: Any) -> Any:
|
|
288
|
+
"""This function is intended to distinguish parameters that were
|
|
289
|
+
explicitly provided by the user versus those that were omitted (or
|
|
290
|
+
explicitly provided with value equal to the default).
|
|
291
|
+
|
|
292
|
+
If the parameter value is unset (or equal to the default value), it
|
|
293
|
+
returns DEFAULT instead of the raw value.
|
|
294
|
+
"""
|
|
240
295
|
if (param.multiple and value == ()) or (value == param.default):
|
|
241
296
|
return DEFAULT
|
|
242
297
|
return value
|
|
243
298
|
|
|
244
|
-
def
|
|
299
|
+
def add_state_field_callback(
|
|
245
300
|
ctx: click.Context,
|
|
246
301
|
_param: click.Argument,
|
|
247
302
|
value: Any,
|
|
@@ -250,6 +305,20 @@ def _paramspecs_from_model(
|
|
|
250
305
|
key: str = "name",
|
|
251
306
|
remove: bool = False,
|
|
252
307
|
) -> None:
|
|
308
|
+
"""Callback that appends each value to ctx.params[optname] as a dictionary
|
|
309
|
+
with the specified key and a "state" field, set to either "present" or
|
|
310
|
+
"absent" depending on the remove argument.
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
Example, --add-user alice --add-user bob --remove-user carol will
|
|
314
|
+
result to:
|
|
315
|
+
|
|
316
|
+
ctx.params["user"] = (
|
|
317
|
+
{"name": "alice", "state": "present"},
|
|
318
|
+
{"name": "bob", "state": "present"},
|
|
319
|
+
{"name": "carol", "state": "absent"},
|
|
320
|
+
)
|
|
321
|
+
"""
|
|
253
322
|
if optname not in ctx.params:
|
|
254
323
|
ctx.params[optname] = ()
|
|
255
324
|
ctx.params[optname] += tuple(
|
|
@@ -257,17 +326,6 @@ def _paramspecs_from_model(
|
|
|
257
326
|
)
|
|
258
327
|
return
|
|
259
328
|
|
|
260
|
-
def is_editable(ftype: Any) -> bool:
|
|
261
|
-
# Check if there's a "state" field (PresenceState type)
|
|
262
|
-
if typing.get_origin(ftype) is typing.Annotated:
|
|
263
|
-
ftype = typing.get_args(ftype)[0]
|
|
264
|
-
assert ftype is not None
|
|
265
|
-
return (
|
|
266
|
-
lenient_issubclass(ftype, pydantic.BaseModel)
|
|
267
|
-
and "state" in ftype.model_fields
|
|
268
|
-
and ftype.model_fields["state"].annotation is PresenceState
|
|
269
|
-
)
|
|
270
|
-
|
|
271
329
|
for fname, field in model_type.model_fields.items():
|
|
272
330
|
if field_annotation(field, cli.Hidden):
|
|
273
331
|
continue
|
|
@@ -381,7 +439,7 @@ def _paramspecs_from_model(
|
|
|
381
439
|
field,
|
|
382
440
|
{
|
|
383
441
|
"callback": functools.partial(
|
|
384
|
-
|
|
442
|
+
add_state_field_callback,
|
|
385
443
|
optname=modelname,
|
|
386
444
|
key=config.item_key,
|
|
387
445
|
),
|
|
@@ -399,7 +457,7 @@ def _paramspecs_from_model(
|
|
|
399
457
|
field,
|
|
400
458
|
{
|
|
401
459
|
"callback": functools.partial(
|
|
402
|
-
|
|
460
|
+
add_state_field_callback,
|
|
403
461
|
optname=modelname,
|
|
404
462
|
remove=True,
|
|
405
463
|
key=config.item_key,
|
|
@@ -447,15 +505,42 @@ def _paramspecs_from_model(
|
|
|
447
505
|
)
|
|
448
506
|
|
|
449
507
|
|
|
508
|
+
def fieldname_and_options(*paramspec: ParamSpec) -> dict[str, str]:
|
|
509
|
+
"""Return a mapping between model field name and corresponding CLI options."""
|
|
510
|
+
r: dict[str, str] = {}
|
|
511
|
+
for pspec in paramspec:
|
|
512
|
+
param = pspec.param
|
|
513
|
+
assert param.name
|
|
514
|
+
r[param.name] = ", ".join(param.opts)
|
|
515
|
+
return r
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def format_error_message(error: ErrorDetails, *paramspec: ParamSpec) -> str:
|
|
519
|
+
"""Format templated error message"""
|
|
520
|
+
try:
|
|
521
|
+
ctx_error = error["ctx"]["error"]
|
|
522
|
+
except KeyError:
|
|
523
|
+
return error["msg"]
|
|
524
|
+
if isinstance(ctx_error, MutuallyExclusiveError):
|
|
525
|
+
options = fieldname_and_options(*paramspec)
|
|
526
|
+
return ctx_error.format(options)
|
|
527
|
+
return str(ctx_error)
|
|
528
|
+
|
|
529
|
+
|
|
450
530
|
@contextmanager
|
|
451
531
|
def catch_validationerror(*paramspec: ParamSpec) -> Iterator[None]:
|
|
452
532
|
try:
|
|
453
533
|
yield None
|
|
454
534
|
except pydantic.ValidationError as e:
|
|
455
535
|
errors = e.errors()
|
|
536
|
+
for err in errors:
|
|
537
|
+
if not err.get("loc"):
|
|
538
|
+
raise click.UsageError(format_error_message(err, *paramspec)) from None
|
|
456
539
|
for pspec in paramspec:
|
|
457
540
|
for err in errors:
|
|
458
541
|
if pspec.match_loc(err["loc"]):
|
|
459
|
-
raise pspec.badparameter_exception(
|
|
542
|
+
raise pspec.badparameter_exception(
|
|
543
|
+
format_error_message(err, *paramspec)
|
|
544
|
+
) from None
|
|
460
545
|
logger.debug("a validation error occurred", exc_info=True)
|
|
461
546
|
raise click.ClickException(str(e)) from None
|
|
@@ -10,7 +10,7 @@ from typing import Any
|
|
|
10
10
|
import click
|
|
11
11
|
import pgtoolkit.conf
|
|
12
12
|
|
|
13
|
-
from pglift import
|
|
13
|
+
from pglift import instances, manager, postgresql
|
|
14
14
|
from pglift.models import Instance, PostgreSQLInstance
|
|
15
15
|
from pglift.types import ConfigChanges, Status
|
|
16
16
|
|
|
@@ -153,9 +153,8 @@ async def edit(obj: Obj, instance: Instance) -> None:
|
|
|
153
153
|
"""Edit managed configuration."""
|
|
154
154
|
pg_instance = instance.postgresql
|
|
155
155
|
with obj.lock, audit():
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
156
|
+
with manager.from_instance(pg_instance):
|
|
157
|
+
actual_config = instances.postgresql_editable_conf(pg_instance)
|
|
159
158
|
edited = click.edit(text=actual_config)
|
|
160
159
|
if edited is None:
|
|
161
160
|
click.echo("no change", err=True)
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
|
+
from pgtoolkit.hba import parse as parse_hba
|
|
10
11
|
|
|
11
12
|
from pglift import diff, hba, manager
|
|
12
13
|
from pglift.models import PostgreSQLInstance, interface
|
|
@@ -16,6 +17,7 @@ from .console import console
|
|
|
16
17
|
from .util import (
|
|
17
18
|
Group,
|
|
18
19
|
Obj,
|
|
20
|
+
async_command,
|
|
19
21
|
audit,
|
|
20
22
|
diff_options,
|
|
21
23
|
dry_run_option,
|
|
@@ -38,7 +40,8 @@ def cli(**kwargs: Any) -> None:
|
|
|
38
40
|
@diff_options["unified"]
|
|
39
41
|
@diff_options["ansible"]
|
|
40
42
|
@click.pass_obj
|
|
41
|
-
|
|
43
|
+
@async_command
|
|
44
|
+
async def add(
|
|
42
45
|
obj: Obj,
|
|
43
46
|
instance: PostgreSQLInstance,
|
|
44
47
|
hbarecord: interface.HbaRecord,
|
|
@@ -56,7 +59,7 @@ def add(
|
|
|
56
59
|
system_configure(dry_run=dry_run),
|
|
57
60
|
manager.from_instance(instance),
|
|
58
61
|
):
|
|
59
|
-
hba.add(instance, hbarecord)
|
|
62
|
+
await hba.add(instance, hbarecord)
|
|
60
63
|
if (diffvalue := diff.get()) is not None:
|
|
61
64
|
for diffitem in diffvalue:
|
|
62
65
|
console.print(diffitem)
|
|
@@ -69,7 +72,8 @@ def add(
|
|
|
69
72
|
@diff_options["unified"]
|
|
70
73
|
@diff_options["ansible"]
|
|
71
74
|
@click.pass_obj
|
|
72
|
-
|
|
75
|
+
@async_command
|
|
76
|
+
async def remove(
|
|
73
77
|
obj: Obj,
|
|
74
78
|
instance: PostgreSQLInstance,
|
|
75
79
|
hbarecord: interface.HbaRecord,
|
|
@@ -87,7 +91,26 @@ def remove(
|
|
|
87
91
|
system_configure(dry_run=dry_run),
|
|
88
92
|
manager.from_instance(instance),
|
|
89
93
|
):
|
|
90
|
-
hba.remove(instance, hbarecord)
|
|
94
|
+
await hba.remove(instance, hbarecord)
|
|
91
95
|
if (diffvalue := diff.get()) is not None:
|
|
92
96
|
for diffitem in diffvalue:
|
|
93
97
|
console.print(diffitem)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@cli.command("edit")
|
|
101
|
+
@pass_postgresql_instance
|
|
102
|
+
@click.pass_obj
|
|
103
|
+
@async_command
|
|
104
|
+
async def edit(obj: Obj, instance: PostgreSQLInstance) -> None:
|
|
105
|
+
"""Edit managed HBA records."""
|
|
106
|
+
with obj.lock, audit():
|
|
107
|
+
with manager.from_instance(instance):
|
|
108
|
+
hba_ = await hba.get(instance)
|
|
109
|
+
actual_hba = "\n".join([str(r) for r in hba_.lines])
|
|
110
|
+
edited = click.edit(text=actual_hba)
|
|
111
|
+
if edited is None:
|
|
112
|
+
click.echo("no change", err=True)
|
|
113
|
+
return
|
|
114
|
+
entries = parse_hba(edited.splitlines())
|
|
115
|
+
with manager.from_instance(instance):
|
|
116
|
+
await hba.save(instance, entries, reload_on_change=True)
|
|
@@ -170,7 +170,11 @@ async def ls(instance: PostgreSQLInstance, output_format: OutputFormat | None) -
|
|
|
170
170
|
else:
|
|
171
171
|
print_table_for(
|
|
172
172
|
rls,
|
|
173
|
-
partial(
|
|
173
|
+
partial(
|
|
174
|
+
model_dump,
|
|
175
|
+
context={"pretty": True},
|
|
176
|
+
exclude={"hba_records", "validity"},
|
|
177
|
+
),
|
|
174
178
|
)
|
|
175
179
|
|
|
176
180
|
|
|
@@ -191,7 +195,11 @@ async def get(
|
|
|
191
195
|
else:
|
|
192
196
|
print_table_for(
|
|
193
197
|
[r],
|
|
194
|
-
partial(
|
|
198
|
+
partial(
|
|
199
|
+
model_dump,
|
|
200
|
+
context={"pretty": True},
|
|
201
|
+
exclude={"hba_records", "validity"},
|
|
202
|
+
),
|
|
195
203
|
box=None,
|
|
196
204
|
)
|
|
197
205
|
|
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import abc
|
|
8
8
|
import asyncio
|
|
9
|
+
import inspect
|
|
9
10
|
import json
|
|
10
11
|
import logging
|
|
11
12
|
import logging.handlers
|
|
@@ -390,12 +391,14 @@ def _instance_identifier(
|
|
|
390
391
|
callback=callback,
|
|
391
392
|
shell_complete=_list_instances,
|
|
392
393
|
)(fn)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
394
|
+
doc = inspect.getdoc(command)
|
|
395
|
+
assert doc
|
|
396
|
+
doc += (
|
|
397
|
+
"\n\nINSTANCE identifies target instance as <version>/<name> where the "
|
|
396
398
|
"<version>/ prefix may be omitted if there is only one instance "
|
|
397
399
|
"matching <name>."
|
|
398
400
|
)
|
|
401
|
+
command.__doc__ = doc
|
|
399
402
|
if not required:
|
|
400
403
|
command.__doc__ += " Required if there is more than one instance on system."
|
|
401
404
|
return command
|
|
@@ -167,7 +167,7 @@ Error cases for instance operations
|
|
|
167
167
|
Usage: pglift instance create [OPTIONS] NAME
|
|
168
168
|
Try 'pglift instance create --help' for help.
|
|
169
169
|
|
|
170
|
-
Error: Invalid value for '--standby-for':
|
|
170
|
+
Error: Invalid value for '--standby-for': missing "=" after "port" in connection info string
|
|
171
171
|
|
|
172
172
|
[2]
|
|
173
173
|
$ pglift instance apply
|
|
@@ -197,11 +197,11 @@ Error cases for instance operations
|
|
|
197
197
|
|
|
198
198
|
$ grep 'pglift_cli.audit' $TMPDIR/pglift-audit.log
|
|
199
199
|
INFO - pglift_cli.audit - command: .*\/pglift --non-interactive site-configure install (re)
|
|
200
|
-
INFO - pglift_cli.audit - command completed \(\d
|
|
200
|
+
INFO - pglift_cli.audit - command completed \(\d+(\.\d+)? seconds\) (re)
|
|
201
201
|
INFO - pglift_cli.audit - command: .*\/pglift --non-interactive instance create main --data-checksums --auth-host=ident --port=\d+ --surole-password=s3per --pgbackrest-stanza=main '--pgbackrest-password=b@ck up!' --prometheus-port=\d+ (re)
|
|
202
|
-
INFO - pglift_cli.audit - command completed \(\d
|
|
202
|
+
INFO - pglift_cli.audit - command completed \(\d+(\.\d+)? seconds\) (re)
|
|
203
203
|
INFO - pglift_cli.audit - command: .*\/pglift --non-interactive instance create main --pgbackrest-stanza=st --surole-password=s3per (re)
|
|
204
|
-
ERROR - pglift_cli.audit - command failed \(\d
|
|
204
|
+
ERROR - pglift_cli.audit - command failed \(\d+(\.\d+)? seconds\) (re)
|
|
205
205
|
|
|
206
206
|
List instances
|
|
207
207
|
|
|
@@ -250,8 +250,8 @@ Stop, alter, (re)start an instance:
|
|
|
250
250
|
--- $TMPDIR/etc/prometheus/postgres_exporter-1*-main.conf (glob)
|
|
251
251
|
+++ $TMPDIR/etc/prometheus/postgres_exporter-1*-main.conf (glob)
|
|
252
252
|
@@ -1,2 +1,2 @@
|
|
253
|
-
DATA_SOURCE_NAME=postgresql://prometheus@:*/postgres?host
|
|
254
|
-
|
|
253
|
+
DATA_SOURCE_NAME=postgresql://prometheus@:*/postgres?host=* (glob)
|
|
254
|
+
*%2Fpostgresql&sslmode=disable (glob)
|
|
255
255
|
-POSTGRES_EXPORTER_OPTS='--web.listen-address :\d+ --log.level info' (re)
|
|
256
256
|
\+POSTGRES_EXPORTER_OPTS='--web.listen-address :\d+ --log.level info' (re)
|
|
257
257
|
--- /dev/null
|
|
@@ -294,7 +294,7 @@ Stop, alter, (re)start an instance:
|
|
|
294
294
|
INFO reloading PostgreSQL configuration for 1\d\/main (re)
|
|
295
295
|
INFO starting Prometheus postgres_exporter 1\d-main (re)
|
|
296
296
|
INFO creating role 'arole'
|
|
297
|
-
INFO
|
|
297
|
+
INFO HBA configuration updated
|
|
298
298
|
INFO reloading PostgreSQL configuration for 1\d\/main (re)
|
|
299
299
|
--- $TMPDIR/srv/pgsql/1*/main/data/postgresql.conf (glob)
|
|
300
300
|
+++ $TMPDIR/srv/pgsql/1*/main/data/postgresql.conf (glob)
|
|
@@ -391,8 +391,11 @@ PostgreSQL configuration
|
|
|
391
391
|
port: \d+ -> 1234 (re)
|
|
392
392
|
DRY RUN: no changes made
|
|
393
393
|
|
|
394
|
-
|
|
394
|
+
HBA configuration management:
|
|
395
395
|
|
|
396
|
+
$ version=$(pglift instance list -o json | jq -r '.[] | .version')
|
|
397
|
+
$ # Adding a comment in the file
|
|
398
|
+
$ printf "\n# a comment" >> $TMPDIR/srv/pgsql/${version}/main/data/pg_hba.conf
|
|
396
399
|
$ pglift pghba add --user bob --method trust
|
|
397
400
|
INFO entry added to HBA configuration
|
|
398
401
|
$ pglift pghba add --dry-run --diff --user bob --method trust \
|
|
@@ -401,8 +404,8 @@ pg_hba.conf management:
|
|
|
401
404
|
--- $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
|
|
402
405
|
+++ $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
|
|
403
406
|
@@ -*,3 +*,4 @@ (glob)
|
|
404
|
-
host all all ::1/128 ident
|
|
405
407
|
local adb arole trust
|
|
408
|
+
# a comment
|
|
406
409
|
local all bob trust
|
|
407
410
|
+host all bob 127.0.0.1 trust
|
|
408
411
|
DRY RUN: no changes made
|
|
@@ -428,7 +431,6 @@ pg_hba.conf management:
|
|
|
428
431
|
> --connection-address 192.168.12.10/32 \
|
|
429
432
|
> --database mybd,myotherdb
|
|
430
433
|
INFO entry added to HBA configuration
|
|
431
|
-
$ version=$(pglift instance list -o json | jq -r '.[] | .version')
|
|
432
434
|
$ grep 'bob' $TMPDIR/srv/pgsql/${version}/main/data/pg_hba.conf
|
|
433
435
|
local all bob trust
|
|
434
436
|
host all bob 127.0.0.1 trust
|
|
@@ -446,7 +448,7 @@ pg_hba.conf management:
|
|
|
446
448
|
--- $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
|
|
447
449
|
+++ $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
|
|
448
450
|
@@ -*,5 +*,4 @@ (glob)
|
|
449
|
-
|
|
451
|
+
# a comment
|
|
450
452
|
local all bob trust
|
|
451
453
|
host all bob 127.0.0.1 trust
|
|
452
454
|
-host myotherdb bob 192.168.12.10 255.255.255.255 trust
|
|
@@ -463,7 +465,7 @@ pg_hba.conf management:
|
|
|
463
465
|
--- $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
|
|
464
466
|
+++ $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
|
|
465
467
|
@@ -*,4 +*,3 @@ (glob)
|
|
466
|
-
|
|
468
|
+
# a comment
|
|
467
469
|
local all bob trust
|
|
468
470
|
host all bob 127.0.0.1 trust
|
|
469
471
|
-host mybd,myotherdb bob,peter 192.168.12.10/32 trust
|
|
@@ -531,6 +533,13 @@ Add and manipulate roles:
|
|
|
531
533
|
INFO removing now empty $TMPDIR/.pgpass
|
|
532
534
|
DRY RUN: no changes made
|
|
533
535
|
|
|
536
|
+
$ pglift role -i main create dba --password=qwerty --encrypted-password=md5azerty
|
|
537
|
+
Usage: pglift role create [OPTIONS] NAME
|
|
538
|
+
Try 'pglift role create --help' for help.
|
|
539
|
+
|
|
540
|
+
Error: '--password' and '--encrypted-password' can't be used together
|
|
541
|
+
[2]
|
|
542
|
+
|
|
534
543
|
$ pglift role alter dba --connection-limit=10 --inherit --no-pgpass --no-login --revoke=pg_read_all_stats --grant=pg_monitor --valid-until=2026-01-01
|
|
535
544
|
INFO altering role 'dba'
|
|
536
545
|
INFO removing entry for 'dba' in \$TMPDIR\/.pgpass \(port=\d+\) (re)
|
|
@@ -583,7 +592,7 @@ Add and manipulate roles:
|
|
|
583
592
|
> EOF
|
|
584
593
|
$ pglift role apply -f $TMPDIR/role.yaml --dry-run
|
|
585
594
|
INFO creating role 'test'
|
|
586
|
-
INFO
|
|
595
|
+
INFO HBA configuration updated
|
|
587
596
|
INFO reloading PostgreSQL configuration for 1\d\/main (re)
|
|
588
597
|
DRY RUN: no changes made
|
|
589
598
|
$ grep 'test' $TMPDIR/srv/pgsql/${version}/main/data/pg_hba.conf
|
|
@@ -593,12 +602,12 @@ Add and manipulate roles:
|
|
|
593
602
|
[1]
|
|
594
603
|
$ pglift role apply -f $TMPDIR/role.yaml --diff
|
|
595
604
|
INFO creating role 'test'
|
|
596
|
-
INFO
|
|
605
|
+
INFO HBA configuration updated
|
|
597
606
|
INFO reloading PostgreSQL configuration for 1\d\/main (re)
|
|
598
607
|
--- $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
|
|
599
608
|
+++ $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
|
|
600
609
|
\@\@ -\d+,3 \+\d+,7 \@\@ (re)
|
|
601
|
-
|
|
610
|
+
# a comment
|
|
602
611
|
local all bob trust
|
|
603
612
|
host all bob 127.0.0.1 trust
|
|
604
613
|
+local mydb test trust
|
|
@@ -1102,7 +1111,7 @@ Databases
|
|
|
1102
1111
|
|
|
1103
1112
|
$ pglift role drop test --dry-run
|
|
1104
1113
|
INFO dropping role 'test'
|
|
1105
|
-
INFO removing entries from
|
|
1114
|
+
INFO removing entries from HBA configuration
|
|
1106
1115
|
DRY RUN: no changes made
|
|
1107
1116
|
$ pglift role drop test
|
|
1108
1117
|
INFO dropping role 'test'
|
|
@@ -1113,15 +1122,41 @@ Databases
|
|
|
1113
1122
|
Usage: pglift role drop [OPTIONS] NAME
|
|
1114
1123
|
Try 'pglift role drop --help' for help.
|
|
1115
1124
|
|
|
1116
|
-
Error:
|
|
1125
|
+
Error: '--drop-owned' and '--reassign-owned' can't be used together
|
|
1117
1126
|
[2]
|
|
1118
1127
|
$ pglift role drop test --drop-owned --dry-run
|
|
1119
1128
|
INFO dropping role 'test'
|
|
1120
|
-
INFO removing entries from
|
|
1129
|
+
INFO removing entries from HBA configuration
|
|
1121
1130
|
DRY RUN: no changes made
|
|
1122
1131
|
$ pglift role drop test --drop-owned
|
|
1123
1132
|
INFO dropping role 'test'
|
|
1124
|
-
INFO removing entries from
|
|
1133
|
+
INFO removing entries from HBA configuration
|
|
1134
|
+
$ pglift role create doctor
|
|
1135
|
+
INFO creating role 'doctor'
|
|
1136
|
+
$ pglift database create tardis_db --owner doctor
|
|
1137
|
+
INFO creating 'tardis_db' database in 1\d/main (re)
|
|
1138
|
+
$ pglift database get tardis_db --output-format=json | jq -r .owner
|
|
1139
|
+
doctor
|
|
1140
|
+
$ pglift role drop doctor --reassign-owned=postgres
|
|
1141
|
+
INFO dropping role 'doctor'
|
|
1142
|
+
$ pglift database get tardis_db --output-format=json | jq -r .owner
|
|
1143
|
+
postgres
|
|
1144
|
+
$ pglift role create who
|
|
1145
|
+
INFO creating role 'who'
|
|
1146
|
+
$ pglift database create unit_db --owner who
|
|
1147
|
+
INFO creating 'unit_db' database in 1\d/main (re)
|
|
1148
|
+
$ pglift database get unit_db --output-format=json | jq -r .owner
|
|
1149
|
+
who
|
|
1150
|
+
$ pglift role drop who --drop-owned --reassign-owned=postgres
|
|
1151
|
+
Usage: pglift role drop [OPTIONS] NAME
|
|
1152
|
+
Try 'pglift role drop --help' for help.
|
|
1153
|
+
|
|
1154
|
+
Error: '--drop-owned' and '--reassign-owned' can't be used together
|
|
1155
|
+
[2]
|
|
1156
|
+
$ pglift role drop who --no-drop-owned --reassign-owned=postgres
|
|
1157
|
+
INFO dropping role 'who'
|
|
1158
|
+
$ pglift database get unit_db --output-format=json | jq -r .owner
|
|
1159
|
+
postgres
|
|
1125
1160
|
|
|
1126
1161
|
Profiles
|
|
1127
1162
|
$ pglift role -i main create dba1 --password mySup3rS3cr3t1377 --login
|
|
@@ -920,6 +920,7 @@ pg_hba.conf management commands
|
|
|
920
920
|
|
|
921
921
|
Commands:
|
|
922
922
|
add Add a record in HBA configuration.
|
|
923
|
+
edit Edit managed HBA records.
|
|
923
924
|
remove Remove a record from HBA configuration.
|
|
924
925
|
$ pglift pghba add --help
|
|
925
926
|
Usage: pglift pghba add [OPTIONS]
|
|
@@ -967,6 +968,13 @@ pg_hba.conf management commands
|
|
|
967
968
|
--diff Include differences resulting from applied
|
|
968
969
|
changes in returned result.
|
|
969
970
|
--help Show this message and exit.
|
|
971
|
+
$ pglift pghba edit --help
|
|
972
|
+
Usage: pglift pghba edit [OPTIONS]
|
|
973
|
+
|
|
974
|
+
Edit managed HBA records.
|
|
975
|
+
|
|
976
|
+
Options:
|
|
977
|
+
--help Show this message and exit.
|
|
970
978
|
|
|
971
979
|
Patroni commands:
|
|
972
980
|
|
|
@@ -60,19 +60,19 @@ With custom ports
|
|
|
60
60
|
Usage: pglift instance create [OPTIONS] NAME
|
|
61
61
|
Try 'pglift instance create --help' for help.
|
|
62
62
|
|
|
63
|
-
Error: Invalid value for '--prometheus-port':
|
|
63
|
+
Error: Invalid value for '--prometheus-port': port \d+ already in use (re)
|
|
64
64
|
[2]
|
|
65
65
|
$ pglift instance create other --port=$PG1PORT --prometheus-port=$PGE2PORT
|
|
66
66
|
Usage: pglift instance create [OPTIONS] NAME
|
|
67
67
|
Try 'pglift instance create --help' for help.
|
|
68
68
|
|
|
69
|
-
Error: Invalid value for '--port':
|
|
69
|
+
Error: Invalid value for '--port': port \d+ already in use (re)
|
|
70
70
|
[2]
|
|
71
71
|
$ pglift instance create other --port=$PG1PORT --prometheus-port=$PGE1PORT
|
|
72
72
|
Usage: pglift instance create [OPTIONS] NAME
|
|
73
73
|
Try 'pglift instance create --help' for help.
|
|
74
74
|
|
|
75
|
-
Error: Invalid value for '--prometheus-port':
|
|
75
|
+
Error: Invalid value for '--prometheus-port': port \d+ already in use (re)
|
|
76
76
|
[2]
|
|
77
77
|
$ pglift instance drop main
|
|
78
78
|
INFO dropping instance 1\d\/main (re)
|
|
@@ -102,13 +102,13 @@ With a port set in postgresql.conf template
|
|
|
102
102
|
Usage: pglift instance create [OPTIONS] NAME
|
|
103
103
|
Try 'pglift instance create --help' for help.
|
|
104
104
|
|
|
105
|
-
Error: Invalid value for '--port':
|
|
105
|
+
Error: Invalid value for '--port': port \d+ already in use (re)
|
|
106
106
|
[2]
|
|
107
107
|
$ pglift instance create other --port=$PG2PORT --prometheus-port=$PGE1PORT
|
|
108
108
|
Usage: pglift instance create [OPTIONS] NAME
|
|
109
109
|
Try 'pglift instance create --help' for help.
|
|
110
110
|
|
|
111
|
-
Error: Invalid value for '--prometheus-port':
|
|
111
|
+
Error: Invalid value for '--prometheus-port': port \d+ already in use (re)
|
|
112
112
|
[2]
|
|
113
113
|
|
|
114
114
|
$ pglift instance drop main
|
|
@@ -134,7 +134,7 @@ With default ports
|
|
|
134
134
|
Usage: pglift instance create [OPTIONS] NAME
|
|
135
135
|
Try 'pglift instance create --help' for help.
|
|
136
136
|
|
|
137
|
-
Error: Invalid value for '--port':
|
|
137
|
+
Error: Invalid value for '--port': port 5432 already in use
|
|
138
138
|
[2]
|
|
139
139
|
|
|
140
140
|
$ pglift instance drop main
|
|
@@ -91,7 +91,7 @@ Check port conflicts
|
|
|
91
91
|
Usage: pglift postgres_exporter install [OPTIONS] NAME DSN PORT
|
|
92
92
|
Try 'pglift postgres_exporter install --help' for help.
|
|
93
93
|
|
|
94
|
-
Error: Invalid value for 'PORT':
|
|
94
|
+
Error: Invalid value for 'PORT': port \d+ already in use (re)
|
|
95
95
|
[2]
|
|
96
96
|
|
|
97
97
|
$ pglift postgres_exporter uninstall test
|
|
@@ -146,7 +146,7 @@ Cannot create a standby with --slot option
|
|
|
146
146
|
Usage: pglift instance create [OPTIONS] NAME
|
|
147
147
|
Try 'pglift instance create --help' for help.
|
|
148
148
|
|
|
149
|
-
Error: Invalid value for '--slot':
|
|
149
|
+
Error: Invalid value for '--slot': replication slots cannot be set on a standby instance
|
|
150
150
|
[2]
|
|
151
151
|
|
|
152
152
|
|
|
@@ -187,7 +187,7 @@ Try to create primary instance with same stanza
|
|
|
187
187
|
Usage: pglift instance create [OPTIONS] NAME
|
|
188
188
|
Try 'pglift instance create --help' for help.
|
|
189
189
|
|
|
190
|
-
Error: Invalid value for '--pgbackrest-stanza':
|
|
190
|
+
Error: Invalid value for '--pgbackrest-stanza': Stanza 'app' already bound to another instance \(datadir=\$TMPDIR\/1\/srv\/pgsql\/1\d\/pg1\/data\) (re)
|
|
191
191
|
[2]
|
|
192
192
|
|
|
193
193
|
Add some data to the primary, check replication
|
|
@@ -36,7 +36,7 @@ def test_audit(tmp_path: Path) -> None:
|
|
|
36
36
|
assert logf.read().splitlines() == [
|
|
37
37
|
"INFO:pglift_cli.audit command: test error",
|
|
38
38
|
"ERROR:pglift oups",
|
|
39
|
-
"ERROR:pglift_cli.audit command failed (0
|
|
39
|
+
"ERROR:pglift_cli.audit command failed (0 seconds)",
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
with audit(["test", "ok"], settings, dry_run=True):
|
|
@@ -44,7 +44,7 @@ def test_audit(tmp_path: Path) -> None:
|
|
|
44
44
|
assert logf.read().splitlines() == [
|
|
45
45
|
"INFO:pglift_cli.audit command: test ok (DRY RUN)",
|
|
46
46
|
"DEBUG:pglift running",
|
|
47
|
-
"INFO:pglift_cli.audit command completed (0
|
|
47
|
+
"INFO:pglift_cli.audit command completed (0 seconds)",
|
|
48
48
|
]
|
|
49
49
|
|
|
50
50
|
with pytest.raises(exceptions.Cancelled):
|
|
@@ -54,5 +54,5 @@ def test_audit(tmp_path: Path) -> None:
|
|
|
54
54
|
assert logf.read().splitlines() == [
|
|
55
55
|
"INFO:pglift_cli.audit command: test cancel",
|
|
56
56
|
"INFO:pglift trying",
|
|
57
|
-
"WARNING:pglift_cli.audit command cancelled (0
|
|
57
|
+
"WARNING:pglift_cli.audit command cancelled (0 seconds)",
|
|
58
58
|
]
|
|
@@ -16,6 +16,8 @@ import pytest
|
|
|
16
16
|
import yaml
|
|
17
17
|
from click.shell_completion import ShellComplete
|
|
18
18
|
from click.testing import CliRunner
|
|
19
|
+
from pgtoolkit.hba import HBARecord
|
|
20
|
+
from pgtoolkit.hba import parse as parse_hba
|
|
19
21
|
from rich.console import ConsoleDimensions
|
|
20
22
|
|
|
21
23
|
from pglift import instances, postgresql
|
|
@@ -499,6 +501,31 @@ def test_pgconf_edit(
|
|
|
499
501
|
assert result.stderr == "bonjour: on -> 'matin\n"
|
|
500
502
|
|
|
501
503
|
|
|
504
|
+
@pytest.mark.usefixtures("installed")
|
|
505
|
+
def test_pghba_edit(runner: CliRunner, obj: Obj, instance: Instance) -> None:
|
|
506
|
+
hba_r = HBARecord(
|
|
507
|
+
conntype="local", user="santaclaus", database="list", method="md5"
|
|
508
|
+
)
|
|
509
|
+
hba_f = postgresql.hba_path(instance.postgresql)
|
|
510
|
+
assert hba_r not in parse_hba(hba_f)
|
|
511
|
+
with (
|
|
512
|
+
patch("click.edit", return_value=str(hba_r), autospec=True),
|
|
513
|
+
# Unfortunately we patch postgresql.is_running(), because we can't
|
|
514
|
+
# inject a custom (fake) instance manager here. Indeed pytest and the
|
|
515
|
+
# pglift CLI do not share the same event loop. Since the instance
|
|
516
|
+
# manager is passed via a ContextVar, and ContextVars are local to their
|
|
517
|
+
# event loop, it is not accessible across that "boundary".
|
|
518
|
+
patch.object(postgresql, "is_running", return_value=False),
|
|
519
|
+
):
|
|
520
|
+
result = runner.invoke(
|
|
521
|
+
cli,
|
|
522
|
+
["pghba", f"--instance={instance}", "edit"],
|
|
523
|
+
obj=obj,
|
|
524
|
+
)
|
|
525
|
+
assert result.exit_code == 0, result.stderr
|
|
526
|
+
assert hba_r in parse_hba(hba_f)
|
|
527
|
+
|
|
528
|
+
|
|
502
529
|
@pytest.mark.usefixtures("installed")
|
|
503
530
|
def test_pgconf_edit_no_change(
|
|
504
531
|
runner: CliRunner, obj: Obj, instance: Instance, postgresql_conf: str
|
|
@@ -87,6 +87,7 @@ def test_as_parameters(runner: CliRunner) -> None:
|
|
|
87
87
|
" --address-coords-lat LAT Latitude.\n"
|
|
88
88
|
" --birth-date DATE Date of birth. [required]\n"
|
|
89
89
|
" --birth-place PLACE Place of birth.\n"
|
|
90
|
+
" --is-dead / --no-is-dead Is dead.\n"
|
|
90
91
|
" --phone-numbers PHONE_NUMBERS Phone numbers. (Can be used multiple times.)\n"
|
|
91
92
|
" --pet PET Owned pets. (Can be used multiple times.)\n"
|
|
92
93
|
" --member-of GROUP Groups the person is a member of. (Can be used\n"
|
|
@@ -128,6 +129,7 @@ def test_as_parameters(runner: CliRunner) -> None:
|
|
|
128
129
|
},
|
|
129
130
|
"age": 42,
|
|
130
131
|
"birth": {"date": "1981-02-18"},
|
|
132
|
+
"is_dead": False,
|
|
131
133
|
"name": "alice",
|
|
132
134
|
"nickname": "**********",
|
|
133
135
|
"relation": "friend",
|
|
@@ -151,6 +153,21 @@ def test_as_parameters(runner: CliRunner) -> None:
|
|
|
151
153
|
in result.stderr
|
|
152
154
|
)
|
|
153
155
|
|
|
156
|
+
result = runner.invoke(
|
|
157
|
+
add_person,
|
|
158
|
+
[
|
|
159
|
+
"foo",
|
|
160
|
+
"friend",
|
|
161
|
+
"--age=17",
|
|
162
|
+
"--birth-date=1987-06-05",
|
|
163
|
+
"--nickname=aaa",
|
|
164
|
+
"--is-dead",
|
|
165
|
+
],
|
|
166
|
+
)
|
|
167
|
+
assert result.exit_code == 2
|
|
168
|
+
assert "--is-dead' and '--age' can't be used together" in result.stderr
|
|
169
|
+
assert "For further information visit" not in result.stderr
|
|
170
|
+
|
|
154
171
|
|
|
155
172
|
def test_as_parameters_update() -> None:
|
|
156
173
|
@click.command("update-person")
|
|
@@ -179,6 +196,7 @@ def test_as_parameters_update() -> None:
|
|
|
179
196
|
" --address-coords-long LONG Longitude.\n"
|
|
180
197
|
" --address-coords-lat LAT Latitude.\n"
|
|
181
198
|
" --birth-date DATE Date of birth. [required]\n"
|
|
199
|
+
" --is-dead / --no-is-dead Is dead.\n"
|
|
182
200
|
" --add-pet PET Add pet. (Can be used multiple times.)\n"
|
|
183
201
|
" --remove-pet PET Remove pet. (Can be used multiple times.)\n"
|
|
184
202
|
" --add-to GROUP Add to group. (Can be used multiple times.)\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|