pyinfra 3.4__tar.gz → 3.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. {pyinfra-3.4 → pyinfra-3.5}/CHANGELOG.md +35 -1
  2. {pyinfra-3.4/pyinfra.egg-info → pyinfra-3.5}/PKG-INFO +3 -3
  3. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/arguments.py +63 -1
  4. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/config.py +6 -0
  5. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/connect.py +19 -2
  6. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/operation.py +54 -1
  7. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/operations.py +119 -56
  8. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/state.py +10 -2
  9. pyinfra-3.5/pyinfra/connectors/scp/__init__.py +1 -0
  10. pyinfra-3.5/pyinfra/connectors/scp/client.py +204 -0
  11. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/ssh.py +39 -7
  12. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/util.py +4 -0
  13. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/dnf.py +8 -4
  14. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/docker.py +28 -8
  15. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/files.py +167 -26
  16. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/server.py +55 -4
  17. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/util/packaging.py +1 -0
  18. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/yum.py +8 -4
  19. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/zypper.py +3 -3
  20. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/crontab.py +1 -1
  21. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/docker.py +130 -29
  22. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/files.py +162 -7
  23. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/git.py +1 -1
  24. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/openrc.py +13 -7
  25. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/pip.py +6 -7
  26. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/pipx.py +19 -7
  27. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/util/docker.py +49 -1
  28. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/util/files.py +70 -2
  29. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/util/packaging.py +98 -55
  30. {pyinfra-3.4 → pyinfra-3.5/pyinfra.egg-info}/PKG-INFO +3 -3
  31. {pyinfra-3.4 → pyinfra-3.5}/pyinfra.egg-info/SOURCES.txt +2 -0
  32. {pyinfra-3.4 → pyinfra-3.5}/pyinfra.egg-info/requires.txt +2 -2
  33. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/main.py +39 -0
  34. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/prints.py +4 -0
  35. {pyinfra-3.4 → pyinfra-3.5}/setup.py +1 -1
  36. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_operations.py +348 -0
  37. {pyinfra-3.4 → pyinfra-3.5}/tests/test_cli/test_cli.py +3 -0
  38. {pyinfra-3.4 → pyinfra-3.5}/LICENSE.md +0 -0
  39. {pyinfra-3.4 → pyinfra-3.5}/MANIFEST.in +0 -0
  40. {pyinfra-3.4 → pyinfra-3.5}/README.md +0 -0
  41. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/__init__.py +0 -0
  42. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/__main__.py +0 -0
  43. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/__init__.py +0 -0
  44. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/arguments_typed.py +0 -0
  45. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/command.py +0 -0
  46. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/connectors.py +0 -0
  47. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/deploy.py +0 -0
  48. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/exceptions.py +0 -0
  49. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/facts.py +0 -0
  50. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/host.py +0 -0
  51. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/inventory.py +0 -0
  52. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/api/util.py +0 -0
  53. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/__init__.py +0 -0
  54. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/base.py +0 -0
  55. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/chroot.py +0 -0
  56. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/docker.py +0 -0
  57. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/dockerssh.py +0 -0
  58. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/local.py +0 -0
  59. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/ssh_util.py +0 -0
  60. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/sshuserclient/__init__.py +0 -0
  61. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/sshuserclient/client.py +0 -0
  62. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/sshuserclient/config.py +0 -0
  63. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/terraform.py +0 -0
  64. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/connectors/vagrant.py +0 -0
  65. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/context.py +0 -0
  66. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/__init__.py +0 -0
  67. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/apk.py +0 -0
  68. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/apt.py +0 -0
  69. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/brew.py +0 -0
  70. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/bsdinit.py +0 -0
  71. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/cargo.py +0 -0
  72. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/choco.py +0 -0
  73. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/crontab.py +0 -0
  74. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/deb.py +0 -0
  75. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/efibootmgr.py +0 -0
  76. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/flatpak.py +0 -0
  77. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/freebsd.py +0 -0
  78. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/gem.py +0 -0
  79. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/git.py +0 -0
  80. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/gpg.py +0 -0
  81. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/hardware.py +0 -0
  82. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/iptables.py +0 -0
  83. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/launchd.py +0 -0
  84. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/lxd.py +0 -0
  85. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/mysql.py +0 -0
  86. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/npm.py +0 -0
  87. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/openrc.py +0 -0
  88. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/opkg.py +0 -0
  89. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/pacman.py +0 -0
  90. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/pip.py +0 -0
  91. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/pipx.py +0 -0
  92. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/pkg.py +0 -0
  93. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/pkgin.py +0 -0
  94. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/podman.py +0 -0
  95. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/postgres.py +0 -0
  96. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/postgresql.py +0 -0
  97. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/rpm.py +0 -0
  98. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/runit.py +0 -0
  99. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/selinux.py +0 -0
  100. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/snap.py +0 -0
  101. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/systemd.py +0 -0
  102. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/sysvinit.py +0 -0
  103. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/upstart.py +0 -0
  104. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/util/__init__.py +0 -0
  105. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/util/databases.py +0 -0
  106. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/util/units.py +0 -0
  107. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/util/win_files.py +0 -0
  108. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/vzctl.py +0 -0
  109. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/xbps.py +0 -0
  110. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/facts/zfs.py +0 -0
  111. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/local.py +0 -0
  112. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/__init__.py +0 -0
  113. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/apk.py +0 -0
  114. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/apt.py +0 -0
  115. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/brew.py +0 -0
  116. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/bsdinit.py +0 -0
  117. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/cargo.py +0 -0
  118. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/choco.py +0 -0
  119. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/dnf.py +0 -0
  120. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/flatpak.py +0 -0
  121. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/freebsd/__init__.py +0 -0
  122. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/freebsd/freebsd_update.py +0 -0
  123. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/freebsd/pkg.py +0 -0
  124. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/freebsd/service.py +0 -0
  125. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/freebsd/sysrc.py +0 -0
  126. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/gem.py +0 -0
  127. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/iptables.py +0 -0
  128. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/launchd.py +0 -0
  129. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/lxd.py +0 -0
  130. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/mysql.py +0 -0
  131. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/npm.py +0 -0
  132. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/opkg.py +0 -0
  133. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/pacman.py +0 -0
  134. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/pkg.py +0 -0
  135. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/pkgin.py +0 -0
  136. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/postgres.py +0 -0
  137. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/postgresql.py +0 -0
  138. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/puppet.py +0 -0
  139. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/python.py +0 -0
  140. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/runit.py +0 -0
  141. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/selinux.py +0 -0
  142. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/server.py +0 -0
  143. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/snap.py +0 -0
  144. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/ssh.py +0 -0
  145. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/systemd.py +0 -0
  146. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/sysvinit.py +0 -0
  147. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/upstart.py +0 -0
  148. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/util/__init__.py +0 -0
  149. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/util/service.py +0 -0
  150. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/vzctl.py +0 -0
  151. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/xbps.py +0 -0
  152. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/yum.py +0 -0
  153. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/zfs.py +0 -0
  154. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/operations/zypper.py +0 -0
  155. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/progress.py +0 -0
  156. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/py.typed +0 -0
  157. {pyinfra-3.4 → pyinfra-3.5}/pyinfra/version.py +0 -0
  158. {pyinfra-3.4 → pyinfra-3.5}/pyinfra.egg-info/dependency_links.txt +0 -0
  159. {pyinfra-3.4 → pyinfra-3.5}/pyinfra.egg-info/entry_points.txt +0 -0
  160. {pyinfra-3.4 → pyinfra-3.5}/pyinfra.egg-info/top_level.txt +0 -0
  161. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/__init__.py +0 -0
  162. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/__main__.py +0 -0
  163. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/commands.py +0 -0
  164. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/exceptions.py +0 -0
  165. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/inventory.py +0 -0
  166. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/log.py +0 -0
  167. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/util.py +0 -0
  168. {pyinfra-3.4 → pyinfra-3.5}/pyinfra_cli/virtualenv.py +0 -0
  169. {pyinfra-3.4 → pyinfra-3.5}/pyproject.toml +0 -0
  170. {pyinfra-3.4 → pyinfra-3.5}/setup.cfg +0 -0
  171. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/__init__.py +0 -0
  172. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api.py +0 -0
  173. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_arguments.py +0 -0
  174. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_command.py +0 -0
  175. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_config.py +0 -0
  176. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_deploys.py +0 -0
  177. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_facts.py +0 -0
  178. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_host.py +0 -0
  179. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_inventory.py +0 -0
  180. {pyinfra-3.4 → pyinfra-3.5}/tests/test_api/test_api_util.py +0 -0
  181. {pyinfra-3.4 → pyinfra-3.5}/tests/test_cli/__init__.py +0 -0
  182. {pyinfra-3.4 → pyinfra-3.5}/tests/test_cli/test_cli_deploy.py +0 -0
  183. {pyinfra-3.4 → pyinfra-3.5}/tests/test_cli/test_cli_exceptions.py +0 -0
  184. {pyinfra-3.4 → pyinfra-3.5}/tests/test_cli/test_cli_inventory.py +0 -0
  185. {pyinfra-3.4 → pyinfra-3.5}/tests/test_cli/test_cli_util.py +0 -0
  186. {pyinfra-3.4 → pyinfra-3.5}/tests/test_cli/test_context_objects.py +0 -0
  187. {pyinfra-3.4 → pyinfra-3.5}/tests/test_cli/util.py +0 -0
  188. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/__init__.py +0 -0
  189. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_chroot.py +0 -0
  190. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_docker.py +0 -0
  191. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_dockerssh.py +0 -0
  192. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_local.py +0 -0
  193. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_ssh.py +0 -0
  194. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_sshuserclient.py +0 -0
  195. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_terraform.py +0 -0
  196. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_util.py +0 -0
  197. {pyinfra-3.4 → pyinfra-3.5}/tests/test_connectors/test_vagrant.py +0 -0
  198. {pyinfra-3.4 → pyinfra-3.5}/tests/test_facts.py +0 -0
  199. {pyinfra-3.4 → pyinfra-3.5}/tests/test_global_arguments.py +0 -0
  200. {pyinfra-3.4 → pyinfra-3.5}/tests/test_operations.py +0 -0
  201. {pyinfra-3.4 → pyinfra-3.5}/tests/test_operations_utils.py +0 -0
  202. {pyinfra-3.4 → pyinfra-3.5}/tests/test_units.py +0 -0
@@ -1,3 +1,37 @@
1
+ # v3.5
2
+
3
+ New release with some really awesome new features, brought to you by the fantastic contributions of the community. New stuff:
4
+
5
+ - add `--diff` argument to show file diffs for potential file changes (@jgelens)
6
+ - add `_retries`, `_retry_delay` and `_retry_until` global arguments (@shohamd4)
7
+ - parallelize disconnecting from hosts (@gwelch-contegix)
8
+ - enable using SCP instead of SFTP for SSH file transfers (@DonDebonair)
9
+
10
+ New and updated operations/facts:
11
+ - facts/server: add `RebootRequired` fact (@wowi42)
12
+ - operations/pip: support PEP-508 package versions (@morrison12)
13
+ - operations+facts/docker: add Docker plugin support (@DonDebonair)
14
+ - operations/files.put: add `atime` and `mtime` arguments (@vram0gh2)
15
+ - operations/openrc: support runlevel when enabling services (@sengo4hd)
16
+ - facts/yum+dnf+zypper: return `repoid` in repository facts
17
+
18
+ Operation/fact fixes:
19
+
20
+ - facts/files.File: add ls fallback support (@mrkbac)
21
+ - operations/openrc: add missing noop messages (@sengo4hd)
22
+ - operations/server.crontab: fix newline when replacing existing values (@Nananas)
23
+ - operations/files.block: fix examples doc (@morrison12)
24
+ - operations/files.block: fix case where file exists but line is missing (@morrison12)
25
+ - operations/files.block: improve handling of special characters in marker lines (@morrison12)
26
+
27
+ Internal/meta:
28
+
29
+ - documentation link fix (@sengo4hd)
30
+
31
+ # v3.4.1
32
+
33
+ - fix config context when getting operation arguments
34
+
1
35
  # v3.4
2
36
 
3
37
  Much delayed 3.4, great collection of additions and improvements. Huge THANK YOU to all contributors as always. New features:
@@ -27,7 +61,7 @@ Operation/fact fixes:
27
61
 
28
62
  Internal/meta:
29
63
 
30
- - remove unncessary setuptools runtime dependency (@karlicoss)
64
+ - remove unnecessary setuptools runtime dependency (@karlicoss)
31
65
 
32
66
  # v3.3.1
33
67
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyinfra
3
- Version: 3.4
3
+ Version: 3.5
4
4
  Summary: pyinfra automates/provisions/manages/deploys infrastructure.
5
5
  Home-page: https://pyinfra.com
6
6
  Author: Nick / Fizzadar
@@ -52,7 +52,7 @@ Requires-Dist: flake8-isort==6.1.2; extra == "test"
52
52
  Requires-Dist: pyyaml==6.0.2; extra == "test"
53
53
  Requires-Dist: mypy; extra == "test"
54
54
  Requires-Dist: types-cryptography; extra == "test"
55
- Requires-Dist: types-paramiko; extra == "test"
55
+ Requires-Dist: types-paramiko<4; extra == "test"
56
56
  Requires-Dist: types-python-dateutil; extra == "test"
57
57
  Requires-Dist: types-PyYAML; extra == "test"
58
58
  Provides-Extra: docs
@@ -73,7 +73,7 @@ Requires-Dist: flake8-isort==6.1.2; extra == "dev"
73
73
  Requires-Dist: pyyaml==6.0.2; extra == "dev"
74
74
  Requires-Dist: mypy; extra == "dev"
75
75
  Requires-Dist: types-cryptography; extra == "dev"
76
- Requires-Dist: types-paramiko; extra == "dev"
76
+ Requires-Dist: types-paramiko<4; extra == "dev"
77
77
  Requires-Dist: types-python-dateutil; extra == "dev"
78
78
  Requires-Dist: types-PyYAML; extra == "dev"
79
79
  Requires-Dist: pyinfra-guzzle_sphinx_theme==0.17; extra == "dev"
@@ -72,6 +72,11 @@ class ConnectorArguments(TypedDict, total=False):
72
72
  _get_pty: bool
73
73
  _stdin: Union[str, Iterable[str]]
74
74
 
75
+ # Retry arguments
76
+ _retries: int
77
+ _retry_delay: Union[int, float]
78
+ _retry_until: Optional[Callable[[dict], bool]]
79
+
75
80
 
76
81
  def generate_env(config: "Config", value: dict) -> dict:
77
82
  env = config.ENV.copy()
@@ -232,11 +237,28 @@ def all_global_arguments() -> List[tuple[str, Type]]:
232
237
  return list(get_type_hints(AllArguments).items())
233
238
 
234
239
 
240
+ # Create a dictionary for retry arguments
241
+ retry_argument_meta: dict[str, ArgumentMeta] = {
242
+ "_retries": ArgumentMeta(
243
+ "Number of times to retry failed operations.",
244
+ default=lambda config: config.RETRY,
245
+ ),
246
+ "_retry_delay": ArgumentMeta(
247
+ "Delay in seconds between retry attempts.",
248
+ default=lambda config: config.RETRY_DELAY,
249
+ ),
250
+ "_retry_until": ArgumentMeta(
251
+ "Callable taking output data that returns True to continue retrying.",
252
+ default=lambda config: None,
253
+ ),
254
+ }
255
+
235
256
  all_argument_meta: dict[str, ArgumentMeta] = {
236
257
  **auth_argument_meta,
237
258
  **shell_argument_meta,
238
259
  **meta_argument_meta,
239
260
  **execution_argument_meta,
261
+ **retry_argument_meta, # Add retry arguments
240
262
  }
241
263
 
242
264
  EXECUTION_KWARG_KEYS = list(ExecutionArguments.__annotations__.keys())
@@ -286,6 +308,45 @@ __argument_docs__ = {
286
308
  ),
287
309
  "Operation meta & callbacks": (meta_argument_meta, "", ""),
288
310
  "Execution strategy": (execution_argument_meta, "", ""),
311
+ "Retry behavior": (
312
+ retry_argument_meta,
313
+ """
314
+ Retry arguments allow you to automatically retry operations that fail. You can specify
315
+ how many times to retry, the delay between retries, and optionally a condition
316
+ function to determine when to stop retrying.
317
+ """,
318
+ """
319
+ .. code:: python
320
+
321
+ # Retry a command up to 3 times with the default 5 second delay
322
+ server.shell(
323
+ name="Run flaky command with retries",
324
+ commands=["flaky_command"],
325
+ _retries=3,
326
+ )
327
+ # Retry with a custom delay
328
+ server.shell(
329
+ name="Run flaky command with custom delay",
330
+ commands=["flaky_command"],
331
+ _retries=2,
332
+ _retry_delay=10, # 10 second delay between retries
333
+ )
334
+ # Retry with a custom condition
335
+ def retry_on_specific_error(output_data):
336
+ # Retry if stderr contains "temporary failure"
337
+ for line in output_data["stderr_lines"]:
338
+ if "temporary failure" in line.lower():
339
+ return True
340
+ return False
341
+
342
+ server.shell(
343
+ name="Run command with conditional retry",
344
+ commands=["flaky_command"],
345
+ _retries=5,
346
+ _retry_until=retry_on_specific_error,
347
+ )
348
+ """,
349
+ ),
289
350
  }
290
351
 
291
352
 
@@ -305,7 +366,8 @@ def pop_global_arguments(
305
366
 
306
367
  config = state.config
307
368
  if ctx_config.isset():
308
- config = config
369
+ config = ctx_config.get()
370
+ assert config is not None
309
371
 
310
372
  cdkwargs = host.current_deploy_kwargs
311
373
  meta_kwargs: dict[str, Any] = cdkwargs or {} # type: ignore[assignment]
@@ -53,6 +53,12 @@ class ConfigDefaults:
53
53
  IGNORE_ERRORS: bool = False
54
54
  # Shell to use to execute commands
55
55
  SHELL: str = "sh"
56
+ # Whether to display full diffs for files
57
+ DIFF: bool = False
58
+ # Number of times to retry failed operations
59
+ RETRY: int = 0
60
+ # Delay in seconds between retry attempts
61
+ RETRY_DELAY: int = 5
56
62
 
57
63
 
58
64
  config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items() if key.isupper()}
@@ -46,5 +46,22 @@ def connect_all(state: "State"):
46
46
 
47
47
 
48
48
  def disconnect_all(state: "State"):
49
- for host in state.activated_hosts: # only hosts we connected to please!
50
- host.disconnect() # normally a noop
49
+ """
50
+ Disconnect from all of the configured servers in parallel. Reads/writes state.inventory.
51
+
52
+ Args:
53
+ state (``pyinfra.api.State`` obj): the state containing an inventory to connect to
54
+ """
55
+ greenlet_to_host = {
56
+ state.pool.spawn(host.disconnect): host
57
+ for host in state.activated_hosts # only hosts we connected to please!
58
+ }
59
+
60
+ with progress_spinner(greenlet_to_host.values()) as progress:
61
+ for greenlet in gevent.iwait(greenlet_to_host.keys()):
62
+ host = greenlet_to_host[greenlet]
63
+ progress(host)
64
+
65
+ for greenlet, host in greenlet_to_host.items():
66
+ # Raise any unexpected exception
67
+ greenlet.get()
@@ -47,6 +47,9 @@ class OperationMeta:
47
47
  _commands: Optional[list[Any]] = None
48
48
  _maybe_is_change: Optional[bool] = None
49
49
  _success: Optional[bool] = None
50
+ _retry_attempts: int = 0
51
+ _max_retries: int = 0
52
+ _retry_succeeded: Optional[bool] = None
50
53
 
51
54
  def __init__(self, hash, is_change: Optional[bool]):
52
55
  self._hash = hash
@@ -59,9 +62,17 @@ class OperationMeta:
59
62
  """
60
63
 
61
64
  if self._commands is not None:
65
+ retry_info = ""
66
+ if self._retry_attempts > 0:
67
+ retry_result = "succeeded" if self._retry_succeeded else "failed"
68
+ retry_info = (
69
+ f", retries={self._retry_attempts}/{self._max_retries} ({retry_result})"
70
+ )
71
+
62
72
  return (
63
73
  "OperationMeta(executed=True, "
64
- f"success={self.did_succeed()}, hash={self._hash}, commands={len(self._commands)})"
74
+ f"success={self.did_succeed()}, hash={self._hash}, "
75
+ f"commands={len(self._commands)}{retry_info})"
65
76
  )
66
77
  return (
67
78
  "OperationMeta(executed=False, "
@@ -74,12 +85,20 @@ class OperationMeta:
74
85
  success: bool,
75
86
  commands: list[Any],
76
87
  combined_output: "CommandOutput",
88
+ retry_attempts: int = 0,
89
+ max_retries: int = 0,
77
90
  ) -> None:
78
91
  if self.is_complete():
79
92
  raise RuntimeError("Cannot complete an already complete operation")
80
93
  self._success = success
81
94
  self._commands = commands
82
95
  self._combined_output = combined_output
96
+ self._retry_attempts = retry_attempts
97
+ self._max_retries = max_retries
98
+
99
+ # Determine if operation succeeded after retries
100
+ if retry_attempts > 0:
101
+ self._retry_succeeded = success
83
102
 
84
103
  def is_complete(self) -> bool:
85
104
  return self._success is not None
@@ -150,6 +169,40 @@ class OperationMeta:
150
169
  def stderr(self) -> str:
151
170
  return "\n".join(self.stderr_lines)
152
171
 
172
+ @property
173
+ def retry_attempts(self) -> int:
174
+ return self._retry_attempts
175
+
176
+ @property
177
+ def max_retries(self) -> int:
178
+ return self._max_retries
179
+
180
+ @property
181
+ def was_retried(self) -> bool:
182
+ """
183
+ Returns whether this operation was retried at least once.
184
+ """
185
+ return self._retry_attempts > 0
186
+
187
+ @property
188
+ def retry_succeeded(self) -> Optional[bool]:
189
+ """
190
+ Returns whether this operation succeeded after retries.
191
+ Returns None if the operation was not retried.
192
+ """
193
+ return self._retry_succeeded
194
+
195
+ def get_retry_info(self) -> dict[str, Any]:
196
+ """
197
+ Returns a dictionary with all retry-related information.
198
+ """
199
+ return {
200
+ "retry_attempts": self._retry_attempts,
201
+ "max_retries": self._max_retries,
202
+ "was_retried": self.was_retried,
203
+ "retry_succeeded": self._retry_succeeded,
204
+ }
205
+
153
206
 
154
207
  def add_op(state: State, op_func, *args, **kwargs):
155
208
  """
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import time
3
4
  import traceback
4
5
  from itertools import product
5
6
  from socket import error as socket_error, timeout as timeout_error
@@ -66,6 +67,11 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
66
67
  continue_on_error = global_arguments["_continue_on_error"]
67
68
  timeout = global_arguments.get("_timeout", 0)
68
69
 
70
+ # Extract retry arguments
71
+ retries = global_arguments.get("_retries", 0)
72
+ retry_delay = global_arguments.get("_retry_delay", 5)
73
+ retry_until = global_arguments.get("_retry_until", None)
74
+
69
75
  executor_kwarg_keys = CONNECTOR_ARGUMENT_KEYS
70
76
  # See: https://github.com/python/mypy/issues/10371
71
77
  base_connector_arguments: ConnectorArguments = cast(
@@ -73,67 +79,114 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
73
79
  {key: global_arguments[key] for key in executor_kwarg_keys if key in global_arguments}, # type: ignore[literal-required] # noqa
74
80
  )
75
81
 
82
+ retry_attempt = 0
76
83
  did_error = False
77
84
  executed_commands = 0
78
- commands = []
85
+ commands: list[PyinfraCommand] = []
79
86
  all_output_lines: list[OutputLine] = []
80
87
 
81
- for command in op_data.command_generator():
82
- commands.append(command)
83
-
84
- status = False
85
-
86
- connector_arguments = base_connector_arguments.copy()
87
- connector_arguments.update(command.connector_arguments)
88
-
89
- if not isinstance(command, PyinfraCommand):
90
- raise TypeError("{0} is an invalid pyinfra command!".format(command))
91
-
92
- if isinstance(command, FunctionCommand):
93
- try:
94
- status = command.execute(state, host, connector_arguments)
95
- except Exception as e:
96
- # Custom functions could do anything, so expect anything!
97
- logger.warning(traceback.format_exc())
98
- host.log_styled(
99
- f"Unexpected error in Python callback: {format_exception(e)}",
100
- fg="red",
101
- log_func=logger.warning,
102
- )
103
-
104
- elif isinstance(command, StringCommand):
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)
118
-
119
- else:
120
- try:
121
- status = command.execute(state, host, connector_arguments)
122
- except (timeout_error, socket_error, SSHException, IOError) as e:
123
- log_host_command_error(host, e, timeout=timeout)
124
-
125
- # Break the loop to trigger a failure
126
- if status is False:
127
- did_error = True
128
- if continue_on_error is True:
129
- continue
130
- break
88
+ # Retry loop
89
+ while retry_attempt <= retries:
90
+ did_error = False
91
+ executed_commands = 0
92
+ commands = []
93
+ all_output_lines = []
94
+
95
+ for command in op_data.command_generator():
96
+ commands.append(command)
97
+ status = False
98
+ connector_arguments = base_connector_arguments.copy()
99
+ connector_arguments.update(command.connector_arguments)
100
+
101
+ if not isinstance(command, PyinfraCommand):
102
+ raise TypeError("{0} is an invalid pyinfra command!".format(command))
103
+
104
+ if isinstance(command, FunctionCommand):
105
+ try:
106
+ status = command.execute(state, host, connector_arguments)
107
+ except Exception as e:
108
+ # Custom functions could do anything, so expect anything!
109
+ logger.warning(traceback.format_exc())
110
+ host.log_styled(
111
+ f"Unexpected error in Python callback: {format_exception(e)}",
112
+ fg="red",
113
+ log_func=logger.warning,
114
+ )
115
+
116
+ elif isinstance(command, StringCommand):
117
+ output_lines = CommandOutput([])
118
+ try:
119
+ status, output_lines = command.execute(
120
+ state,
121
+ host,
122
+ connector_arguments,
123
+ )
124
+ except (timeout_error, socket_error, SSHException) as e:
125
+ log_host_command_error(host, e, timeout=timeout)
126
+ all_output_lines.extend(output_lines)
127
+ # If we failed and have not already printed the stderr, print it
128
+ if status is False and not state.print_output:
129
+ print_host_combined_output(host, output_lines)
130
+
131
+ else:
132
+ try:
133
+ status = command.execute(state, host, connector_arguments)
134
+ except (timeout_error, socket_error, SSHException, IOError) as e:
135
+ log_host_command_error(host, e, timeout=timeout)
136
+
137
+ # Break the loop to trigger a failure
138
+ if status is False:
139
+ did_error = True
140
+ if continue_on_error is True:
141
+ continue
142
+ break
143
+
144
+ executed_commands += 1
145
+
146
+ # Check if we should retry
147
+ should_retry = False
148
+ if retry_attempt < retries:
149
+ # Retry on error
150
+ if did_error:
151
+ should_retry = True
152
+ # Retry on condition if no error
153
+ elif retry_until and not did_error:
154
+ try:
155
+ output_data = {
156
+ "stdout_lines": [
157
+ line.line for line in all_output_lines if line.buffer_name == "stdout"
158
+ ],
159
+ "stderr_lines": [
160
+ line.line for line in all_output_lines if line.buffer_name == "stderr"
161
+ ],
162
+ "commands": [str(command) for command in commands],
163
+ "executed_commands": executed_commands,
164
+ "host": host.name,
165
+ "operation": ", ".join(state.get_op_meta(op_hash).names) or "Operation",
166
+ }
167
+ should_retry = retry_until(output_data)
168
+ except Exception as e:
169
+ host.log_styled(
170
+ f"Error in retry_until function: {format_exception(e)}",
171
+ fg="red",
172
+ log_func=logger.warning,
173
+ )
174
+
175
+ if should_retry:
176
+ retry_attempt += 1
177
+ state.trigger_callbacks("operation_host_retry", host, op_hash, retry_attempt, retries)
178
+ op_name = ", ".join(state.get_op_meta(op_hash).names) or "Operation"
179
+ host.log_styled(
180
+ f"Retrying {op_name} (attempt {retry_attempt}/{retries}) after {retry_delay}s...",
181
+ fg="yellow",
182
+ log_func=logger.info,
183
+ )
184
+ time.sleep(retry_delay)
185
+ continue
131
186
 
132
- executed_commands += 1
187
+ break
133
188
 
134
189
  # Handle results
135
- #
136
-
137
190
  op_success = return_status = not did_error
138
191
  host_results = state.get_results_for_host(host)
139
192
 
@@ -142,10 +195,13 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
142
195
  host_results.success_ops += 1
143
196
 
144
197
  _status_log = "Success" if executed_commands > 0 else "No changes"
198
+ if retry_attempt > 0:
199
+ _status_log = f"{_status_log} on retry {retry_attempt}"
200
+
145
201
  _click_log_status = click.style(_status_log, "green")
146
202
  logger.info("{0}{1}".format(host.print_prefix, _click_log_status))
147
203
 
148
- state.trigger_callbacks("operation_host_success", host, op_hash)
204
+ state.trigger_callbacks("operation_host_success", host, op_hash, retry_attempt)
149
205
  else:
150
206
  if ignore_errors:
151
207
  host_results.ignored_error_ops += 1
@@ -156,6 +212,11 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
156
212
  host_results.partial_ops += 1
157
213
 
158
214
  _command_description = f"executed {executed_commands} commands"
215
+ if retry_attempt > 0:
216
+ _command_description = (
217
+ f"{_command_description} (failed after {retry_attempt}/{retries} retries)"
218
+ )
219
+
159
220
  log_error_or_warning(host, ignore_errors, _command_description, continue_on_error)
160
221
 
161
222
  # Ignored, op "completes" w/ ignored error
@@ -164,12 +225,14 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
164
225
  return_status = True
165
226
 
166
227
  # Unignored error -> False
167
- state.trigger_callbacks("operation_host_error", host, op_hash)
228
+ state.trigger_callbacks("operation_host_error", host, op_hash, retry_attempt, retries)
168
229
 
169
230
  op_data.operation_meta.set_complete(
170
231
  op_success,
171
232
  commands,
172
233
  CommandOutput(all_output_lines),
234
+ retry_attempts=retry_attempt,
235
+ max_retries=retries,
173
236
  )
174
237
 
175
238
  return return_status
@@ -70,11 +70,19 @@ class BaseStateCallback:
70
70
  pass
71
71
 
72
72
  @staticmethod
73
- def operation_host_success(state: "State", host: "Host", op_hash):
73
+ def operation_host_success(state: "State", host: "Host", op_hash, retry_count: int = 0):
74
74
  pass
75
75
 
76
76
  @staticmethod
77
- def operation_host_error(state: "State", host: "Host", op_hash):
77
+ def operation_host_error(
78
+ state: "State", host: "Host", op_hash, retry_count: int = 0, max_retries: int = 0
79
+ ):
80
+ pass
81
+
82
+ @staticmethod
83
+ def operation_host_retry(
84
+ state: "State", host: "Host", op_hash, retry_num: int, max_retries: int
85
+ ):
78
86
  pass
79
87
 
80
88
  @staticmethod
@@ -0,0 +1 @@
1
+ from .client import SCPClient # noqa: F401