pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.1__py2.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.
Files changed (148) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +115 -97
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +139 -39
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/deploy.py +19 -19
  8. pyinfra/api/exceptions.py +35 -4
  9. pyinfra/api/facts.py +62 -86
  10. pyinfra/api/host.py +102 -15
  11. pyinfra/api/inventory.py +4 -0
  12. pyinfra/api/operation.py +184 -118
  13. pyinfra/api/operations.py +66 -113
  14. pyinfra/api/state.py +53 -34
  15. pyinfra/api/util.py +64 -33
  16. pyinfra/connectors/base.py +65 -20
  17. pyinfra/connectors/chroot.py +15 -13
  18. pyinfra/connectors/docker.py +62 -72
  19. pyinfra/connectors/dockerssh.py +20 -19
  20. pyinfra/connectors/local.py +32 -22
  21. pyinfra/connectors/ssh.py +162 -86
  22. pyinfra/connectors/sshuserclient/client.py +1 -1
  23. pyinfra/connectors/terraform.py +57 -39
  24. pyinfra/connectors/util.py +26 -27
  25. pyinfra/connectors/vagrant.py +27 -26
  26. pyinfra/context.py +1 -0
  27. pyinfra/facts/apk.py +7 -2
  28. pyinfra/facts/apt.py +15 -7
  29. pyinfra/facts/brew.py +28 -13
  30. pyinfra/facts/bsdinit.py +9 -6
  31. pyinfra/facts/cargo.py +6 -3
  32. pyinfra/facts/choco.py +8 -4
  33. pyinfra/facts/deb.py +21 -9
  34. pyinfra/facts/dnf.py +11 -6
  35. pyinfra/facts/docker.py +30 -5
  36. pyinfra/facts/files.py +49 -33
  37. pyinfra/facts/gem.py +7 -2
  38. pyinfra/facts/git.py +14 -21
  39. pyinfra/facts/gpg.py +4 -1
  40. pyinfra/facts/hardware.py +186 -138
  41. pyinfra/facts/launchd.py +7 -2
  42. pyinfra/facts/lxd.py +8 -2
  43. pyinfra/facts/mysql.py +19 -12
  44. pyinfra/facts/npm.py +3 -1
  45. pyinfra/facts/openrc.py +8 -2
  46. pyinfra/facts/pacman.py +13 -5
  47. pyinfra/facts/pip.py +2 -0
  48. pyinfra/facts/pkg.py +5 -1
  49. pyinfra/facts/pkgin.py +7 -2
  50. pyinfra/facts/postgres.py +170 -0
  51. pyinfra/facts/postgresql.py +5 -162
  52. pyinfra/facts/rpm.py +21 -15
  53. pyinfra/facts/runit.py +70 -0
  54. pyinfra/facts/selinux.py +12 -4
  55. pyinfra/facts/server.py +240 -82
  56. pyinfra/facts/snap.py +8 -2
  57. pyinfra/facts/systemd.py +37 -13
  58. pyinfra/facts/sysvinit.py +7 -4
  59. pyinfra/facts/upstart.py +7 -2
  60. pyinfra/facts/util/packaging.py +3 -2
  61. pyinfra/facts/vzctl.py +8 -4
  62. pyinfra/facts/xbps.py +7 -2
  63. pyinfra/facts/yum.py +10 -5
  64. pyinfra/facts/zypper.py +9 -4
  65. pyinfra/operations/apk.py +5 -3
  66. pyinfra/operations/apt.py +28 -25
  67. pyinfra/operations/brew.py +60 -29
  68. pyinfra/operations/bsdinit.py +6 -4
  69. pyinfra/operations/cargo.py +3 -1
  70. pyinfra/operations/choco.py +3 -1
  71. pyinfra/operations/dnf.py +16 -20
  72. pyinfra/operations/docker.py +339 -0
  73. pyinfra/operations/files.py +187 -168
  74. pyinfra/operations/gem.py +3 -1
  75. pyinfra/operations/git.py +23 -25
  76. pyinfra/operations/iptables.py +33 -25
  77. pyinfra/operations/launchd.py +5 -6
  78. pyinfra/operations/lxd.py +7 -4
  79. pyinfra/operations/mysql.py +59 -55
  80. pyinfra/operations/npm.py +8 -1
  81. pyinfra/operations/openrc.py +5 -3
  82. pyinfra/operations/pacman.py +6 -7
  83. pyinfra/operations/pip.py +19 -12
  84. pyinfra/operations/pkg.py +3 -1
  85. pyinfra/operations/pkgin.py +5 -3
  86. pyinfra/operations/postgres.py +349 -0
  87. pyinfra/operations/postgresql.py +18 -335
  88. pyinfra/operations/puppet.py +3 -1
  89. pyinfra/operations/python.py +8 -19
  90. pyinfra/operations/runit.py +182 -0
  91. pyinfra/operations/selinux.py +47 -29
  92. pyinfra/operations/server.py +138 -67
  93. pyinfra/operations/snap.py +3 -1
  94. pyinfra/operations/ssh.py +18 -16
  95. pyinfra/operations/systemd.py +18 -12
  96. pyinfra/operations/sysvinit.py +7 -5
  97. pyinfra/operations/upstart.py +7 -5
  98. pyinfra/operations/util/__init__.py +12 -0
  99. pyinfra/operations/util/docker.py +177 -0
  100. pyinfra/operations/util/files.py +24 -16
  101. pyinfra/operations/util/packaging.py +54 -38
  102. pyinfra/operations/util/service.py +39 -47
  103. pyinfra/operations/vzctl.py +12 -10
  104. pyinfra/operations/xbps.py +5 -3
  105. pyinfra/operations/yum.py +15 -19
  106. pyinfra/operations/zypper.py +9 -10
  107. pyinfra/version.py +5 -2
  108. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/METADATA +51 -58
  109. pyinfra-3.0.1.dist-info/RECORD +168 -0
  110. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/WHEEL +1 -1
  111. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/entry_points.txt +0 -3
  112. pyinfra_cli/__main__.py +4 -3
  113. pyinfra_cli/commands.py +3 -2
  114. pyinfra_cli/exceptions.py +75 -43
  115. pyinfra_cli/inventory.py +52 -31
  116. pyinfra_cli/log.py +10 -2
  117. pyinfra_cli/main.py +88 -65
  118. pyinfra_cli/prints.py +37 -109
  119. pyinfra_cli/util.py +15 -10
  120. tests/test_api/test_api.py +2 -0
  121. tests/test_api/test_api_arguments.py +9 -9
  122. tests/test_api/test_api_deploys.py +15 -19
  123. tests/test_api/test_api_facts.py +4 -5
  124. tests/test_api/test_api_operations.py +18 -20
  125. tests/test_api/test_api_util.py +41 -2
  126. tests/test_cli/test_cli.py +14 -50
  127. tests/test_cli/test_cli_deploy.py +10 -12
  128. tests/test_cli/test_cli_exceptions.py +50 -19
  129. tests/test_cli/test_cli_inventory.py +66 -0
  130. tests/test_cli/util.py +1 -1
  131. tests/test_connectors/test_dockerssh.py +11 -8
  132. tests/test_connectors/test_ssh.py +88 -23
  133. tests/test_connectors/test_sshuserclient.py +1 -1
  134. tests/test_connectors/test_terraform.py +11 -8
  135. tests/test_connectors/test_vagrant.py +6 -6
  136. pyinfra/connectors/ansible.py +0 -175
  137. pyinfra/connectors/mech.py +0 -189
  138. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  139. pyinfra/connectors/winrm.py +0 -312
  140. pyinfra/facts/windows.py +0 -366
  141. pyinfra/facts/windows_files.py +0 -90
  142. pyinfra/operations/windows.py +0 -59
  143. pyinfra/operations/windows_files.py +0 -538
  144. pyinfra-3.0.dev0.dist-info/RECORD +0 -170
  145. tests/test_connectors/test_ansible.py +0 -64
  146. tests/test_connectors/test_mech.py +0 -126
  147. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/LICENSE.md +0 -0
  148. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/top_level.txt +0 -0
pyinfra/api/operations.py CHANGED
@@ -10,11 +10,11 @@ import gevent
10
10
  from paramiko import SSHException
11
11
 
12
12
  from pyinfra import logger
13
- from pyinfra.connectors.util import CommandOutput
13
+ from pyinfra.connectors.util import CommandOutput, OutputLine
14
14
  from pyinfra.context import ctx_host, ctx_state
15
15
  from pyinfra.progress import progress_spinner
16
16
 
17
- from .arguments import ConnectorArguments, get_connector_argument_keys
17
+ from .arguments import CONNECTOR_ARGUMENT_KEYS, ConnectorArguments
18
18
  from .command import FunctionCommand, PyinfraCommand, StringCommand
19
19
  from .exceptions import PyinfraError
20
20
  from .util import (
@@ -22,7 +22,6 @@ from .util import (
22
22
  log_error_or_warning,
23
23
  log_host_command_error,
24
24
  log_operation_start,
25
- memoize,
26
25
  print_host_combined_output,
27
26
  )
28
27
 
@@ -31,92 +30,53 @@ if TYPE_CHECKING:
31
30
  from .state import State
32
31
 
33
32
 
34
- @memoize
35
- def show_pre_or_post_condition_warning(condition_name):
36
- logger.warning("The `{0}` argument is in beta!".format(condition_name))
33
+ # Run a single host operation
34
+ #
37
35
 
38
36
 
39
- def run_host_op(state: "State", host: "Host", op_hash) -> Optional[bool]:
37
+ def run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
40
38
  state.trigger_callbacks("operation_host_start", host, op_hash)
41
39
 
42
40
  if op_hash not in state.ops[host]:
43
41
  logger.info("{0}{1}".format(host.print_prefix, click.style("Skipped", "blue")))
44
42
  return True
45
43
 
46
- op_data = state.get_op_data_for_host(host, op_hash)
47
- global_arguments = op_data.global_arguments
48
-
49
44
  op_meta = state.get_op_meta(op_hash)
50
-
51
- ignore_errors = global_arguments["_ignore_errors"]
52
- continue_on_error = global_arguments["_continue_on_error"]
53
-
54
45
  logger.debug("Starting operation %r on %s", op_meta.names, host)
55
46
 
56
- executor_kwarg_keys = get_connector_argument_keys()
57
- base_connector_arguments: ConnectorArguments = (
58
- cast( # https://github.com/python/mypy/issues/10371
59
- ConnectorArguments,
60
- {key: global_arguments[key] for key in executor_kwarg_keys if key in global_arguments},
61
- )
62
- )
63
-
64
- def _run_shell_command(command, connector_arguments):
65
- status = False
66
- combined_output_lines = CommandOutput([])
67
-
68
- try:
69
- status, combined_output_lines = command.execute(state, host, connector_arguments)
70
- except (timeout_error, socket_error, SSHException) as e:
71
- log_host_command_error(
72
- host,
73
- e,
74
- timeout=global_arguments["_timeout"],
75
- )
76
-
77
- # If we failed and have no already printed the stderr, print it
78
- if status is False and not state.print_output:
79
- print_host_combined_output(host, combined_output_lines)
80
-
81
- return status, combined_output_lines
82
-
83
- def run_condition(condition_name: str) -> bool:
84
- condition_value = global_arguments[condition_name]
85
- if not condition_value:
86
- return True
87
-
88
- show_pre_or_post_condition_warning(condition_name)
89
-
90
- _shell_command_status, _ = _run_shell_command(
91
- StringCommand(condition_value),
92
- base_connector_arguments,
93
- )
94
-
95
- if _shell_command_status:
96
- return True
47
+ if host.executing_op_hash is None:
48
+ host.executing_op_hash = op_hash
49
+ else:
50
+ host.nested_executing_op_hash = op_hash
97
51
 
98
- _log_msg = f"{condition_name} failed: {condition_value}"
99
- log_error_or_warning(host, ignore_errors, description=_log_msg)
52
+ try:
53
+ return _run_host_op(state, host, op_hash)
54
+ finally:
55
+ if host.nested_executing_op_hash:
56
+ host.nested_executing_op_hash = None
57
+ else:
58
+ host.executing_op_hash = None
100
59
 
101
- if ignore_errors:
102
- return True
103
60
 
104
- state.trigger_callbacks("operation_host_error", host, op_hash)
105
- return False
61
+ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
62
+ op_data = state.get_op_data_for_host(host, op_hash)
63
+ global_arguments = op_data.global_arguments
106
64
 
107
- if not run_condition("_precondition"):
108
- return False
65
+ ignore_errors = global_arguments["_ignore_errors"]
66
+ continue_on_error = global_arguments["_continue_on_error"]
67
+ timeout = global_arguments.get("_timeout", 0)
109
68
 
110
- if host.executing_op_hash is None:
111
- host.executing_op_hash = op_hash
112
- else:
113
- host.nested_executing_op_hash = op_hash
69
+ executor_kwarg_keys = CONNECTOR_ARGUMENT_KEYS
70
+ # See: https://github.com/python/mypy/issues/10371
71
+ base_connector_arguments: ConnectorArguments = cast(
72
+ ConnectorArguments,
73
+ {key: global_arguments[key] for key in executor_kwarg_keys if key in global_arguments}, # type: ignore[literal-required] # noqa
74
+ )
114
75
 
115
- return_status: Optional[bool] = False
116
76
  did_error = False
117
77
  executed_commands = 0
118
78
  commands = []
119
- all_combined_output_lines = []
79
+ all_output_lines: list[OutputLine] = []
120
80
 
121
81
  for command in op_data.command_generator():
122
82
  commands.append(command)
@@ -126,54 +86,58 @@ def run_host_op(state: "State", host: "Host", op_hash) -> Optional[bool]:
126
86
  connector_arguments = base_connector_arguments.copy()
127
87
  connector_arguments.update(command.connector_arguments)
128
88
 
129
- # Now we attempt to execute the command
130
- #
131
-
132
89
  if not isinstance(command, PyinfraCommand):
133
90
  raise TypeError("{0} is an invalid pyinfra command!".format(command))
134
91
 
135
92
  if isinstance(command, FunctionCommand):
136
93
  try:
137
94
  status = command.execute(state, host, connector_arguments)
138
- except Exception as e: # Custom functions could do anything, so expect anything!
139
- _formatted_exc = format_exception(e)
140
- _error_msg = "Unexpected error in Python callback: {0}".format(_formatted_exc)
141
- _error_msg_styled = click.style(_error_msg, "red")
142
- _error_log = "{0}{1}".format(host.print_prefix, _error_msg_styled)
95
+ except Exception as e:
96
+ # Custom functions could do anything, so expect anything!
143
97
  logger.warning(traceback.format_exc())
144
- logger.error(_error_log)
98
+ host.log_styled(
99
+ f"Unexpected error in Python callback: {format_exception(e)}",
100
+ fg="red",
101
+ log_func=logger.warning,
102
+ )
145
103
 
146
104
  elif isinstance(command, StringCommand):
147
- status, combined_output_lines = _run_shell_command(command, connector_arguments)
148
- all_combined_output_lines.extend(combined_output_lines)
105
+ output_lines = CommandOutput([])
106
+ try:
107
+ status, output_lines = command.execute(
108
+ state,
109
+ host,
110
+ connector_arguments,
111
+ )
112
+ except (timeout_error, socket_error, SSHException) as e:
113
+ log_host_command_error(host, e, timeout=timeout)
114
+ all_output_lines.extend(output_lines)
115
+ # If we failed and have not already printed the stderr, print it
116
+ if status is False and not state.print_output:
117
+ print_host_combined_output(host, output_lines)
149
118
 
150
119
  else:
151
120
  try:
152
121
  status = command.execute(state, host, connector_arguments)
153
122
  except (timeout_error, socket_error, SSHException, IOError) as e:
154
- _timeout = global_arguments["_timeout"]
155
- log_host_command_error(host, e, timeout=_timeout)
123
+ log_host_command_error(host, e, timeout=timeout)
156
124
 
157
125
  # Break the loop to trigger a failure
158
126
  if status is False:
127
+ did_error = True
159
128
  if continue_on_error is True:
160
- did_error = True
161
129
  continue
162
130
  break
163
131
 
164
132
  executed_commands += 1
165
133
 
166
- # Commands didn't break, so count our successes & return True!
167
- else:
168
- if not run_condition("_postcondition"):
169
- return False
170
-
171
- if not did_error:
172
- return_status = True
134
+ # Handle results
135
+ #
173
136
 
137
+ op_success = return_status = not did_error
174
138
  host_results = state.get_results_for_host(host)
175
139
 
176
- if return_status is True:
140
+ if did_error is False:
177
141
  host_results.ops += 1
178
142
  host_results.success_ops += 1
179
143
 
@@ -181,10 +145,6 @@ def run_host_op(state: "State", host: "Host", op_hash) -> Optional[bool]:
181
145
  _click_log_status = click.style(_status_log, "green")
182
146
  logger.info("{0}{1}".format(host.print_prefix, _click_log_status))
183
147
 
184
- # Trigger any success handler
185
- if global_arguments["_on_success"]:
186
- global_arguments["_on_success"](state, host, op_hash)
187
-
188
148
  state.trigger_callbacks("operation_host_success", host, op_hash)
189
149
  else:
190
150
  if ignore_errors:
@@ -198,31 +158,25 @@ def run_host_op(state: "State", host: "Host", op_hash) -> Optional[bool]:
198
158
  _command_description = f"executed {executed_commands} commands"
199
159
  log_error_or_warning(host, ignore_errors, _command_description, continue_on_error)
200
160
 
201
- # Always trigger any error handler
202
- if global_arguments["_on_error"]:
203
- global_arguments["_on_error"](state, host, op_hash)
204
-
205
161
  # Ignored, op "completes" w/ ignored error
206
162
  if ignore_errors:
207
163
  host_results.ops += 1
164
+ return_status = True
208
165
 
209
166
  # Unignored error -> False
210
167
  state.trigger_callbacks("operation_host_error", host, op_hash)
211
168
 
212
- if ignore_errors:
213
- return_status = True
169
+ op_data.operation_meta.set_complete(
170
+ op_success,
171
+ commands,
172
+ CommandOutput(all_output_lines),
173
+ )
214
174
 
215
- if return_status is False or (return_status is True and executed_commands > 0):
216
- op_data.operation_meta.set_result(return_status)
217
- op_data.operation_meta.set_commands(commands)
218
- op_data.operation_meta.set_combined_output_lines(all_combined_output_lines)
175
+ return return_status
219
176
 
220
- if host.nested_executing_op_hash:
221
- host.nested_executing_op_hash = None
222
- else:
223
- host.executing_op_hash = None
224
177
 
225
- return return_status
178
+ # Run all operations strategies
179
+ #
226
180
 
227
181
 
228
182
  def _run_host_op_with_context(state: "State", host: "Host", op_hash: str):
@@ -323,10 +277,9 @@ def _run_single_op(state: "State", op_hash: str):
323
277
  batches = [list(state.inventory.iter_active_hosts())]
324
278
 
325
279
  # If parallel set break up the inventory into a series of batches
326
- if op_meta.global_arguments["_parallel"]:
327
- parallel = op_meta.global_arguments["_parallel"]
280
+ parallel = op_meta.global_arguments["_parallel"]
281
+ if parallel:
328
282
  hosts = list(state.inventory.iter_active_hosts())
329
-
330
283
  batches = [hosts[i : i + parallel] for i in range(0, len(hosts), parallel)]
331
284
 
332
285
  for batch in batches:
pyinfra/api/state.py CHANGED
@@ -1,11 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from contextlib import contextmanager
3
+ from collections import defaultdict
4
4
  from dataclasses import dataclass
5
+ from enum import IntEnum
5
6
  from graphlib import CycleError, TopologicalSorter
6
7
  from multiprocessing import cpu_count
7
- from typing import TYPE_CHECKING, Iterator, Optional
8
- from uuid import uuid4
8
+ from typing import TYPE_CHECKING, Callable, Iterator, Optional
9
9
 
10
10
  from gevent.pool import Pool
11
11
  from paramiko import PKey
@@ -14,7 +14,6 @@ from pyinfra import logger
14
14
 
15
15
  from .config import Config
16
16
  from .exceptions import PyinfraError
17
- from .util import sha1_hash
18
17
 
19
18
  if TYPE_CHECKING:
20
19
  from pyinfra.api.arguments import AllArguments
@@ -83,22 +82,35 @@ class BaseStateCallback:
83
82
  pass
84
83
 
85
84
 
85
+ class StateStage(IntEnum):
86
+ # Setup - collect inventory & data
87
+ Setup = 1
88
+ # Connect - connect to the inventory
89
+ Connect = 2
90
+ # Prepare - detect operation changes
91
+ Prepare = 3
92
+ # Execute - execute operations
93
+ Execute = 4
94
+ # Disconnect - disconnect from the inventory
95
+ Disconnect = 5
96
+
97
+
86
98
  class StateOperationMeta:
87
99
  names: set[str]
88
100
  args: list[str]
89
- op_order: tuple[int]
101
+ op_order: tuple[int, ...]
90
102
  global_arguments: "AllArguments"
91
103
 
92
- def __init__(self, op_order: tuple[int]):
104
+ def __init__(self, op_order: tuple[int, ...]):
93
105
  self.op_order = op_order
94
106
  self.names = set()
95
107
  self.args = []
96
- self.global_arguments = {}
108
+ self.global_arguments = {} # type: ignore
97
109
 
98
110
 
99
111
  @dataclass
100
112
  class StateOperationHostData:
101
- command_generator: Iterator["PyinfraCommand"]
113
+ command_generator: Callable[[], Iterator["PyinfraCommand"]]
102
114
  global_arguments: "AllArguments"
103
115
  operation_meta: "OperationMeta"
104
116
  parent_op_hash: Optional[str] = None
@@ -138,6 +150,11 @@ class State:
138
150
  # Main gevent pool
139
151
  pool: "Pool"
140
152
 
153
+ # Current stage this state is in
154
+ current_stage: StateStage = StateStage.Setup
155
+ # Warning counters by stage
156
+ stage_warnings: dict[StateStage, int] = defaultdict(int)
157
+
141
158
  # Whether we are executing operations (ie hosts are all ready)
142
159
  is_executing: bool = False
143
160
 
@@ -157,6 +174,7 @@ class State:
157
174
  current_deploy_filename: Optional[str] = None
158
175
  current_exec_filename: Optional[str] = None
159
176
  current_op_file_number: int = 0
177
+ should_raise_failed_hosts: Optional[Callable[["State"], bool]] = None
160
178
 
161
179
  def __init__(
162
180
  self,
@@ -165,7 +183,8 @@ class State:
165
183
  check_for_changes: bool = True,
166
184
  **kwargs,
167
185
  ):
168
- """Initializes the state, the main Pyinfra
186
+ """
187
+ Initializes the state, the main Pyinfra
169
188
 
170
189
  Args:
171
190
  inventory (Optional[Inventory], optional): The inventory. Defaults to None.
@@ -254,6 +273,17 @@ class State:
254
273
 
255
274
  self.initialised = True
256
275
 
276
+ def set_stage(self, stage: StateStage) -> None:
277
+ if stage < self.current_stage:
278
+ raise Exception("State stage cannot go backwards!")
279
+ self.current_stage = stage
280
+
281
+ def increment_warning_counter(self) -> None:
282
+ self.stage_warnings[self.current_stage] += 1
283
+
284
+ def get_warning_counter(self) -> int:
285
+ return self.stage_warnings[self.current_stage]
286
+
257
287
  def should_check_for_changes(self):
258
288
  return self.check_for_changes
259
289
 
@@ -269,16 +299,6 @@ class State:
269
299
  func = getattr(handler, method_name)
270
300
  func(self, *args, **kwargs)
271
301
 
272
- @contextmanager
273
- def preserve_loop_order(self, items):
274
- logger.warning(
275
- (
276
- "Using `state.preserve_loop_order` is not longer required for operations to be "
277
- "executed in correct loop order and can be safely removed."
278
- ),
279
- )
280
- yield lambda: items
281
-
282
302
  def get_op_order(self):
283
303
  ts: TopologicalSorter = TopologicalSorter()
284
304
 
@@ -324,10 +344,19 @@ class State:
324
344
  def get_results_for_host(self, host: "Host") -> StateHostResults:
325
345
  return self.results[host]
326
346
 
327
- def get_op_data_for_host(self, host: "Host", op_hash: str):
347
+ def get_op_data_for_host(
348
+ self,
349
+ host: "Host",
350
+ op_hash: str,
351
+ ) -> StateOperationHostData:
328
352
  return self.ops[host][op_hash]
329
353
 
330
- def set_op_data_for_host(self, host: "Host", op_hash: str, op_data: StateOperationHostData):
354
+ def set_op_data_for_host(
355
+ self,
356
+ host: "Host",
357
+ op_hash: str,
358
+ op_data: StateOperationHostData,
359
+ ):
331
360
  self.ops[host][op_hash] = op_data
332
361
 
333
362
  def activate_host(self, host: "Host"):
@@ -375,6 +404,9 @@ class State:
375
404
  percent_failed = (1 - len(active_hosts) / activated_count) * 100
376
405
 
377
406
  if percent_failed > self.config.FAIL_PERCENT:
407
+ if self.should_raise_failed_hosts and self.should_raise_failed_hosts(self) is False:
408
+ return
409
+
378
410
  raise PyinfraError(
379
411
  "Over {0}% of hosts failed ({1}%)".format(
380
412
  self.config.FAIL_PERCENT,
@@ -392,16 +424,3 @@ class State:
392
424
  if not isinstance(limit_hosts, list):
393
425
  return True
394
426
  return host in limit_hosts
395
-
396
- def get_temp_filename(self, hash_key: Optional[str] = None, hash_filename: bool = True):
397
- """
398
- Generate a temporary filename for this deploy.
399
- """
400
-
401
- if not hash_key:
402
- hash_key = str(uuid4())
403
-
404
- if hash_filename:
405
- hash_key = sha1_hash(hash_key)
406
-
407
- return "{0}/pyinfra-{1}".format(self.config.TEMP_DIR, hash_key)
pyinfra/api/util.py CHANGED
@@ -1,13 +1,17 @@
1
+ from __future__ import annotations
2
+
1
3
  from functools import wraps
2
4
  from hashlib import sha1
3
5
  from inspect import getframeinfo, stack
6
+ from io import BytesIO, StringIO
4
7
  from os import getcwd, path, stat
5
8
  from socket import error as socket_error, timeout as timeout_error
6
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
9
+ from typing import IO, TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union
7
10
 
8
11
  import click
9
12
  from jinja2 import Environment, FileSystemLoader, StrictUndefined
10
13
  from paramiko import SSHException
14
+ from typeguard import TypeCheckError, check_type
11
15
 
12
16
  import pyinfra
13
17
  from pyinfra import logger
@@ -24,7 +28,7 @@ BLOCKSIZE = 65536
24
28
  TEMPLATES: Dict[Any, Any] = {}
25
29
  FILE_SHAS: Dict[Any, Any] = {}
26
30
 
27
- PYINFRA_API_DIR = path.dirname(__file__)
31
+ PYINFRA_INSTALL_DIR = path.normpath(path.join(path.dirname(__file__), ".."))
28
32
 
29
33
 
30
34
  def get_file_path(state: "State", filename: str):
@@ -105,16 +109,16 @@ def get_caller_frameinfo(frame_offset: int = 0):
105
109
 
106
110
 
107
111
  def get_operation_order_from_stack(state: "State"):
112
+
108
113
  stack_items = list(reversed(stack()))
109
114
 
115
+ i = 0
110
116
  # Find the *first* occurrence of our deploy file in the reversed stack
111
117
  if state.current_deploy_filename:
112
118
  for i, stack_item in enumerate(stack_items):
113
119
  frame = getframeinfo(stack_item[0])
114
120
  if frame.filename == state.current_deploy_filename:
115
121
  break
116
- else:
117
- i = 0
118
122
 
119
123
  # Now generate a list of line numbers *following that file*
120
124
  line_numbers = []
@@ -125,7 +129,7 @@ def get_operation_order_from_stack(state: "State"):
125
129
  for stack_item in stack_items[i:]:
126
130
  frame = getframeinfo(stack_item[0])
127
131
 
128
- if frame.filename.startswith(PYINFRA_API_DIR):
132
+ if frame.filename.startswith(PYINFRA_INSTALL_DIR):
129
133
  continue
130
134
 
131
135
  line_numbers.append(frame.lineno)
@@ -135,7 +139,7 @@ def get_operation_order_from_stack(state: "State"):
135
139
  return line_numbers
136
140
 
137
141
 
138
- def get_template(filename_or_io: str):
142
+ def get_template(filename_or_io: str | IO):
139
143
  """
140
144
  Gets a jinja2 ``Template`` object for the input filename or string, with caching
141
145
  based on the filename of the template, or the SHA1 of the input string.
@@ -237,7 +241,7 @@ def log_error_or_warning(
237
241
  )
238
242
 
239
243
 
240
- def log_host_command_error(host: "Host", e: Exception, timeout: int = 0) -> None:
244
+ def log_host_command_error(host: "Host", e: Exception, timeout: int | None = 0) -> None:
241
245
  if isinstance(e, timeout_error):
242
246
  logger.error(
243
247
  "{0}{1}".format(
@@ -297,19 +301,27 @@ def make_hash(obj):
297
301
  if isinstance(obj, int)
298
302
  # Constants - the values can change between hosts but we should still
299
303
  # group them under the same operation hash.
300
- else "_PYINFRA_CONSTANT"
301
- if obj in (True, False, None)
302
- # Plain strings
303
- else obj
304
- if isinstance(obj, str)
305
- # Objects with __name__s
306
- else obj.__name__
307
- if hasattr(obj, "__name__")
308
- # Objects with names
309
- else obj.name
310
- if hasattr(obj, "name")
311
- # Repr anything else
312
- else repr(obj)
304
+ else (
305
+ "_PYINFRA_CONSTANT"
306
+ if obj in (True, False, None)
307
+ # Plain strings
308
+ else (
309
+ obj
310
+ if isinstance(obj, str)
311
+ # Objects with __name__s
312
+ else (
313
+ obj.__name__
314
+ if hasattr(obj, "__name__")
315
+ # Objects with names
316
+ else (
317
+ obj.name
318
+ if hasattr(obj, "name")
319
+ # Repr anything else
320
+ else repr(obj)
321
+ )
322
+ )
323
+ )
324
+ )
313
325
  )
314
326
 
315
327
  return sha1_hash(hash_string)
@@ -321,7 +333,11 @@ class get_file_io:
321
333
  will open and close filenames, and leave IO objects alone.
322
334
  """
323
335
 
324
- close = False
336
+ filename_or_io: Union[str, IO[Any]]
337
+ mode: str
338
+
339
+ _close: bool = False
340
+ _file_io: IO[Any]
325
341
 
326
342
  def __init__(self, filename_or_io, mode="rb"):
327
343
  if not (
@@ -336,29 +352,32 @@ class get_file_io:
336
352
  ),
337
353
  )
338
354
 
355
+ # Convert any StringIO/BytesIO to the other to match the desired mode
356
+ if isinstance(filename_or_io, StringIO) and mode == "rb":
357
+ filename_or_io.seek(0)
358
+ filename_or_io = BytesIO(filename_or_io.read().encode())
359
+ if isinstance(filename_or_io, BytesIO) and mode == "r":
360
+ filename_or_io.seek(0)
361
+ filename_or_io = StringIO(filename_or_io.read().decode())
362
+
339
363
  self.filename_or_io = filename_or_io
340
364
  self.mode = mode
341
365
 
342
366
  def __enter__(self):
343
- # If we have a read attribute, just use the object as-is
344
- if hasattr(self.filename_or_io, "read"):
345
- file_io = self.filename_or_io
346
-
347
- # Otherwise, assume a filename and open it up
348
- else:
367
+ if isinstance(self.filename_or_io, str):
349
368
  file_io = open(self.filename_or_io, self.mode)
350
-
351
- # Attach to self for closing on __exit__
352
- self.file_io = file_io
353
- self.close = True
369
+ self._file_io = file_io
370
+ self._close = True
371
+ else:
372
+ file_io = self.filename_or_io
354
373
 
355
374
  # Ensure we're at the start of the file
356
375
  file_io.seek(0)
357
376
  return file_io
358
377
 
359
378
  def __exit__(self, type, value, traceback):
360
- if self.close:
361
- self.file_io.close()
379
+ if self._close:
380
+ self._file_io.close()
362
381
 
363
382
  @property
364
383
  def cache_key(self):
@@ -405,3 +424,15 @@ def get_path_permissions_mode(pathname: str):
405
424
 
406
425
  mode_octal = oct(stat(pathname).st_mode)
407
426
  return mode_octal[-3:]
427
+
428
+
429
+ def raise_if_bad_type(
430
+ value: Any,
431
+ type_: Type,
432
+ exception: type[Exception],
433
+ message_prefix: str,
434
+ ):
435
+ try:
436
+ check_type(value, type_)
437
+ except TypeCheckError as e:
438
+ raise exception(f"{message_prefix}: {e}")