pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +115 -97
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +139 -39
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/deploy.py +19 -19
  8. pyinfra/api/exceptions.py +35 -4
  9. pyinfra/api/facts.py +62 -86
  10. pyinfra/api/host.py +102 -15
  11. pyinfra/api/inventory.py +4 -0
  12. pyinfra/api/operation.py +184 -118
  13. pyinfra/api/operations.py +66 -113
  14. pyinfra/api/state.py +53 -34
  15. pyinfra/api/util.py +64 -33
  16. pyinfra/connectors/base.py +65 -20
  17. pyinfra/connectors/chroot.py +15 -13
  18. pyinfra/connectors/docker.py +62 -72
  19. pyinfra/connectors/dockerssh.py +20 -19
  20. pyinfra/connectors/local.py +32 -22
  21. pyinfra/connectors/ssh.py +162 -86
  22. pyinfra/connectors/sshuserclient/client.py +1 -1
  23. pyinfra/connectors/terraform.py +57 -39
  24. pyinfra/connectors/util.py +26 -27
  25. pyinfra/connectors/vagrant.py +27 -26
  26. pyinfra/context.py +1 -0
  27. pyinfra/facts/apk.py +7 -2
  28. pyinfra/facts/apt.py +15 -7
  29. pyinfra/facts/brew.py +28 -13
  30. pyinfra/facts/bsdinit.py +9 -6
  31. pyinfra/facts/cargo.py +6 -3
  32. pyinfra/facts/choco.py +8 -4
  33. pyinfra/facts/deb.py +21 -9
  34. pyinfra/facts/dnf.py +11 -6
  35. pyinfra/facts/docker.py +30 -5
  36. pyinfra/facts/files.py +49 -33
  37. pyinfra/facts/gem.py +7 -2
  38. pyinfra/facts/git.py +14 -21
  39. pyinfra/facts/gpg.py +4 -1
  40. pyinfra/facts/hardware.py +186 -138
  41. pyinfra/facts/launchd.py +7 -2
  42. pyinfra/facts/lxd.py +8 -2
  43. pyinfra/facts/mysql.py +19 -12
  44. pyinfra/facts/npm.py +3 -1
  45. pyinfra/facts/openrc.py +8 -2
  46. pyinfra/facts/pacman.py +13 -5
  47. pyinfra/facts/pip.py +2 -0
  48. pyinfra/facts/pkg.py +5 -1
  49. pyinfra/facts/pkgin.py +7 -2
  50. pyinfra/facts/postgres.py +170 -0
  51. pyinfra/facts/postgresql.py +5 -162
  52. pyinfra/facts/rpm.py +21 -15
  53. pyinfra/facts/runit.py +70 -0
  54. pyinfra/facts/selinux.py +12 -4
  55. pyinfra/facts/server.py +240 -82
  56. pyinfra/facts/snap.py +8 -2
  57. pyinfra/facts/systemd.py +37 -13
  58. pyinfra/facts/sysvinit.py +7 -4
  59. pyinfra/facts/upstart.py +7 -2
  60. pyinfra/facts/util/packaging.py +3 -2
  61. pyinfra/facts/vzctl.py +8 -4
  62. pyinfra/facts/xbps.py +7 -2
  63. pyinfra/facts/yum.py +10 -5
  64. pyinfra/facts/zypper.py +9 -4
  65. pyinfra/operations/apk.py +5 -3
  66. pyinfra/operations/apt.py +28 -25
  67. pyinfra/operations/brew.py +60 -29
  68. pyinfra/operations/bsdinit.py +6 -4
  69. pyinfra/operations/cargo.py +3 -1
  70. pyinfra/operations/choco.py +3 -1
  71. pyinfra/operations/dnf.py +16 -20
  72. pyinfra/operations/docker.py +339 -0
  73. pyinfra/operations/files.py +187 -168
  74. pyinfra/operations/gem.py +3 -1
  75. pyinfra/operations/git.py +23 -25
  76. pyinfra/operations/iptables.py +33 -25
  77. pyinfra/operations/launchd.py +5 -6
  78. pyinfra/operations/lxd.py +7 -4
  79. pyinfra/operations/mysql.py +59 -55
  80. pyinfra/operations/npm.py +8 -1
  81. pyinfra/operations/openrc.py +5 -3
  82. pyinfra/operations/pacman.py +6 -7
  83. pyinfra/operations/pip.py +19 -12
  84. pyinfra/operations/pkg.py +3 -1
  85. pyinfra/operations/pkgin.py +5 -3
  86. pyinfra/operations/postgres.py +349 -0
  87. pyinfra/operations/postgresql.py +18 -335
  88. pyinfra/operations/puppet.py +3 -1
  89. pyinfra/operations/python.py +8 -19
  90. pyinfra/operations/runit.py +182 -0
  91. pyinfra/operations/selinux.py +47 -29
  92. pyinfra/operations/server.py +138 -67
  93. pyinfra/operations/snap.py +3 -1
  94. pyinfra/operations/ssh.py +18 -16
  95. pyinfra/operations/systemd.py +18 -12
  96. pyinfra/operations/sysvinit.py +7 -5
  97. pyinfra/operations/upstart.py +7 -5
  98. pyinfra/operations/util/__init__.py +12 -0
  99. pyinfra/operations/util/docker.py +177 -0
  100. pyinfra/operations/util/files.py +24 -16
  101. pyinfra/operations/util/packaging.py +54 -38
  102. pyinfra/operations/util/service.py +39 -47
  103. pyinfra/operations/vzctl.py +12 -10
  104. pyinfra/operations/xbps.py +5 -3
  105. pyinfra/operations/yum.py +15 -19
  106. pyinfra/operations/zypper.py +9 -10
  107. pyinfra/version.py +5 -2
  108. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/METADATA +51 -58
  109. pyinfra-3.0.1.dist-info/RECORD +168 -0
  110. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/WHEEL +1 -1
  111. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/entry_points.txt +0 -3
  112. pyinfra_cli/__main__.py +4 -3
  113. pyinfra_cli/commands.py +3 -2
  114. pyinfra_cli/exceptions.py +75 -43
  115. pyinfra_cli/inventory.py +52 -31
  116. pyinfra_cli/log.py +10 -2
  117. pyinfra_cli/main.py +88 -65
  118. pyinfra_cli/prints.py +37 -109
  119. pyinfra_cli/util.py +15 -10
  120. tests/test_api/test_api.py +2 -0
  121. tests/test_api/test_api_arguments.py +9 -9
  122. tests/test_api/test_api_deploys.py +15 -19
  123. tests/test_api/test_api_facts.py +4 -5
  124. tests/test_api/test_api_operations.py +18 -20
  125. tests/test_api/test_api_util.py +41 -2
  126. tests/test_cli/test_cli.py +14 -50
  127. tests/test_cli/test_cli_deploy.py +10 -12
  128. tests/test_cli/test_cli_exceptions.py +50 -19
  129. tests/test_cli/test_cli_inventory.py +66 -0
  130. tests/test_cli/util.py +1 -1
  131. tests/test_connectors/test_dockerssh.py +11 -8
  132. tests/test_connectors/test_ssh.py +88 -23
  133. tests/test_connectors/test_sshuserclient.py +1 -1
  134. tests/test_connectors/test_terraform.py +11 -8
  135. tests/test_connectors/test_vagrant.py +6 -6
  136. pyinfra/connectors/ansible.py +0 -175
  137. pyinfra/connectors/mech.py +0 -189
  138. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  139. pyinfra/connectors/winrm.py +0 -312
  140. pyinfra/facts/windows.py +0 -366
  141. pyinfra/facts/windows_files.py +0 -90
  142. pyinfra/operations/windows.py +0 -59
  143. pyinfra/operations/windows_files.py +0 -538
  144. pyinfra-3.0.dev0.dist-info/RECORD +0 -170
  145. tests/test_connectors/test_ansible.py +0 -64
  146. tests/test_connectors/test_mech.py +0 -126
  147. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/LICENSE.md +0 -0
  148. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,3 @@
1
- """
2
- The ``@vagrant`` connector reads the current Vagrant status and generates an
3
- inventory for any running VMs.
4
-
5
- .. code:: shell
6
-
7
- # Run on all hosts
8
- pyinfra @vagrant ...
9
-
10
- # Run on a specific VM
11
- pyinfra @vagrant/my-vm-name ...
12
-
13
- # Run on multiple named VMs
14
- pyinfra @vagrant/my-vm-name,@vagrant/another-vm-name ...
15
- """
16
-
17
1
  import json
18
2
  from os import path
19
3
  from queue import Queue
@@ -109,13 +93,13 @@ def _make_name_data(host):
109
93
  "ssh_hostname": host["HostName"],
110
94
  }
111
95
 
112
- for config_key, data_key in (
113
- ("Port", "ssh_port"),
114
- ("User", "ssh_user"),
115
- ("IdentityFile", "ssh_key"),
96
+ for config_key, data_key, data_cast in (
97
+ ("Port", "ssh_port", int),
98
+ ("User", "ssh_user", str),
99
+ ("IdentityFile", "ssh_key", str),
116
100
  ):
117
101
  if config_key in host:
118
- data[data_key] = host[config_key]
102
+ data[data_key] = data_cast(host[config_key])
119
103
 
120
104
  # Update any configured JSON data
121
105
  if vagrant_host in vagrant_options.get("data", {}):
@@ -131,9 +115,25 @@ def _make_name_data(host):
131
115
 
132
116
 
133
117
  class VagrantInventoryConnector(BaseConnector):
118
+ """
119
+ The ``@vagrant`` connector reads the current Vagrant status and generates an
120
+ inventory for any running VMs.
121
+
122
+ .. code:: shell
123
+
124
+ # Run on all hosts
125
+ pyinfra @vagrant ...
126
+
127
+ # Run on a specific VM
128
+ pyinfra @vagrant/my-vm-name ...
129
+
130
+ # Run on multiple named VMs
131
+ pyinfra @vagrant/my-vm-name,@vagrant/another-vm-name ...
132
+ """
133
+
134
134
  @staticmethod
135
- def make_names_data(limit=None):
136
- vagrant_ssh_info = get_vagrant_config(limit)
135
+ def make_names_data(name=None):
136
+ vagrant_ssh_info = get_vagrant_config(name)
137
137
 
138
138
  logger.debug("Got Vagrant SSH info: \n%s", vagrant_ssh_info)
139
139
 
@@ -170,10 +170,11 @@ class VagrantInventoryConnector(BaseConnector):
170
170
  hosts.append(_make_name_data(current_host))
171
171
 
172
172
  if not hosts:
173
- if limit:
173
+ if name:
174
174
  raise InventoryError(
175
- "No running Vagrant instances matching `{0}` found!".format(limit)
175
+ "No running Vagrant instances matching `{0}` found!".format(name)
176
176
  )
177
177
  raise InventoryError("No running Vagrant instances found!")
178
178
 
179
- return hosts
179
+ for host in hosts:
180
+ yield host
pyinfra/context.py CHANGED
@@ -4,6 +4,7 @@ are imported and used throughout pyinfra and end user deploy code (CLI mode).
4
4
 
5
5
  These variables always represent the current executing pyinfra context.
6
6
  """
7
+
7
8
  from contextlib import contextmanager
8
9
  from types import ModuleType
9
10
  from typing import TYPE_CHECKING
pyinfra/facts/apk.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
@@ -16,8 +18,11 @@ class ApkPackages(FactBase):
16
18
  }
17
19
  """
18
20
 
19
- command = "apk list --installed"
20
- requires_command = "apk"
21
+ def command(self) -> str:
22
+ return "apk list --installed"
23
+
24
+ def requires_command(self) -> str:
25
+ return "apk"
21
26
 
22
27
  default = dict
23
28
 
pyinfra/facts/apt.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra.api import FactBase
@@ -50,11 +52,14 @@ class AptSources(FactBase):
50
52
  ]
51
53
  """
52
54
 
53
- command = make_cat_files_command(
54
- "/etc/apt/sources.list",
55
- "/etc/apt/sources.list.d/*.list",
56
- )
57
- requires_command = "apt" # if apt installed, above should exist
55
+ def command(self) -> str:
56
+ return make_cat_files_command(
57
+ "/etc/apt/sources.list",
58
+ "/etc/apt/sources.list.d/*.list",
59
+ )
60
+
61
+ def requires_command(self) -> str:
62
+ return "apt" # if apt installed, above should exist
58
63
 
59
64
  default = list
60
65
 
@@ -84,5 +89,8 @@ class AptKeys(GpgFactBase):
84
89
  """
85
90
 
86
91
  # This requires both apt-key *and* apt-key itself requires gpg
87
- command = "! command -v gpg || apt-key list --with-colons"
88
- requires_command = "apt-key"
92
+ def command(self) -> str:
93
+ return "! command -v gpg || apt-key list --with-colons"
94
+
95
+ def requires_command(self) -> str:
96
+ return "apt-key"
pyinfra/facts/brew.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra import logger
@@ -35,18 +37,22 @@ class BrewVersion(FactBase):
35
37
 
36
38
  """
37
39
 
38
- command = "brew --version"
39
- requires_command = "brew"
40
+ def command(self) -> str:
41
+ return "brew --version"
42
+
43
+ def requires_command(self) -> str:
44
+ return "brew"
40
45
 
41
46
  @staticmethod
42
47
  def default():
43
48
  return [0, 0, 0]
44
49
 
45
50
  def process(self, output):
46
- m = VERSION_MATCHER.match(output[0])
51
+ out = list(output)[0]
52
+ m = VERSION_MATCHER.match(out)
47
53
  if m is not None:
48
54
  return [int(m.group(key)) for key in ["major", "minor", "patch"]]
49
- logger.warning("could not parse version string from brew: %s", output[0])
55
+ logger.warning("could not parse version string from brew: %s", out)
50
56
  return self.default()
51
57
 
52
58
 
@@ -61,8 +67,11 @@ class BrewPackages(FactBase):
61
67
  }
62
68
  """
63
69
 
64
- command = "brew list --versions"
65
- requires_command = "brew"
70
+ def command(self) -> str:
71
+ return "brew list --versions"
72
+
73
+ def requires_command(self) -> str:
74
+ return "brew"
66
75
 
67
76
  default = dict
68
77
 
@@ -81,11 +90,14 @@ class BrewCasks(BrewPackages):
81
90
  }
82
91
  """
83
92
 
84
- command = (
85
- r'if brew --version | grep -q -e "Homebrew\ +(1\.|2\.[0-5]).*" 1>/dev/null;'
86
- r"then brew cask list --versions; else brew list --cask --versions; fi"
87
- )
88
- requires_command = "brew"
93
+ def command(self) -> str:
94
+ return (
95
+ r'if brew --version | grep -q -e "Homebrew\ +(1\.|2\.[0-5]).*" 1>/dev/null;'
96
+ r"then brew cask list --versions; else brew list --cask --versions; fi"
97
+ )
98
+
99
+ def requires_command(self) -> str:
100
+ return "brew"
89
101
 
90
102
 
91
103
  class BrewTaps(FactBase):
@@ -93,8 +105,11 @@ class BrewTaps(FactBase):
93
105
  Returns a list of brew taps.
94
106
  """
95
107
 
96
- command = "brew tap"
97
- requires_command = "brew"
108
+ def command(self) -> str:
109
+ return "brew tap"
110
+
111
+ def requires_command(self) -> str:
112
+ return "brew"
98
113
 
99
114
  default = list
100
115
 
pyinfra/facts/bsdinit.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from .sysvinit import InitdStatus
2
4
 
3
5
 
@@ -7,11 +9,12 @@ class RcdStatus(InitdStatus):
7
9
  BSD init scripts are well behaved and as such their output can be trusted.
8
10
  """
9
11
 
10
- command = """
11
- for SERVICE in `find /etc/rc.d /usr/local/etc/rc.d -type f`; do
12
- $SERVICE status 2> /dev/null || $SERVICE check 2> /dev/null
13
- echo "`basename $SERVICE`=$?"
14
- done
15
- """
12
+ def command(self) -> str:
13
+ return """
14
+ for SERVICE in `find /etc/rc.d /usr/local/etc/rc.d -type f`; do
15
+ $SERVICE status 2> /dev/null || $SERVICE check 2> /dev/null
16
+ echo "`basename $SERVICE`=$?"
17
+ done
18
+ """
16
19
 
17
20
  default = dict
pyinfra/facts/cargo.py CHANGED
@@ -1,5 +1,7 @@
1
1
  # encoding: utf8
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from pyinfra.api import FactBase
4
6
 
5
7
  from .util.packaging import parse_packages
@@ -20,10 +22,11 @@ class CargoPackages(FactBase):
20
22
 
21
23
  default = dict
22
24
 
23
- requires_command = "cargo"
24
-
25
- def command(self):
25
+ def command(self) -> str:
26
26
  return "cargo install --list"
27
27
 
28
+ def requires_command(self) -> str:
29
+ return "cargo"
30
+
28
31
  def process(self, output):
29
32
  return parse_packages(CARGO_REGEX, output)
pyinfra/facts/choco.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
@@ -16,7 +18,9 @@ class ChocoPackages(FactBase):
16
18
  }
17
19
  """
18
20
 
19
- command = "choco list --local-only"
21
+ def command(self) -> str:
22
+ return "choco list"
23
+
20
24
  shell_executable = "ps"
21
25
 
22
26
  default = dict
@@ -30,8 +34,8 @@ class ChocoVersion(FactBase):
30
34
  Returns the choco (Chocolatey) version.
31
35
  """
32
36
 
33
- command = "choco --version"
37
+ def command(self) -> str:
38
+ return "choco --version"
34
39
 
35
- @staticmethod
36
- def process(output):
40
+ def process(self, output):
37
41
  return "".join(output).replace("\n", "")
pyinfra/facts/deb.py CHANGED
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
4
+ import shlex
2
5
 
3
6
  from pyinfra.api import FactBase
4
7
 
@@ -13,8 +16,11 @@ class DebArch(FactBase):
13
16
  Returns the architecture string used in apt repository sources, eg ``amd64``.
14
17
  """
15
18
 
16
- command = "dpkg --print-architecture"
17
- requires_command = "dpkg"
19
+ def command(self) -> str:
20
+ return "dpkg --print-architecture"
21
+
22
+ def requires_command(self) -> str:
23
+ return "dpkg"
18
24
 
19
25
 
20
26
  class DebPackages(FactBase):
@@ -28,8 +34,11 @@ class DebPackages(FactBase):
28
34
  }
29
35
  """
30
36
 
31
- command = "dpkg -l"
32
- requires_command = "dpkg"
37
+ def command(self) -> str:
38
+ return "dpkg -l"
39
+
40
+ def requires_command(self) -> str:
41
+ return "dpkg"
33
42
 
34
43
  default = dict
35
44
 
@@ -48,14 +57,17 @@ class DebPackage(FactBase):
48
57
  """
49
58
 
50
59
  _regexes = {
51
- "name": r"^Package: ({0})$".format(DEB_PACKAGE_NAME_REGEX),
52
- "version": r"^Version: ({0})$".format(DEB_PACKAGE_VERSION_REGEX),
60
+ "name": r"^Package:\s+({0})$".format(DEB_PACKAGE_NAME_REGEX),
61
+ "version": r"^Version:\s+({0})$".format(DEB_PACKAGE_VERSION_REGEX),
53
62
  }
54
63
 
55
- requires_command = "dpkg"
64
+ def requires_command(self, package) -> str:
65
+ return "dpkg"
56
66
 
57
- def command(self, name):
58
- return "! test -e {0} && (dpkg -s {0} 2>/dev/null || true) || dpkg -I {0}".format(name)
67
+ def command(self, package):
68
+ return "! test -e {0} && (dpkg -s {0} 2>/dev/null || true) || dpkg -I {0}".format(
69
+ shlex.quote(package)
70
+ )
59
71
 
60
72
  def process(self, output):
61
73
  data = {}
pyinfra/facts/dnf.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util import make_cat_files_command
@@ -21,12 +23,15 @@ class DnfRepositories(FactBase):
21
23
  ]
22
24
  """
23
25
 
24
- command = make_cat_files_command(
25
- "/etc/dnf.conf",
26
- "/etc/dnf.repos.d/*.repo",
27
- "/etc/yum.repos.d/*.repo",
28
- )
29
- requires_command = "dnf"
26
+ def command(self) -> str:
27
+ return make_cat_files_command(
28
+ "/etc/dnf.conf",
29
+ "/etc/dnf.repos.d/*.repo",
30
+ "/etc/yum.repos.d/*.repo",
31
+ )
32
+
33
+ def requires_command(self) -> str:
34
+ return "dnf"
30
35
 
31
36
  default = list
32
37
 
pyinfra/facts/docker.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from pyinfra.api import FactBase
@@ -7,7 +9,9 @@ class DockerFactBase(FactBase):
7
9
  abstract = True
8
10
 
9
11
  docker_type: str
10
- requires_command = "docker"
12
+
13
+ def requires_command(self, *args, **kwargs) -> str:
14
+ return "docker"
11
15
 
12
16
  def process(self, output):
13
17
  output = "".join(output)
@@ -19,7 +23,8 @@ class DockerSystemInfo(DockerFactBase):
19
23
  Returns ``docker system info`` output in JSON format.
20
24
  """
21
25
 
22
- command = 'docker system info --format="{{json .}}"'
26
+ def command(self) -> str:
27
+ return 'docker system info --format="{{json .}}"'
23
28
 
24
29
 
25
30
  # All Docker objects
@@ -31,7 +36,8 @@ class DockerContainers(DockerFactBase):
31
36
  Returns ``docker inspect`` output for all Docker containers.
32
37
  """
33
38
 
34
- command = "docker container inspect `docker ps -qa`"
39
+ def command(self) -> str:
40
+ return "docker container inspect `docker ps -qa`"
35
41
 
36
42
 
37
43
  class DockerImages(DockerFactBase):
@@ -39,7 +45,8 @@ class DockerImages(DockerFactBase):
39
45
  Returns ``docker inspect`` output for all Docker images.
40
46
  """
41
47
 
42
- command = "docker image inspect `docker images -q`"
48
+ def command(self) -> str:
49
+ return "docker image inspect `docker images -q`"
43
50
 
44
51
 
45
52
  class DockerNetworks(DockerFactBase):
@@ -47,7 +54,8 @@ class DockerNetworks(DockerFactBase):
47
54
  Returns ``docker inspect`` output for all Docker networks.
48
55
  """
49
56
 
50
- command = "docker network inspect `docker network ls -q`"
57
+ def command(self) -> str:
58
+ return "docker network inspect `docker network ls -q`"
51
59
 
52
60
 
53
61
  # Single Docker objects
@@ -84,3 +92,20 @@ class DockerNetwork(DockerSingleMixin):
84
92
  """
85
93
 
86
94
  docker_type = "network"
95
+
96
+
97
+ class DockerVolumes(DockerFactBase):
98
+ """
99
+ Returns ``docker inspect`` output for all Docker volumes.
100
+ """
101
+
102
+ def command(self) -> str:
103
+ return "docker volume inspect `docker volume ls -q`"
104
+
105
+
106
+ class DockerVolume(DockerSingleMixin):
107
+ """
108
+ Returns ``docker inspect`` output for a single Docker container.
109
+ """
110
+
111
+ docker_type = "volume"
pyinfra/facts/files.py CHANGED
@@ -2,10 +2,14 @@
2
2
  The files facts provide information about the filesystem and it's contents on the target host.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import re
6
8
  import stat
7
9
  from datetime import datetime
8
- from typing import List, Tuple
10
+ from typing import TYPE_CHECKING, List, Optional, Tuple, Union
11
+
12
+ from typing_extensions import Literal, NotRequired, TypedDict
9
13
 
10
14
  from pyinfra.api.command import QuoteString, make_formatted_string_command
11
15
  from pyinfra.api.facts import FactBase
@@ -64,7 +68,25 @@ def _parse_mode(mode: str) -> int:
64
68
  return int(oct(out)[2:])
65
69
 
66
70
 
67
- class File(FactBase):
71
+ def _parse_datetime(value: str) -> Optional[datetime]:
72
+ value = try_int(value)
73
+ if isinstance(value, int):
74
+ return datetime.utcfromtimestamp(value)
75
+ return None
76
+
77
+
78
+ class FileDict(TypedDict):
79
+ mode: int
80
+ size: Union[int, str]
81
+ atime: Optional[datetime]
82
+ mtime: Optional[datetime]
83
+ ctime: Optional[datetime]
84
+ user: str
85
+ group: str
86
+ link_target: NotRequired[str]
87
+
88
+
89
+ class File(FactBase[Union[FileDict, Literal[False], None]]):
68
90
  """
69
91
  Returns information about a file on the remote system:
70
92
 
@@ -98,36 +120,23 @@ class File(FactBase):
98
120
  bsd_stat_command=BSD_STAT_COMMAND,
99
121
  )
100
122
 
101
- def process(self, output):
123
+ def process(self, output) -> Union[FileDict, Literal[False], None]:
102
124
  match = re.match(STAT_REGEX, output[0])
103
125
  if not match:
104
126
  return None
105
127
 
106
- data = {}
107
- path_type = None
108
-
109
- for key, value in (
110
- ("user", match.group(1)),
111
- ("group", match.group(2)),
112
- ("mode", match.group(3)),
113
- ("atime", match.group(4)),
114
- ("mtime", match.group(5)),
115
- ("ctime", match.group(6)),
116
- ("size", match.group(7)),
117
- ):
118
- if key == "mode":
119
- path_type = FLAG_TO_TYPE[value[0]]
120
- value = _parse_mode(value[1:])
121
-
122
- elif key == "size":
123
- value = try_int(value)
124
-
125
- elif key in ("atime", "mtime", "ctime"):
126
- value = try_int(value)
127
- if isinstance(value, int):
128
- value = datetime.utcfromtimestamp(value)
129
-
130
- data[key] = value
128
+ mode = match.group(3)
129
+ path_type = FLAG_TO_TYPE[mode[0]]
130
+
131
+ data: FileDict = {
132
+ "user": match.group(1),
133
+ "group": match.group(2),
134
+ "mode": _parse_mode(mode[1:]),
135
+ "atime": _parse_datetime(match.group(4)),
136
+ "mtime": _parse_datetime(match.group(5)),
137
+ "ctime": _parse_datetime(match.group(6)),
138
+ "size": try_int(match.group(7)),
139
+ }
131
140
 
132
141
  if path_type != self.type:
133
142
  return False
@@ -205,7 +214,13 @@ class Socket(File):
205
214
  type = "socket"
206
215
 
207
216
 
208
- class HashFileFactBase(FactBase):
217
+ if TYPE_CHECKING:
218
+ FactBaseOptionalStr = FactBase[Optional[str]]
219
+ else:
220
+ FactBaseOptionalStr = FactBase
221
+
222
+
223
+ class HashFileFactBase(FactBaseOptionalStr):
209
224
  _raw_cmd: str
210
225
  _regexes: Tuple[str, str]
211
226
 
@@ -229,13 +244,14 @@ class HashFileFactBase(FactBase):
229
244
  self.path = path
230
245
  return make_formatted_string_command(self._raw_cmd, QuoteString(path))
231
246
 
232
- def process(self, output):
247
+ def process(self, output) -> Optional[str]:
233
248
  output = output[0]
234
249
  escaped_path = re.escape(self.path)
235
250
  for regex in self._regexes:
236
251
  matches = re.match(regex % escaped_path, output)
237
252
  if matches:
238
253
  return matches.group(1)
254
+ return None
239
255
 
240
256
 
241
257
  class Sha1File(HashFileFactBase, digits=40, cmds=["sha1sum", "shasum", "sha1"]):
@@ -296,8 +312,7 @@ class FindFilesBase(FactBase):
296
312
  default = list
297
313
  type_flag: str
298
314
 
299
- @staticmethod
300
- def process(output):
315
+ def process(self, output):
301
316
  return output
302
317
 
303
318
  def command(self, path, quote_path=True):
@@ -337,7 +352,8 @@ class Flags(FactBase):
337
352
  Returns a list of the file flags set for the specified file or directory.
338
353
  """
339
354
 
340
- requires_command = "chflags" # don't try to retrieve them if we can't set them
355
+ def requires_command(self, path) -> str:
356
+ return "chflags" # don't try to retrieve them if we can't set them
341
357
 
342
358
  def command(self, path):
343
359
  return make_formatted_string_command(
pyinfra/facts/gem.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
@@ -16,8 +18,11 @@ class GemPackages(FactBase):
16
18
  }
17
19
  """
18
20
 
19
- command = "gem list --local"
20
- requires_command = "gem"
21
+ def command(self) -> str:
22
+ return "gem list --local"
23
+
24
+ def requires_command(self) -> str:
25
+ return "gem"
21
26
 
22
27
  default = dict
23
28