pyinfra 3.0b0__py2.py3-none-any.whl → 3.0b2__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 (115) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +12 -5
  3. pyinfra/api/arguments_typed.py +19 -6
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +115 -13
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/exceptions.py +19 -0
  8. pyinfra/api/facts.py +34 -33
  9. pyinfra/api/host.py +51 -12
  10. pyinfra/api/inventory.py +4 -0
  11. pyinfra/api/operation.py +88 -42
  12. pyinfra/api/operations.py +10 -11
  13. pyinfra/api/state.py +11 -2
  14. pyinfra/api/util.py +24 -16
  15. pyinfra/connectors/base.py +4 -7
  16. pyinfra/connectors/chroot.py +5 -6
  17. pyinfra/connectors/docker.py +13 -19
  18. pyinfra/connectors/dockerssh.py +5 -4
  19. pyinfra/connectors/local.py +7 -7
  20. pyinfra/connectors/ssh.py +46 -25
  21. pyinfra/connectors/terraform.py +9 -6
  22. pyinfra/connectors/util.py +7 -8
  23. pyinfra/connectors/vagrant.py +11 -10
  24. pyinfra/context.py +1 -0
  25. pyinfra/facts/apk.py +2 -0
  26. pyinfra/facts/apt.py +2 -0
  27. pyinfra/facts/brew.py +2 -0
  28. pyinfra/facts/bsdinit.py +2 -0
  29. pyinfra/facts/cargo.py +2 -0
  30. pyinfra/facts/choco.py +3 -1
  31. pyinfra/facts/deb.py +9 -4
  32. pyinfra/facts/dnf.py +2 -0
  33. pyinfra/facts/docker.py +2 -0
  34. pyinfra/facts/files.py +2 -0
  35. pyinfra/facts/gem.py +2 -0
  36. pyinfra/facts/gpg.py +2 -0
  37. pyinfra/facts/hardware.py +30 -22
  38. pyinfra/facts/launchd.py +2 -0
  39. pyinfra/facts/lxd.py +2 -0
  40. pyinfra/facts/mysql.py +12 -6
  41. pyinfra/facts/npm.py +1 -0
  42. pyinfra/facts/openrc.py +2 -0
  43. pyinfra/facts/pacman.py +6 -2
  44. pyinfra/facts/pip.py +2 -0
  45. pyinfra/facts/pkg.py +2 -0
  46. pyinfra/facts/pkgin.py +2 -0
  47. pyinfra/facts/postgres.py +168 -0
  48. pyinfra/facts/postgresql.py +5 -162
  49. pyinfra/facts/rpm.py +12 -9
  50. pyinfra/facts/server.py +10 -13
  51. pyinfra/facts/snap.py +2 -0
  52. pyinfra/facts/systemd.py +28 -10
  53. pyinfra/facts/upstart.py +2 -0
  54. pyinfra/facts/util/packaging.py +3 -2
  55. pyinfra/facts/vzctl.py +2 -0
  56. pyinfra/facts/xbps.py +2 -0
  57. pyinfra/facts/yum.py +2 -0
  58. pyinfra/facts/zypper.py +2 -0
  59. pyinfra/operations/apk.py +3 -1
  60. pyinfra/operations/apt.py +16 -18
  61. pyinfra/operations/brew.py +10 -8
  62. pyinfra/operations/bsdinit.py +5 -3
  63. pyinfra/operations/cargo.py +3 -1
  64. pyinfra/operations/choco.py +3 -1
  65. pyinfra/operations/dnf.py +15 -19
  66. pyinfra/operations/files.py +86 -69
  67. pyinfra/operations/gem.py +3 -1
  68. pyinfra/operations/git.py +18 -16
  69. pyinfra/operations/iptables.py +33 -25
  70. pyinfra/operations/launchd.py +5 -6
  71. pyinfra/operations/lxd.py +7 -4
  72. pyinfra/operations/mysql.py +57 -53
  73. pyinfra/operations/npm.py +8 -1
  74. pyinfra/operations/openrc.py +5 -3
  75. pyinfra/operations/pacman.py +4 -5
  76. pyinfra/operations/pip.py +16 -9
  77. pyinfra/operations/pkg.py +3 -1
  78. pyinfra/operations/pkgin.py +3 -1
  79. pyinfra/operations/postgres.py +349 -0
  80. pyinfra/operations/postgresql.py +18 -335
  81. pyinfra/operations/puppet.py +3 -1
  82. pyinfra/operations/python.py +7 -3
  83. pyinfra/operations/selinux.py +42 -16
  84. pyinfra/operations/server.py +48 -43
  85. pyinfra/operations/snap.py +3 -1
  86. pyinfra/operations/ssh.py +12 -10
  87. pyinfra/operations/systemd.py +13 -9
  88. pyinfra/operations/sysvinit.py +6 -4
  89. pyinfra/operations/upstart.py +5 -3
  90. pyinfra/operations/util/files.py +24 -16
  91. pyinfra/operations/util/packaging.py +53 -37
  92. pyinfra/operations/util/service.py +18 -13
  93. pyinfra/operations/vzctl.py +12 -10
  94. pyinfra/operations/xbps.py +3 -1
  95. pyinfra/operations/yum.py +14 -18
  96. pyinfra/operations/zypper.py +8 -9
  97. pyinfra/version.py +5 -2
  98. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/METADATA +31 -29
  99. pyinfra-3.0b2.dist-info/RECORD +163 -0
  100. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/WHEEL +1 -1
  101. pyinfra_cli/commands.py +3 -2
  102. pyinfra_cli/inventory.py +38 -19
  103. pyinfra_cli/main.py +2 -0
  104. pyinfra_cli/prints.py +27 -105
  105. pyinfra_cli/util.py +3 -1
  106. tests/test_api/test_api_deploys.py +5 -5
  107. tests/test_api/test_api_operations.py +5 -5
  108. tests/test_connectors/test_ssh.py +105 -0
  109. tests/test_connectors/test_terraform.py +11 -8
  110. tests/test_connectors/test_vagrant.py +6 -6
  111. pyinfra-3.0b0.dist-info/RECORD +0 -162
  112. pyinfra_cli/inventory_dsl.py +0 -23
  113. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/LICENSE.md +0 -0
  114. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/entry_points.txt +0 -0
  115. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ from pyinfra.api import FactBase, MaskString, QuoteString, StringCommand
4
+ from pyinfra.api.util import try_int
5
+
6
+ from .util.databases import parse_columns_and_rows
7
+
8
+
9
+ def make_psql_command(
10
+ database: str | None = None,
11
+ user: str | None = None,
12
+ password: str | None = None,
13
+ host: str | None = None,
14
+ port: str | int | None = None,
15
+ executable="psql",
16
+ ) -> StringCommand:
17
+ target_bits: list[str] = []
18
+
19
+ if password:
20
+ target_bits.append(MaskString('PGPASSWORD="{0}"'.format(password)))
21
+
22
+ target_bits.append(executable)
23
+
24
+ if database:
25
+ target_bits.append("-d {0}".format(database))
26
+
27
+ if user:
28
+ target_bits.append("-U {0}".format(user))
29
+
30
+ if host:
31
+ target_bits.append("-h {0}".format(host))
32
+
33
+ if port:
34
+ target_bits.append("-p {0}".format(port))
35
+
36
+ return StringCommand(*target_bits)
37
+
38
+
39
+ def make_execute_psql_command(command, **psql_kwargs):
40
+ return StringCommand(
41
+ make_psql_command(**psql_kwargs),
42
+ "-Ac",
43
+ QuoteString(command), # quote this whole item as a single shell argument
44
+ )
45
+
46
+
47
+ class PostgresFactBase(FactBase):
48
+ abstract = True
49
+
50
+ psql_command: str
51
+ requires_command = "psql"
52
+
53
+ def command(
54
+ self,
55
+ psql_user=None,
56
+ psql_password=None,
57
+ psql_host=None,
58
+ psql_port=None,
59
+ ):
60
+ return make_execute_psql_command(
61
+ self.psql_command,
62
+ user=psql_user,
63
+ password=psql_password,
64
+ host=psql_host,
65
+ port=psql_port,
66
+ )
67
+
68
+
69
+ class PostgresRoles(PostgresFactBase):
70
+ """
71
+ Returns a dict of PostgreSQL roles and data:
72
+
73
+ .. code:: python
74
+
75
+ {
76
+ "pyinfra": {
77
+ "super": true,
78
+ "createrole": false,
79
+ "createdb": false,
80
+ ...
81
+ },
82
+ }
83
+ """
84
+
85
+ default = dict
86
+ psql_command = "SELECT * FROM pg_catalog.pg_roles"
87
+
88
+ def process(self, output):
89
+ # Remove the last line of the output (row count)
90
+ output = output[:-1]
91
+ rows = parse_columns_and_rows(
92
+ output,
93
+ "|",
94
+ # Remove the "rol" prefix on column names
95
+ remove_column_prefix="rol",
96
+ )
97
+
98
+ users = {}
99
+
100
+ for details in rows:
101
+ for key, value in list(details.items()):
102
+ if key in ("oid", "connlimit"):
103
+ details[key] = try_int(value)
104
+
105
+ if key in (
106
+ "super",
107
+ "inherit",
108
+ "createrole",
109
+ "createdb",
110
+ "canlogin",
111
+ "replication",
112
+ "bypassrls",
113
+ ):
114
+ details[key] = value == "t"
115
+
116
+ users[details.pop("name")] = details
117
+
118
+ return users
119
+
120
+
121
+ class PostgresDatabases(PostgresFactBase):
122
+ """
123
+ Returns a dict of PostgreSQL databases and metadata:
124
+
125
+ .. code:: python
126
+
127
+ {
128
+ "pyinfra_stuff": {
129
+ "encoding": "UTF8",
130
+ "collate": "en_US.UTF-8",
131
+ "ctype": "en_US.UTF-8",
132
+ ...
133
+ },
134
+ }
135
+ """
136
+
137
+ default = dict
138
+ psql_command = "SELECT pg_catalog.pg_encoding_to_char(encoding), * FROM pg_catalog.pg_database"
139
+
140
+ def process(self, output):
141
+ # Remove the last line of the output (row count)
142
+ output = output[:-1]
143
+ rows = parse_columns_and_rows(
144
+ output,
145
+ "|",
146
+ # Remove the "dat" prefix on column names
147
+ remove_column_prefix="dat",
148
+ )
149
+
150
+ databases = {}
151
+
152
+ for details in rows:
153
+ details["encoding"] = details.pop("pg_encoding_to_char")
154
+
155
+ for key, value in list(details.items()):
156
+ if key.endswith("id") or key in (
157
+ "dba",
158
+ "tablespace",
159
+ "connlimit",
160
+ ):
161
+ details[key] = try_int(value)
162
+
163
+ if key in ("istemplate", "allowconn"):
164
+ details[key] = value == "t"
165
+
166
+ databases[details.pop("name")] = details
167
+
168
+ return databases
@@ -1,168 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from pyinfra.api import FactBase, MaskString, QuoteString, StringCommand
4
- from pyinfra.api.util import try_int
3
+ from .postgres import PostgresDatabases, PostgresRoles
5
4
 
6
- from .util.databases import parse_columns_and_rows
7
5
 
6
+ class PostgresqlRoles(PostgresRoles):
7
+ deprecated = True
8
8
 
9
- def make_psql_command(
10
- database=None,
11
- user=None,
12
- password=None,
13
- host=None,
14
- port=None,
15
- executable="psql",
16
- ):
17
- target_bits: list[str] = []
18
9
 
19
- if password:
20
- target_bits.append(MaskString('PGPASSWORD="{0}"'.format(password)))
21
-
22
- target_bits.append(executable)
23
-
24
- if database:
25
- target_bits.append("-d {0}".format(database))
26
-
27
- if user:
28
- target_bits.append("-U {0}".format(user))
29
-
30
- if host:
31
- target_bits.append("-h {0}".format(host))
32
-
33
- if port:
34
- target_bits.append("-p {0}".format(port))
35
-
36
- return StringCommand(*target_bits)
37
-
38
-
39
- def make_execute_psql_command(command, **psql_kwargs):
40
- return StringCommand(
41
- make_psql_command(**psql_kwargs),
42
- "-Ac",
43
- QuoteString(command), # quote this whole item as a single shell argument
44
- )
45
-
46
-
47
- class PostgresqlFactBase(FactBase):
48
- abstract = True
49
-
50
- psql_command: str
51
- requires_command = "psql"
52
-
53
- def command(
54
- self,
55
- psql_user=None,
56
- psql_password=None,
57
- psql_host=None,
58
- psql_port=None,
59
- ):
60
- return make_execute_psql_command(
61
- self.psql_command,
62
- user=psql_user,
63
- password=psql_password,
64
- host=psql_host,
65
- port=psql_port,
66
- )
67
-
68
-
69
- class PostgresqlRoles(PostgresqlFactBase):
70
- """
71
- Returns a dict of PostgreSQL roles and data:
72
-
73
- .. code:: python
74
-
75
- {
76
- "pyinfra": {
77
- "super": true,
78
- "createrole": false,
79
- "createdb": false,
80
- ...
81
- },
82
- }
83
- """
84
-
85
- default = dict
86
- psql_command = "SELECT * FROM pg_catalog.pg_roles"
87
-
88
- def process(self, output):
89
- # Remove the last line of the output (row count)
90
- output = output[:-1]
91
- rows = parse_columns_and_rows(
92
- output,
93
- "|",
94
- # Remove the "rol" prefix on column names
95
- remove_column_prefix="rol",
96
- )
97
-
98
- users = {}
99
-
100
- for details in rows:
101
- for key, value in list(details.items()):
102
- if key in ("oid", "connlimit"):
103
- details[key] = try_int(value)
104
-
105
- if key in (
106
- "super",
107
- "inherit",
108
- "createrole",
109
- "createdb",
110
- "canlogin",
111
- "replication",
112
- "bypassrls",
113
- ):
114
- details[key] = value == "t"
115
-
116
- users[details.pop("name")] = details
117
-
118
- return users
119
-
120
-
121
- class PostgresqlDatabases(PostgresqlFactBase):
122
- """
123
- Returns a dict of PostgreSQL databases and metadata:
124
-
125
- .. code:: python
126
-
127
- {
128
- "pyinfra_stuff": {
129
- "encoding": "UTF8",
130
- "collate": "en_US.UTF-8",
131
- "ctype": "en_US.UTF-8",
132
- ...
133
- },
134
- }
135
- """
136
-
137
- default = dict
138
- psql_command = "SELECT pg_catalog.pg_encoding_to_char(encoding), * FROM pg_catalog.pg_database"
139
-
140
- def process(self, output):
141
- # Remove the last line of the output (row count)
142
- output = output[:-1]
143
- rows = parse_columns_and_rows(
144
- output,
145
- "|",
146
- # Remove the "dat" prefix on column names
147
- remove_column_prefix="dat",
148
- )
149
-
150
- databases = {}
151
-
152
- for details in rows:
153
- details["encoding"] = details.pop("pg_encoding_to_char")
154
-
155
- for key, value in list(details.items()):
156
- if key.endswith("id") or key in (
157
- "dba",
158
- "tablespace",
159
- "connlimit",
160
- ):
161
- details[key] = try_int(value)
162
-
163
- if key in ("istemplate", "allowconn"):
164
- details[key] = value == "t"
165
-
166
- databases[details.pop("name")] = details
167
-
168
- return databases
10
+ class PostgresqlDatabases(PostgresDatabases):
11
+ deprecated = True
pyinfra/facts/rpm.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
 
@@ -19,7 +22,7 @@ class RpmPackages(FactBase):
19
22
  }
20
23
  """
21
24
 
22
- command = 'rpm --queryformat "{0}" -qa'.format(rpm_query_format)
25
+ command = "rpm --queryformat {0} -qa".format(shlex.quote(rpm_query_format))
23
26
  requires_command = "rpm"
24
27
 
25
28
  default = dict
@@ -42,12 +45,12 @@ class RpmPackage(FactBase):
42
45
 
43
46
  requires_command = "rpm"
44
47
 
45
- def command(self, name):
48
+ def command(self, package):
46
49
  return (
47
- 'rpm --queryformat "{0}" -q {1} || '
50
+ "rpm --queryformat {0} -q {1} || "
48
51
  "! test -e {1} || "
49
- 'rpm --queryformat "{0}" -qp {1} 2> /dev/null'
50
- ).format(rpm_query_format, name)
52
+ "rpm --queryformat {0} -qp {1} 2> /dev/null"
53
+ ).format(shlex.quote(rpm_query_format), shlex.quote(package))
51
54
 
52
55
  def process(self, output):
53
56
  for line in output:
@@ -69,11 +72,11 @@ class RpmPackageProvides(FactBase):
69
72
  requires_command = "repoquery"
70
73
 
71
74
  @staticmethod
72
- def command(name):
75
+ def command(package):
73
76
  # Accept failure here (|| true) for invalid/unknown packages
74
- return 'repoquery --queryformat "{0}" --whatprovides {1} || true'.format(
75
- rpm_query_format,
76
- name,
77
+ return "repoquery --queryformat {0} --whatprovides {1} || true".format(
78
+ shlex.quote(rpm_query_format),
79
+ shlex.quote(package),
77
80
  )
78
81
 
79
82
  @staticmethod
pyinfra/facts/server.py CHANGED
@@ -5,7 +5,7 @@ import re
5
5
  import shutil
6
6
  from datetime import datetime
7
7
  from tempfile import mkdtemp
8
- from typing import Dict, List, NewType, Optional, Union
8
+ from typing import Dict, List, Optional, Union
9
9
 
10
10
  from dateutil.parser import parse as parse_date
11
11
  from distro import distro
@@ -25,12 +25,14 @@ class User(FactBase):
25
25
  command = "echo $USER"
26
26
 
27
27
 
28
- class Home(FactBase):
28
+ class Home(FactBase[Optional[str]]):
29
29
  """
30
- Returns the home directory of the current user.
30
+ Returns the home directory of the given user, or the current user if no user is given.
31
31
  """
32
32
 
33
- command = "echo $HOME"
33
+ @staticmethod
34
+ def command(user=""):
35
+ return f"echo ~{user}"
34
36
 
35
37
 
36
38
  class Path(FactBase):
@@ -348,8 +350,7 @@ class Groups(FactBase[List[str]]):
348
350
  command = "cat /etc/group"
349
351
  default = list
350
352
 
351
- @staticmethod
352
- def process(output) -> list[str]:
353
+ def process(self, output) -> list[str]:
353
354
  groups: list[str] = []
354
355
 
355
356
  for line in output:
@@ -359,9 +360,6 @@ class Groups(FactBase[List[str]]):
359
360
  return groups
360
361
 
361
362
 
362
- CrontabCommand = NewType("CrontabCommand", int)
363
-
364
-
365
363
  class CrontabDict(TypedDict):
366
364
  minute: NotRequired[Union[int, str]]
367
365
  hour: NotRequired[Union[int, str]]
@@ -372,7 +370,7 @@ class CrontabDict(TypedDict):
372
370
  special_time: NotRequired[str]
373
371
 
374
372
 
375
- class Crontab(FactBase[Dict[CrontabCommand, CrontabDict]]):
373
+ class Crontab(FactBase[Dict[str, CrontabDict]]):
376
374
  """
377
375
  Returns a dictionary of cron command -> execution time.
378
376
 
@@ -402,9 +400,8 @@ class Crontab(FactBase[Dict[CrontabCommand, CrontabDict]]):
402
400
  return "crontab -l -u {0} || true".format(user)
403
401
  return "crontab -l || true"
404
402
 
405
- @staticmethod
406
- def process(output):
407
- crons: dict[Command, CrontabDict] = {}
403
+ def process(self, output):
404
+ crons: dict[str, CrontabDict] = {}
408
405
  current_comments = []
409
406
 
410
407
  for line in output:
pyinfra/facts/snap.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
pyinfra/facts/systemd.py CHANGED
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Dict
4
+ from typing import Dict, Iterable
3
5
 
4
- from pyinfra.api import FactBase
6
+ from pyinfra.api import FactBase, FactTypeError, QuoteString, StringCommand
5
7
 
6
8
  # Valid unit names consist of a "name prefix" and a dot and a suffix specifying the unit type.
7
9
  # The "unit prefix" must consist of one or more valid characters
@@ -22,18 +24,16 @@ SYSTEMD_UNIT_NAME_REGEX = (
22
24
 
23
25
  def _make_systemctl_cmd(user_mode=False, machine=None, user_name=None):
24
26
  # base command for normal and user mode
25
- systemctl_cmd = "systemctl --user" if user_mode else "systemctl"
27
+ systemctl_cmd = ["systemctl --user"] if user_mode else ["systemctl"]
26
28
 
27
29
  # add user and machine flag if given in args
28
30
  if machine is not None:
29
31
  if user_name is not None:
30
- machine_opt = "--machine={1}@{0}".format(machine, user_name)
32
+ systemctl_cmd.append("--machine={1}@{0}".format(machine, user_name))
31
33
  else:
32
- machine_opt = "--machine={0}".format(machine)
33
-
34
- systemctl_cmd = "{0} {1}".format(systemctl_cmd, machine_opt)
34
+ systemctl_cmd.append("--machine={0}".format(machine))
35
35
 
36
- return systemctl_cmd
36
+ return StringCommand(*systemctl_cmd)
37
37
 
38
38
 
39
39
  class SystemdStatus(FactBase[Dict[str, bool]]):
@@ -59,14 +59,32 @@ class SystemdStatus(FactBase[Dict[str, bool]]):
59
59
  state_key = "SubState"
60
60
  state_values = ["running", "waiting", "exited"]
61
61
 
62
- def command(self, user_mode=False, machine=None, user_name=None):
62
+ def command(self, user_mode=False, machine=None, user_name=None, services=None):
63
63
  fact_cmd = _make_systemctl_cmd(
64
64
  user_mode=user_mode,
65
65
  machine=machine,
66
66
  user_name=user_name,
67
67
  )
68
68
 
69
- return f"{fact_cmd} show --all --property Id --property {self.state_key} '*'"
69
+ if services is None:
70
+ service_strs = [QuoteString("*")]
71
+ elif isinstance(services, str):
72
+ service_strs = [QuoteString(services)]
73
+ elif isinstance(services, Iterable):
74
+ service_strs = [QuoteString(s) for s in services]
75
+ else:
76
+ raise FactTypeError(f"Invalid type passed for services argument: {type(services)}")
77
+
78
+ return StringCommand(
79
+ fact_cmd,
80
+ "show",
81
+ "--all",
82
+ "--property",
83
+ "Id",
84
+ "--property",
85
+ self.state_key,
86
+ *service_strs,
87
+ )
70
88
 
71
89
  def process(self, output) -> Dict[str, bool]:
72
90
  services: Dict[str, bool] = {}
pyinfra/facts/upstart.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
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
+ from typing import Iterable
4
5
 
5
6
 
6
- def parse_packages(regex, output):
7
+ def parse_packages(regex: str, output: Iterable[str]) -> dict[str, set[str]]:
7
8
  packages: dict[str, set[str]] = {}
8
9
 
9
10
  for line in output:
@@ -34,7 +35,7 @@ def _parse_yum_or_zypper_repositories(output):
34
35
  current_repo["name"] = line[1:-1]
35
36
 
36
37
  if current_repo and "=" in line:
37
- key, value = line.split("=", 1)
38
+ key, value = re.split(r"\s*=\s*", line, maxsplit=1)
38
39
  current_repo[key] = value
39
40
 
40
41
  if current_repo:
pyinfra/facts/vzctl.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
pyinfra/facts/xbps.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
pyinfra/facts/yum.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
pyinfra/facts/zypper.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
pyinfra/operations/apk.py CHANGED
@@ -2,6 +2,8 @@
2
2
  Manage apk packages.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  from pyinfra import host
6
8
  from pyinfra.api import operation
7
9
  from pyinfra.facts.apk import ApkPackages
@@ -40,7 +42,7 @@ _update = update._inner # noqa: E305
40
42
 
41
43
  @operation()
42
44
  def packages(
43
- packages=None,
45
+ packages: str | list[str] | None = None,
44
46
  present=True,
45
47
  latest=False,
46
48
  update=False,