pyinfra 2.9.2__py2.py3-none-any.whl → 3.0__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 (156) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +265 -253
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +68 -53
  5. pyinfra/api/config.py +139 -32
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +7 -26
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +102 -137
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +21 -25
  13. pyinfra/api/operation.py +240 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +79 -86
  17. pyinfra/connectors/base.py +147 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +220 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +196 -208
  22. pyinfra/connectors/ssh.py +530 -613
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +211 -137
  27. pyinfra/connectors/vagrant.py +60 -53
  28. pyinfra/context.py +4 -2
  29. pyinfra/facts/apk.py +2 -0
  30. pyinfra/facts/apt.py +2 -0
  31. pyinfra/facts/brew.py +2 -0
  32. pyinfra/facts/bsdinit.py +2 -0
  33. pyinfra/facts/cargo.py +2 -0
  34. pyinfra/facts/choco.py +2 -0
  35. pyinfra/facts/deb.py +7 -2
  36. pyinfra/facts/dnf.py +2 -0
  37. pyinfra/facts/docker.py +19 -0
  38. pyinfra/facts/files.py +47 -32
  39. pyinfra/facts/gem.py +2 -0
  40. pyinfra/facts/git.py +3 -1
  41. pyinfra/facts/gpg.py +3 -1
  42. pyinfra/facts/hardware.py +34 -24
  43. pyinfra/facts/iptables.py +5 -3
  44. pyinfra/facts/launchd.py +2 -0
  45. pyinfra/facts/lxd.py +2 -0
  46. pyinfra/facts/mysql.py +13 -6
  47. pyinfra/facts/npm.py +1 -0
  48. pyinfra/facts/openrc.py +2 -0
  49. pyinfra/facts/pacman.py +6 -2
  50. pyinfra/facts/pip.py +2 -0
  51. pyinfra/facts/pkg.py +2 -0
  52. pyinfra/facts/pkgin.py +2 -0
  53. pyinfra/facts/postgres.py +168 -0
  54. pyinfra/facts/postgresql.py +6 -160
  55. pyinfra/facts/rpm.py +12 -9
  56. pyinfra/facts/runit.py +68 -0
  57. pyinfra/facts/selinux.py +3 -1
  58. pyinfra/facts/server.py +80 -36
  59. pyinfra/facts/snap.py +2 -0
  60. pyinfra/facts/systemd.py +31 -12
  61. pyinfra/facts/sysvinit.py +10 -10
  62. pyinfra/facts/upstart.py +2 -0
  63. pyinfra/facts/util/packaging.py +7 -4
  64. pyinfra/facts/vzctl.py +2 -0
  65. pyinfra/facts/xbps.py +2 -0
  66. pyinfra/facts/yum.py +2 -0
  67. pyinfra/facts/zypper.py +2 -0
  68. pyinfra/local.py +4 -5
  69. pyinfra/operations/apk.py +6 -4
  70. pyinfra/operations/apt.py +46 -65
  71. pyinfra/operations/brew.py +17 -22
  72. pyinfra/operations/bsdinit.py +9 -7
  73. pyinfra/operations/cargo.py +4 -2
  74. pyinfra/operations/choco.py +4 -2
  75. pyinfra/operations/dnf.py +19 -23
  76. pyinfra/operations/docker.py +339 -0
  77. pyinfra/operations/files.py +188 -386
  78. pyinfra/operations/gem.py +4 -2
  79. pyinfra/operations/git.py +24 -53
  80. pyinfra/operations/iptables.py +29 -35
  81. pyinfra/operations/launchd.py +6 -7
  82. pyinfra/operations/lxd.py +8 -13
  83. pyinfra/operations/mysql.py +62 -81
  84. pyinfra/operations/npm.py +9 -2
  85. pyinfra/operations/openrc.py +6 -4
  86. pyinfra/operations/pacman.py +7 -8
  87. pyinfra/operations/pip.py +25 -24
  88. pyinfra/operations/pkg.py +4 -2
  89. pyinfra/operations/pkgin.py +6 -4
  90. pyinfra/operations/postgres.py +349 -0
  91. pyinfra/operations/postgresql.py +18 -379
  92. pyinfra/operations/puppet.py +3 -1
  93. pyinfra/operations/python.py +8 -19
  94. pyinfra/operations/runit.py +182 -0
  95. pyinfra/operations/selinux.py +47 -44
  96. pyinfra/operations/server.py +111 -127
  97. pyinfra/operations/snap.py +4 -4
  98. pyinfra/operations/ssh.py +20 -33
  99. pyinfra/operations/systemd.py +19 -15
  100. pyinfra/operations/sysvinit.py +9 -16
  101. pyinfra/operations/upstart.py +9 -7
  102. pyinfra/operations/util/__init__.py +12 -0
  103. pyinfra/operations/util/docker.py +177 -0
  104. pyinfra/operations/util/files.py +24 -16
  105. pyinfra/operations/util/packaging.py +55 -57
  106. pyinfra/operations/util/service.py +39 -51
  107. pyinfra/operations/vzctl.py +12 -10
  108. pyinfra/operations/xbps.py +6 -4
  109. pyinfra/operations/yum.py +18 -22
  110. pyinfra/operations/zypper.py +12 -13
  111. pyinfra/version.py +5 -2
  112. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
  113. pyinfra-3.0.dist-info/RECORD +167 -0
  114. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
  115. pyinfra-3.0.dist-info/entry_points.txt +11 -0
  116. pyinfra_cli/__main__.py +4 -3
  117. pyinfra_cli/commands.py +7 -2
  118. pyinfra_cli/exceptions.py +78 -42
  119. pyinfra_cli/inventory.py +40 -6
  120. pyinfra_cli/log.py +17 -3
  121. pyinfra_cli/main.py +133 -90
  122. pyinfra_cli/prints.py +95 -127
  123. pyinfra_cli/util.py +62 -29
  124. tests/test_api/test_api.py +2 -0
  125. tests/test_api/test_api_arguments.py +13 -13
  126. tests/test_api/test_api_deploys.py +28 -29
  127. tests/test_api/test_api_facts.py +60 -98
  128. tests/test_api/test_api_operations.py +101 -201
  129. tests/test_cli/test_cli.py +18 -49
  130. tests/test_cli/test_cli_deploy.py +11 -37
  131. tests/test_cli/test_cli_exceptions.py +50 -19
  132. tests/test_cli/util.py +1 -1
  133. tests/test_connectors/test_chroot.py +6 -6
  134. tests/test_connectors/test_docker.py +4 -4
  135. tests/test_connectors/test_dockerssh.py +38 -50
  136. tests/test_connectors/test_local.py +11 -12
  137. tests/test_connectors/test_ssh.py +105 -93
  138. tests/test_connectors/test_terraform.py +9 -15
  139. tests/test_connectors/test_util.py +24 -46
  140. tests/test_connectors/test_vagrant.py +7 -7
  141. pyinfra/api/operation.pyi +0 -117
  142. pyinfra/connectors/ansible.py +0 -171
  143. pyinfra/connectors/mech.py +0 -186
  144. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  145. pyinfra/connectors/winrm.py +0 -320
  146. pyinfra/facts/windows.py +0 -366
  147. pyinfra/facts/windows_files.py +0 -90
  148. pyinfra/operations/windows.py +0 -59
  149. pyinfra/operations/windows_files.py +0 -551
  150. pyinfra-2.9.2.dist-info/RECORD +0 -170
  151. pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
  152. tests/test_connectors/test_ansible.py +0 -64
  153. tests/test_connectors/test_mech.py +0 -126
  154. tests/test_connectors/test_winrm.py +0 -76
  155. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
  156. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
pyinfra/api/host.py CHANGED
@@ -1,22 +1,41 @@
1
+ from __future__ import annotations
2
+
1
3
  from contextlib import contextmanager
2
- from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Union
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Callable,
8
+ Generator,
9
+ Optional,
10
+ Type,
11
+ TypeVar,
12
+ Union,
13
+ cast,
14
+ overload,
15
+ )
16
+ from uuid import uuid4
3
17
 
4
18
  import click
5
- from gevent.lock import BoundedSemaphore
19
+ from typing_extensions import Unpack
6
20
 
7
21
  from pyinfra import logger
8
- from pyinfra.connectors.util import remove_any_sudo_askpass_file
22
+ from pyinfra.connectors.base import BaseConnector
23
+ from pyinfra.connectors.util import CommandOutput, remove_any_sudo_askpass_file
9
24
 
10
25
  from .connectors import get_execution_connector
11
26
  from .exceptions import ConnectError
12
- from .facts import create_host_fact, delete_host_fact, get_host_fact, reload_host_fact
27
+ from .facts import FactBase, ShortFactBase, get_host_fact
28
+ from .util import memoize, sha1_hash
13
29
 
14
30
  if TYPE_CHECKING:
31
+ from pyinfra.api.arguments import AllArguments
15
32
  from pyinfra.api.inventory import Inventory
16
33
  from pyinfra.api.state import State
17
34
 
18
35
 
19
- def extract_callable_datas(datas: List[Union[Callable[..., Any], Any]]) -> Generator[Any, Any, Any]:
36
+ def extract_callable_datas(
37
+ datas: list[Union[Callable[..., Any], Any]],
38
+ ) -> Generator[Any, Any, Any]:
20
39
  for data in datas:
21
40
  # Support for dynamic data, ie @deploy wrapped data defaults where
22
41
  # the data is stored on the state temporarily.
@@ -31,7 +50,7 @@ class HostData:
31
50
  Combines multiple AttrData's to search for attributes.
32
51
  """
33
52
 
34
- override_datas: Dict[str, Any]
53
+ override_datas: dict[str, Any]
35
54
 
36
55
  def __init__(self, host: "Host", *datas):
37
56
  self.__dict__["host"] = host
@@ -82,28 +101,36 @@ class Host:
82
101
  data.
83
102
  """
84
103
 
85
- connection = None
86
104
  state: "State"
105
+ connector_cls: type[BaseConnector]
106
+ connector: BaseConnector
107
+ connected: bool = False
87
108
 
88
109
  # Current context inside an @operation function (op gen stage)
89
110
  in_op: bool = False
111
+ in_callback_op: bool = False
90
112
  current_op_hash: Optional[str] = None
91
- current_op_global_kwargs: Dict[str, Any]
113
+ current_op_global_arguments: Optional["AllArguments"] = None
92
114
 
93
- # Current context inside a @deploy function (op gen stage)
115
+ # Current context inside a @deploy function which become part of the op data
94
116
  in_deploy: bool = False
95
117
  current_deploy_name: Optional[str] = None
96
118
  current_deploy_kwargs = None
97
- current_deploy_data = None
119
+
120
+ # @deploy decorator data is a bit different - we need to handle the case
121
+ # where we're evaluating an operation at runtime (current_op_) but also
122
+ # when ordering operations (current_) outside of an operation context.
123
+ current_op_deploy_data: Optional[dict[str, Any]] = None
124
+ current_deploy_data: Optional[dict[str, Any]] = None
98
125
 
99
126
  # Current context during operation execution
100
- executing_op_hash = None
101
- nested_executing_op_hash = None
127
+ executing_op_hash: Optional[str] = None
128
+ nested_executing_op_hash: Optional[str] = None
102
129
 
103
- loop_position: List[int]
130
+ loop_position: list[int]
104
131
 
105
132
  # Arbitrary data dictionary connectors may use
106
- connector_data: Dict[str, Any]
133
+ connector_data: dict[str, Any]
107
134
 
108
135
  def loop(self, iterable):
109
136
  self.loop_position.append(0)
@@ -117,26 +144,20 @@ class Host:
117
144
  name: str,
118
145
  inventory: "Inventory",
119
146
  groups,
120
- executor=get_execution_connector("ssh"),
147
+ connector_cls=get_execution_connector("ssh"),
121
148
  ):
122
149
  self.inventory = inventory
123
150
  self.groups = groups
124
- self.executor = executor
151
+ self.connector_cls = connector_cls
125
152
  self.name = name
126
153
 
127
154
  self.loop_position = []
128
155
 
129
156
  self.connector_data = {}
130
- self.current_op_global_kwargs = {}
131
-
132
- # Fact data store
133
- # TODO: how to not have Any here?
134
- self.facts: Dict[str, Any] = {}
135
- self.facts_lock = BoundedSemaphore()
136
157
 
137
158
  # Append only list of operation hashes as called on this host, used to
138
159
  # generate a DAG to create the final operation order.
139
- self.op_hash_order: List[str] = []
160
+ self.op_hash_order: list[str] = []
140
161
 
141
162
  # Create the (waterfall data: override, host, group, global)
142
163
  self.data = HostData(
@@ -149,16 +170,20 @@ class Host:
149
170
  self.get_deploy_data,
150
171
  )
151
172
 
173
+ def init(self, state: "State") -> None:
174
+ self.state = state
175
+ self.connector = self.connector_cls(state, self)
176
+
177
+ longest_name_len = max([len(host.name) for host in self.inventory])
178
+ padding_diff = longest_name_len - len(self.name)
179
+ self.print_prefix_padding = "".join(" " for _ in range(0, padding_diff))
180
+
152
181
  def __str__(self):
153
182
  return "{0}".format(self.name)
154
183
 
155
184
  def __repr__(self):
156
185
  return "Host({0})".format(self.name)
157
186
 
158
- @property
159
- def connected(self) -> bool:
160
- return self.connection is not None
161
-
162
187
  @property
163
188
  def host_data(self):
164
189
  return self.inventory.get_host_data(self.name)
@@ -168,30 +193,37 @@ class Host:
168
193
  return self.inventory.get_groups_data(self.groups)
169
194
 
170
195
  @property
171
- def print_prefix(self):
196
+ def print_prefix(self) -> str:
172
197
  if self.nested_executing_op_hash:
173
- return "{0}[{1}] {2} ".format(
198
+ return "{0}[{1}] {2}{3} ".format(
174
199
  click.style(""), # reset
175
200
  click.style(self.name, bold=True),
176
201
  click.style("nested", "blue"),
202
+ self.print_prefix_padding,
177
203
  )
178
204
 
179
- return "{0}[{1}] ".format(
205
+ return "{0}[{1}]{2} ".format(
180
206
  click.style(""), # reset
181
207
  click.style(self.name, bold=True),
208
+ self.print_prefix_padding,
182
209
  )
183
210
 
184
- def style_print_prefix(self, *args, **kwargs):
185
- return "{0}[{1}] ".format(
211
+ def style_print_prefix(self, *args, **kwargs) -> str:
212
+ return "{0}[{1}]{2} ".format(
186
213
  click.style(""), # reset
187
214
  click.style(self.name, *args, **kwargs),
215
+ self.print_prefix_padding,
188
216
  )
189
217
 
190
- def get_deploy_data(self):
191
- if self.current_deploy_data:
192
- return self.current_deploy_data
218
+ def log(self, message, log_func=logger.info):
219
+ log_func(f"{self.print_prefix}{message}")
220
+
221
+ def log_styled(self, message, log_func=logger.info, **kwargs):
222
+ message_styled = click.style(message, **kwargs)
223
+ self.log(message_styled, log_func=log_func)
193
224
 
194
- return {}
225
+ def get_deploy_data(self):
226
+ return self.current_op_deploy_data or self.current_deploy_data or {}
195
227
 
196
228
  def noop(self, description):
197
229
  """
@@ -201,8 +233,25 @@ class Host:
201
233
  handler = logger.info if self.state.print_noop_info else logger.debug
202
234
  handler("{0}noop: {1}".format(self.print_prefix, description))
203
235
 
236
+ def when(self, condition: Callable[[], bool]):
237
+ return self.deploy(
238
+ "",
239
+ cast("AllArguments", {"_if": [condition]}),
240
+ {},
241
+ in_deploy=False,
242
+ )
243
+
244
+ def arguments(self, **arguments: Unpack["AllArguments"]):
245
+ return self.deploy("", arguments, {}, in_deploy=False)
246
+
204
247
  @contextmanager
205
- def deploy(self, name: str, kwargs, data, in_deploy: bool = True):
248
+ def deploy(
249
+ self,
250
+ name: str,
251
+ kwargs: Optional["AllArguments"],
252
+ data: Optional[dict],
253
+ in_deploy: bool = True,
254
+ ):
206
255
  """
207
256
  Wraps a group of operations as a deploy, this should not be used
208
257
  directly, instead use ``pyinfra.api.deploy.deploy``.
@@ -219,6 +268,13 @@ class Host:
219
268
  old_deploy_data = self.current_deploy_data
220
269
  self.in_deploy = in_deploy
221
270
 
271
+ # Combine any old _ifs with the new ones
272
+ if old_deploy_kwargs and kwargs:
273
+ old_ifs = old_deploy_kwargs["_if"]
274
+ new_ifs = kwargs["_if"]
275
+ if old_ifs and new_ifs:
276
+ kwargs["_if"] = old_ifs + new_ifs
277
+
222
278
  # Set the new values
223
279
  self.current_deploy_name = name
224
280
  self.current_deploy_kwargs = kwargs
@@ -245,33 +301,53 @@ class Host:
245
301
  old_deploy_data,
246
302
  )
247
303
 
248
- # Host facts
249
- #
304
+ @memoize
305
+ def _get_temp_directory(self):
306
+ temp_directory = self.state.config.TEMP_DIR
250
307
 
251
- def get_fact(self, name_or_cls, *args, **kwargs):
252
- """
253
- Get a fact for this host, reading from the cache if present.
254
- """
255
- return get_host_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
308
+ if temp_directory is None:
309
+ # Unfortunate, but very hard to avoid, circular dependency, this method is memoized so
310
+ # performance isn't a concern.
311
+ from pyinfra.facts.server import TmpDir
256
312
 
257
- def reload_fact(self, name_or_cls, *args, **kwargs):
258
- """
259
- Get a fact for this host without using any cached value, always re-fetch the fact data
260
- from the host and then cache it.
261
- """
262
- return reload_host_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
313
+ temp_directory = self.get_fact(TmpDir)
263
314
 
264
- def create_fact(self, name_or_cls, data=None, kwargs=None):
315
+ if not temp_directory:
316
+ temp_directory = self.state.config.DEFAULT_TEMP_DIR
317
+
318
+ return temp_directory
319
+
320
+ def get_temp_filename(self, hash_key: Optional[str] = None, hash_filename: bool = True):
265
321
  """
266
- Create a new fact for this host in the fact cache.
322
+ Generate a temporary filename for this deploy.
267
323
  """
268
- return create_host_fact(self.state, self, name_or_cls, data, kwargs=kwargs)
269
324
 
270
- def delete_fact(self, name_or_cls, kwargs=None):
325
+ temp_directory = self._get_temp_directory()
326
+
327
+ if not hash_key:
328
+ hash_key = str(uuid4())
329
+
330
+ if hash_filename:
331
+ hash_key = sha1_hash(hash_key)
332
+
333
+ return "{0}/pyinfra-{1}".format(temp_directory, hash_key)
334
+
335
+ # Host facts
336
+ #
337
+
338
+ T = TypeVar("T")
339
+
340
+ @overload
341
+ def get_fact(self, name_or_cls: Type[FactBase[T]], *args, **kwargs) -> T: ...
342
+
343
+ @overload
344
+ def get_fact(self, name_or_cls: Type[ShortFactBase[T]], *args, **kwargs) -> T: ...
345
+
346
+ def get_fact(self, name_or_cls, *args, **kwargs):
271
347
  """
272
- Remove an existing fact for this host in the fact cache.
348
+ Get a fact for this host, reading from the cache if present.
273
349
  """
274
- return delete_host_fact(self.state, self, name_or_cls, kwargs=kwargs)
350
+ return get_host_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
275
351
 
276
352
  # Connector proxy
277
353
  #
@@ -286,11 +362,11 @@ class Host:
286
362
  """
287
363
 
288
364
  self._check_state()
289
- if not self.connection:
365
+ if not self.connected:
290
366
  self.state.trigger_callbacks("host_before_connect", self)
291
367
 
292
368
  try:
293
- self.connection = self.executor.connect(self.state, self)
369
+ self.connector.connect()
294
370
  except ConnectError as e:
295
371
  if show_errors:
296
372
  log_message = "{0}{1}".format(
@@ -316,8 +392,7 @@ class Host:
316
392
 
317
393
  logger.info(log_message)
318
394
  self.state.trigger_callbacks("host_connect", self)
319
-
320
- return self.connection
395
+ self.connected = True
321
396
 
322
397
  def disconnect(self):
323
398
  """
@@ -325,50 +400,43 @@ class Host:
325
400
  """
326
401
  self._check_state()
327
402
 
328
- # Disconnect is an optional function for executors if needed
329
- disconnect_func = getattr(self.executor, "disconnect", None)
403
+ # Disconnect is an optional function for connectors if needed
404
+ disconnect_func = getattr(self.connector, "disconnect", None)
330
405
  if disconnect_func:
331
- return disconnect_func(self.state, self)
406
+ return disconnect_func()
332
407
 
333
408
  # TODO: consider whether this should be here!
334
409
  remove_any_sudo_askpass_file(self)
335
410
 
336
411
  self.state.trigger_callbacks("host_disconnect", self)
337
412
 
338
- def run_shell_command(self, *args, **kwargs):
413
+ def run_shell_command(self, *args, **kwargs) -> tuple[bool, CommandOutput]:
339
414
  """
340
415
  Low level method to execute a shell command on the host via it's configured connector.
341
416
  """
342
417
  self._check_state()
343
- return self.executor.run_shell_command(self.state, self, *args, **kwargs)
418
+ return self.connector.run_shell_command(*args, **kwargs)
344
419
 
345
- def put_file(self, *args, **kwargs):
420
+ def put_file(self, *args, **kwargs) -> bool:
346
421
  """
347
422
  Low level method to upload a file to the host via it's configured connector.
348
423
  """
349
424
  self._check_state()
350
- return self.executor.put_file(self.state, self, *args, **kwargs)
425
+ return self.connector.put_file(*args, **kwargs)
351
426
 
352
- def get_file(self, *args, **kwargs):
427
+ def get_file(self, *args, **kwargs) -> bool:
353
428
  """
354
429
  Low level method to download a file from the host via it's configured connector.
355
430
  """
356
431
  self._check_state()
357
- return self.executor.get_file(self.state, self, *args, **kwargs)
358
-
359
- # Rsync - optional executor specific ability
432
+ return self.connector.get_file(*args, **kwargs)
360
433
 
361
- def check_can_rsync(self):
362
- check_can_rsync_func = getattr(self.executor, "check_can_rsync", None)
363
- if check_can_rsync_func:
364
- return check_can_rsync_func(self)
434
+ # Rsync - optional connector specific ability
365
435
 
366
- raise NotImplementedError(
367
- "The {0} connector does not support rsync!".format(
368
- self.executor.__name__,
369
- ),
370
- )
436
+ def check_can_rsync(self) -> None:
437
+ self._check_state()
438
+ return self.connector.check_can_rsync()
371
439
 
372
- def rsync(self, *args, **kwargs):
440
+ def rsync(self, *args, **kwargs) -> bool:
373
441
  self._check_state()
374
- return self.executor.rsync(self.state, self, *args, **kwargs)
442
+ return self.connector.rsync(*args, **kwargs)
pyinfra/api/inventory.py CHANGED
@@ -1,5 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections import defaultdict
2
- from typing import TYPE_CHECKING, Any, Iterator, List
4
+ from typing import TYPE_CHECKING, Any, Iterator
3
5
 
4
6
  from .connectors import get_all_connectors, get_execution_connectors
5
7
  from .exceptions import NoConnectorError, NoGroupError, NoHostError
@@ -9,7 +11,7 @@ if TYPE_CHECKING:
9
11
  from pyinfra.api.state import State
10
12
 
11
13
 
12
- def extract_name_data(names: List[Any]):
14
+ def extract_name_data(names: list[Any]):
13
15
  for name in names:
14
16
  data = {}
15
17
 
@@ -35,11 +37,15 @@ class Inventory:
35
37
 
36
38
  state: "State"
37
39
 
40
+ @staticmethod
41
+ def empty():
42
+ return Inventory(([], {}))
43
+
38
44
  def __init__(self, names_data, override_data=None, **groups):
39
45
  # Setup basics
40
46
  self.groups = defaultdict(list) # lists of Host objects
41
- self.host_data = defaultdict(dict) # dict of name -> data
42
- self.group_data = defaultdict(dict) # dict of name -> data
47
+ self.host_data: dict[str, dict] = defaultdict(dict) # dict of name -> data
48
+ self.group_data: dict[str, dict] = defaultdict(dict) # dict of name -> data
43
49
  self.override_data = override_data or {}
44
50
 
45
51
  names, data = names_data
@@ -48,14 +54,14 @@ class Inventory:
48
54
  self.data = data
49
55
 
50
56
  # Create the actual host instances and groups
51
- self.hosts = self.make_hosts_and_groups(names, groups)
57
+ self.make_hosts_and_groups(names, groups)
52
58
 
53
- def make_hosts_and_groups(self, names, groups):
59
+ def make_hosts_and_groups(self, names, groups) -> None:
54
60
  all_connectors = get_all_connectors()
55
61
  execution_connectors = get_execution_connectors()
56
62
 
57
63
  # Map name -> data
58
- name_to_data = defaultdict(dict)
64
+ name_to_data: dict[str, dict] = defaultdict(dict)
59
65
  # Map name -> group names
60
66
  name_to_group_names = defaultdict(list)
61
67
 
@@ -73,14 +79,14 @@ class Inventory:
73
79
  for name, data in extract_name_data(names):
74
80
  name_to_data[name].update(data)
75
81
 
76
- # Now, use the above to fill self.host_data and populate names_executors
77
- names_executors = []
82
+ # Now, use the above to fill self.host_data and populate names_connectors
83
+ names_connectors = []
78
84
 
79
85
  for name, _ in extract_name_data(names):
80
86
  host_data = name_to_data[name]
81
87
 
82
88
  # Default to executing commands with the ssh connector
83
- executor = execution_connectors["ssh"]
89
+ connector_cls = execution_connectors["ssh"]
84
90
 
85
91
  if name[0] == "@":
86
92
  connector_name = name[1:]
@@ -96,7 +102,7 @@ class Inventory:
96
102
 
97
103
  # Execution connector? Simple, just set it for their host
98
104
  if connector_name in execution_connectors:
99
- executor = execution_connectors[connector_name]
105
+ connector_cls = execution_connectors[connector_name]
100
106
 
101
107
  names_data = all_connectors[connector_name].make_names_data(arg_string)
102
108
  connector_inventory_name = name
@@ -111,7 +117,7 @@ class Inventory:
111
117
 
112
118
  # Assign the name/data/groups from the connector
113
119
  self.host_data[sub_name] = sub_data
114
- names_executors.append((sub_name, executor))
120
+ names_connectors.append((sub_name, connector_cls))
115
121
  name_to_group_names[sub_name].extend(sub_groups)
116
122
 
117
123
  # If we have a connector inventory name, copy any groups attached
@@ -124,10 +130,10 @@ class Inventory:
124
130
  # Now we can actually make Host instances
125
131
  hosts: dict[str, "Host"] = {}
126
132
 
127
- for name, executor in names_executors:
133
+ for name, connector_cls in names_connectors:
128
134
  host_groups = name_to_group_names[name]
129
135
 
130
- host = Host(name, inventory=self, groups=host_groups, executor=executor)
136
+ host = Host(name, inventory=self, groups=host_groups, connector_cls=connector_cls)
131
137
  hosts[name] = host
132
138
 
133
139
  # And push into any groups
@@ -135,7 +141,7 @@ class Inventory:
135
141
  if host not in self.groups[group_name]:
136
142
  self.groups[group_name].append(host)
137
143
 
138
- return hosts
144
+ self.hosts = hosts
139
145
 
140
146
  def __len__(self) -> int:
141
147
  """
@@ -241,13 +247,3 @@ class Inventory:
241
247
  data.update(self.get_group_data(group))
242
248
 
243
249
  return data
244
-
245
- def get_deploy_data(self):
246
- """
247
- Gets any default data attached to the current deploy, if any.
248
- """
249
-
250
- if self.state and self.state.deploy_data:
251
- return self.state.deploy_data
252
-
253
- return {}