pyinfra 3.6__py3-none-any.whl → 3.6.1__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.
pyinfra/api/arguments.py CHANGED
@@ -70,12 +70,12 @@ class ConnectorArguments(TypedDict, total=False):
70
70
  _success_exit_codes: Iterable[int]
71
71
  _timeout: int
72
72
  _get_pty: bool
73
- _stdin: Union[str, Iterable[str]]
73
+ _stdin: Union[str, list[str], Iterable[str]]
74
74
 
75
75
  # Retry arguments
76
76
  _retries: int
77
77
  _retry_delay: Union[int, float]
78
- _retry_until: Optional[Callable[[dict], bool]]
78
+ _retry_until: Callable[[dict], bool]
79
79
 
80
80
  # Temp directory argument
81
81
  _temp_dir: str
@@ -1,16 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import (
4
- TYPE_CHECKING,
5
- Callable,
6
- Generator,
7
- Generic,
8
- Iterable,
9
- List,
10
- Mapping,
11
- Optional,
12
- Union,
13
- )
3
+ from typing import TYPE_CHECKING, Callable, Generator, Generic, Iterable, List, Mapping, Union
14
4
 
15
5
  from typing_extensions import ParamSpec, Protocol
16
6
 
@@ -36,36 +26,41 @@ class PyinfraOperation(Generic[P], Protocol):
36
26
  #
37
27
  # Auth
38
28
  _sudo: bool = False,
39
- _sudo_user: Optional[str] = None,
29
+ _sudo_user: None | str = None,
40
30
  _use_sudo_login: bool = False,
41
- _sudo_password: Optional[str] = None,
31
+ _sudo_password: None | str = None,
42
32
  _preserve_sudo_env: bool = False,
43
- _su_user: Optional[str] = None,
33
+ _su_user: None | str = None,
44
34
  _use_su_login: bool = False,
45
35
  _preserve_su_env: bool = False,
46
- _su_shell: Optional[str] = None,
36
+ _su_shell: None | str = None,
47
37
  _doas: bool = False,
48
- _doas_user: Optional[str] = None,
38
+ _doas_user: None | str = None,
49
39
  # Shell arguments
50
- _shell_executable: Optional[str] = None,
51
- _chdir: Optional[str] = None,
52
- _env: Optional[Mapping[str, str]] = None,
40
+ _shell_executable: None | str = None,
41
+ _chdir: None | str = None,
42
+ _env: None | Mapping[str, str] = None,
53
43
  # Connector control
54
44
  _success_exit_codes: Iterable[int] = (0,),
55
- _timeout: Optional[int] = None,
45
+ _timeout: None | int = None,
56
46
  _get_pty: bool = False,
57
- _stdin: Union[None, str, list[str], tuple[str, ...]] = None,
47
+ _stdin: None | Union[str, list[str], Iterable[str]] = None,
48
+ # Retry arguments
49
+ _retries: None | int = None,
50
+ _retry_delay: None | Union[int, float] = None,
51
+ _retry_until: None | Callable[[dict], bool] = None,
52
+ _temp_dir: None | str = None,
58
53
  #
59
54
  # MetaArguments
60
55
  #
61
- name: Optional[str] = None,
56
+ name: None | str = None,
62
57
  _ignore_errors: bool = False,
63
58
  _continue_on_error: bool = False,
64
59
  _if: Union[List[Callable[[], bool]], Callable[[], bool], None] = None,
65
60
  #
66
61
  # ExecutionArguments
67
62
  #
68
- _parallel: Optional[int] = None,
63
+ _parallel: None | int = None,
69
64
  _run_once: bool = False,
70
65
  _serial: bool = False,
71
66
  #
pyinfra/api/command.py CHANGED
@@ -242,13 +242,19 @@ class FunctionCommand(PyinfraCommand):
242
242
  self.function(*self.args, **self.kwargs)
243
243
  return
244
244
 
245
- def execute_function() -> None:
245
+ def execute_function() -> None | Exception:
246
246
  with ctx_config.use(state.config.copy()):
247
247
  with ctx_host.use(host):
248
- self.function(*self.args, **self.kwargs)
248
+ try:
249
+ self.function(*self.args, **self.kwargs)
250
+ except Exception as e:
251
+ return e
252
+ return None
249
253
 
250
254
  greenlet = gevent.spawn(execute_function)
251
- return greenlet.get()
255
+ exception = greenlet.get()
256
+ if exception is not None:
257
+ raise exception
252
258
 
253
259
 
254
260
  class RsyncCommand(PyinfraCommand):
pyinfra/api/deploy.py CHANGED
@@ -41,7 +41,7 @@ def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs)
41
41
  ).format(get_call_location()),
42
42
  )
43
43
 
44
- hosts = kwargs.pop("host", state.inventory.iter_active_hosts())
44
+ hosts = kwargs.pop("host", state.inventory.get_active_hosts())
45
45
  if isinstance(hosts, Host):
46
46
  hosts = [hosts]
47
47
 
pyinfra/api/exceptions.py CHANGED
@@ -54,6 +54,12 @@ class OperationValueError(OperationError, ValueError):
54
54
  """
55
55
 
56
56
 
57
+ class NestedOperationError(OperationError):
58
+ """
59
+ Exception raised when a nested (immediately executed) operation fails.
60
+ """
61
+
62
+
57
63
  class DeployError(PyinfraError):
58
64
  """
59
65
  User exception for raising in deploys or sub deploys.
pyinfra/api/facts.py CHANGED
@@ -14,7 +14,7 @@ import inspect
14
14
  import re
15
15
  from inspect import getcallargs
16
16
  from socket import error as socket_error, timeout as timeout_error
17
- from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Optional, Type, TypeVar, cast
17
+ from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, Type, TypeVar, cast
18
18
 
19
19
  import click
20
20
  import gevent
@@ -87,7 +87,7 @@ class FactBase(Generic[T]):
87
87
 
88
88
  return cast(T, None)
89
89
 
90
- def process(self, output: Iterable[str]) -> T:
90
+ def process(self, output: list[str]) -> T:
91
91
  # NOTE: TypeVar does not support a default, so we have to cast this str -> T
92
92
  return cast(T, "\n".join(output))
93
93
 
@@ -152,7 +152,7 @@ def get_facts(state, *args, **kwargs):
152
152
  with ctx_state.use(state):
153
153
  greenlet_to_host = {
154
154
  state.pool.spawn(get_host_fact, host, *args, **kwargs): host
155
- for host in state.inventory.iter_active_hosts()
155
+ for host in state.inventory.get_active_hosts()
156
156
  }
157
157
 
158
158
  results = {}
pyinfra/api/inventory.py CHANGED
@@ -158,11 +158,11 @@ class Inventory:
158
158
 
159
159
  return iter(self.hosts.values())
160
160
 
161
- def iter_active_hosts(self) -> Iterator["Host"]:
161
+ def get_active_hosts(self) -> list["Host"]:
162
162
  """
163
163
  Iterates over active inventory hosts.
164
164
  """
165
- return iter(self.state.active_hosts)
165
+ return list(self.state.active_hosts)
166
166
 
167
167
  def len_active_hosts(self) -> int:
168
168
  """
pyinfra/api/operation.py CHANGED
@@ -22,7 +22,7 @@ from pyinfra.context import ctx_host, ctx_state
22
22
  from .arguments import EXECUTION_KWARG_KEYS, AllArguments, pop_global_arguments
23
23
  from .arguments_typed import PyinfraOperation
24
24
  from .command import PyinfraCommand, StringCommand
25
- from .exceptions import OperationValueError, PyinfraError
25
+ from .exceptions import NestedOperationError, OperationValueError, PyinfraError
26
26
  from .host import Host
27
27
  from .operations import run_host_op
28
28
  from .state import State, StateOperationHostData, StateOperationMeta, StateStage
@@ -221,7 +221,7 @@ def add_op(state: State, op_func, *args, **kwargs):
221
221
  ),
222
222
  )
223
223
 
224
- hosts = kwargs.pop("host", state.inventory.iter_active_hosts())
224
+ hosts = kwargs.pop("host", state.inventory.get_active_hosts())
225
225
  if isinstance(hosts, Host):
226
226
  hosts = [hosts]
227
227
 
@@ -266,7 +266,9 @@ def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> Py
266
266
  state = context.state
267
267
  host = context.host
268
268
 
269
- if state.current_stage < StateStage.Prepare or state.current_stage > StateStage.Execute:
269
+ if pyinfra.is_cli and (
270
+ state.current_stage < StateStage.Prepare or state.current_stage > StateStage.Execute
271
+ ):
270
272
  raise Exception("Cannot call operations outside of Prepare/Execute stages")
271
273
 
272
274
  if host.in_op:
@@ -470,8 +472,11 @@ def execute_immediately(state, host, op_hash):
470
472
  op_meta = state.get_op_meta(op_hash)
471
473
  op_data = state.get_op_data_for_host(host, op_hash)
472
474
  op_data.parent_op_hash = host.executing_op_hash
475
+
473
476
  log_operation_start(op_meta, op_types=["nested"], prefix="")
474
- run_host_op(state, host, op_hash)
477
+
478
+ if run_host_op(state, host, op_hash) is False:
479
+ raise NestedOperationError(op_hash)
475
480
 
476
481
 
477
482
  def _get_arg_value(arg):
pyinfra/api/operations.py CHANGED
@@ -4,7 +4,7 @@ import time
4
4
  import traceback
5
5
  from itertools import product
6
6
  from socket import error as socket_error, timeout as timeout_error
7
- from typing import TYPE_CHECKING, Optional, cast
7
+ from typing import TYPE_CHECKING, cast
8
8
 
9
9
  import click
10
10
  import gevent
@@ -17,7 +17,7 @@ from pyinfra.progress import progress_spinner
17
17
 
18
18
  from .arguments import CONNECTOR_ARGUMENT_KEYS, ConnectorArguments
19
19
  from .command import FunctionCommand, PyinfraCommand, StringCommand
20
- from .exceptions import PyinfraError
20
+ from .exceptions import NestedOperationError, PyinfraError
21
21
  from .util import (
22
22
  format_exception,
23
23
  log_error_or_warning,
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
35
35
  #
36
36
 
37
37
 
38
- def run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
38
+ def run_host_op(state: "State", host: "Host", op_hash: str) -> bool:
39
39
  state.trigger_callbacks("operation_host_start", host, op_hash)
40
40
 
41
41
  if op_hash not in state.ops[host]:
@@ -59,7 +59,7 @@ def run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
59
59
  host.executing_op_hash = None
60
60
 
61
61
 
62
- def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
62
+ def _run_host_op(state: "State", host: "Host", op_hash: str) -> bool:
63
63
  op_data = state.get_op_data_for_host(host, op_hash)
64
64
  global_arguments = op_data.global_arguments
65
65
 
@@ -104,6 +104,8 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
104
104
  if isinstance(command, FunctionCommand):
105
105
  try:
106
106
  status = command.execute(state, host, connector_arguments)
107
+ except NestedOperationError:
108
+ host.log_styled("Error in nested operation", fg="red", log_func=logger.error)
107
109
  except Exception as e:
108
110
  # Custom functions could do anything, so expect anything!
109
111
  logger.warning(traceback.format_exc())
@@ -278,7 +280,7 @@ def _run_serial_ops(state: "State"):
278
280
  Run all ops for all servers, one server at a time.
279
281
  """
280
282
 
281
- for host in list(state.inventory.iter_active_hosts()):
283
+ for host in list(state.inventory.get_active_hosts()):
282
284
  host_operations = product([host], state.get_op_order())
283
285
  with progress_spinner(host_operations) as progress:
284
286
  try:
@@ -296,7 +298,7 @@ def _run_no_wait_ops(state: "State"):
296
298
  Run all ops for all servers at once.
297
299
  """
298
300
 
299
- hosts_operations = product(state.inventory.iter_active_hosts(), state.get_op_order())
301
+ hosts_operations = product(state.inventory.get_active_hosts(), state.get_op_order())
300
302
  with progress_spinner(hosts_operations) as progress:
301
303
  # Spawn greenlet for each host to run *all* ops
302
304
  if state.pool is None:
@@ -308,7 +310,7 @@ def _run_no_wait_ops(state: "State"):
308
310
  host,
309
311
  progress=progress,
310
312
  )
311
- for host in state.inventory.iter_active_hosts()
313
+ for host in state.inventory.get_active_hosts()
312
314
  ]
313
315
  gevent.joinall(greenlets)
314
316
 
@@ -326,9 +328,9 @@ def _run_single_op(state: "State", op_hash: str):
326
328
  failed_hosts = set()
327
329
 
328
330
  if op_meta.global_arguments["_serial"]:
329
- with progress_spinner(state.inventory.iter_active_hosts()) as progress:
331
+ with progress_spinner(state.inventory.get_active_hosts()) as progress:
330
332
  # For each host, run the op
331
- for host in state.inventory.iter_active_hosts():
333
+ for host in state.inventory.get_active_hosts():
332
334
  result = _run_host_op_with_context(state, host, op_hash)
333
335
  progress(host)
334
336
 
@@ -337,12 +339,12 @@ def _run_single_op(state: "State", op_hash: str):
337
339
 
338
340
  else:
339
341
  # Start with the whole inventory in one batch
340
- batches = [list(state.inventory.iter_active_hosts())]
342
+ batches = [list(state.inventory.get_active_hosts())]
341
343
 
342
344
  # If parallel set break up the inventory into a series of batches
343
345
  parallel = op_meta.global_arguments["_parallel"]
344
346
  if parallel:
345
- hosts = list(state.inventory.iter_active_hosts())
347
+ hosts = list(state.inventory.get_active_hosts())
346
348
  batches = [hosts[i : i + parallel] for i in range(0, len(hosts), parallel)]
347
349
 
348
350
  for batch in batches:
@@ -28,15 +28,17 @@ HOST_KEYS_LOCK = BoundedSemaphore()
28
28
  class StrictPolicy(MissingHostKeyPolicy):
29
29
  @override
30
30
  def missing_host_key(self, client, hostname, key):
31
- logger.error("No host key for {0} found in known_hosts".format(hostname))
32
- raise SSHException(
33
- "StrictPolicy: No host key for {0} found in known_hosts".format(hostname),
34
- )
31
+ logger.error("No host key for %s found in known_hosts", hostname)
32
+ raise SSHException(f"StrictPolicy: No host key for {hostname} found in known_hosts")
35
33
 
36
34
 
37
35
  def append_hostkey(client, hostname, key):
38
36
  """Append hostname to the clients host_keys_file"""
39
37
 
38
+ if client._host_keys_filename is None:
39
+ logger.warning("No host keys filename, not saving key for: %s", hostname)
40
+ return
41
+
40
42
  with HOST_KEYS_LOCK:
41
43
  # The paramiko client saves host keys incorrectly whereas the host keys object does
42
44
  # this correctly, so use that with the client filename variable.
@@ -67,30 +69,28 @@ class AcceptNewPolicy(MissingHostKeyPolicy):
67
69
  )
68
70
 
69
71
  append_hostkey(client, hostname, key)
70
- logger.warning("Added host key for {0} to known_hosts".format(hostname))
72
+ logger.warning("Added host key for %s to known_hosts", hostname)
71
73
 
72
74
 
73
75
  class AskPolicy(MissingHostKeyPolicy):
74
76
  @override
75
77
  def missing_host_key(self, client, hostname, key):
76
78
  should_continue = input(
77
- "No host key for {0} found in known_hosts, do you want to continue [y/n] ".format(
78
- hostname,
79
- ),
79
+ f"No host key for {hostname} found in known_hosts, do you want to continue [y/n] ",
80
80
  )
81
81
  if should_continue.lower() != "y":
82
82
  raise SSHException(
83
- "AskPolicy: No host key for {0} found in known_hosts".format(hostname),
83
+ f"AskPolicy: No host key for {hostname} found in known_hosts",
84
84
  )
85
85
  append_hostkey(client, hostname, key)
86
- logger.warning("Added host key for {0} to known_hosts".format(hostname))
86
+ logger.warning("Added host key for %s to known_hosts", hostname)
87
87
  return
88
88
 
89
89
 
90
90
  class WarningPolicy(MissingHostKeyPolicy):
91
91
  @override
92
92
  def missing_host_key(self, client, hostname, key):
93
- logger.warning("No host key for {0} found in known_hosts".format(hostname))
93
+ logger.warning("No host key for %s found in known_hosts", hostname)
94
94
 
95
95
 
96
96
  def get_missing_host_key_policy(policy):
@@ -102,7 +102,7 @@ def get_missing_host_key_policy(policy):
102
102
  return StrictPolicy()
103
103
  if policy == "accept-new":
104
104
  return AcceptNewPolicy()
105
- raise SSHException("Invalid value StrictHostKeyChecking={}".format(policy))
105
+ raise SSHException(f"Invalid value StrictHostKeyChecking={policy}")
106
106
 
107
107
 
108
108
  @memoize
@@ -120,17 +120,24 @@ def get_ssh_config(user_config_file=None):
120
120
 
121
121
 
122
122
  @memoize
123
- def get_host_keys(filename):
123
+ def get_host_keys(filenames):
124
+ """
125
+ Load host keys from one or more files.
126
+
127
+ Args:
128
+ filenames: A tuple of filenames to load host keys from.
129
+ """
124
130
  with HOST_KEYS_LOCK:
125
131
  host_keys = HostKeys()
126
132
 
127
- try:
128
- host_keys.load(filename)
129
- # When paramiko encounters a bad host keys line it sometimes bails the
130
- # entire load incorrectly.
131
- # See: https://github.com/paramiko/paramiko/pull/1990
132
- except Exception as e:
133
- logger.warning("Failed to load host keys from {0}: {1}".format(filename, e))
133
+ for filename in filenames:
134
+ try:
135
+ host_keys.load(filename)
136
+ # When paramiko encounters a bad host keys line it sometimes bails the
137
+ # entire load incorrectly.
138
+ # See: https://github.com/paramiko/paramiko/pull/1990
139
+ except Exception as e:
140
+ logger.warning("Failed to load host keys from %s: %s", filename, e)
134
141
 
135
142
  return host_keys
136
143
 
@@ -157,7 +164,7 @@ class SSHClient(ParamikoClient):
157
164
  config,
158
165
  forward_agent,
159
166
  missing_host_key_policy,
160
- host_keys_file,
167
+ host_keys_files,
161
168
  keep_alive,
162
169
  ) = self.parse_config(
163
170
  hostname,
@@ -169,11 +176,13 @@ class SSHClient(ParamikoClient):
169
176
  config.update(kwargs)
170
177
 
171
178
  if _pyinfra_ssh_known_hosts_file:
172
- host_keys_file = _pyinfra_ssh_known_hosts_file
179
+ host_keys_files = (path.expanduser(_pyinfra_ssh_known_hosts_file),)
173
180
 
174
181
  # Overwrite paramiko empty defaults with @memoize-d host keys object
175
- self._host_keys = get_host_keys(host_keys_file)
176
- self._host_keys_filename = host_keys_file
182
+ self._host_keys = get_host_keys(host_keys_files)
183
+ # Use the first file for writing new host keys
184
+ if len(host_keys_files) > 0:
185
+ self._host_keys_filename = host_keys_files[0]
177
186
 
178
187
  if _pyinfra_ssh_paramiko_connect_kwargs:
179
188
  config.update(_pyinfra_ssh_paramiko_connect_kwargs)
@@ -217,11 +226,18 @@ class SSHClient(ParamikoClient):
217
226
  keep_alive = 0
218
227
  forward_agent = False
219
228
  missing_host_key_policy = get_missing_host_key_policy(strict_host_key_checking)
220
- host_keys_file = path.expanduser("~/.ssh/known_hosts") # OpenSSH default
229
+ host_keys_files = (path.expanduser("~/.ssh/known_hosts"),)
221
230
 
222
231
  ssh_config = get_ssh_config(ssh_config_file)
223
232
  if not ssh_config:
224
- return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive
233
+ return (
234
+ hostname,
235
+ cfg,
236
+ forward_agent,
237
+ missing_host_key_policy,
238
+ host_keys_files,
239
+ keep_alive,
240
+ )
225
241
 
226
242
  host_config = ssh_config.lookup(hostname)
227
243
  forward_agent = host_config.get("forwardagent") == "yes"
@@ -233,7 +249,10 @@ class SSHClient(ParamikoClient):
233
249
  )
234
250
 
235
251
  if "userknownhostsfile" in host_config:
236
- host_keys_file = path.expanduser(host_config["userknownhostsfile"])
252
+ # OpenSSH supports multiple space-separated known hosts files
253
+ host_keys_files = tuple(
254
+ path.expanduser(f) for f in host_config["userknownhostsfile"].split()
255
+ )
237
256
 
238
257
  if "hostname" in host_config:
239
258
  hostname = host_config["hostname"]
@@ -275,7 +294,7 @@ class SSHClient(ParamikoClient):
275
294
  sock = c.gateway(hostname, cfg["port"], target, target_config["port"])
276
295
  cfg["sock"] = sock
277
296
 
278
- return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive
297
+ return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_files, keep_alive
279
298
 
280
299
  @staticmethod
281
300
  def derive_shorthand(ssh_config, host_string):
pyinfra/facts/crontab.py CHANGED
@@ -52,9 +52,11 @@ class CrontabFile:
52
52
 
53
53
  name_comment = "# pyinfra-name={0}".format(name)
54
54
  for cmd in self.commands:
55
+ if "command" not in cmd:
56
+ continue
55
57
  if cmd.get("command") == command:
56
58
  return cmd
57
- if cmd.get("comments") and name_comment in cmd["comments"]:
59
+ if name_comment in cmd.get("comments", []):
58
60
  return cmd
59
61
  return None
60
62
 
pyinfra/facts/files.py CHANGED
@@ -547,6 +547,8 @@ class FindFilesBase(FactBase):
547
547
  command.append("-regex")
548
548
  command.append(maybe_quote(regex))
549
549
 
550
+ command.extend(args)
551
+
550
552
  command.append("||")
551
553
  command.append("true")
552
554
 
@@ -664,13 +666,21 @@ class Block(FactBase):
664
666
 
665
667
  class FileContents(FactBase):
666
668
  """
667
- Returns the contents of a file as a list of lines. Returns ``None`` if the file does not exist.
669
+ Returns the contents of a file as a list of lines, or ``None`` if the file does not exist.
668
670
  """
669
671
 
670
672
  @override
671
673
  def command(self, path):
672
- return make_formatted_string_command("cat {0}", QuoteString(path))
674
+ self.missing_flag = "{0}{1}".format(MISSING, path)
675
+ return make_formatted_string_command(
676
+ "( test -e {0} && cat {0} ) || echo {1}",
677
+ QuoteString(path),
678
+ QuoteString(self.missing_flag),
679
+ )
673
680
 
674
681
  @override
675
682
  def process(self, output):
683
+ # If output is the missing flag, the file doesn't exist
684
+ if output and output[0] == self.missing_flag:
685
+ return None
676
686
  return output
pyinfra/facts/flatpak.py CHANGED
@@ -74,4 +74,4 @@ class FlatpakPackages(FlatpakBaseFact):
74
74
 
75
75
  @override
76
76
  def process(self, output):
77
- return [flatpak for flatpak in output[1:]]
77
+ return [flatpak for i, flatpak in enumerate(output) if i > 1 or flatpak != "Application ID"]
@@ -32,6 +32,8 @@ def container(
32
32
  present: bool = True,
33
33
  force: bool = False,
34
34
  start: bool = True,
35
+ restart_policy: str | None = None,
36
+ auto_remove: bool = False,
35
37
  ):
36
38
  """
37
39
  Manage Docker containers
@@ -47,6 +49,8 @@ def container(
47
49
  + force: remove a container with same name and create a new one
48
50
  + present: whether the container should be up and running
49
51
  + start: start or stop the container
52
+ + restart_policy: restart policy to apply when a container exits
53
+ + auto_remove: automatically remove the container and its associated anonymous volumes when it exits
50
54
 
51
55
  **Examples:**
52
56
 
@@ -64,6 +68,8 @@ def container(
64
68
  networks=["proxy", "services"],
65
69
  volumes=["nginx_data:/usr/share/nginx/html"],
66
70
  pull_always=True,
71
+ restart_policy="unless-stopped",
72
+ auto_remove=True,
67
73
  )
68
74
 
69
75
  # Stop a container
@@ -89,6 +95,8 @@ def container(
89
95
  env_vars or list(),
90
96
  labels or list(),
91
97
  pull_always,
98
+ restart_policy,
99
+ auto_remove,
92
100
  )
93
101
  existent_container = host.get_fact(DockerContainer, object_id=container)
94
102
 
@@ -53,7 +53,12 @@ def sql(
53
53
  )
54
54
 
55
55
 
56
- @operation()
56
+ @operation(
57
+ idempotent_notice=(
58
+ "This operation will always execute commands when a password is provided, "
59
+ "as pyinfra cannot reliably validate the current password."
60
+ ),
61
+ )
57
62
  def role(
58
63
  role: str,
59
64
  present: bool = True,
@@ -165,6 +165,8 @@ class ContainerSpec:
165
165
  env_vars: list[str] = field(default_factory=list)
166
166
  labels: list[str] = field(default_factory=list)
167
167
  pull_always: bool = False
168
+ restart_policy: str | None = None
169
+ auto_remove: bool = False
168
170
 
169
171
  def container_create_args(self):
170
172
  args = []
@@ -186,6 +188,12 @@ class ContainerSpec:
186
188
  if self.pull_always:
187
189
  args.append("--pull always")
188
190
 
191
+ if self.restart_policy:
192
+ args.append("--restart {0}".format(self.restart_policy))
193
+
194
+ if self.auto_remove:
195
+ args.append("--rm")
196
+
189
197
  args.append(self.image)
190
198
 
191
199
  return args
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyinfra
3
- Version: 3.6
3
+ Version: 3.6.1
4
4
  Summary: pyinfra automates/provisions/manages/deploys infrastructure.
5
5
  Project-URL: homepage, https://pyinfra.com
6
6
  Project-URL: documentation, https://docs.pyinfra.com
@@ -6,20 +6,20 @@ pyinfra/progress.py,sha256=X3hXZ4Flh_L9FE4ZEWxWoG0R4dA5UPd1FCO-Exd5Xtc,4193
6
6
  pyinfra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  pyinfra/version.py,sha256=LZf50PHDzEZv65w0G-iMICoQ9US0U5LWHAOEmNtkF3I,216
8
8
  pyinfra/api/__init__.py,sha256=89brynFpoKWCE513go2pK2wfbGaU1V1gajX3UMm9LVA,964
9
- pyinfra/api/arguments.py,sha256=mG6A2JgDL6RSFIibjKaocVxbWuFZEtzJ_Is8r6OZ4Mc,12438
10
- pyinfra/api/arguments_typed.py,sha256=IZuhpmDfW9CP6ASS5Ie-3Wcnxl6bDNR3egU4Mfhbsb4,2303
11
- pyinfra/api/command.py,sha256=NwF2syxV3zxCbBdHzvJ6ve5G-xwdNTjPHFPwguKVcYs,7741
9
+ pyinfra/api/arguments.py,sha256=0c_xENyzPlAyD41lXpL5rPbxU8Rny-uWyXgd8izcg9w,12439
10
+ pyinfra/api/arguments_typed.py,sha256=8a3vPKkz28i9Q1Byo3DDJRLL2CuM8dbPGH-12Afh0ZY,2434
11
+ pyinfra/api/command.py,sha256=X-gK2K8oyoHAPu4HE4tI93G57mkY4nGx7GmQZwEgV3M,7949
12
12
  pyinfra/api/config.py,sha256=gVDV-aGh6LYOnHtBaivICrd3RBfjFRWy3-K9sG__eP8,9321
13
13
  pyinfra/api/connect.py,sha256=jkx07iUL29u9pHHKH4WcNtvxwOA4DIbF7ixguFyuFjo,1984
14
14
  pyinfra/api/connectors.py,sha256=nie7JuLxMSC6gqPjmjuCisQ11R-eAQDtMMWF6YbSQ48,659
15
- pyinfra/api/deploy.py,sha256=Upd92oThQN0zhKbKW8vyPvBuoYiEGStuiEy7kNhZ00Y,3167
16
- pyinfra/api/exceptions.py,sha256=dp11JB5OUn48slAldVN8l8m8F8DpUXULIy0-VonzMS0,2004
17
- pyinfra/api/facts.py,sha256=kT9KPKnDoBnz8Gck21yYDuYXzRzHio-B5ZwSqyj89Iw,9885
15
+ pyinfra/api/deploy.py,sha256=xR5VRGkRoax3dBVLX8vn_pS2x9RL0u3IrCWCPeJ_SFQ,3166
16
+ pyinfra/api/exceptions.py,sha256=Nf6QFRzQBLfqb1kmVY0JXwLZ0YAIZE4nxUcxBEgRSqA,2141
17
+ pyinfra/api/facts.py,sha256=u_8M4ijdZnQszdRpIWPTGtZ_fSWt_Zv_jUIq-uJsBAA,9870
18
18
  pyinfra/api/host.py,sha256=192rj8fsrHXudfnxzPlYxljXU24pReeWjXtrcCe9Kj4,14214
19
- pyinfra/api/inventory.py,sha256=i_LBI-Gn5FF-9SVDBH6xefTtvFzjuz12QQiFPGK2TrQ,7864
19
+ pyinfra/api/inventory.py,sha256=tMvnAstPqAGdj8bAxCeH27KxlueUHj7VnBNVBRXqWZM,7859
20
20
  pyinfra/api/metadata.py,sha256=73BjwxKKA4mgP7D60K7Z8YIwPC11YN4IXaq26d209BI,1884
21
- pyinfra/api/operation.py,sha256=WtZyVwPoh9_AytqcCf_W6sMHhWEfSex9RE54aMBHU5M,17053
22
- pyinfra/api/operations.py,sha256=JEVLcLeVO81PXd4Vmvn3XuAovZguURoNXBexYQWBnyo,13718
21
+ pyinfra/api/operation.py,sha256=Ae_V_8ObPEQntU5_vXbOn2KeMvKTBYO1LSvjJFT2zQ0,17176
22
+ pyinfra/api/operations.py,sha256=kUGpC9ytA7Zaw9cJZ7zFTtLFFuyMqr2PAWaOmyAzeD0,13846
23
23
  pyinfra/api/state.py,sha256=cj-JvxOljeDshWvRpq8AMQxdGaUaht8KyuyR3mEsI-Y,12859
24
24
  pyinfra/api/util.py,sha256=fwlgiFGFpHPQQ6BVT3SRDDunZBS3fZ7ApTEFuRI6r5M,13276
25
25
  pyinfra/connectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -36,7 +36,7 @@ pyinfra/connectors/vagrant.py,sha256=0TT73ks64I4Yl-JSZjMBbpWA3VYBkqqLB-fUS8pS8GY
36
36
  pyinfra/connectors/scp/__init__.py,sha256=jnO-_8GfkKWhsFcDjAxjOkuUT2RbS22b8P_xPrX889U,44
37
37
  pyinfra/connectors/scp/client.py,sha256=l_fPsbgz-7U6Y50ssuKKPFxD_cFoIPtaVXMCYDotbDI,6399
38
38
  pyinfra/connectors/sshuserclient/__init__.py,sha256=Qc4RO2wknSWIiNTwOeQ0y2TeiuKHmyWDW2Dz4MOo9CE,44
39
- pyinfra/connectors/sshuserclient/client.py,sha256=Ei2_mzCMNJopbpvpeLsdSiNb98rxEEy7uCOmpJbfd2o,10506
39
+ pyinfra/connectors/sshuserclient/client.py,sha256=WUmpo2LUOSXucUwVHb3EgLVxQIoCJ7pBFqFiCynjlMI,11069
40
40
  pyinfra/connectors/sshuserclient/config.py,sha256=FZkPrUYXkURZcFUHBGWw9lLC9uiH3DJ0rBYXJePchxw,2774
41
41
  pyinfra/facts/__init__.py,sha256=myTXSOZmAqmU88Fyifn035h9Lr6Gj2mlka_jDcXyKGw,347
42
42
  pyinfra/facts/apk.py,sha256=UEMHzhx2Wx3qq-OcjetWgE2iZ7_EjI-bszLxSN6PJa0,799
@@ -45,13 +45,13 @@ pyinfra/facts/brew.py,sha256=nE6YVc2S9zasyJPZmPR5FMeGKPViZYEcpnnBQlDf1EU,2792
45
45
  pyinfra/facts/bsdinit.py,sha256=SVY4hagjyy1yz8FKWhIbX9fHm5AugvTFl4xQh2FFO74,631
46
46
  pyinfra/facts/cargo.py,sha256=qgOClhwZm4COcncDzOZccCzs67nPBi_x6VGiF2UA0sA,687
47
47
  pyinfra/facts/choco.py,sha256=mpLleSqNqiaGRgyrhgceno2iPB1_1yjn8UJ90pvOZCs,886
48
- pyinfra/facts/crontab.py,sha256=mEY7bWvvjir23aUhNwm1mjCmFYHjVsWqrgbAXu1wt3M,5774
48
+ pyinfra/facts/crontab.py,sha256=mm5eHCA0jL0zDflZKUyEk3V1j_opVZLvEezgwEXrucs,5820
49
49
  pyinfra/facts/deb.py,sha256=1dR1puwY5wyyhhYYwaEBLjKU9sIyaNBNBlamVZ2KQg0,2074
50
50
  pyinfra/facts/dnf.py,sha256=wXatfZWVrrdLY7LM-vHKMg8Md1FiwkqHxmgRYbQqw90,1208
51
51
  pyinfra/facts/docker.py,sha256=fqIqMR6HwSYpTUAjhCX8Hk57pcyL6ShIl98H32Ly6HM,3233
52
52
  pyinfra/facts/efibootmgr.py,sha256=JPJSokE_RV9JstEPJRECnqSU-B0JCxmrocY8zBOva7M,3555
53
- pyinfra/facts/files.py,sha256=xLMXbmYQ8x6Cri44Nh9BOGU0o5OAX39BtXzoOgoZyMM,19462
54
- pyinfra/facts/flatpak.py,sha256=ovi3duwTqqwvt5GoDRN7R-PpkvR6sQ1SmgEADcSnkUE,1646
53
+ pyinfra/facts/files.py,sha256=y6aOC8k2UH6421t3NXgc01BfPmcy7pLFdQACJYlHFWE,19798
54
+ pyinfra/facts/flatpak.py,sha256=Fwyaq2YP0SL_ChlGk3cTvujtb7bg1XBdICaEY1dzbBY,1696
55
55
  pyinfra/facts/freebsd.py,sha256=za42Di2M2-hcSTPei1YE6BsJxqapm9jysshs9hKJZaY,2161
56
56
  pyinfra/facts/gem.py,sha256=aX2-vcEqkxUIP0UJ_SVp9bf4B944oyDjsuujjs5q_9w,654
57
57
  pyinfra/facts/git.py,sha256=Zfzpdccsz2InviirJO17EkEFTVNqYQclSlXJIRFkD_s,1699
@@ -99,7 +99,7 @@ pyinfra/operations/cargo.py,sha256=mXWd6pb0IR6kzJMmPHwXZN-VJ-B_y8AdOFlrRzDQOZI,1
99
99
  pyinfra/operations/choco.py,sha256=nIj4bWhChOd5DkybpbD-oupaoODgS7lYx6Vrou5ksuc,1547
100
100
  pyinfra/operations/crontab.py,sha256=L1U_fBvgXkbfbpzb6OzUBrrY-RuvvPlbW5FqDmAT8rI,6644
101
101
  pyinfra/operations/dnf.py,sha256=wMFUoUB679bVydt01N7Sd7Cs16RhAaLca-zsmQU86rk,5727
102
- pyinfra/operations/docker.py,sha256=J_vQwQmCZEuNASFrRjeudrr_ZrpbStdXybGGej4VMxc,14161
102
+ pyinfra/operations/docker.py,sha256=vsNm8PVh7_oIi8RmnpHeOekX-1gliNTwvzo9-2zVJY8,14525
103
103
  pyinfra/operations/files.py,sha256=jqkYIPn94z4MRqH1eU-ooOJzuQyle8wYiyDvFfXDMPI,68492
104
104
  pyinfra/operations/flatpak.py,sha256=Eif5KZkWOVElKF4hL5xOyk_oZEOziHqyyDxGHZ1KPYk,2366
105
105
  pyinfra/operations/gem.py,sha256=YtVUKVp1zYPAxy2t1ryw-vgucBVYJASOxhauLOvRj6U,1175
@@ -116,7 +116,7 @@ pyinfra/operations/pip.py,sha256=uTfK36_vBNqCXsWMU4iypOLhnhKduJK6f0b9srlMEqg,603
116
116
  pyinfra/operations/pipx.py,sha256=oWcJXKogC43cKNsf625FU4ClIAV6KZA26o2cRoa3avQ,2844
117
117
  pyinfra/operations/pkg.py,sha256=m5okKIXU1xIIcNQXQnFKXbL97ZcokmGn-hnruokE7is,2341
118
118
  pyinfra/operations/pkgin.py,sha256=6bZyvdjYDqn-0a-r23O_122r1QSjHP8SkJiWZ_k231A,2037
119
- pyinfra/operations/postgres.py,sha256=GlIn5aAVNOjs7cSyF3CFY6LO3dC0ZQQ0wAs_y0JJHis,13417
119
+ pyinfra/operations/postgres.py,sha256=FN7wDYgtjqShbzSr-_CKbYY0PSlj2XdVijzaveUYols,13601
120
120
  pyinfra/operations/postgresql.py,sha256=agZjL2W4yxigk9ThIC0V_3wvmcWVdX308aJO24WkN6g,833
121
121
  pyinfra/operations/puppet.py,sha256=e9vO6SQnkMoyVWjy3oP08GaXgyIPajoA2QJ4g4ib4-M,907
122
122
  pyinfra/operations/python.py,sha256=5IXcywwhwITPRJAs8BEL5H5vSPvk_QFMbhF8Iuexp_s,2067
@@ -139,22 +139,22 @@ pyinfra/operations/freebsd/pkg.py,sha256=3AyfI0-_9F4ho47KqZsOMQocwNtTF2q9g0i6Tff
139
139
  pyinfra/operations/freebsd/service.py,sha256=1f7nTHELnhs3HBSrMFsmopVgYFMIwB8Se88yneRQ8Rw,3198
140
140
  pyinfra/operations/freebsd/sysrc.py,sha256=eg7u_JsCge_uKq3Ysc_mohUc6qgJrOZStp_B_l2Hav4,2330
141
141
  pyinfra/operations/util/__init__.py,sha256=ZAHjeCXtLo0TIOSfZ9h0Sh5IXXRCspfHs3RR1l8tQCE,366
142
- pyinfra/operations/util/docker.py,sha256=OCjlyxjuNP6Ki8zU6-cQkhgM-FgKsylD-dEhR7JNc8Y,11920
142
+ pyinfra/operations/util/docker.py,sha256=aOL8iEOPlg5LpC1ix37_XbzwEG5V05wz-PhyrsBKWQI,12152
143
143
  pyinfra/operations/util/files.py,sha256=PFJDccNTwXK4tIoFB8ycRj7yD1x7LpSflBy7mPQtJCg,7148
144
144
  pyinfra/operations/util/packaging.py,sha256=RXZgUlWqEBtArK7wJfXE2Ndvl_aP0YjjksxxCnPpexk,12086
145
145
  pyinfra/operations/util/service.py,sha256=kJd1zj4-sAaGIp5Ts7yAJznogWaGr8oQTztwenLAr7Y,1309
146
146
  pyinfra_cli/__init__.py,sha256=G0X7tNdqT45uWuK3aHIKxMdDeCgJ7zHo6vbxoG6zy_8,284
147
- pyinfra_cli/cli.py,sha256=vlD9YyJaSy0mZu2J6pvDvBxka6cu6pRCvDj_ZZUDqXY,21285
147
+ pyinfra_cli/cli.py,sha256=xD24zgSB1VdOx9F8avD405lj-ASWsTsZ21TC_nYdTkY,21418
148
148
  pyinfra_cli/commands.py,sha256=J-mCJYvDebJ8M7o3HreB2zToa871-xO6_KjVhPLeHho,1832
149
149
  pyinfra_cli/exceptions.py,sha256=RRaOprL7SmVv--FLy4x7fxeTitx9wYI0Y3_h01LfhJA,4901
150
- pyinfra_cli/inventory.py,sha256=JYSixJZKY8GNWlOxh-nGDsAknCdaAktlWAmdg13kvNk,11771
150
+ pyinfra_cli/inventory.py,sha256=tU0qaF5jFlYu0tbs961QLdjHU0wJL3Tr7Sk9TM36Ggw,12648
151
151
  pyinfra_cli/log.py,sha256=mD96MH2owQQ5AsYRw7osCKENdp-E3Wum5IDr6qhSIa4,2268
152
152
  pyinfra_cli/main.py,sha256=1CR3IS-O6BkAzkn7UW6pdKktTmN4Qnt0_jPdkhueRM8,936
153
153
  pyinfra_cli/prints.py,sha256=1h6vgKVRKUxcGz_HdyEEDUvkp-lgiiVGwx3hc9rw24A,10434
154
- pyinfra_cli/util.py,sha256=8KKW5LTHX4ebbwbHqMvLqwjZ11mOHI-xIYn-cZCWltg,6722
154
+ pyinfra_cli/util.py,sha256=P5ULsCweyNc7OkqmQTEplNfzNtNyxhQWfcp2540YeE0,6721
155
155
  pyinfra_cli/virtualenv.py,sha256=wRNxOPcUkbD_Pzuj-Lnrz1KxGmsLlb2ObmCTFrdD-S8,2474
156
- pyinfra-3.6.dist-info/METADATA,sha256=8nSBGBw02ni-nBp6iJsQ6hzoHPoytQcD8jKptRxm4Jg,5771
157
- pyinfra-3.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
158
- pyinfra-3.6.dist-info/entry_points.txt,sha256=b1nLI6oVRvkeQDS00xcYGdGl4XVR_3tMbKs6T58-NW4,507
159
- pyinfra-3.6.dist-info/licenses/LICENSE.md,sha256=BzCnRYLJv0yb-FJuEd_XOrrQSOEQKzIVo0yHT8taNnM,1076
160
- pyinfra-3.6.dist-info/RECORD,,
156
+ pyinfra-3.6.1.dist-info/METADATA,sha256=CbbyXsZMgFh0FnvreQoIElKZRPl-UzFiDupEpWsGMPQ,5773
157
+ pyinfra-3.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
158
+ pyinfra-3.6.1.dist-info/entry_points.txt,sha256=b1nLI6oVRvkeQDS00xcYGdGl4XVR_3tMbKs6T58-NW4,507
159
+ pyinfra-3.6.1.dist-info/licenses/LICENSE.md,sha256=BzCnRYLJv0yb-FJuEd_XOrrQSOEQKzIVo0yHT8taNnM,1076
160
+ pyinfra-3.6.1.dist-info/RECORD,,
pyinfra_cli/cli.py CHANGED
@@ -595,10 +595,6 @@ def _set_config(
595
595
  if path.exists(config_filename):
596
596
  exec_file(config_filename)
597
597
 
598
- # Lock the current config, this allows us to restore this version after
599
- # executing deploy files that may alter them.
600
- config.lock_current_state()
601
-
602
598
  # Arg based config overrides
603
599
  if sudo:
604
600
  config.SUDO = True
@@ -632,6 +628,11 @@ def _set_config(
632
628
  if retry_delay is not None:
633
629
  config.RETRY_DELAY = retry_delay
634
630
 
631
+ # Lock the current config, this allows us to restore this version after
632
+ # executing deploy files that may alter them. This must happen after CLI
633
+ # args are applied so they persist across multiple deploy files.
634
+ config.lock_current_state()
635
+
635
636
  return config
636
637
 
637
638
 
@@ -772,6 +773,8 @@ def _prepare_exec_operations(state, config, operations):
772
773
  def _prepare_deploy_operations(state, config, operations):
773
774
  # Number of "steps" to make = number of files * number of hosts
774
775
  for i, filename in enumerate(operations):
776
+ config.lock_current_state()
777
+
775
778
  _log_styled_msg = click.style(filename, bold=True)
776
779
  logger.info("Loading: {0}".format(_log_styled_msg))
777
780
 
pyinfra_cli/inventory.py CHANGED
@@ -177,8 +177,11 @@ def make_inventory(
177
177
  "nor refers to a python module"
178
178
  )
179
179
  return Inventory.empty()
180
- else:
180
+ elif callable(inventory_func):
181
181
  return make_inventory_from_func(inventory_func, override_data)
182
+ else:
183
+ # The inventory is an iterable (list/tuple) of hosts from a module attribute
184
+ return make_inventory_from_iterable(inventory_func, override_data)
182
185
 
183
186
 
184
187
  def make_inventory_from_func(
@@ -235,6 +238,28 @@ def make_inventory_from_func(
235
238
  )
236
239
 
237
240
 
241
+ def make_inventory_from_iterable(
242
+ hosts: List[HostType],
243
+ override_data: Optional[Dict[Any, Any]] = None,
244
+ ):
245
+ """
246
+ Builds a ``pyinfra.api.Inventory`` from an iterable of hosts loaded from a module attribute.
247
+ """
248
+ logger.warning("Loading inventory via module attribute is in alpha!")
249
+
250
+ if not isinstance(hosts, (list, tuple)):
251
+ raise TypeError(f"Inventory attribute is not a list or tuple: {type(hosts).__name__}")
252
+
253
+ for host in hosts:
254
+ if not isinstance(host, ALLOWED_HOST_TYPES):
255
+ raise TypeError(f"Invalid host in inventory: {host}")
256
+
257
+ return Inventory(
258
+ (list(hosts), {}),
259
+ override_data=override_data,
260
+ )
261
+
262
+
238
263
  def make_inventory_from_files(
239
264
  inventory_filename: str,
240
265
  override_data=None,
pyinfra_cli/util.py CHANGED
@@ -218,7 +218,7 @@ def _parallel_load_hosts(state: "State", callback: Callable, name: str):
218
218
  return e
219
219
 
220
220
  greenlet_to_host = {
221
- state.pool.spawn(load_file, host): host for host in state.inventory.iter_active_hosts()
221
+ state.pool.spawn(load_file, host): host for host in state.inventory.get_active_hosts()
222
222
  }
223
223
 
224
224
  with progress_spinner(greenlet_to_host.values()) as progress:
File without changes