pyinfra 3.0.1__tar.gz → 3.1__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 (184) hide show
  1. {pyinfra-3.0.1 → pyinfra-3.1}/CHANGELOG.md +36 -0
  2. {pyinfra-3.0.1/pyinfra.egg-info → pyinfra-3.1}/PKG-INFO +1 -1
  3. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/host.py +5 -2
  4. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/operation.py +12 -10
  5. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/util.py +4 -2
  6. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/ssh.py +1 -1
  7. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/terraform.py +30 -20
  8. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/apt.py +1 -1
  9. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/files.py +8 -1
  10. pyinfra-3.1/pyinfra/facts/flatpak.py +70 -0
  11. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/systemd.py +4 -1
  12. pyinfra-3.1/pyinfra/facts/zfs.py +57 -0
  13. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/docker.py +4 -4
  14. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/files.py +9 -2
  15. pyinfra-3.1/pyinfra/operations/flatpak.py +79 -0
  16. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/mysql.py +5 -5
  17. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/postgres.py +3 -3
  18. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/server.py +9 -9
  19. pyinfra-3.1/pyinfra/operations/zfs.py +175 -0
  20. {pyinfra-3.0.1 → pyinfra-3.1/pyinfra.egg-info}/PKG-INFO +1 -1
  21. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra.egg-info/SOURCES.txt +4 -0
  22. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/inventory.py +34 -2
  23. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/main.py +8 -8
  24. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/prints.py +17 -1
  25. {pyinfra-3.0.1 → pyinfra-3.1}/setup.cfg +1 -2
  26. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_inventory.py +21 -0
  27. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_cli/test_cli_deploy.py +7 -2
  28. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_terraform.py +6 -6
  29. {pyinfra-3.0.1 → pyinfra-3.1}/LICENSE.md +0 -0
  30. {pyinfra-3.0.1 → pyinfra-3.1}/MANIFEST.in +0 -0
  31. {pyinfra-3.0.1 → pyinfra-3.1}/README.md +0 -0
  32. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/__init__.py +0 -0
  33. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/__main__.py +0 -0
  34. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/__init__.py +0 -0
  35. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/arguments.py +0 -0
  36. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/arguments_typed.py +0 -0
  37. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/command.py +0 -0
  38. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/config.py +0 -0
  39. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/connect.py +0 -0
  40. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/connectors.py +0 -0
  41. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/deploy.py +0 -0
  42. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/exceptions.py +0 -0
  43. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/facts.py +0 -0
  44. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/inventory.py +0 -0
  45. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/operations.py +0 -0
  46. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/api/state.py +0 -0
  47. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/__init__.py +0 -0
  48. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/base.py +0 -0
  49. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/chroot.py +0 -0
  50. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/docker.py +0 -0
  51. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/dockerssh.py +0 -0
  52. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/local.py +0 -0
  53. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/ssh_util.py +0 -0
  54. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/sshuserclient/__init__.py +0 -0
  55. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/sshuserclient/client.py +0 -0
  56. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/sshuserclient/config.py +0 -0
  57. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/util.py +0 -0
  58. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/connectors/vagrant.py +0 -0
  59. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/context.py +0 -0
  60. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/__init__.py +0 -0
  61. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/apk.py +0 -0
  62. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/brew.py +0 -0
  63. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/bsdinit.py +0 -0
  64. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/cargo.py +0 -0
  65. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/choco.py +0 -0
  66. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/deb.py +0 -0
  67. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/dnf.py +0 -0
  68. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/docker.py +0 -0
  69. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/gem.py +0 -0
  70. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/git.py +0 -0
  71. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/gpg.py +0 -0
  72. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/hardware.py +0 -0
  73. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/iptables.py +0 -0
  74. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/launchd.py +0 -0
  75. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/lxd.py +0 -0
  76. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/mysql.py +0 -0
  77. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/npm.py +0 -0
  78. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/openrc.py +0 -0
  79. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/pacman.py +0 -0
  80. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/pip.py +0 -0
  81. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/pkg.py +0 -0
  82. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/pkgin.py +0 -0
  83. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/postgres.py +0 -0
  84. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/postgresql.py +0 -0
  85. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/rpm.py +0 -0
  86. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/runit.py +0 -0
  87. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/selinux.py +0 -0
  88. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/server.py +0 -0
  89. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/snap.py +0 -0
  90. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/sysvinit.py +0 -0
  91. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/upstart.py +0 -0
  92. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/util/__init__.py +0 -0
  93. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/util/databases.py +0 -0
  94. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/util/packaging.py +0 -0
  95. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/util/win_files.py +0 -0
  96. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/vzctl.py +0 -0
  97. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/xbps.py +0 -0
  98. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/yum.py +0 -0
  99. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/facts/zypper.py +0 -0
  100. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/local.py +0 -0
  101. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/__init__.py +0 -0
  102. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/apk.py +0 -0
  103. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/apt.py +0 -0
  104. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/brew.py +0 -0
  105. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/bsdinit.py +0 -0
  106. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/cargo.py +0 -0
  107. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/choco.py +0 -0
  108. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/dnf.py +0 -0
  109. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/gem.py +0 -0
  110. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/git.py +0 -0
  111. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/iptables.py +0 -0
  112. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/launchd.py +0 -0
  113. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/lxd.py +0 -0
  114. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/npm.py +0 -0
  115. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/openrc.py +0 -0
  116. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/pacman.py +0 -0
  117. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/pip.py +0 -0
  118. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/pkg.py +0 -0
  119. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/pkgin.py +0 -0
  120. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/postgresql.py +0 -0
  121. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/puppet.py +0 -0
  122. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/python.py +0 -0
  123. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/runit.py +0 -0
  124. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/selinux.py +0 -0
  125. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/snap.py +0 -0
  126. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/ssh.py +0 -0
  127. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/systemd.py +0 -0
  128. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/sysvinit.py +0 -0
  129. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/upstart.py +0 -0
  130. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/util/__init__.py +0 -0
  131. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/util/docker.py +0 -0
  132. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/util/files.py +0 -0
  133. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/util/packaging.py +0 -0
  134. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/util/service.py +0 -0
  135. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/vzctl.py +0 -0
  136. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/xbps.py +0 -0
  137. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/yum.py +0 -0
  138. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/operations/zypper.py +0 -0
  139. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/progress.py +0 -0
  140. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/py.typed +0 -0
  141. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra/version.py +0 -0
  142. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra.egg-info/dependency_links.txt +0 -0
  143. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra.egg-info/entry_points.txt +0 -0
  144. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra.egg-info/requires.txt +0 -0
  145. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra.egg-info/top_level.txt +0 -0
  146. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/__init__.py +0 -0
  147. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/__main__.py +0 -0
  148. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/commands.py +0 -0
  149. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/exceptions.py +0 -0
  150. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/log.py +0 -0
  151. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/util.py +0 -0
  152. {pyinfra-3.0.1 → pyinfra-3.1}/pyinfra_cli/virtualenv.py +0 -0
  153. {pyinfra-3.0.1 → pyinfra-3.1}/pyproject.toml +0 -0
  154. {pyinfra-3.0.1 → pyinfra-3.1}/setup.py +0 -0
  155. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/__init__.py +0 -0
  156. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api.py +0 -0
  157. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_arguments.py +0 -0
  158. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_command.py +0 -0
  159. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_config.py +0 -0
  160. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_deploys.py +0 -0
  161. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_facts.py +0 -0
  162. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_host.py +0 -0
  163. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_operations.py +0 -0
  164. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_api/test_api_util.py +0 -0
  165. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_cli/__init__.py +0 -0
  166. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_cli/test_cli.py +0 -0
  167. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_cli/test_cli_exceptions.py +0 -0
  168. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_cli/test_cli_inventory.py +0 -0
  169. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_cli/test_cli_util.py +0 -0
  170. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_cli/test_context_objects.py +0 -0
  171. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_cli/util.py +0 -0
  172. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/__init__.py +0 -0
  173. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_chroot.py +0 -0
  174. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_docker.py +0 -0
  175. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_dockerssh.py +0 -0
  176. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_local.py +0 -0
  177. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_ssh.py +0 -0
  178. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_sshuserclient.py +0 -0
  179. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_util.py +0 -0
  180. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_connectors/test_vagrant.py +0 -0
  181. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_facts.py +0 -0
  182. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_global_arguments.py +0 -0
  183. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_operations.py +0 -0
  184. {pyinfra-3.0.1 → pyinfra-3.1}/tests/test_operations_utils.py +0 -0
@@ -1,3 +1,39 @@
1
+ # v3.1
2
+
3
+ Here's pyinfra 3.1 - a release primarily driven by contributors new and old - a HUGE THANK YOU to all of you who dedicate time to work on pushing pyinfra forward. New stuff:
4
+
5
+ - Add `zfs` operations (`dataset`, `snapshot`, `volume`, `filesystem`) facts (`Pools`, `Datasets`, `Filesystems`, `Snapshots`, `Volumes`) (@taliaferro)
6
+ - Add `flatpak` operations (`packages`) and facts (`FlatpakPackage`, `FlatpakPackages`) (@JustScreaMy)
7
+ - Add `jinja_env_kwargs` argument to `files.template` operation (@DonDebonair)
8
+ - Add using dictionaries as `@terraform` output (map from group -> hosts)
9
+ - Add default `@terraform` output key - `pyinfra_inventory.value`, promote connector to beta
10
+ - Add support for multiple keys in each `server.authorized_keys` file (@matthijskooijman)
11
+ - Add print all dependency versions with `--support` flag (@kytta)
12
+
13
+ Fixes:
14
+
15
+ - Fix when `ssh_hostname` is set as override data, don't do inventory hostname check
16
+ - Fix `apt.AptSources` parsing special characters (@CondensedTea)
17
+ - Fix `server.reboot` connection detection (@bauen1 + @lemmi)
18
+ - Fix systemd flagging of sockets running (@bauen1)
19
+ - Fix mysql dump quoting (@simonhammes)
20
+ - Fix tilde expansion in files facts (@simonhammes)
21
+ - Fix host lookup check with SSH alias config (@simonhammes)
22
+ - Fix crontab comparison (@andrew-d)
23
+
24
+ Docs/internal tweaks:
25
+
26
+ - Improve operations documentation (@bauen1)
27
+ - Default to local machine if `user_name` set in systecmt (@bauen1)
28
+ - Improve efficiency of Docker operations (@apecnascimento)
29
+ - Shallow copy `host.data` data to mutation
30
+
31
+ # v3.0.2
32
+
33
+ - Fix `OperationMeta.did_change`: this is now a function as originally designed
34
+ - Add quick test for `host.when` context manager
35
+ - Remove extra detected changes note when not relevant
36
+
1
37
  # v3.0.1
2
38
 
3
39
  - Switch to `command -v` not `which` in `server.Which` fact (@lemmi)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyinfra
3
- Version: 3.0.1
3
+ Version: 3.1
4
4
  Summary: pyinfra automates/provisions/manages/deploys infrastructure.
5
5
  Home-page: https://pyinfra.com
6
6
  Author: Nick / Fizzadar
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from contextlib import contextmanager
4
+ from copy import copy
4
5
  from typing import (
5
6
  TYPE_CHECKING,
6
7
  Any,
@@ -41,7 +42,6 @@ def extract_callable_datas(
41
42
  # the data is stored on the state temporarily.
42
43
  if callable(data):
43
44
  data = data()
44
-
45
45
  yield data
46
46
 
47
47
 
@@ -66,7 +66,10 @@ class HostData:
66
66
  def __getattr__(self, key: str):
67
67
  for data in extract_callable_datas(self.datas):
68
68
  try:
69
- return data[key]
69
+ # Take a shallow copy of the object here, we don't want modifications
70
+ # to host.data.<X> to stick, instead setting host.data.<Y> = is the
71
+ # correct way to achieve this (see __setattr__).
72
+ return copy(data[key])
70
73
  except KeyError:
71
74
  pass
72
75
 
@@ -60,7 +60,7 @@ class OperationMeta:
60
60
  if self._commands is not None:
61
61
  return (
62
62
  "OperationMeta(executed=True, "
63
- f"success={self.did_succeed}, hash={self._hash}, commands={len(self._commands)})"
63
+ f"success={self.did_succeed()}, hash={self._hash}, commands={len(self._commands)})"
64
64
  )
65
65
  return (
66
66
  "OperationMeta(executed=False, "
@@ -87,6 +87,12 @@ class OperationMeta:
87
87
  if not self.is_complete():
88
88
  raise RuntimeError("Cannot evaluate operation result before execution")
89
89
 
90
+ @property
91
+ def executed(self) -> bool:
92
+ if self._commands is None:
93
+ return False
94
+ return len(self._commands) > 0
95
+
90
96
  @property
91
97
  def will_change(self) -> bool:
92
98
  if self._maybe_is_change is not None:
@@ -100,16 +106,12 @@ class OperationMeta:
100
106
  self._maybe_is_change = False
101
107
  return False
102
108
 
103
- def _did_change(self) -> bool:
109
+ def did_change(self) -> bool:
110
+ self._raise_if_not_complete()
104
111
  return bool(self._success and len(self._commands or []) > 0)
105
112
 
106
- @property
107
- def did_change(self):
108
- return context.host.when(self._did_change)
109
-
110
- @property
111
- def did_not_change(self):
112
- return context.host.when(lambda: not self._did_change())
113
+ def did_not_change(self) -> bool:
114
+ return not self.did_change()
113
115
 
114
116
  def did_succeed(self, _raise_if_not_complete=True) -> bool:
115
117
  if _raise_if_not_complete:
@@ -124,7 +126,7 @@ class OperationMeta:
124
126
  @property
125
127
  def changed(self) -> bool:
126
128
  if self.is_complete():
127
- return self._did_change()
129
+ return self.did_change()
128
130
  return self.will_change
129
131
 
130
132
  @property
@@ -139,12 +139,13 @@ def get_operation_order_from_stack(state: "State"):
139
139
  return line_numbers
140
140
 
141
141
 
142
- def get_template(filename_or_io: str | IO):
142
+ def get_template(filename_or_io: str | IO, jinja_env_kwargs: dict[str, Any] | None = None):
143
143
  """
144
144
  Gets a jinja2 ``Template`` object for the input filename or string, with caching
145
145
  based on the filename of the template, or the SHA1 of the input string.
146
146
  """
147
-
147
+ if jinja_env_kwargs is None:
148
+ jinja_env_kwargs = {}
148
149
  file_data = get_file_io(filename_or_io, mode="r")
149
150
  cache_key = file_data.cache_key
150
151
 
@@ -158,6 +159,7 @@ def get_template(filename_or_io: str | IO):
158
159
  undefined=StrictUndefined,
159
160
  keep_trailing_newline=True,
160
161
  loader=FileSystemLoader(getcwd()),
162
+ **jinja_env_kwargs,
161
163
  ).from_string(template_string)
162
164
 
163
165
  if cache_key:
@@ -606,7 +606,7 @@ class SSHConnector(BaseConnector):
606
606
  rsync_flags=" ".join(flags),
607
607
  ssh_flags=" ".join(ssh_flags),
608
608
  remote_rsync_command=remote_rsync_command,
609
- user=user,
609
+ user=user or "",
610
610
  hostname=hostname,
611
611
  src=src,
612
612
  dest=dest,
@@ -10,16 +10,15 @@ from .base import BaseConnector
10
10
 
11
11
  @memoize
12
12
  def show_warning():
13
- logger.warning("The @terraform connector is in alpha!")
13
+ logger.warning("The @terraform connector is in beta!")
14
14
 
15
15
 
16
16
  def _flatten_dict_gen(d, parent_key, sep):
17
17
  for k, v in d.items():
18
18
  new_key = parent_key + sep + k if parent_key else k
19
+ yield new_key, v
19
20
  if isinstance(v, dict):
20
21
  yield from _flatten_dict(v, new_key, sep=sep).items()
21
- else:
22
- yield new_key, v
23
22
 
24
23
 
25
24
  def _flatten_dict(d: dict, parent_key: str = "", sep: str = "."):
@@ -82,7 +81,9 @@ class TerraformInventoryConnector(BaseConnector):
82
81
  show_warning()
83
82
 
84
83
  if not name:
85
- name = ""
84
+ # This is the default which allows one to create a Terraform output
85
+ # "pyinfra" and directly call: pyinfra @terraform ...
86
+ name = "pyinfra_inventory.value"
86
87
 
87
88
  with progress_spinner({"fetch terraform output"}):
88
89
  tf_output_raw = local.shell("terraform output -json")
@@ -96,27 +97,36 @@ class TerraformInventoryConnector(BaseConnector):
96
97
  keys = "\n".join(f" - {k}" for k in tf_output.keys())
97
98
  raise InventoryError(f"No Terraform output with key: `{name}`, valid keys:\n{keys}")
98
99
 
99
- if not isinstance(tf_output_value, list):
100
+ if not isinstance(tf_output_value, (list, dict)):
100
101
  raise InventoryError(
101
102
  "Invalid Terraform output type, should be `list`, got "
102
103
  f"`{type(tf_output_value).__name__}`",
103
104
  )
104
105
 
105
- for ssh_target in tf_output_value:
106
- if isinstance(ssh_target, dict):
107
- name = ssh_target.pop("name", ssh_target.get("ssh_hostname"))
108
- if name is None:
109
- raise InventoryError(
110
- "Invalid Terraform list item, missing `name` or `ssh_hostname` keys",
111
- )
112
- yield f"@terraform/{name}", ssh_target, ["@terraform"]
113
-
114
- elif isinstance(ssh_target, str):
115
- data = {"ssh_hostname": ssh_target}
116
- yield f"@terraform/{ssh_target}", data, ["@terraform"]
106
+ if isinstance(tf_output_value, list):
107
+ tf_output_value = {
108
+ "all": tf_output_value,
109
+ }
117
110
 
118
- else:
111
+ for group_name, hosts in tf_output_value.items():
112
+ if not isinstance(hosts, list):
119
113
  raise InventoryError(
120
- "Invalid Terraform list item, should be `dict` or `str` got "
121
- f"`{type(ssh_target).__name__}`",
114
+ "Invalid Terraform map value type, all values should be `list`, got "
115
+ f"`{type(hosts).__name__}`",
122
116
  )
117
+ for host in hosts:
118
+ if isinstance(host, dict):
119
+ name = host.pop("name", host.get("ssh_hostname"))
120
+ if name is None:
121
+ raise InventoryError(
122
+ "Invalid Terraform list item, missing `name` or `ssh_hostname` keys",
123
+ )
124
+ yield f"@terraform/{name}", host, ["@terraform", group_name]
125
+ elif isinstance(host, str):
126
+ data = {"ssh_hostname": host}
127
+ yield f"@terraform/{host}", data, ["@terraform", group_name]
128
+ else:
129
+ raise InventoryError(
130
+ "Invalid Terraform list item, should be `dict` or `str` got "
131
+ f"`{type(host).__name__}`",
132
+ )
@@ -9,7 +9,7 @@ from .util import make_cat_files_command
9
9
 
10
10
 
11
11
  def parse_apt_repo(name):
12
- regex = r"^(deb(?:-src)?)(?:\s+\[([^\]]+)\])?\s+([^\s]+)\s+([a-z-]+)\s+([a-z-\s]*)$"
12
+ regex = r"^(deb(?:-src)?)(?:\s+\[([^\]]+)\])?\s+([^\s]+)\s+([^\s]+)\s+([a-z-\s]*)$"
13
13
 
14
14
  matches = re.match(regex, name)
15
15
 
@@ -5,6 +5,7 @@ The files facts provide information about the filesystem and it's contents on th
5
5
  from __future__ import annotations
6
6
 
7
7
  import re
8
+ import shlex
8
9
  import stat
9
10
  from datetime import datetime
10
11
  from typing import TYPE_CHECKING, List, Optional, Tuple, Union
@@ -109,13 +110,19 @@ class File(FactBase[Union[FileDict, Literal[False], None]]):
109
110
  type = "file"
110
111
 
111
112
  def command(self, path):
113
+ if path.startswith("~/"):
114
+ # Do not quote leading tilde to ensure that it gets properly expanded by the shell
115
+ path = f"~/{shlex.quote(path[2:])}"
116
+ else:
117
+ path = QuoteString(path)
118
+
112
119
  return make_formatted_string_command(
113
120
  (
114
121
  # only stat if the path exists (file or symlink)
115
122
  "! (test -e {0} || test -L {0} ) || "
116
123
  "( {linux_stat_command} {0} 2> /dev/null || {bsd_stat_command} {0} )"
117
124
  ),
118
- QuoteString(path),
125
+ path,
119
126
  linux_stat_command=LINUX_STAT_COMMAND,
120
127
  bsd_stat_command=BSD_STAT_COMMAND,
121
128
  )
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from pyinfra.api import FactBase
6
+
7
+
8
+ class FlatpakBaseFact(FactBase):
9
+ abstract = True
10
+
11
+ def requires_command(self, *args, **kwargs) -> str:
12
+ return "flatpak"
13
+
14
+
15
+ class FlatpakPackage(FlatpakBaseFact):
16
+ """
17
+ Returns information for an installed flatpak package
18
+
19
+ .. code:: python
20
+
21
+ {
22
+ "id": "org.signal.Signal",
23
+ "ref": "app/org.signal.Signal/x86_64/stable",
24
+ "version": "7.12.0"
25
+ }
26
+ """
27
+
28
+ default = dict
29
+ _regexes = {
30
+ "id": "^[ ]+ID:[ ]+(.*)$",
31
+ "ref": r"^[ ]+Ref:[ ]+(.*)$",
32
+ "version": r"^[ ]+Version:[ ]+([\w\d.-]+).*$",
33
+ }
34
+
35
+ def command(self, package):
36
+ return f"flatpak info {package}"
37
+
38
+ def process(self, output):
39
+ data = {}
40
+ for line in output:
41
+ for regex_name, regex in self._regexes.items():
42
+ matches = re.match(regex, line)
43
+ if matches:
44
+ data[regex_name] = matches.group(1)
45
+
46
+ return data
47
+
48
+
49
+ class FlatpakPackages(FlatpakBaseFact):
50
+ """
51
+ Returns a list of installed flatpak packages:
52
+
53
+ .. code:: python
54
+
55
+ [
56
+ "org.gnome.Platform",
57
+ "org.kde.Platform",
58
+ "org.kde.Sdk",
59
+ "org.libreoffice.LibreOffice",
60
+ "org.videolan.VLC"
61
+ ]
62
+ """
63
+
64
+ default = list
65
+
66
+ def command(self):
67
+ return "flatpak list --columns=application"
68
+
69
+ def process(self, output):
70
+ return [flatpak for flatpak in output[1:]]
@@ -32,6 +32,9 @@ def _make_systemctl_cmd(user_mode=False, machine=None, user_name=None):
32
32
  systemctl_cmd.append("--machine={1}@{0}".format(machine, user_name))
33
33
  else:
34
34
  systemctl_cmd.append("--machine={0}".format(machine))
35
+ elif user_name is not None:
36
+ # If only the user is given, assume that the connection should be made to the local machine
37
+ systemctl_cmd.append("--machine={0}@.host".format(user_name))
35
38
 
36
39
  return StringCommand(*systemctl_cmd)
37
40
 
@@ -58,7 +61,7 @@ class SystemdStatus(FactBase[Dict[str, bool]]):
58
61
  default = dict
59
62
 
60
63
  state_key = "SubState"
61
- state_values = ["running", "waiting", "exited"]
64
+ state_values = ["running", "waiting", "exited", "listening"]
62
65
 
63
66
  def command(
64
67
  self,
@@ -0,0 +1,57 @@
1
+ """
2
+ Manage ZFS filesystems.
3
+ """
4
+
5
+ from pyinfra.api import FactBase, ShortFactBase
6
+
7
+
8
+ def _process_zfs_props_table(output):
9
+ datasets: dict = {}
10
+ for line in output:
11
+ dataset, property, value, source = tuple(line.split("\t"))
12
+ if dataset not in datasets:
13
+ datasets[dataset] = {}
14
+ datasets[dataset][property] = value
15
+ return datasets
16
+
17
+
18
+ class Pools(FactBase):
19
+ def command(self):
20
+ return "zpool get -H all"
21
+
22
+ @staticmethod
23
+ def process(output):
24
+ return _process_zfs_props_table(output)
25
+
26
+
27
+ class Datasets(FactBase):
28
+ def command(self):
29
+ return "zfs get -H all"
30
+
31
+ @staticmethod
32
+ def process(output):
33
+ return _process_zfs_props_table(output)
34
+
35
+
36
+ class Filesystems(ShortFactBase):
37
+ fact = Datasets
38
+
39
+ @staticmethod
40
+ def process_data(data):
41
+ return {name: props for name, props in data.items() if props.get("type") == "filesystem"}
42
+
43
+
44
+ class Snapshots(ShortFactBase):
45
+ fact = Datasets
46
+
47
+ @staticmethod
48
+ def process_data(data):
49
+ return {name: props for name, props in data.items() if props.get("type") == "snapshot"}
50
+
51
+
52
+ class Volumes(ShortFactBase):
53
+ fact = Datasets
54
+
55
+ @staticmethod
56
+ def process_data(data):
57
+ return {name: props for name, props in data.items() if props.get("type") == "volume"}
@@ -4,7 +4,7 @@ Manager Docker Containers, Volumes and Networks
4
4
 
5
5
  from pyinfra import host
6
6
  from pyinfra.api import operation
7
- from pyinfra.facts.docker import DockerContainers, DockerNetworks, DockerVolumes
7
+ from pyinfra.facts.docker import DockerContainer, DockerNetwork, DockerVolume
8
8
 
9
9
  from .util.docker import handle_docker
10
10
 
@@ -68,7 +68,7 @@ def container(
68
68
  )
69
69
  """
70
70
 
71
- existent_container = [c for c in host.get_fact(DockerContainers) if container in c["Name"]]
71
+ existent_container = host.get_fact(DockerContainer, object_id=container)
72
72
 
73
73
  if force:
74
74
  if existent_container:
@@ -183,7 +183,7 @@ def volume(volume, driver="", labels=None, present=True):
183
183
  )
184
184
  """
185
185
 
186
- existent_volume = [v for v in host.get_fact(DockerVolumes) if v["Name"] == volume]
186
+ existent_volume = host.get_fact(DockerVolume, object_id=volume)
187
187
 
188
188
  if present:
189
189
 
@@ -257,7 +257,7 @@ def network(
257
257
  present=True,
258
258
  )
259
259
  """
260
- existent_network = [n for n in host.get_fact(DockerNetworks) if n["Name"] == network]
260
+ existent_network = host.get_fact(DockerNetwork, object_id=network)
261
261
 
262
262
  if present:
263
263
  if existent_network:
@@ -915,7 +915,8 @@ def template(
915
915
  user: str | None = None,
916
916
  group: str | None = None,
917
917
  mode: str | None = None,
918
- create_remote_dir=True,
918
+ create_remote_dir: bool = True,
919
+ jinja_env_kwargs: dict[str, Any] | None = None,
919
920
  **data,
920
921
  ):
921
922
  '''
@@ -927,12 +928,18 @@ def template(
927
928
  + group: group to own the files
928
929
  + mode: permissions of the files
929
930
  + create_remote_dir: create the remote directory if it doesn't exist
931
+ + jinja_env_kwargs: keyword arguments to be passed into the jinja Environment()
930
932
 
931
933
  ``create_remote_dir``:
932
934
  If the remote directory does not exist it will be created using the same
933
935
  user & group as passed to ``files.put``. The mode will *not* be copied over,
934
936
  if this is required call ``files.directory`` separately.
935
937
 
938
+ ``jinja_env_kwargs``:
939
+ To have more control over how jinja2 renders your template, you can pass
940
+ a dict with arguments that will be passed as keyword args to the jinja2
941
+ `Environment() <https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.Environment>`_.
942
+
936
943
  Notes:
937
944
  Common convention is to store templates in a "templates" directory and
938
945
  have a filename suffix with '.j2' (for jinja2).
@@ -1002,7 +1009,7 @@ def template(
1002
1009
 
1003
1010
  # Render and make file-like it's output
1004
1011
  try:
1005
- output = get_template(src).render(data)
1012
+ output = get_template(src, jinja_env_kwargs).render(data)
1006
1013
  except (TemplateRuntimeError, TemplateSyntaxError, UndefinedError) as e:
1007
1014
  trace_frames = [
1008
1015
  frame
@@ -0,0 +1,79 @@
1
+ """
2
+ Manage flatpak packages. See https://www.flatpak.org/
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pyinfra import host
8
+ from pyinfra.api import operation
9
+ from pyinfra.facts.flatpak import FlatpakPackages
10
+
11
+
12
+ @operation()
13
+ def packages(
14
+ packages: str | list[str] | None = None,
15
+ present=True,
16
+ ):
17
+ """
18
+ Install/remove a flatpak package
19
+
20
+ + packages: List of packages
21
+ + present: whether the package should be installed
22
+
23
+ **Examples:**
24
+
25
+ .. code:: python
26
+
27
+ # Install vlc flatpak
28
+ flatpak.package(
29
+ name="Install vlc",
30
+ packages="org.videolan.VLC",
31
+ )
32
+
33
+ # Install multiple flatpaks
34
+ flatpak.package(
35
+ name="Install vlc and kodi",
36
+ packages=["org.videolan.VLC", "tv.kodi.Kodi"],
37
+ )
38
+
39
+ # Remove vlc
40
+ flatpak.package(
41
+ name="Remove vlc",
42
+ packages="org.videolan.VLC",
43
+ present=False,
44
+ )
45
+ """
46
+
47
+ if packages is None:
48
+ return
49
+
50
+ if isinstance(packages, str):
51
+ packages = [packages]
52
+
53
+ flatpak_packages = host.get_fact(FlatpakPackages)
54
+
55
+ install_packages = []
56
+ remove_packages = []
57
+
58
+ for package in packages:
59
+ # it's installed
60
+ if package in flatpak_packages:
61
+ if not present:
62
+ # we don't want it
63
+ remove_packages.append(package)
64
+
65
+ # it's not installed
66
+ if package not in flatpak_packages:
67
+ # we want it
68
+ if present:
69
+ install_packages.append(package)
70
+
71
+ # we don't want it
72
+ else:
73
+ host.noop(f"flatpak package {package} is not installed")
74
+
75
+ if install_packages:
76
+ yield " ".join(["flatpak", "install", "--noninteractive"] + install_packages)
77
+
78
+ if remove_packages:
79
+ yield " ".join(["flatpak", "uninstall", "--noninteractive"] + remove_packages)
@@ -554,7 +554,7 @@ def dump(
554
554
  )
555
555
  """
556
556
 
557
- yield "{0} > {1}".format(
557
+ yield StringCommand(
558
558
  make_mysql_command(
559
559
  executable="mysqldump",
560
560
  database=database,
@@ -563,7 +563,8 @@ def dump(
563
563
  host=mysql_host,
564
564
  port=mysql_port,
565
565
  ),
566
- dest,
566
+ ">",
567
+ QuoteString(dest),
567
568
  )
568
569
 
569
570
 
@@ -595,7 +596,7 @@ def load(
595
596
  )
596
597
  """
597
598
 
598
- commands_bits = [
599
+ yield StringCommand(
599
600
  make_mysql_command(
600
601
  database=database,
601
602
  user=mysql_user,
@@ -605,5 +606,4 @@ def load(
605
606
  ),
606
607
  "<",
607
608
  QuoteString(src),
608
- ]
609
- yield StringCommand(*commands_bits)
609
+ )
@@ -16,7 +16,7 @@ See example/postgresql.py for detailed example
16
16
  from __future__ import annotations
17
17
 
18
18
  from pyinfra import host
19
- from pyinfra.api import MaskString, StringCommand, operation
19
+ from pyinfra.api import MaskString, QuoteString, StringCommand, operation
20
20
  from pyinfra.facts.postgres import (
21
21
  PostgresDatabases,
22
22
  PostgresRoles,
@@ -302,7 +302,7 @@ def dump(
302
302
  port=psql_port,
303
303
  ),
304
304
  ">",
305
- dest,
305
+ QuoteString(dest),
306
306
  )
307
307
 
308
308
 
@@ -345,5 +345,5 @@ def load(
345
345
  port=psql_port,
346
346
  ),
347
347
  "<",
348
- src,
348
+ QuoteString(src),
349
349
  )