pyinfra 2.9.2__py2.py3-none-any.whl → 3.0b1__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 (126) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +261 -255
  3. pyinfra/api/arguments_typed.py +77 -0
  4. pyinfra/api/command.py +66 -53
  5. pyinfra/api/config.py +27 -22
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +2 -24
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +77 -113
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +17 -25
  13. pyinfra/api/operation.py +232 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +55 -70
  17. pyinfra/connectors/base.py +150 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +227 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +195 -207
  22. pyinfra/connectors/ssh.py +528 -615
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +212 -137
  27. pyinfra/connectors/vagrant.py +55 -48
  28. pyinfra/context.py +3 -2
  29. pyinfra/facts/docker.py +1 -0
  30. pyinfra/facts/files.py +45 -32
  31. pyinfra/facts/git.py +3 -1
  32. pyinfra/facts/gpg.py +1 -1
  33. pyinfra/facts/hardware.py +4 -2
  34. pyinfra/facts/iptables.py +5 -3
  35. pyinfra/facts/mysql.py +1 -0
  36. pyinfra/facts/postgres.py +168 -0
  37. pyinfra/facts/postgresql.py +5 -161
  38. pyinfra/facts/selinux.py +3 -1
  39. pyinfra/facts/server.py +77 -30
  40. pyinfra/facts/systemd.py +29 -12
  41. pyinfra/facts/sysvinit.py +10 -10
  42. pyinfra/facts/util/packaging.py +4 -2
  43. pyinfra/local.py +4 -5
  44. pyinfra/operations/apk.py +3 -3
  45. pyinfra/operations/apt.py +25 -47
  46. pyinfra/operations/brew.py +7 -14
  47. pyinfra/operations/bsdinit.py +4 -4
  48. pyinfra/operations/cargo.py +1 -1
  49. pyinfra/operations/choco.py +1 -1
  50. pyinfra/operations/dnf.py +4 -4
  51. pyinfra/operations/files.py +108 -321
  52. pyinfra/operations/gem.py +1 -1
  53. pyinfra/operations/git.py +6 -37
  54. pyinfra/operations/iptables.py +2 -10
  55. pyinfra/operations/launchd.py +1 -1
  56. pyinfra/operations/lxd.py +1 -9
  57. pyinfra/operations/mysql.py +5 -28
  58. pyinfra/operations/npm.py +1 -1
  59. pyinfra/operations/openrc.py +1 -1
  60. pyinfra/operations/pacman.py +3 -3
  61. pyinfra/operations/pip.py +14 -15
  62. pyinfra/operations/pkg.py +1 -1
  63. pyinfra/operations/pkgin.py +3 -3
  64. pyinfra/operations/postgres.py +347 -0
  65. pyinfra/operations/postgresql.py +17 -380
  66. pyinfra/operations/python.py +2 -17
  67. pyinfra/operations/selinux.py +5 -28
  68. pyinfra/operations/server.py +59 -84
  69. pyinfra/operations/snap.py +1 -3
  70. pyinfra/operations/ssh.py +8 -23
  71. pyinfra/operations/systemd.py +7 -7
  72. pyinfra/operations/sysvinit.py +3 -12
  73. pyinfra/operations/upstart.py +4 -4
  74. pyinfra/operations/util/__init__.py +12 -0
  75. pyinfra/operations/util/files.py +2 -2
  76. pyinfra/operations/util/packaging.py +6 -24
  77. pyinfra/operations/util/service.py +18 -37
  78. pyinfra/operations/vzctl.py +2 -2
  79. pyinfra/operations/xbps.py +3 -3
  80. pyinfra/operations/yum.py +4 -4
  81. pyinfra/operations/zypper.py +4 -4
  82. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/METADATA +19 -22
  83. pyinfra-3.0b1.dist-info/RECORD +163 -0
  84. pyinfra-3.0b1.dist-info/entry_points.txt +11 -0
  85. pyinfra_cli/__main__.py +2 -0
  86. pyinfra_cli/commands.py +7 -2
  87. pyinfra_cli/exceptions.py +83 -42
  88. pyinfra_cli/inventory.py +19 -4
  89. pyinfra_cli/log.py +17 -3
  90. pyinfra_cli/main.py +133 -90
  91. pyinfra_cli/prints.py +93 -129
  92. pyinfra_cli/util.py +60 -29
  93. tests/test_api/test_api.py +2 -0
  94. tests/test_api/test_api_arguments.py +13 -13
  95. tests/test_api/test_api_deploys.py +28 -29
  96. tests/test_api/test_api_facts.py +60 -98
  97. tests/test_api/test_api_operations.py +100 -200
  98. tests/test_cli/test_cli.py +18 -49
  99. tests/test_cli/test_cli_deploy.py +11 -37
  100. tests/test_cli/test_cli_exceptions.py +50 -19
  101. tests/test_cli/util.py +1 -1
  102. tests/test_connectors/test_chroot.py +6 -6
  103. tests/test_connectors/test_docker.py +4 -4
  104. tests/test_connectors/test_dockerssh.py +38 -50
  105. tests/test_connectors/test_local.py +11 -12
  106. tests/test_connectors/test_ssh.py +66 -107
  107. tests/test_connectors/test_terraform.py +9 -15
  108. tests/test_connectors/test_util.py +24 -46
  109. tests/test_connectors/test_vagrant.py +4 -4
  110. pyinfra/api/operation.pyi +0 -117
  111. pyinfra/connectors/ansible.py +0 -171
  112. pyinfra/connectors/mech.py +0 -186
  113. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  114. pyinfra/connectors/winrm.py +0 -320
  115. pyinfra/facts/windows.py +0 -366
  116. pyinfra/facts/windows_files.py +0 -90
  117. pyinfra/operations/windows.py +0 -59
  118. pyinfra/operations/windows_files.py +0 -551
  119. pyinfra-2.9.2.dist-info/RECORD +0 -170
  120. pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
  121. tests/test_connectors/test_ansible.py +0 -64
  122. tests/test_connectors/test_mech.py +0 -126
  123. tests/test_connectors/test_winrm.py +0 -76
  124. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/LICENSE.md +0 -0
  125. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/WHEEL +0 -0
  126. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/top_level.txt +0 -0
pyinfra_cli/commands.py CHANGED
@@ -1,5 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
5
+ from pyinfra.api.facts import FactBase
6
+
3
7
  from .exceptions import CliError
4
8
  from .util import parse_cli_arg, try_import_module_attribute
5
9
 
@@ -32,7 +36,7 @@ def get_func_and_args(commands):
32
36
 
33
37
 
34
38
  def get_facts_and_args(commands):
35
- facts = []
39
+ facts: list[tuple[FactBase, tuple, dict]] = []
36
40
 
37
41
  current_fact = None
38
42
 
@@ -42,7 +46,7 @@ def get_facts_and_args(commands):
42
46
  raise CliError("Invalid fact commands: `{0}`".format(commands))
43
47
 
44
48
  key, value = command.split("=", 1)
45
- current_fact[2][key] = value
49
+ current_fact[2][key] = parse_cli_arg(value)
46
50
  continue
47
51
 
48
52
  if current_fact:
@@ -53,6 +57,7 @@ def get_facts_and_args(commands):
53
57
  raise CliError(f"Invalid fact: `{command}`, should be in the format `module.cls`")
54
58
 
55
59
  fact_cls = try_import_module_attribute(command, prefix="pyinfra.facts")
60
+ assert fact_cls is not None
56
61
  current_fact = (fact_cls, (), {})
57
62
 
58
63
  if current_fact:
pyinfra_cli/exceptions.py CHANGED
@@ -1,67 +1,108 @@
1
+ import abc
1
2
  import sys
2
- from traceback import format_exception, format_tb
3
+ from inspect import getframeinfo
4
+ from os import path
5
+ from traceback import format_exception, format_tb, walk_tb
6
+ from types import TracebackType
3
7
 
4
8
  import click
5
9
 
6
- from pyinfra import host, logger, state
7
- from pyinfra.api.exceptions import PyinfraError
8
- from pyinfra.context import ctx_host
9
-
10
-
11
- class CliError(PyinfraError, click.ClickException):
12
- def show(self):
10
+ from pyinfra import logger
11
+ from pyinfra.api.exceptions import (
12
+ ArgumentTypeError,
13
+ ConnectorDataTypeError,
14
+ OperationError,
15
+ PyinfraError,
16
+ )
17
+ from pyinfra.api.util import PYINFRA_INSTALL_DIR
18
+
19
+
20
+ def get_frame_line_from_tb(tb: TracebackType):
21
+ frame_lines = list(walk_tb(tb))
22
+ frame_lines.reverse()
23
+ for frame, line in frame_lines:
24
+ info = getframeinfo(frame)
25
+ if info.filename.startswith(PYINFRA_INSTALL_DIR):
26
+ continue
27
+ if info.filename.startswith("/"):
28
+ continue
29
+ if not path.exists(info.filename):
30
+ continue
31
+ return info
32
+
33
+
34
+ class WrappedError(click.ClickException):
35
+ def __init__(self, e: Exception):
36
+ self.traceback = e.__traceback__
37
+ self.exception = e
38
+
39
+ # Pull message from the wrapped exception
40
+ message = getattr(e, "message", e.args[0])
41
+ if not isinstance(message, str):
42
+ message = repr(message)
43
+ self.message = message
44
+
45
+ def show(self, file=None):
13
46
  name = "unknown error"
14
47
 
15
- if isinstance(self, PyinfraError):
48
+ if isinstance(self.exception, ConnectorDataTypeError):
49
+ name = "Connector data type error"
50
+ elif isinstance(self.exception, ArgumentTypeError):
51
+ name = "Argument type error"
52
+ elif isinstance(self.exception, OperationError):
53
+ name = "Operation error"
54
+ elif isinstance(self.exception, PyinfraError):
16
55
  name = "pyinfra error"
56
+ elif isinstance(self.exception, IOError):
57
+ name = "Local IO error"
58
+
59
+ if self.traceback:
60
+ info = get_frame_line_from_tb(self.traceback)
61
+ if info:
62
+ name = f"{name} in {info.filename} line {info.lineno}"
63
+
64
+ logger.warning(
65
+ "--> {0}: {1}".format(
66
+ click.style(name, "red", bold=True),
67
+ self,
68
+ ),
69
+ )
17
70
 
18
- elif isinstance(self, IOError):
19
- name = "local IO error"
20
-
21
- if ctx_host.isset():
22
- # Get any operation meta + name
23
- op_name = None
24
- current_op_hash = host.current_op_hash
25
- current_op_meta = state.op_meta.get(current_op_hash)
26
- if current_op_meta:
27
- op_name = ", ".join(current_op_meta["names"])
28
-
29
- sys.stderr.write(
30
- "--> {0}{1}{2}: ".format(
31
- host.print_prefix,
32
- click.style(name, "red", bold=True),
33
- " (operation={0})".format(op_name) if op_name else "",
34
- ),
35
- )
36
- else:
37
- sys.stderr.write(
38
- "--> {0}: ".format(click.style(name, "red", bold=True)),
39
- )
40
71
 
41
- logger.warning(self)
72
+ class CliError(click.ClickException):
73
+ def show(self, file=None):
74
+ logger.warning(
75
+ "--> {0}: {1}".format(
76
+ click.style("pyinfra error", "red", bold=True),
77
+ self,
78
+ ),
79
+ )
80
+
42
81
 
82
+ class UnexpectedMixin(abc.ABC):
83
+ exception: Exception
84
+ traceback: TracebackType
43
85
 
44
- class UnexpectedMixin:
45
86
  def get_traceback_lines(self):
46
- traceback = getattr(self.e, "_traceback")
87
+ traceback = getattr(self.exception, "_traceback")
47
88
  return format_tb(traceback)
48
89
 
49
90
  def get_traceback(self):
50
91
  return "".join(self.get_traceback_lines())
51
92
 
52
93
  def get_exception(self):
53
- return "".join(format_exception(self.e.__class__, self.e, None))
94
+ return "".join(format_exception(self.exception.__class__, self.exception, None))
54
95
 
55
96
 
56
97
  class UnexpectedExternalError(click.ClickException, UnexpectedMixin):
57
98
  def __init__(self, e, filename):
58
99
  _, _, traceback = sys.exc_info()
59
100
  e._traceback = traceback
60
- self.e = e
101
+ self.exception = e
61
102
  self.filename = filename
62
103
 
63
- def show(self):
64
- click.echo(
104
+ def show(self, file=None):
105
+ logger.warning(
65
106
  "--> {0}:\n".format(
66
107
  click.style(
67
108
  "An exception occurred in: {0}".format(self.filename),
@@ -69,9 +110,9 @@ class UnexpectedExternalError(click.ClickException, UnexpectedMixin):
69
110
  bold=True,
70
111
  ),
71
112
  ),
72
- err=True,
73
113
  )
74
114
 
115
+ click.echo("Traceback (most recent call last):", err=True)
75
116
  click.echo(self.get_traceback(), err=True, nl=False)
76
117
  click.echo(self.get_exception(), err=True)
77
118
 
@@ -80,9 +121,9 @@ class UnexpectedInternalError(click.ClickException, UnexpectedMixin):
80
121
  def __init__(self, e):
81
122
  _, _, traceback = sys.exc_info()
82
123
  e._traceback = traceback
83
- self.e = e
124
+ self.exception = e
84
125
 
85
- def show(self):
126
+ def show(self, file=None):
86
127
  click.echo(
87
128
  "--> {0}:\n".format(
88
129
  click.style(
@@ -99,7 +140,7 @@ class UnexpectedInternalError(click.ClickException, UnexpectedMixin):
99
140
 
100
141
  # Syntax errors contain the filename/line/etc, but other exceptions
101
142
  # don't, so print the *last* call to stderr.
102
- if not isinstance(self.e, SyntaxError):
143
+ if not isinstance(self.exception, SyntaxError):
103
144
  sys.stderr.write(traceback_lines[-1])
104
145
 
105
146
  exception = self.get_exception()
pyinfra_cli/inventory.py CHANGED
@@ -1,3 +1,5 @@
1
+ import re
2
+ import socket
1
3
  from collections import defaultdict
2
4
  from os import listdir, path
3
5
  from types import GeneratorType
@@ -85,10 +87,23 @@ def make_inventory(
85
87
  cwd: Optional[str] = None,
86
88
  group_data_directories=None,
87
89
  ):
88
- # First, try loading the inventory as if it's a Python import function
89
- try:
90
- inventory_func = try_import_module_attribute(inventory)
91
- except (CliError, ValueError):
90
+ inventory_func = None
91
+
92
+ # (Un)fortunately the CLI is pretty flexible for inventory inputs; we support a single hostname,
93
+ # a Python module.function import or a Python file path. All of these are kind of similar, and
94
+ # we want error handling to be a good user experience.
95
+ # Thus, we'll check for everything but also drop a warning to the console if the inventory looks
96
+ # like either an import or hostname but neither works.
97
+ if re.match("[a-zA-Z0-9\\._]+[\\.:][a-zA-Z0-9_]+", inventory):
98
+ # First, try loading the inventory as if it's a Python import function
99
+ inventory_func = try_import_module_attribute(inventory, raise_for_none=False)
100
+ if inventory_func is None:
101
+ try:
102
+ socket.gethostbyname(inventory)
103
+ except socket.gaierror:
104
+ logger.warning(f"{inventory} is neither a valid Python import or hostname.")
105
+
106
+ if inventory_func is None:
92
107
  # If not an import, load as if from the filesystem *or* comma separated list, which also
93
108
  # loads any all.py group data files (imported functions do not load group data).
94
109
  return make_inventory_from_files(inventory, override_data, cwd, group_data_directories)
pyinfra_cli/log.py CHANGED
@@ -2,7 +2,8 @@ import logging
2
2
 
3
3
  import click
4
4
 
5
- from pyinfra import logger
5
+ from pyinfra import logger, state
6
+ from pyinfra.context import ctx_state
6
7
 
7
8
 
8
9
  class LogHandler(logging.Handler):
@@ -15,6 +16,8 @@ class LogHandler(logging.Handler):
15
16
 
16
17
 
17
18
  class LogFormatter(logging.Formatter):
19
+ previous_was_header = True
20
+
18
21
  level_to_format = {
19
22
  logging.DEBUG: lambda s: click.style(s, "green"),
20
23
  logging.WARNING: lambda s: click.style(s, "yellow"),
@@ -39,21 +42,32 @@ class LogFormatter(logging.Formatter):
39
42
 
40
43
  # We only handle strings here
41
44
  if isinstance(message, str):
42
- if "-->" not in message:
45
+ if ctx_state.isset() and record.levelno is logging.WARNING:
46
+ state.increment_warning_counter()
47
+
48
+ if "-->" in message:
49
+ if not self.previous_was_header:
50
+ click.echo(err=True)
51
+ else:
43
52
  message = " {0}".format(message)
44
53
 
45
54
  if record.levelno in self.level_to_format:
46
55
  message = self.level_to_format[record.levelno](message)
47
56
 
57
+ self.previous_was_header = "-->" in message
48
58
  return message
49
59
 
50
60
  # If not a string, pass to standard Formatter
51
61
  return super().format(record)
52
62
 
53
63
 
54
- def setup_logging(log_level):
64
+ def setup_logging(log_level, other_log_level=None):
65
+ if other_log_level:
66
+ logging.basicConfig(level=other_log_level)
67
+
55
68
  logger.setLevel(log_level)
56
69
  handler = LogHandler()
57
70
  formatter = LogFormatter()
58
71
  handler.setFormatter(formatter)
59
72
  logger.addHandler(handler)
73
+ logger.propagate = False