pyinfra 3.1.1__tar.gz → 3.2__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 (194) hide show
  1. {pyinfra-3.1.1 → pyinfra-3.2}/CHANGELOG.md +38 -0
  2. {pyinfra-3.1.1/pyinfra.egg-info → pyinfra-3.2}/PKG-INFO +11 -12
  3. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/arguments.py +9 -2
  4. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/deploy.py +4 -2
  5. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/host.py +5 -3
  6. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/docker.py +17 -6
  7. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/sshuserclient/client.py +26 -14
  8. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/apk.py +3 -1
  9. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/apt.py +60 -0
  10. pyinfra-3.2/pyinfra/facts/crontab.py +190 -0
  11. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/docker.py +6 -0
  12. pyinfra-3.2/pyinfra/facts/efibootmgr.py +108 -0
  13. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/files.py +93 -6
  14. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/git.py +3 -2
  15. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/mysql.py +1 -2
  16. pyinfra-3.2/pyinfra/facts/opkg.py +233 -0
  17. pyinfra-3.2/pyinfra/facts/pipx.py +74 -0
  18. pyinfra-3.2/pyinfra/facts/podman.py +47 -0
  19. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/postgres.py +2 -0
  20. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/server.py +39 -77
  21. pyinfra-3.2/pyinfra/facts/util/units.py +30 -0
  22. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/zfs.py +22 -19
  23. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/local.py +3 -2
  24. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/apt.py +27 -20
  25. pyinfra-3.2/pyinfra/operations/crontab.py +189 -0
  26. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/docker.py +13 -12
  27. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/files.py +18 -0
  28. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/git.py +23 -7
  29. pyinfra-3.2/pyinfra/operations/opkg.py +88 -0
  30. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/pip.py +3 -2
  31. pyinfra-3.2/pyinfra/operations/pipx.py +90 -0
  32. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/postgres.py +15 -11
  33. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/runit.py +2 -0
  34. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/server.py +3 -177
  35. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/zfs.py +3 -3
  36. {pyinfra-3.1.1 → pyinfra-3.2/pyinfra.egg-info}/PKG-INFO +11 -12
  37. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/SOURCES.txt +10 -0
  38. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/requires.txt +10 -11
  39. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/inventory.py +26 -9
  40. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/prints.py +18 -3
  41. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/util.py +3 -0
  42. {pyinfra-3.1.1 → pyinfra-3.2}/setup.py +5 -6
  43. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli_deploy.py +15 -13
  44. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli_inventory.py +53 -0
  45. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_sshuserclient.py +68 -1
  46. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_facts.py +3 -0
  47. pyinfra-3.2/tests/test_units.py +30 -0
  48. {pyinfra-3.1.1 → pyinfra-3.2}/LICENSE.md +0 -0
  49. {pyinfra-3.1.1 → pyinfra-3.2}/MANIFEST.in +0 -0
  50. {pyinfra-3.1.1 → pyinfra-3.2}/README.md +0 -0
  51. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/__init__.py +0 -0
  52. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/__main__.py +0 -0
  53. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/__init__.py +0 -0
  54. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/arguments_typed.py +0 -0
  55. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/command.py +0 -0
  56. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/config.py +0 -0
  57. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/connect.py +0 -0
  58. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/connectors.py +0 -0
  59. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/exceptions.py +0 -0
  60. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/facts.py +0 -0
  61. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/inventory.py +0 -0
  62. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/operation.py +0 -0
  63. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/operations.py +0 -0
  64. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/state.py +0 -0
  65. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/util.py +0 -0
  66. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/__init__.py +0 -0
  67. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/base.py +0 -0
  68. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/chroot.py +0 -0
  69. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/dockerssh.py +0 -0
  70. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/local.py +0 -0
  71. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/ssh.py +0 -0
  72. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/ssh_util.py +0 -0
  73. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/sshuserclient/__init__.py +0 -0
  74. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/sshuserclient/config.py +0 -0
  75. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/terraform.py +0 -0
  76. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/util.py +0 -0
  77. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/vagrant.py +0 -0
  78. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/context.py +0 -0
  79. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/__init__.py +0 -0
  80. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/brew.py +0 -0
  81. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/bsdinit.py +0 -0
  82. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/cargo.py +0 -0
  83. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/choco.py +0 -0
  84. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/deb.py +0 -0
  85. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/dnf.py +0 -0
  86. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/flatpak.py +0 -0
  87. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/gem.py +0 -0
  88. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/gpg.py +0 -0
  89. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/hardware.py +0 -0
  90. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/iptables.py +0 -0
  91. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/launchd.py +0 -0
  92. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/lxd.py +0 -0
  93. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/npm.py +0 -0
  94. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/openrc.py +0 -0
  95. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/pacman.py +0 -0
  96. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/pip.py +0 -0
  97. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/pkg.py +0 -0
  98. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/pkgin.py +0 -0
  99. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/postgresql.py +0 -0
  100. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/rpm.py +0 -0
  101. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/runit.py +0 -0
  102. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/selinux.py +0 -0
  103. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/snap.py +0 -0
  104. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/systemd.py +0 -0
  105. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/sysvinit.py +0 -0
  106. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/upstart.py +0 -0
  107. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/util/__init__.py +0 -0
  108. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/util/databases.py +0 -0
  109. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/util/packaging.py +0 -0
  110. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/util/win_files.py +0 -0
  111. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/vzctl.py +0 -0
  112. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/xbps.py +0 -0
  113. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/yum.py +0 -0
  114. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/zypper.py +0 -0
  115. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/__init__.py +0 -0
  116. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/apk.py +0 -0
  117. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/brew.py +0 -0
  118. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/bsdinit.py +0 -0
  119. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/cargo.py +0 -0
  120. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/choco.py +0 -0
  121. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/dnf.py +0 -0
  122. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/flatpak.py +0 -0
  123. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/gem.py +0 -0
  124. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/iptables.py +0 -0
  125. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/launchd.py +0 -0
  126. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/lxd.py +0 -0
  127. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/mysql.py +0 -0
  128. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/npm.py +0 -0
  129. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/openrc.py +0 -0
  130. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/pacman.py +0 -0
  131. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/pkg.py +0 -0
  132. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/pkgin.py +0 -0
  133. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/postgresql.py +0 -0
  134. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/puppet.py +0 -0
  135. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/python.py +0 -0
  136. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/selinux.py +0 -0
  137. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/snap.py +0 -0
  138. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/ssh.py +0 -0
  139. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/systemd.py +0 -0
  140. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/sysvinit.py +0 -0
  141. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/upstart.py +0 -0
  142. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/__init__.py +0 -0
  143. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/docker.py +0 -0
  144. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/files.py +0 -0
  145. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/packaging.py +0 -0
  146. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/service.py +0 -0
  147. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/vzctl.py +0 -0
  148. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/xbps.py +0 -0
  149. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/yum.py +0 -0
  150. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/zypper.py +0 -0
  151. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/progress.py +0 -0
  152. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/py.typed +0 -0
  153. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/version.py +0 -0
  154. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/dependency_links.txt +0 -0
  155. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/entry_points.txt +0 -0
  156. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/top_level.txt +0 -0
  157. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/__init__.py +0 -0
  158. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/__main__.py +0 -0
  159. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/commands.py +0 -0
  160. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/exceptions.py +0 -0
  161. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/log.py +0 -0
  162. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/main.py +0 -0
  163. {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/virtualenv.py +0 -0
  164. {pyinfra-3.1.1 → pyinfra-3.2}/pyproject.toml +0 -0
  165. {pyinfra-3.1.1 → pyinfra-3.2}/setup.cfg +0 -0
  166. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/__init__.py +0 -0
  167. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api.py +0 -0
  168. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_arguments.py +0 -0
  169. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_command.py +0 -0
  170. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_config.py +0 -0
  171. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_deploys.py +0 -0
  172. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_facts.py +0 -0
  173. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_host.py +0 -0
  174. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_inventory.py +0 -0
  175. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_operations.py +0 -0
  176. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_util.py +0 -0
  177. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/__init__.py +0 -0
  178. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli.py +0 -0
  179. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli_exceptions.py +0 -0
  180. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli_util.py +0 -0
  181. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_context_objects.py +0 -0
  182. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/util.py +0 -0
  183. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/__init__.py +0 -0
  184. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_chroot.py +0 -0
  185. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_docker.py +0 -0
  186. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_dockerssh.py +0 -0
  187. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_local.py +0 -0
  188. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_ssh.py +0 -0
  189. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_terraform.py +0 -0
  190. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_util.py +0 -0
  191. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_vagrant.py +0 -0
  192. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_global_arguments.py +0 -0
  193. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_operations.py +0 -0
  194. {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_operations_utils.py +0 -0
@@ -1,3 +1,41 @@
1
+ # v3.2
2
+
3
+ Hello 2025! Here's pyinfra 3.2 - with another incredible round of contributions from the community, THANK YOU ALL. New stuff:
4
+
5
+ - Add total counts to results summary (@NichtJens)
6
+ - Enable passing extra data via `local.include` (@TimothyWillard)
7
+ - Validate inventory files and display warnings for unexpected variables (@simonhammes)
8
+
9
+ New operations/facts:
10
+
11
+ - Add `pipx` operations (`packages`, `upgrade_all`, `ensure_path`) facts (`PipxPackages`, `PipxEnvironment`) and operations (@maisim)
12
+ - Add `server.OsRelease` fact (@wowi42)
13
+ - Add `podman.PodmanSystemInfo` and `podman.PodmanPs` facts (@bauen1)
14
+ - Add many extra arguments (including generic args) to `files.FindFiles*` facts (@JakkuSakura)
15
+ - Add `system` argument to `git.config` operation (@Pirols)
16
+ - Add `psql_database` argument to postgres operations & facts (@hamishfagg)
17
+ - Add `files.Sha384File` fact and `sha384sum` argument to `files.download` operation (@simonhammes)
18
+ - Add `apt.SimulateOperationWillChange` fact (@bauen1)
19
+ - Detect changes in `apt.upgrade` and `apt.dist_upgrade` operations (@bauen1)
20
+ - Add `fibootmgr.EFIBootMgr` fact (@bauen1)
21
+ - Add opkg facts and operations (@morrison12)
22
+
23
+ Fixes:
24
+
25
+ - Multiple fixes for `server.crontab` operation and facts (@JakkuSakura)
26
+ - Correctly handle `latest` argument with requirements file in `pip.packages` operation (@amiraliakbari)
27
+ - Fix regex used to parse installed apk packages (@simonhammes)
28
+ - Fix SSH connector overwriting known hosts files (@vo452)
29
+
30
+ Docs/internal tweaks:
31
+
32
+ - Add type annotations for many more operations (@simonhammes)
33
+ - Add typos CI checking to replace flake8-spellcheck (@simonhammes)
34
+ - Bump CI actions and dependencies (@simonhammes)
35
+ - Require JSON tests to include all arguments
36
+ - Remove unused `configparser` dependency (@bkmgit)
37
+ - Many small documentation fixes/tweaks
38
+
1
39
  # v3.1.1
2
40
 
3
41
  - Improve errors with 2.x style `@decorator` (vs `@decorator()`) functions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyinfra
3
- Version: 3.1.1
3
+ Version: 3.2
4
4
  Summary: pyinfra automates/provisions/manages/deploys infrastructure.
5
5
  Home-page: https://pyinfra.com
6
6
  Author: Nick / Fizzadar
@@ -33,7 +33,6 @@ Requires-Dist: click>2
33
33
  Requires-Dist: jinja2<4,>2
34
34
  Requires-Dist: python-dateutil<3,>2
35
35
  Requires-Dist: setuptools
36
- Requires-Dist: configparser
37
36
  Requires-Dist: pywinrm
38
37
  Requires-Dist: typeguard
39
38
  Requires-Dist: distro<2,>=1.6
@@ -42,12 +41,12 @@ Requires-Dist: graphlib_backport; python_version < "3.9"
42
41
  Requires-Dist: typing-extensions; python_version < "3.11"
43
42
  Requires-Dist: importlib_metadata>=3.6; python_version < "3.10"
44
43
  Provides-Extra: test
45
- Requires-Dist: pytest==8.2.1; extra == "test"
46
- Requires-Dist: coverage==7.5.1; extra == "test"
44
+ Requires-Dist: pytest==8.3.3; extra == "test"
45
+ Requires-Dist: coverage==7.6.1; extra == "test"
47
46
  Requires-Dist: pytest-cov==5.0.0; extra == "test"
48
- Requires-Dist: black==24.4.2; extra == "test"
47
+ Requires-Dist: black==24.8.0; extra == "test"
49
48
  Requires-Dist: isort==5.13.2; extra == "test"
50
- Requires-Dist: flake8==7.0.0; extra == "test"
49
+ Requires-Dist: flake8==7.1.1; extra == "test"
51
50
  Requires-Dist: flake8-black==0.3.6; extra == "test"
52
51
  Requires-Dist: flake8-isort==6.1.1; extra == "test"
53
52
  Requires-Dist: mypy; extra == "test"
@@ -58,15 +57,15 @@ Requires-Dist: types-PyYAML; extra == "test"
58
57
  Requires-Dist: types-setuptools; extra == "test"
59
58
  Provides-Extra: docs
60
59
  Requires-Dist: pyinfra-guzzle_sphinx_theme==0.16; extra == "docs"
61
- Requires-Dist: myst-parser==2.0.0; extra == "docs"
60
+ Requires-Dist: myst-parser==3.0.1; extra == "docs"
62
61
  Requires-Dist: sphinx==6.2.1; extra == "docs"
63
62
  Provides-Extra: dev
64
- Requires-Dist: pytest==8.2.1; extra == "dev"
65
- Requires-Dist: coverage==7.5.1; extra == "dev"
63
+ Requires-Dist: pytest==8.3.3; extra == "dev"
64
+ Requires-Dist: coverage==7.6.1; extra == "dev"
66
65
  Requires-Dist: pytest-cov==5.0.0; extra == "dev"
67
- Requires-Dist: black==24.4.2; extra == "dev"
66
+ Requires-Dist: black==24.8.0; extra == "dev"
68
67
  Requires-Dist: isort==5.13.2; extra == "dev"
69
- Requires-Dist: flake8==7.0.0; extra == "dev"
68
+ Requires-Dist: flake8==7.1.1; extra == "dev"
70
69
  Requires-Dist: flake8-black==0.3.6; extra == "dev"
71
70
  Requires-Dist: flake8-isort==6.1.1; extra == "dev"
72
71
  Requires-Dist: mypy; extra == "dev"
@@ -76,7 +75,7 @@ Requires-Dist: types-python-dateutil; extra == "dev"
76
75
  Requires-Dist: types-PyYAML; extra == "dev"
77
76
  Requires-Dist: types-setuptools; extra == "dev"
78
77
  Requires-Dist: pyinfra-guzzle_sphinx_theme==0.16; extra == "dev"
79
- Requires-Dist: myst-parser==2.0.0; extra == "dev"
78
+ Requires-Dist: myst-parser==3.0.1; extra == "dev"
80
79
  Requires-Dist: sphinx==6.2.1; extra == "dev"
81
80
  Requires-Dist: wheel; extra == "dev"
82
81
  Requires-Dist: twine; extra == "dev"
@@ -248,6 +248,12 @@ __argument_docs__ = {
248
248
  "Privilege & user escalation": (
249
249
  auth_argument_meta,
250
250
  """
251
+ .. caution::
252
+ When combining privilege escalation arguments it is important to know the order they
253
+ are applied: ``doas`` -> ``sudo`` -> ``su``. For example
254
+ ``_sudo=True,_su_user="pyinfra"`` yields a command like ``sudo su pyinfra..``.
255
+ """,
256
+ """
251
257
  .. code:: python
252
258
 
253
259
  # Execute a command with sudo
@@ -268,6 +274,7 @@ __argument_docs__ = {
268
274
  ),
269
275
  "Shell control & features": (
270
276
  shell_argument_meta,
277
+ "",
271
278
  """
272
279
  .. code:: python
273
280
 
@@ -279,8 +286,8 @@ __argument_docs__ = {
279
286
  )
280
287
  """,
281
288
  ),
282
- "Operation meta & callbacks": (meta_argument_meta, ""),
283
- "Execution strategy": (execution_argument_meta, ""),
289
+ "Operation meta & callbacks": (meta_argument_meta, "", ""),
290
+ "Execution strategy": (execution_argument_meta, "", ""),
284
291
  }
285
292
 
286
293
 
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
23
23
  from pyinfra.api.state import State
24
24
 
25
25
 
26
- def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs):
26
+ def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs) -> None:
27
27
  """
28
28
  Prepare & add an deploy to pyinfra.state by executing it on all hosts.
29
29
 
@@ -54,7 +54,9 @@ def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs)
54
54
  P = ParamSpec("P")
55
55
 
56
56
 
57
- def deploy(name: Optional[str] = None, data_defaults=None):
57
+ def deploy(
58
+ name: Optional[str] = None, data_defaults: Optional[dict] = None
59
+ ) -> Callable[[Callable[P, Any]], PyinfraOperation[P]]:
58
60
  """
59
61
  Decorator that takes a deploy function (normally from a pyinfra_* package)
60
62
  and wraps any operations called inside with any deploy-wide kwargs/data.
@@ -218,17 +218,19 @@ class Host:
218
218
  self.print_prefix_padding,
219
219
  )
220
220
 
221
- def log(self, message, log_func=logger.info):
221
+ def log(self, message: str, log_func: Callable[[str], Any] = logger.info) -> None:
222
222
  log_func(f"{self.print_prefix}{message}")
223
223
 
224
- def log_styled(self, message, log_func=logger.info, **kwargs):
224
+ def log_styled(
225
+ self, message: str, log_func: Callable[[str], Any] = logger.info, **kwargs
226
+ ) -> None:
225
227
  message_styled = click.style(message, **kwargs)
226
228
  self.log(message_styled, log_func=log_func)
227
229
 
228
230
  def get_deploy_data(self):
229
231
  return self.current_op_deploy_data or self.current_deploy_data or {}
230
232
 
231
- def noop(self, description):
233
+ def noop(self, description: str) -> None:
232
234
  """
233
235
  Log a description for a noop operation.
234
236
  """
@@ -58,13 +58,20 @@ def _start_docker_image(image_name):
58
58
 
59
59
  class DockerConnector(BaseConnector):
60
60
  """
61
- The docker connector allows you to build Docker images or modify running
62
- Docker containers. You can pass either an image name or existing container ID:
61
+ The Docker connector allows you to use pyinfra to create new Docker images or modify running
62
+ Docker containers.
63
63
 
64
- + Image - will create a new container from the image, execute operations \
65
- against it, save into a new Docker image and remove the container
66
- + Existing container ID - will execute operations against the running \
67
- container, leaving it running
64
+ .. note::
65
+
66
+ The Docker connector allows pyinfra to target Docker containers as inventory and is
67
+ unrelated to the :doc:`../operations/docker` & :doc:`../facts/docker`.
68
+
69
+ You can pass either an image name or existing container ID:
70
+
71
+ + Image - will create a new container from the image, execute operations against it, save into \
72
+ a new Docker image and remove the container
73
+ + Existing container ID - will execute operations against the running container, leaving it \
74
+ running
68
75
 
69
76
  .. code:: shell
70
77
 
@@ -76,6 +83,10 @@ class DockerConnector(BaseConnector):
76
83
 
77
84
  # Execute against a running container
78
85
  pyinfra @docker/2beb8c15a1b1 ...
86
+
87
+ The Docker connector is great for testing pyinfra operations locally, rather than connecting to
88
+ a remote host over SSH each time. This gives you a fast, local-first devloop to iterate on when
89
+ writing deploys, operations or facts.
79
90
  """
80
91
 
81
92
  handles_execution = True
@@ -14,6 +14,7 @@ from paramiko import (
14
14
  SSHException,
15
15
  )
16
16
  from paramiko.agent import AgentRequestHandler
17
+ from paramiko.hostkeys import HostKeyEntry
17
18
 
18
19
  from pyinfra import logger
19
20
  from pyinfra.api.util import memoize
@@ -31,6 +32,28 @@ class StrictPolicy(MissingHostKeyPolicy):
31
32
  )
32
33
 
33
34
 
35
+ def append_hostkey(client, hostname, key):
36
+ """Append hostname to the clients host_keys_file"""
37
+
38
+ with HOST_KEYS_LOCK:
39
+ # The paramiko client saves host keys incorrectly whereas the host keys object does
40
+ # this correctly, so use that with the client filename variable.
41
+ # See: https://github.com/paramiko/paramiko/pull/1989
42
+ host_key_entry = HostKeyEntry([hostname], key)
43
+ if host_key_entry is None:
44
+ raise SSHException(
45
+ "Append Hostkey: Failed to parse host {0}, could not append to hostfile".format(
46
+ hostname
47
+ ),
48
+ )
49
+ with open(client._host_keys_filename, "a") as host_keys_file:
50
+ hk_entry = host_key_entry.to_line()
51
+ if hk_entry is None:
52
+ raise SSHException(f"Append Hostkey: Failed to append hostkey ({host_key_entry})")
53
+
54
+ host_keys_file.write(hk_entry)
55
+
56
+
34
57
  class AcceptNewPolicy(MissingHostKeyPolicy):
35
58
  def missing_host_key(self, client, hostname, key):
36
59
  logger.warning(
@@ -40,13 +63,8 @@ class AcceptNewPolicy(MissingHostKeyPolicy):
40
63
  ),
41
64
  )
42
65
 
43
- with HOST_KEYS_LOCK:
44
- host_keys = client.get_host_keys()
45
- host_keys.add(hostname, key.get_name(), key)
46
- # The paramiko client saves host keys incorrectly whereas the host keys object does
47
- # this correctly, so use that with the client filename variable.
48
- # See: https://github.com/paramiko/paramiko/pull/1989
49
- host_keys.save(client._host_keys_filename)
66
+ append_hostkey(client, hostname, key)
67
+ logger.warning("Added host key for {0} to known_hosts".format(hostname))
50
68
 
51
69
 
52
70
  class AskPolicy(MissingHostKeyPolicy):
@@ -60,13 +78,7 @@ class AskPolicy(MissingHostKeyPolicy):
60
78
  raise SSHException(
61
79
  "AskPolicy: No host key for {0} found in known_hosts".format(hostname),
62
80
  )
63
- with HOST_KEYS_LOCK:
64
- host_keys = client.get_host_keys()
65
- host_keys.add(hostname, key.get_name(), key)
66
- # The paramiko client saves host keys incorrectly whereas the host keys object does
67
- # this correctly, so use that with the client filename variable.
68
- # See: https://github.com/paramiko/paramiko/pull/1989
69
- host_keys.save(client._host_keys_filename)
81
+ append_hostkey(client, hostname, key)
70
82
  logger.warning("Added host key for {0} to known_hosts".format(hostname))
71
83
  return
72
84
 
@@ -4,7 +4,9 @@ from pyinfra.api import FactBase
4
4
 
5
5
  from .util.packaging import parse_packages
6
6
 
7
- APK_REGEX = r"^([a-zA-Z0-9\-_]+)-([0-9\.]+\-?[a-z0-9]*)\s"
7
+ # Source: https://superuser.com/a/1472405
8
+ # Modified to return version and release inside a single group and removed extra capturing groups
9
+ APK_REGEX = r"(.+)-([^-]+-r[^-]+) \S+ \{\S+\} \(.+?\)"
8
10
 
9
11
 
10
12
  class ApkPackages(FactBase):
@@ -2,12 +2,36 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
 
5
+ from typing_extensions import TypedDict
6
+
5
7
  from pyinfra.api import FactBase
6
8
 
7
9
  from .gpg import GpgFactBase
8
10
  from .util import make_cat_files_command
9
11
 
10
12
 
13
+ def noninteractive_apt(command: str, force=False):
14
+ args = ["DEBIAN_FRONTEND=noninteractive apt-get -y"]
15
+
16
+ if force:
17
+ args.append("--force-yes")
18
+
19
+ args.extend(
20
+ (
21
+ '-o Dpkg::Options::="--force-confdef"',
22
+ '-o Dpkg::Options::="--force-confold"',
23
+ command,
24
+ ),
25
+ )
26
+
27
+ return " ".join(args)
28
+
29
+
30
+ APT_CHANGES_RE = re.compile(
31
+ r"^(\d+) upgraded, (\d+) newly installed, (\d+) to remove and (\d+) not upgraded.$"
32
+ )
33
+
34
+
11
35
  def parse_apt_repo(name):
12
36
  regex = r"^(deb(?:-src)?)(?:\s+\[([^\]]+)\])?\s+([^\s]+)\s+([^\s]+)\s+([a-z-\s\d]*)$"
13
37
 
@@ -94,3 +118,39 @@ class AptKeys(GpgFactBase):
94
118
 
95
119
  def requires_command(self) -> str:
96
120
  return "apt-key"
121
+
122
+
123
+ class AptSimulationDict(TypedDict):
124
+ upgraded: int
125
+ newly_installed: int
126
+ removed: int
127
+ not_upgraded: int
128
+
129
+
130
+ class SimulateOperationWillChange(FactBase[AptSimulationDict]):
131
+ """
132
+ Simulate an 'apt-get' operation and try to detect if any changes would be performed.
133
+ """
134
+
135
+ def command(self, command: str) -> str:
136
+ # LC_ALL=C: Ensure the output is in english, as we want to parse it
137
+ return "LC_ALL=C " + noninteractive_apt(f"{command} --dry-run")
138
+
139
+ def requires_command(self, command: str) -> str:
140
+ return "apt-get"
141
+
142
+ def process(self, output) -> AptSimulationDict:
143
+ # We are looking for a line similar to
144
+ # "3 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
145
+ for line in output:
146
+ result = APT_CHANGES_RE.match(line)
147
+ if result is not None:
148
+ return {
149
+ "upgraded": int(result[1]),
150
+ "newly_installed": int(result[2]),
151
+ "removed": int(result[3]),
152
+ "not_upgraded": int(result[4]),
153
+ }
154
+
155
+ # We did not find the line we expected:
156
+ raise Exception("Did not find proposed changes in output")
@@ -0,0 +1,190 @@
1
+ import re
2
+ from typing import Dict, List, Optional, TypedDict, Union
3
+
4
+ from typing_extensions import NotRequired
5
+
6
+ from pyinfra.api import FactBase
7
+ from pyinfra.api.util import try_int
8
+
9
+
10
+ class CrontabDict(TypedDict):
11
+ command: NotRequired[str]
12
+ # handles cases like CRON_TZ=UTC
13
+ env: NotRequired[str]
14
+ minute: NotRequired[Union[int, str]]
15
+ hour: NotRequired[Union[int, str]]
16
+ month: NotRequired[Union[int, str]]
17
+ day_of_month: NotRequired[Union[int, str]]
18
+ day_of_week: NotRequired[Union[int, str]]
19
+ comments: NotRequired[List[str]]
20
+ special_time: NotRequired[str]
21
+
22
+
23
+ # for compatibility, also keeps a dict of command -> crontab dict
24
+ class CrontabFile:
25
+ commands: List[CrontabDict]
26
+
27
+ def __init__(self, input_dict: Optional[Dict[str, CrontabDict]] = None):
28
+ super().__init__()
29
+ self.commands = []
30
+ if input_dict:
31
+ for command, others in input_dict.items():
32
+ val = others.copy()
33
+ val["command"] = command
34
+ self.add_item(val)
35
+
36
+ def add_item(self, item: CrontabDict):
37
+ self.commands.append(item)
38
+
39
+ def __len__(self):
40
+ return len(self.commands)
41
+
42
+ def __bool__(self):
43
+ return len(self) > 0
44
+
45
+ def items(self):
46
+ return {item.get("command") or item.get("env"): item for item in self.commands}
47
+
48
+ def get_command(
49
+ self, command: Optional[str] = None, name: Optional[str] = None
50
+ ) -> Optional[CrontabDict]:
51
+ assert command or name, "Either command or name must be provided"
52
+
53
+ name_comment = "# pyinfra-name={0}".format(name)
54
+ for cmd in self.commands:
55
+ if cmd.get("command") == command:
56
+ return cmd
57
+ if cmd.get("comments") and name_comment in cmd["comments"]:
58
+ return cmd
59
+ return None
60
+
61
+ def get_env(self, env: str) -> Optional[CrontabDict]:
62
+ for cmd in self.commands:
63
+ if cmd.get("env") == env:
64
+ return cmd
65
+ return None
66
+
67
+ def get(self, item: str) -> Optional[CrontabDict]:
68
+ return self.get_command(command=item, name=item) or self.get_env(item)
69
+
70
+ def __getitem__(self, item) -> Optional[CrontabDict]:
71
+ return self.get(item)
72
+
73
+ def __repr__(self):
74
+ return f"CrontabResult({self.commands})"
75
+
76
+ # noinspection PyMethodMayBeStatic
77
+ def format_item(self, item: CrontabDict):
78
+ lines = []
79
+ for comment in item.get("comments", []):
80
+ lines.append(comment)
81
+
82
+ if "env" in item:
83
+ lines.append(item["env"])
84
+ elif "special_time" in item:
85
+ lines.append(f"{item['special_time']} {item['command']}")
86
+ else:
87
+ lines.append(
88
+ f"{item['minute']} {item['hour']} "
89
+ f"{item['day_of_month']} {item['month']} {item['day_of_week']} "
90
+ f"{item['command']}"
91
+ )
92
+ return "\n".join(lines)
93
+
94
+ def __str__(self):
95
+ return "\n".join(self.format_item(item) for item in self.commands)
96
+
97
+ def to_json(self):
98
+ return self.commands
99
+
100
+
101
+ _crontab_env_re = re.compile(r"^\s*([A-Z_]+)=(.*)$")
102
+
103
+
104
+ class Crontab(FactBase[CrontabFile]):
105
+ """
106
+ Returns a dictionary of CrontabFile.
107
+
108
+ .. code:: python
109
+
110
+ # CrontabFile.items()
111
+ {
112
+ "/path/to/command": {
113
+ "minute": "*",
114
+ "hour": "*",
115
+ "month": "*",
116
+ "day_of_month": "*",
117
+ "day_of_week": "*",
118
+ },
119
+ "echo another command": {
120
+ "special_time": "@daily",
121
+ },
122
+ }
123
+ # or CrontabFile.to_json()
124
+ [
125
+ {
126
+ command: "/path/to/command",
127
+ minute: "*",
128
+ hour: "*",
129
+ month: "*",
130
+ day_of_month: "*",
131
+ day_of_week: "*",
132
+ },
133
+ {
134
+ "command": "echo another command
135
+ "special_time": "@daily",
136
+ }
137
+ ]
138
+ """
139
+
140
+ default = CrontabFile
141
+
142
+ def requires_command(self, user=None) -> str:
143
+ return "crontab"
144
+
145
+ def command(self, user=None):
146
+ if user:
147
+ return "crontab -l -u {0} || true".format(user)
148
+ return "crontab -l || true"
149
+
150
+ def process(self, output):
151
+ crons = CrontabFile()
152
+ current_comments = []
153
+
154
+ for line in output:
155
+ line = line.strip()
156
+ if not line or line.startswith("#"):
157
+ current_comments.append(line)
158
+ continue
159
+
160
+ if line.startswith("@"):
161
+ special_time, command = line.split(None, 1)
162
+ item = CrontabDict(
163
+ command=command,
164
+ special_time=special_time,
165
+ comments=current_comments,
166
+ )
167
+ crons.add_item(item)
168
+
169
+ elif _crontab_env_re.match(line):
170
+ # handle environment variables
171
+ item = CrontabDict(
172
+ env=line,
173
+ comments=current_comments,
174
+ )
175
+ crons.add_item(item)
176
+ else:
177
+ minute, hour, day_of_month, month, day_of_week, command = line.split(None, 5)
178
+ item = CrontabDict(
179
+ command=command,
180
+ minute=try_int(minute),
181
+ hour=try_int(hour),
182
+ month=try_int(month),
183
+ day_of_month=try_int(day_of_month),
184
+ day_of_week=try_int(day_of_week),
185
+ comments=current_comments,
186
+ )
187
+ crons.add_item(item)
188
+
189
+ current_comments = []
190
+ return crons
@@ -1,3 +1,9 @@
1
+ """
2
+ Facts about Docker containers, volumes and networks. These facts give you information from the view
3
+ of the current inventory host. See the :doc:`../connectors/docker` to use Docker containers as
4
+ inventory directly.
5
+ """
6
+
1
7
  from __future__ import annotations
2
8
 
3
9
  import json
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, List, Optional, Tuple, TypedDict
4
+
5
+ from pyinfra.api import FactBase
6
+
7
+ BootEntry = Tuple[bool, str]
8
+ EFIBootMgrInfoDict = TypedDict(
9
+ "EFIBootMgrInfoDict",
10
+ {
11
+ "BootNext": Optional[int],
12
+ "BootCurrent": Optional[int],
13
+ "Timeout": Optional[int],
14
+ "BootOrder": Optional[List[int]],
15
+ "Entries": Dict[int, BootEntry],
16
+ },
17
+ )
18
+
19
+
20
+ class EFIBootMgr(FactBase[Optional[EFIBootMgrInfoDict]]):
21
+ """
22
+ Returns information about the UEFI boot variables:
23
+
24
+ .. code:: python
25
+
26
+ {
27
+ "BootNext": 6,
28
+ "BootCurrent": 6,
29
+ "Timeout": 0,
30
+ "BootOrder": [1,4,3],
31
+ "Entries": {
32
+ 1: (True, "myefi1"),
33
+ 2: (False, "myefi2.efi"),
34
+ 3: (True, "myefi3.efi"),
35
+ 4: (True, "grub2.efi"),
36
+ },
37
+ }
38
+ """
39
+
40
+ def requires_command(self, *args: Any, **kwargs: Any) -> str:
41
+ return "efibootmgr"
42
+
43
+ def command(self) -> str:
44
+ # FIXME: Use '|| true' to properly handle the case where
45
+ # 'efibootmgr' is run on a non-UEFI system
46
+ return "efibootmgr || true"
47
+
48
+ def process(self, output: Iterable[str]) -> Optional[EFIBootMgrInfoDict]:
49
+ # This parsing code closely follows the printing code of efibootmgr
50
+ # at <https://github.com/rhboot/efibootmgr/blob/main/src/efibootmgr.c#L2020-L2048>
51
+
52
+ info: EFIBootMgrInfoDict = {
53
+ "BootNext": None,
54
+ "BootCurrent": None,
55
+ "Timeout": None,
56
+ "BootOrder": [],
57
+ "Entries": {},
58
+ }
59
+
60
+ output = iter(output)
61
+
62
+ line: Optional[str] = next(output, None)
63
+
64
+ if line is None:
65
+ # efibootmgr run on a non-UEFI system, likely printed
66
+ # "EFI variables are not supported on this system."
67
+ # to stderr
68
+ return None
69
+
70
+ # 1. Maybe have BootNext
71
+ if line and line.startswith("BootNext: "):
72
+ info["BootNext"] = int(line[len("BootNext: ") :], 16)
73
+ line = next(output, None)
74
+
75
+ # 2. Maybe have BootCurrent
76
+ if line and line.startswith("BootCurrent: "):
77
+ info["BootCurrent"] = int(line[len("BootCurrent: ") :], 16)
78
+ line = next(output, None)
79
+
80
+ # 3. Maybe have Timeout
81
+ if line and line.startswith("Timeout: "):
82
+ info["Timeout"] = int(line[len("Timeout: ") : -len(" seconds")])
83
+ line = next(output, None)
84
+
85
+ # 4. `show_order`
86
+ if line and line.startswith("BootOrder: "):
87
+ entries = line[len("BootOrder: ") :]
88
+ info["BootOrder"] = list(map(lambda x: int(x, 16), entries.split(",")))
89
+ line = next(output, None)
90
+
91
+ # 5. `show_vars`: The actual boot entries
92
+ while line is not None and line.startswith("Boot"):
93
+ number = int(line[4:8], 16)
94
+
95
+ # Entries marked with a * are active
96
+ active = line[8:9] == "*"
97
+
98
+ # TODO: Maybe split and parse (name vs. arguments ?), might require --verbose ?
99
+ entry = line[10:]
100
+ info["Entries"][number] = (active, entry)
101
+ line = next(output, None)
102
+
103
+ # 6. `show_mirror`
104
+ # Currently not implemented, since I haven't actually encountered this in the wild.
105
+ if line is not None:
106
+ raise ValueError(f"Unexpected line '{line}' while parsing")
107
+
108
+ return info