pyinfra 0.11.dev3__py3-none-any.whl → 3.6__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 (204) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +19 -3
  4. pyinfra/api/arguments.py +413 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +73 -18
  12. pyinfra/api/facts.py +267 -200
  13. pyinfra/api/host.py +416 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/metadata.py +69 -0
  16. pyinfra/api/operation.py +432 -262
  17. pyinfra/api/operations.py +273 -260
  18. pyinfra/api/state.py +302 -248
  19. pyinfra/api/util.py +309 -369
  20. pyinfra/connectors/base.py +173 -0
  21. pyinfra/connectors/chroot.py +212 -0
  22. pyinfra/connectors/docker.py +405 -0
  23. pyinfra/connectors/dockerssh.py +297 -0
  24. pyinfra/connectors/local.py +238 -0
  25. pyinfra/connectors/scp/__init__.py +1 -0
  26. pyinfra/connectors/scp/client.py +204 -0
  27. pyinfra/connectors/ssh.py +727 -0
  28. pyinfra/connectors/ssh_util.py +114 -0
  29. pyinfra/connectors/sshuserclient/client.py +309 -0
  30. pyinfra/connectors/sshuserclient/config.py +102 -0
  31. pyinfra/connectors/terraform.py +135 -0
  32. pyinfra/connectors/util.py +417 -0
  33. pyinfra/connectors/vagrant.py +183 -0
  34. pyinfra/context.py +145 -0
  35. pyinfra/facts/__init__.py +7 -6
  36. pyinfra/facts/apk.py +22 -7
  37. pyinfra/facts/apt.py +117 -60
  38. pyinfra/facts/brew.py +100 -15
  39. pyinfra/facts/bsdinit.py +23 -0
  40. pyinfra/facts/cargo.py +37 -0
  41. pyinfra/facts/choco.py +47 -0
  42. pyinfra/facts/crontab.py +195 -0
  43. pyinfra/facts/deb.py +94 -0
  44. pyinfra/facts/dnf.py +48 -0
  45. pyinfra/facts/docker.py +96 -23
  46. pyinfra/facts/efibootmgr.py +113 -0
  47. pyinfra/facts/files.py +629 -58
  48. pyinfra/facts/flatpak.py +77 -0
  49. pyinfra/facts/freebsd.py +70 -0
  50. pyinfra/facts/gem.py +19 -6
  51. pyinfra/facts/git.py +59 -14
  52. pyinfra/facts/gpg.py +150 -0
  53. pyinfra/facts/hardware.py +313 -167
  54. pyinfra/facts/iptables.py +72 -62
  55. pyinfra/facts/launchd.py +44 -0
  56. pyinfra/facts/lxd.py +17 -4
  57. pyinfra/facts/mysql.py +122 -86
  58. pyinfra/facts/npm.py +17 -9
  59. pyinfra/facts/openrc.py +71 -0
  60. pyinfra/facts/opkg.py +246 -0
  61. pyinfra/facts/pacman.py +50 -7
  62. pyinfra/facts/pip.py +24 -7
  63. pyinfra/facts/pipx.py +82 -0
  64. pyinfra/facts/pkg.py +15 -6
  65. pyinfra/facts/pkgin.py +35 -0
  66. pyinfra/facts/podman.py +54 -0
  67. pyinfra/facts/postgres.py +178 -0
  68. pyinfra/facts/postgresql.py +6 -147
  69. pyinfra/facts/rpm.py +105 -0
  70. pyinfra/facts/runit.py +77 -0
  71. pyinfra/facts/selinux.py +161 -0
  72. pyinfra/facts/server.py +762 -285
  73. pyinfra/facts/snap.py +88 -0
  74. pyinfra/facts/systemd.py +139 -0
  75. pyinfra/facts/sysvinit.py +59 -0
  76. pyinfra/facts/upstart.py +35 -0
  77. pyinfra/facts/util/__init__.py +17 -0
  78. pyinfra/facts/util/databases.py +4 -6
  79. pyinfra/facts/util/packaging.py +37 -6
  80. pyinfra/facts/util/units.py +30 -0
  81. pyinfra/facts/util/win_files.py +99 -0
  82. pyinfra/facts/vzctl.py +20 -13
  83. pyinfra/facts/xbps.py +35 -0
  84. pyinfra/facts/yum.py +34 -40
  85. pyinfra/facts/zfs.py +77 -0
  86. pyinfra/facts/zypper.py +42 -0
  87. pyinfra/local.py +45 -83
  88. pyinfra/operations/__init__.py +12 -0
  89. pyinfra/operations/apk.py +99 -0
  90. pyinfra/operations/apt.py +496 -0
  91. pyinfra/operations/brew.py +232 -0
  92. pyinfra/operations/bsdinit.py +59 -0
  93. pyinfra/operations/cargo.py +45 -0
  94. pyinfra/operations/choco.py +61 -0
  95. pyinfra/operations/crontab.py +194 -0
  96. pyinfra/operations/dnf.py +213 -0
  97. pyinfra/operations/docker.py +492 -0
  98. pyinfra/operations/files.py +2014 -0
  99. pyinfra/operations/flatpak.py +95 -0
  100. pyinfra/operations/freebsd/__init__.py +12 -0
  101. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  102. pyinfra/operations/freebsd/pkg.py +219 -0
  103. pyinfra/operations/freebsd/service.py +116 -0
  104. pyinfra/operations/freebsd/sysrc.py +92 -0
  105. pyinfra/operations/gem.py +48 -0
  106. pyinfra/operations/git.py +420 -0
  107. pyinfra/operations/iptables.py +312 -0
  108. pyinfra/operations/launchd.py +45 -0
  109. pyinfra/operations/lxd.py +69 -0
  110. pyinfra/operations/mysql.py +610 -0
  111. pyinfra/operations/npm.py +57 -0
  112. pyinfra/operations/openrc.py +63 -0
  113. pyinfra/operations/opkg.py +89 -0
  114. pyinfra/operations/pacman.py +82 -0
  115. pyinfra/operations/pip.py +206 -0
  116. pyinfra/operations/pipx.py +103 -0
  117. pyinfra/operations/pkg.py +71 -0
  118. pyinfra/operations/pkgin.py +92 -0
  119. pyinfra/operations/postgres.py +437 -0
  120. pyinfra/operations/postgresql.py +30 -0
  121. pyinfra/operations/puppet.py +41 -0
  122. pyinfra/operations/python.py +73 -0
  123. pyinfra/operations/runit.py +184 -0
  124. pyinfra/operations/selinux.py +190 -0
  125. pyinfra/operations/server.py +1100 -0
  126. pyinfra/operations/snap.py +118 -0
  127. pyinfra/operations/ssh.py +217 -0
  128. pyinfra/operations/systemd.py +150 -0
  129. pyinfra/operations/sysvinit.py +142 -0
  130. pyinfra/operations/upstart.py +68 -0
  131. pyinfra/operations/util/__init__.py +12 -0
  132. pyinfra/operations/util/docker.py +407 -0
  133. pyinfra/operations/util/files.py +247 -0
  134. pyinfra/operations/util/packaging.py +338 -0
  135. pyinfra/operations/util/service.py +46 -0
  136. pyinfra/operations/vzctl.py +137 -0
  137. pyinfra/operations/xbps.py +78 -0
  138. pyinfra/operations/yum.py +213 -0
  139. pyinfra/operations/zfs.py +176 -0
  140. pyinfra/operations/zypper.py +193 -0
  141. pyinfra/progress.py +44 -32
  142. pyinfra/py.typed +0 -0
  143. pyinfra/version.py +9 -1
  144. pyinfra-3.6.dist-info/METADATA +142 -0
  145. pyinfra-3.6.dist-info/RECORD +160 -0
  146. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
  147. pyinfra-3.6.dist-info/entry_points.txt +12 -0
  148. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
  149. pyinfra_cli/__init__.py +1 -0
  150. pyinfra_cli/cli.py +793 -0
  151. pyinfra_cli/commands.py +66 -0
  152. pyinfra_cli/exceptions.py +155 -65
  153. pyinfra_cli/inventory.py +233 -89
  154. pyinfra_cli/log.py +39 -43
  155. pyinfra_cli/main.py +26 -495
  156. pyinfra_cli/prints.py +215 -156
  157. pyinfra_cli/util.py +172 -105
  158. pyinfra_cli/virtualenv.py +25 -20
  159. pyinfra/api/connectors/__init__.py +0 -21
  160. pyinfra/api/connectors/ansible.py +0 -99
  161. pyinfra/api/connectors/docker.py +0 -178
  162. pyinfra/api/connectors/local.py +0 -169
  163. pyinfra/api/connectors/ssh.py +0 -402
  164. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  165. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  166. pyinfra/api/connectors/util.py +0 -63
  167. pyinfra/api/connectors/vagrant.py +0 -155
  168. pyinfra/facts/init.py +0 -176
  169. pyinfra/facts/util/files.py +0 -102
  170. pyinfra/hook.py +0 -41
  171. pyinfra/modules/__init__.py +0 -11
  172. pyinfra/modules/apk.py +0 -64
  173. pyinfra/modules/apt.py +0 -272
  174. pyinfra/modules/brew.py +0 -122
  175. pyinfra/modules/files.py +0 -711
  176. pyinfra/modules/gem.py +0 -30
  177. pyinfra/modules/git.py +0 -115
  178. pyinfra/modules/init.py +0 -344
  179. pyinfra/modules/iptables.py +0 -271
  180. pyinfra/modules/lxd.py +0 -45
  181. pyinfra/modules/mysql.py +0 -347
  182. pyinfra/modules/npm.py +0 -47
  183. pyinfra/modules/pacman.py +0 -60
  184. pyinfra/modules/pip.py +0 -99
  185. pyinfra/modules/pkg.py +0 -43
  186. pyinfra/modules/postgresql.py +0 -245
  187. pyinfra/modules/puppet.py +0 -20
  188. pyinfra/modules/python.py +0 -37
  189. pyinfra/modules/server.py +0 -524
  190. pyinfra/modules/ssh.py +0 -150
  191. pyinfra/modules/util/files.py +0 -52
  192. pyinfra/modules/util/packaging.py +0 -118
  193. pyinfra/modules/vzctl.py +0 -133
  194. pyinfra/modules/yum.py +0 -171
  195. pyinfra/pseudo_modules.py +0 -64
  196. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  197. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  198. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  199. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  200. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  201. pyinfra_cli/__main__.py +0 -40
  202. pyinfra_cli/config.py +0 -92
  203. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  204. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/__init__.py CHANGED
@@ -1,24 +1,21 @@
1
- '''
1
+ """
2
2
  Welcome to pyinfra.
3
- '''
3
+ """
4
4
 
5
5
  import logging
6
6
 
7
-
8
7
  # Global flag set True by `pyinfra_cli.__main__`
9
8
  is_cli = False
10
9
 
11
10
  # Global pyinfra logger
12
- logger = logging.getLogger('pyinfra')
11
+ logger = logging.getLogger("pyinfra")
12
+
13
+ # Trigger context module creation
14
+ from .context import config, host, init_base_classes, inventory, state # noqa
13
15
 
14
16
  # Setup package level version
15
17
  from .version import __version__ # noqa
16
18
 
17
- # Trigger pseudo_* creation
18
- from . import pseudo_modules # noqa
19
-
20
- # Trigger fact index creation
21
- from . import facts # noqa
22
-
23
- # Trigger module imports
24
- from . import modules # noqa # pragma: no cover
19
+ # Initialise base classes - this sets the context modules to point at the underlying
20
+ # class objects (Host, etc), which makes ipython/etc work as expected.
21
+ init_base_classes()
pyinfra/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from pyinfra_cli.main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
pyinfra/api/__init__.py CHANGED
@@ -1,11 +1,27 @@
1
+ from .command import FileDownloadCommand # noqa: F401 # pragma: no cover
2
+ from .command import ( # noqa: F401
3
+ FileUploadCommand,
4
+ FunctionCommand,
5
+ MaskString,
6
+ QuoteString,
7
+ RsyncCommand,
8
+ StringCommand,
9
+ )
1
10
  from .config import Config # noqa: F401 # pragma: no cover
2
11
  from .deploy import deploy # noqa: F401 # pragma: no cover
3
- from .exceptions import ( # noqa: F401 # pragma: no cover
4
- DeployError,
12
+ from .exceptions import DeployError # noqa: F401 # pragma: no cover
13
+ from .exceptions import ( # noqa: F401
14
+ FactError,
15
+ FactTypeError,
16
+ FactValueError,
17
+ FactProcessError,
5
18
  InventoryError,
6
19
  OperationError,
20
+ OperationTypeError,
21
+ OperationValueError,
7
22
  )
8
23
  from .facts import FactBase, ShortFactBase # noqa: F401 # pragma: no cover
24
+ from .host import Host # noqa: F401 # pragma: no cover
9
25
  from .inventory import Inventory # noqa: F401 # pragma: no cover
10
26
  from .operation import operation # noqa: F401 # pragma: no cover
11
- from .state import State # noqa: F401 # pragma: no cover
27
+ from .state import BaseStateCallback, State # noqa: F401 # pragma: no cover
@@ -0,0 +1,413 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ Callable,
7
+ Generic,
8
+ Iterable,
9
+ List,
10
+ Mapping,
11
+ Optional,
12
+ Type,
13
+ TypeVar,
14
+ Union,
15
+ cast,
16
+ get_type_hints,
17
+ )
18
+
19
+ from typing_extensions import TypedDict
20
+
21
+ from pyinfra.api.exceptions import ArgumentTypeError
22
+ from pyinfra.api.util import raise_if_bad_type
23
+ from pyinfra.context import ctx_config
24
+
25
+ if TYPE_CHECKING:
26
+ from pyinfra.api import Config, Host, State
27
+
28
+ T = TypeVar("T")
29
+ default_sentinel = object()
30
+
31
+
32
+ class ArgumentMeta(Generic[T]):
33
+ description: str
34
+ default: Callable[["Config"], T]
35
+ handler: Optional[Callable[["Config", T], T]]
36
+
37
+ def __init__(self, description, default, handler=None) -> None:
38
+ self.description = description
39
+ self.default = default
40
+ self.handler = handler
41
+
42
+
43
+ # Connector arguments
44
+ # These are arguments passed to the various connectors that provide the underlying
45
+ # API to read/write external systems.
46
+
47
+
48
+ # Note: ConnectorArguments is specifically not total as it's used to type many
49
+ # functions via Unpack and we don't want to specify every kwarg.
50
+ class ConnectorArguments(TypedDict, total=False):
51
+ # Auth arguments
52
+ _sudo: bool
53
+ _sudo_user: str
54
+ _use_sudo_login: bool
55
+ _sudo_password: str
56
+ _preserve_sudo_env: bool
57
+ _su_user: str
58
+ _use_su_login: bool
59
+ _preserve_su_env: bool
60
+ _su_shell: str
61
+ _doas: bool
62
+ _doas_user: str
63
+
64
+ # Shell arguments
65
+ _shell_executable: str
66
+ _chdir: str
67
+ _env: Mapping[str, str]
68
+
69
+ # Connector control (outside of command generation)
70
+ _success_exit_codes: Iterable[int]
71
+ _timeout: int
72
+ _get_pty: bool
73
+ _stdin: Union[str, Iterable[str]]
74
+
75
+ # Retry arguments
76
+ _retries: int
77
+ _retry_delay: Union[int, float]
78
+ _retry_until: Optional[Callable[[dict], bool]]
79
+
80
+ # Temp directory argument
81
+ _temp_dir: str
82
+
83
+
84
+ def generate_env(config: "Config", value: dict) -> dict:
85
+ env = config.ENV.copy()
86
+ env.update(value)
87
+ return env
88
+
89
+
90
+ auth_argument_meta: dict[str, ArgumentMeta] = {
91
+ "_sudo": ArgumentMeta(
92
+ "Execute/apply any changes with sudo.",
93
+ default=lambda config: config.SUDO,
94
+ ),
95
+ "_sudo_user": ArgumentMeta(
96
+ "Execute/apply any changes with sudo as a non-root user.",
97
+ default=lambda config: config.SUDO_USER,
98
+ ),
99
+ "_use_sudo_login": ArgumentMeta(
100
+ "Execute sudo with a login shell.",
101
+ default=lambda config: config.USE_SUDO_LOGIN,
102
+ ),
103
+ "_sudo_password": ArgumentMeta(
104
+ "Password to sudo with. If needed and not specified pyinfra will prompt for it.",
105
+ default=lambda config: config.SUDO_PASSWORD,
106
+ ),
107
+ "_preserve_sudo_env": ArgumentMeta(
108
+ "Preserve the shell environment of the connecting user when using sudo.",
109
+ default=lambda config: config.PRESERVE_SUDO_ENV,
110
+ ),
111
+ "_su_user": ArgumentMeta(
112
+ "Execute/apply any changes with this user using su.",
113
+ default=lambda config: config.SU_USER,
114
+ ),
115
+ "_use_su_login": ArgumentMeta(
116
+ "Execute su with a login shell.",
117
+ default=lambda config: config.USE_SU_LOGIN,
118
+ ),
119
+ "_preserve_su_env": ArgumentMeta(
120
+ "Preserve the shell environment of the connecting user when using su.",
121
+ default=lambda config: config.PRESERVE_SU_ENV,
122
+ ),
123
+ "_su_shell": ArgumentMeta(
124
+ "Use this shell (instead of user login shell) when using ``_su``). "
125
+ + "Only available under Linux, for use when using `su` with a user that "
126
+ + "has nologin/similar as their login shell.",
127
+ default=lambda config: config.SU_SHELL,
128
+ ),
129
+ "_doas": ArgumentMeta(
130
+ "Execute/apply any changes with doas.",
131
+ default=lambda config: config.DOAS,
132
+ ),
133
+ "_doas_user": ArgumentMeta(
134
+ "Execute/apply any changes with doas as a non-root user.",
135
+ default=lambda config: config.DOAS_USER,
136
+ ),
137
+ }
138
+
139
+ shell_argument_meta: dict[str, ArgumentMeta] = {
140
+ "_shell_executable": ArgumentMeta(
141
+ "The shell executable to use for executing commands.",
142
+ default=lambda config: config.SHELL,
143
+ ),
144
+ "_chdir": ArgumentMeta(
145
+ "Directory to switch to before executing the command.",
146
+ default=lambda _: None,
147
+ ),
148
+ "_env": ArgumentMeta(
149
+ "Dictionary of environment variables to set.",
150
+ default=lambda _: {},
151
+ handler=generate_env,
152
+ ),
153
+ "_success_exit_codes": ArgumentMeta(
154
+ "List of exit codes to consider a success.",
155
+ default=lambda _: [0],
156
+ ),
157
+ "_timeout": ArgumentMeta(
158
+ "Timeout for *each* command executed during the operation.",
159
+ default=lambda _: None,
160
+ ),
161
+ "_get_pty": ArgumentMeta(
162
+ "Whether to get a pseudoTTY when executing any commands.",
163
+ default=lambda _: False,
164
+ ),
165
+ "_stdin": ArgumentMeta(
166
+ "String or buffer to send to the stdin of any commands.",
167
+ default=lambda _: None,
168
+ ),
169
+ "_temp_dir": ArgumentMeta(
170
+ "Temporary directory on the remote host for file operations.",
171
+ default=lambda config: config.TEMP_DIR,
172
+ ),
173
+ }
174
+
175
+
176
+ # Meta arguments
177
+ # These provide/extend additional operation metadata
178
+
179
+
180
+ class MetaArguments(TypedDict):
181
+ name: str
182
+ _ignore_errors: bool
183
+ _continue_on_error: bool
184
+ _if: Union[List[Callable[[], bool]], Callable[[], bool], None]
185
+
186
+
187
+ meta_argument_meta: dict[str, ArgumentMeta] = {
188
+ # NOTE: name is the only non-_-prefixed argument
189
+ "name": ArgumentMeta(
190
+ "Name of the operation.",
191
+ default=lambda _: None,
192
+ ),
193
+ "_ignore_errors": ArgumentMeta(
194
+ "Ignore errors when executing the operation.",
195
+ default=lambda config: config.IGNORE_ERRORS,
196
+ ),
197
+ "_continue_on_error": ArgumentMeta(
198
+ (
199
+ "Continue executing operation commands after error. "
200
+ "Only applies when ``_ignore_errors`` is true."
201
+ ),
202
+ default=lambda _: False,
203
+ ),
204
+ "_if": ArgumentMeta(
205
+ "Only run this operation if these functions return True",
206
+ default=lambda _: [],
207
+ ),
208
+ }
209
+
210
+
211
+ # Execution arguments
212
+ # These alter how pyinfra is to execute an operation. Notably these must all have the same value
213
+ # over every target host for the same operation.
214
+
215
+
216
+ class ExecutionArguments(TypedDict):
217
+ _parallel: int
218
+ _run_once: bool
219
+ _serial: bool
220
+
221
+
222
+ execution_argument_meta: dict[str, ArgumentMeta] = {
223
+ "_parallel": ArgumentMeta(
224
+ "Run this operation in batches of hosts.",
225
+ default=lambda config: config.PARALLEL,
226
+ ),
227
+ "_run_once": ArgumentMeta(
228
+ "Only execute this operation once, on the first host to see it.",
229
+ default=lambda _: False,
230
+ ),
231
+ "_serial": ArgumentMeta(
232
+ "Run this operation host by host, rather than in parallel.",
233
+ default=lambda _: False,
234
+ ),
235
+ }
236
+
237
+
238
+ class AllArguments(ConnectorArguments, MetaArguments, ExecutionArguments):
239
+ pass
240
+
241
+
242
+ def all_global_arguments() -> List[tuple[str, Type]]:
243
+ """Return all global arguments and their types."""
244
+ return list(get_type_hints(AllArguments).items())
245
+
246
+
247
+ # Create a dictionary for retry arguments
248
+ retry_argument_meta: dict[str, ArgumentMeta] = {
249
+ "_retries": ArgumentMeta(
250
+ "Number of times to retry failed operations.",
251
+ default=lambda config: config.RETRY,
252
+ ),
253
+ "_retry_delay": ArgumentMeta(
254
+ "Delay in seconds between retry attempts.",
255
+ default=lambda config: config.RETRY_DELAY,
256
+ ),
257
+ "_retry_until": ArgumentMeta(
258
+ "Callable taking output data that returns True to continue retrying.",
259
+ default=lambda config: None,
260
+ ),
261
+ }
262
+
263
+ all_argument_meta: dict[str, ArgumentMeta] = {
264
+ **auth_argument_meta,
265
+ **shell_argument_meta,
266
+ **meta_argument_meta,
267
+ **execution_argument_meta,
268
+ **retry_argument_meta, # Add retry arguments
269
+ }
270
+
271
+ EXECUTION_KWARG_KEYS = list(ExecutionArguments.__annotations__.keys())
272
+ CONNECTOR_ARGUMENT_KEYS = list(ConnectorArguments.__annotations__.keys())
273
+
274
+ __argument_docs__ = {
275
+ "Privilege & user escalation": (
276
+ auth_argument_meta,
277
+ """
278
+ .. caution::
279
+ When combining privilege escalation arguments it is important to know the order they
280
+ are applied: ``doas`` -> ``sudo`` -> ``su``. For example
281
+ ``_sudo=True,_su_user="pyinfra"`` yields a command like ``sudo su pyinfra..``.
282
+ """,
283
+ """
284
+ .. code:: python
285
+
286
+ # Execute a command with sudo
287
+ server.user(
288
+ name="Create pyinfra user using sudo",
289
+ user="pyinfra",
290
+ _sudo=True,
291
+ )
292
+
293
+ # Execute a command with a specific sudo password
294
+ server.user(
295
+ name="Create pyinfra user using sudo",
296
+ user="pyinfra",
297
+ _sudo=True,
298
+ _sudo_password="my-secret-password",
299
+ )
300
+ """,
301
+ ),
302
+ "Shell control & features": (
303
+ shell_argument_meta,
304
+ "",
305
+ """
306
+ .. code:: python
307
+
308
+ # Execute from a specific directory
309
+ server.shell(
310
+ name="Bootstrap nginx params",
311
+ commands=["openssl dhparam -out dhparam.pem 4096"],
312
+ _chdir="/etc/ssl/certs",
313
+ )
314
+ """,
315
+ ),
316
+ "Operation meta & callbacks": (meta_argument_meta, "", ""),
317
+ "Execution strategy": (execution_argument_meta, "", ""),
318
+ "Retry behavior": (
319
+ retry_argument_meta,
320
+ """
321
+ Retry arguments allow you to automatically retry operations that fail. You can specify
322
+ how many times to retry, the delay between retries, and optionally a condition
323
+ function to determine when to stop retrying.
324
+ """,
325
+ """
326
+ .. code:: python
327
+
328
+ # Retry a command up to 3 times with the default 5 second delay
329
+ server.shell(
330
+ name="Run flaky command with retries",
331
+ commands=["flaky_command"],
332
+ _retries=3,
333
+ )
334
+ # Retry with a custom delay
335
+ server.shell(
336
+ name="Run flaky command with custom delay",
337
+ commands=["flaky_command"],
338
+ _retries=2,
339
+ _retry_delay=10, # 10 second delay between retries
340
+ )
341
+ # Retry with a custom condition
342
+ def retry_on_specific_error(output_data):
343
+ # Retry if stderr contains "temporary failure"
344
+ for line in output_data["stderr_lines"]:
345
+ if "temporary failure" in line.lower():
346
+ return True
347
+ return False
348
+
349
+ server.shell(
350
+ name="Run command with conditional retry",
351
+ commands=["flaky_command"],
352
+ _retries=5,
353
+ _retry_until=retry_on_specific_error,
354
+ )
355
+ """,
356
+ ),
357
+ }
358
+
359
+
360
+ def pop_global_arguments(
361
+ state: "State",
362
+ host: "Host",
363
+ kwargs: dict[str, Any],
364
+ ) -> tuple[AllArguments, list[str]]:
365
+ """
366
+ Pop and return operation global keyword arguments, in preferred order:
367
+
368
+ + From the current context (a direct @operator or @deploy function being called)
369
+ + From any current @deploy context (deploy kwargs)
370
+ + From the host data variables
371
+ + From the config variables
372
+ """
373
+
374
+ config = state.config
375
+ if ctx_config.isset():
376
+ config = ctx_config.get()
377
+ assert config is not None
378
+
379
+ cdkwargs = host.current_deploy_kwargs
380
+ meta_kwargs: dict[str, Any] = cdkwargs or {} # type: ignore[assignment]
381
+
382
+ arguments: dict[str, Any] = {}
383
+ found_keys: list[str] = []
384
+
385
+ for key, type_ in all_global_arguments():
386
+ argument_meta = all_argument_meta[key]
387
+ handler = argument_meta.handler
388
+ default: Any = argument_meta.default(config)
389
+
390
+ host_default = getattr(host.data, key, default_sentinel)
391
+ if host_default is not default_sentinel:
392
+ default = host_default
393
+
394
+ if key in kwargs:
395
+ found_keys.append(key)
396
+ value = kwargs.pop(key)
397
+ else:
398
+ value = meta_kwargs.get(key, default)
399
+
400
+ if handler:
401
+ value = handler(config, value)
402
+
403
+ if value != default:
404
+ raise_if_bad_type(
405
+ value,
406
+ type_,
407
+ ArgumentTypeError,
408
+ f"Invalid argument `{key}`:",
409
+ )
410
+
411
+ # TODO: why is type failing here?
412
+ arguments[key] = value # type: ignore
413
+ return cast(AllArguments, arguments), found_keys
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
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
+ )
14
+
15
+ from typing_extensions import ParamSpec, Protocol
16
+
17
+ if TYPE_CHECKING:
18
+ from pyinfra.api.operation import OperationMeta
19
+
20
+ P = ParamSpec("P")
21
+
22
+
23
+ # Unfortunately we have to re-type out all of the global arguments here because
24
+ # Python typing doesn't (yet) support merging kwargs. This acts as the operation
25
+ # decorator output function which merges actual operation args/kwargs in paramspec
26
+ # with the global arguments available in all operations.
27
+ # The nature of "global arguments" is somewhat opposed to static typing as it's
28
+ # indirect and somewhat magic, but we are where we are.
29
+ class PyinfraOperation(Generic[P], Protocol):
30
+ _inner: Callable[P, Generator]
31
+
32
+ def __call__(
33
+ self,
34
+ #
35
+ # ConnectorArguments
36
+ #
37
+ # Auth
38
+ _sudo: bool = False,
39
+ _sudo_user: Optional[str] = None,
40
+ _use_sudo_login: bool = False,
41
+ _sudo_password: Optional[str] = None,
42
+ _preserve_sudo_env: bool = False,
43
+ _su_user: Optional[str] = None,
44
+ _use_su_login: bool = False,
45
+ _preserve_su_env: bool = False,
46
+ _su_shell: Optional[str] = None,
47
+ _doas: bool = False,
48
+ _doas_user: Optional[str] = None,
49
+ # Shell arguments
50
+ _shell_executable: Optional[str] = None,
51
+ _chdir: Optional[str] = None,
52
+ _env: Optional[Mapping[str, str]] = None,
53
+ # Connector control
54
+ _success_exit_codes: Iterable[int] = (0,),
55
+ _timeout: Optional[int] = None,
56
+ _get_pty: bool = False,
57
+ _stdin: Union[None, str, list[str], tuple[str, ...]] = None,
58
+ #
59
+ # MetaArguments
60
+ #
61
+ name: Optional[str] = None,
62
+ _ignore_errors: bool = False,
63
+ _continue_on_error: bool = False,
64
+ _if: Union[List[Callable[[], bool]], Callable[[], bool], None] = None,
65
+ #
66
+ # ExecutionArguments
67
+ #
68
+ _parallel: Optional[int] = None,
69
+ _run_once: bool = False,
70
+ _serial: bool = False,
71
+ #
72
+ # op args
73
+ #
74
+ *args: P.args,
75
+ #
76
+ # op kwargs
77
+ #
78
+ **kwargs: P.kwargs,
79
+ ) -> "OperationMeta": ...