pyinfra 3.0b1__py2.py3-none-any.whl → 3.0b3__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 (108) hide show
  1. pyinfra/api/arguments.py +9 -3
  2. pyinfra/api/arguments_typed.py +8 -5
  3. pyinfra/api/command.py +5 -3
  4. pyinfra/api/config.py +115 -13
  5. pyinfra/api/connectors.py +5 -2
  6. pyinfra/api/facts.py +33 -32
  7. pyinfra/api/host.py +5 -5
  8. pyinfra/api/inventory.py +4 -0
  9. pyinfra/api/operation.py +22 -14
  10. pyinfra/api/util.py +24 -16
  11. pyinfra/connectors/base.py +3 -6
  12. pyinfra/connectors/docker.py +2 -9
  13. pyinfra/connectors/local.py +2 -2
  14. pyinfra/connectors/ssh.py +2 -2
  15. pyinfra/connectors/util.py +6 -7
  16. pyinfra/connectors/vagrant.py +5 -5
  17. pyinfra/context.py +1 -0
  18. pyinfra/facts/apk.py +2 -0
  19. pyinfra/facts/apt.py +2 -0
  20. pyinfra/facts/brew.py +2 -0
  21. pyinfra/facts/bsdinit.py +2 -0
  22. pyinfra/facts/cargo.py +2 -0
  23. pyinfra/facts/choco.py +2 -0
  24. pyinfra/facts/deb.py +7 -2
  25. pyinfra/facts/dnf.py +2 -0
  26. pyinfra/facts/docker.py +18 -0
  27. pyinfra/facts/files.py +2 -0
  28. pyinfra/facts/gem.py +2 -0
  29. pyinfra/facts/gpg.py +2 -0
  30. pyinfra/facts/hardware.py +30 -22
  31. pyinfra/facts/launchd.py +2 -0
  32. pyinfra/facts/lxd.py +2 -0
  33. pyinfra/facts/mysql.py +12 -6
  34. pyinfra/facts/npm.py +1 -0
  35. pyinfra/facts/openrc.py +2 -0
  36. pyinfra/facts/pacman.py +6 -2
  37. pyinfra/facts/pip.py +2 -0
  38. pyinfra/facts/pkg.py +2 -0
  39. pyinfra/facts/pkgin.py +2 -0
  40. pyinfra/facts/postgres.py +6 -6
  41. pyinfra/facts/postgresql.py +2 -0
  42. pyinfra/facts/rpm.py +12 -9
  43. pyinfra/facts/runit.py +68 -0
  44. pyinfra/facts/server.py +10 -13
  45. pyinfra/facts/snap.py +2 -0
  46. pyinfra/facts/systemd.py +2 -0
  47. pyinfra/facts/upstart.py +2 -0
  48. pyinfra/facts/util/packaging.py +3 -2
  49. pyinfra/facts/vzctl.py +2 -0
  50. pyinfra/facts/xbps.py +2 -0
  51. pyinfra/facts/yum.py +2 -0
  52. pyinfra/facts/zypper.py +2 -0
  53. pyinfra/operations/apk.py +3 -1
  54. pyinfra/operations/apt.py +16 -18
  55. pyinfra/operations/brew.py +10 -8
  56. pyinfra/operations/bsdinit.py +5 -3
  57. pyinfra/operations/cargo.py +3 -1
  58. pyinfra/operations/choco.py +3 -1
  59. pyinfra/operations/dnf.py +15 -19
  60. pyinfra/operations/docker.py +339 -0
  61. pyinfra/operations/files.py +81 -66
  62. pyinfra/operations/gem.py +3 -1
  63. pyinfra/operations/git.py +18 -16
  64. pyinfra/operations/iptables.py +27 -25
  65. pyinfra/operations/launchd.py +5 -6
  66. pyinfra/operations/lxd.py +7 -4
  67. pyinfra/operations/mysql.py +57 -53
  68. pyinfra/operations/npm.py +8 -1
  69. pyinfra/operations/openrc.py +5 -3
  70. pyinfra/operations/pacman.py +4 -5
  71. pyinfra/operations/pip.py +11 -9
  72. pyinfra/operations/pkg.py +3 -1
  73. pyinfra/operations/pkgin.py +3 -1
  74. pyinfra/operations/postgres.py +39 -37
  75. pyinfra/operations/postgresql.py +2 -0
  76. pyinfra/operations/puppet.py +3 -1
  77. pyinfra/operations/python.py +7 -3
  78. pyinfra/operations/runit.py +182 -0
  79. pyinfra/operations/selinux.py +42 -16
  80. pyinfra/operations/server.py +52 -43
  81. pyinfra/operations/snap.py +3 -1
  82. pyinfra/operations/ssh.py +12 -10
  83. pyinfra/operations/systemd.py +12 -8
  84. pyinfra/operations/sysvinit.py +6 -4
  85. pyinfra/operations/upstart.py +5 -3
  86. pyinfra/operations/util/docker.py +177 -0
  87. pyinfra/operations/util/files.py +24 -16
  88. pyinfra/operations/util/packaging.py +53 -37
  89. pyinfra/operations/util/service.py +25 -18
  90. pyinfra/operations/vzctl.py +12 -10
  91. pyinfra/operations/xbps.py +3 -1
  92. pyinfra/operations/yum.py +14 -18
  93. pyinfra/operations/zypper.py +8 -9
  94. pyinfra/version.py +5 -2
  95. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/METADATA +30 -28
  96. pyinfra-3.0b3.dist-info/RECORD +167 -0
  97. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/WHEEL +1 -1
  98. pyinfra_cli/exceptions.py +0 -5
  99. pyinfra_cli/inventory.py +38 -19
  100. pyinfra_cli/prints.py +15 -11
  101. pyinfra_cli/util.py +3 -1
  102. tests/test_api/test_api_operations.py +1 -1
  103. tests/test_connectors/test_ssh.py +66 -13
  104. tests/test_connectors/test_vagrant.py +3 -3
  105. pyinfra-3.0b1.dist-info/RECORD +0 -163
  106. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/LICENSE.md +0 -0
  107. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/entry_points.txt +0 -0
  108. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,339 @@
1
+ """
2
+ Manager Docker Containers, Volumes and Networks
3
+ """
4
+
5
+ from pyinfra import host
6
+ from pyinfra.api import operation
7
+ from pyinfra.facts.docker import DockerContainers, DockerNetworks, DockerVolumes
8
+
9
+ from .util.docker import handle_docker
10
+
11
+
12
+ @operation()
13
+ def container(
14
+ container,
15
+ image="",
16
+ ports=None,
17
+ networks=None,
18
+ volumes=None,
19
+ env_vars=None,
20
+ pull_always=False,
21
+ present=True,
22
+ force=False,
23
+ start=True,
24
+ ):
25
+ """
26
+ Manage Docker containers
27
+
28
+ + container: name to identify the container
29
+ + image: container image and tag ex: nginx:alpine
30
+ + networks: network list to attach on container
31
+ + ports: port list to expose
32
+ + volumes: volume list to map on container
33
+ + env_vars: environment varible list to inject on container
34
+ + pull_always: force image pull
35
+ + force: remove a contaner with same name and create a new one
36
+ + present: whether the container should be up and running
37
+ + start: start or stop the container
38
+
39
+ **Examples:**
40
+
41
+ .. code:: python
42
+
43
+ # Run a container
44
+ docker.container(
45
+ name="Deploy Nginx container",
46
+ container="nginx",
47
+ image="nginx:alpine",
48
+ ports=["80:80"],
49
+ present=True,
50
+ force=True,
51
+ networks=["proxy", "services"],
52
+ volumes=["nginx_data:/usr/share/nginx/html"],
53
+ pull_always=True,
54
+ )
55
+
56
+ # Stop a container
57
+ docker.container(
58
+ name="Stop Nginx container",
59
+ container="nginx",
60
+ start=False,
61
+ )
62
+
63
+ # Start a container
64
+ docker.container(
65
+ name="Start Nginx container",
66
+ container="nginx",
67
+ start=True,
68
+ )
69
+ """
70
+
71
+ existent_container = [c for c in host.get_fact(DockerContainers) if container in c["Name"]]
72
+
73
+ if force:
74
+ if existent_container:
75
+ yield handle_docker(
76
+ resource="container",
77
+ command="remove",
78
+ container=container,
79
+ )
80
+
81
+ if present:
82
+ if not existent_container or force:
83
+ yield handle_docker(
84
+ resource="container",
85
+ command="create",
86
+ container=container,
87
+ image=image,
88
+ ports=ports,
89
+ networks=networks,
90
+ volumes=volumes,
91
+ env_vars=env_vars,
92
+ pull_always=pull_always,
93
+ present=present,
94
+ force=force,
95
+ start=start,
96
+ )
97
+
98
+ if existent_container and start:
99
+ if existent_container[0]["State"]["Status"] != "running":
100
+ yield handle_docker(
101
+ resource="container",
102
+ command="start",
103
+ container=container,
104
+ )
105
+
106
+ if existent_container and not start:
107
+ if existent_container[0]["State"]["Status"] == "running":
108
+ yield handle_docker(
109
+ resource="container",
110
+ command="stop",
111
+ container=container,
112
+ )
113
+
114
+ if existent_container and not present:
115
+ yield handle_docker(
116
+ resource="container",
117
+ command="remove",
118
+ container=container,
119
+ )
120
+
121
+
122
+ @operation(is_idempotent=False)
123
+ def image(image, present=True):
124
+ """
125
+ Manage Docker images
126
+
127
+ + image: Image and tag ex: nginx:alpine
128
+ + present: whether the Docker image should be exist
129
+
130
+ **Examples:**
131
+
132
+ .. code:: python
133
+
134
+ # Pull a Docker image
135
+ docker.image(
136
+ name="Pull nginx image",
137
+ image="nginx:alpine",
138
+ present=True,
139
+ )
140
+
141
+ # Remove a Docker image
142
+ docker.image(
143
+ name="Remove nginx image",
144
+ image:"nginx:image",
145
+ present=False,
146
+ )
147
+ """
148
+
149
+ if present:
150
+ yield handle_docker(
151
+ resource="image",
152
+ command="pull",
153
+ image=image,
154
+ )
155
+
156
+ else:
157
+ yield handle_docker(
158
+ resource="image",
159
+ command="remove",
160
+ image=image,
161
+ )
162
+
163
+
164
+ @operation()
165
+ def volume(volume, driver="", labels=None, present=True):
166
+ """
167
+ Manage Docker volumes
168
+
169
+ + volume: Volume name
170
+ + driver: Docker volume storage driver
171
+ + labels: Label list to attach in the volume
172
+ + present: whether the Docker volume should exist
173
+
174
+ **Examples:**
175
+
176
+ .. code:: python
177
+
178
+ # Create a Docker volume
179
+ docker.volume(
180
+ name="Create nginx volume",
181
+ volume="nginx_data",
182
+ present=True
183
+ )
184
+ """
185
+
186
+ existent_volume = [v for v in host.get_fact(DockerVolumes) if v["Name"] == volume]
187
+
188
+ if present:
189
+
190
+ if existent_volume:
191
+ host.noop("Volume alredy exist!")
192
+ return
193
+
194
+ yield handle_docker(
195
+ resource="volume",
196
+ command="create",
197
+ volume=volume,
198
+ driver=driver,
199
+ labels=labels,
200
+ present=present,
201
+ )
202
+
203
+ else:
204
+ if existent_volume is None:
205
+ host.noop("There is no {0} volume!".format(volume))
206
+ return
207
+
208
+ yield handle_docker(
209
+ resource="volume",
210
+ command="remove",
211
+ volume=volume,
212
+ )
213
+
214
+
215
+ @operation()
216
+ def network(
217
+ network,
218
+ driver="",
219
+ gateway="",
220
+ ip_range="",
221
+ ipam_driver="",
222
+ subnet="",
223
+ scope="",
224
+ opts=None,
225
+ ipam_opts=None,
226
+ labels=None,
227
+ ingress=False,
228
+ attachable=False,
229
+ present=True,
230
+ ):
231
+ """
232
+ Manage docker networks
233
+
234
+ + network_name: Image name
235
+ + driver: Container image and tag ex: nginx:alpine
236
+ + gateway: IPv4 or IPv6 Gateway for the master subnet
237
+ + ip_range: Allocate container ip from a sub-range
238
+ + ipam_driver: IP Address Management Driver
239
+ + subnet: Subnet in CIDR format that represents a network segment
240
+ + scope: Control the network's scope
241
+ + opts: Set driver specific options
242
+ + ipam_opts: Set IPAM driver specific options
243
+ + labels: Label list to attach in the network
244
+ + ingress: Create swarm routing-mesh network
245
+ + attachable: Enable manual container attachment
246
+ + present: whether the Docker network should exist
247
+
248
+ **Examples:**
249
+
250
+ .. code:: python
251
+
252
+ # Create Docker network
253
+ docker.network(
254
+ name="Create nginx network",
255
+ network_name="nginx",
256
+ attachable=True,
257
+ present=True,
258
+ )
259
+ """
260
+ existent_network = [n for n in host.get_fact(DockerNetworks) if n["Name"] == network]
261
+
262
+ if present:
263
+ if existent_network:
264
+ host.noop("Alredy exist a network with {0} name!".format(network))
265
+ return
266
+
267
+ yield handle_docker(
268
+ resource="network",
269
+ command="create",
270
+ network=network,
271
+ driver=driver,
272
+ gateway=gateway,
273
+ ip_range=ip_range,
274
+ ipam_driver=ipam_driver,
275
+ subnet=subnet,
276
+ scope=scope,
277
+ opts=opts,
278
+ ipam_opts=ipam_opts,
279
+ labels=labels,
280
+ ingress=ingress,
281
+ attachable=attachable,
282
+ present=present,
283
+ )
284
+
285
+ else:
286
+ if existent_network is None:
287
+ host.noop("Ther is not network with {0} name!".format(network))
288
+ return
289
+
290
+ yield handle_docker(
291
+ resource="network",
292
+ command="create",
293
+ network=network,
294
+ )
295
+
296
+
297
+ @operation(is_idempotent=False)
298
+ def prune(
299
+ all=False,
300
+ volume=False,
301
+ filter="",
302
+ ):
303
+ """
304
+ Execute a docker system prune.
305
+
306
+ + all: Remove all unused images not just dangling ones
307
+ + volumes: Prune anonymous volumes
308
+ + filter: Provide filter values (e.g. "label=<key>=<value>" or "until=24h")
309
+
310
+ **Examples:**
311
+
312
+ .. code:: python
313
+
314
+ # Remove dangling images
315
+ docker.prune(
316
+ name="remove dangling images",
317
+ )
318
+
319
+ # Remove all images and volumes
320
+ docker.prune(
321
+ name="Remove all images and volumes",
322
+ all=True,
323
+ volumes=True,
324
+ )
325
+
326
+ # Remove images older than 90 days
327
+ docker.prune(
328
+ name="Remove unused older than 90 days",
329
+ filter="until=2160h"
330
+ )
331
+ """
332
+
333
+ yield handle_docker(
334
+ resource="system",
335
+ command="prune",
336
+ all=all,
337
+ volume=volume,
338
+ filter=filter,
339
+ )
@@ -12,7 +12,7 @@ from datetime import timedelta
12
12
  from fnmatch import fnmatch
13
13
  from io import StringIO
14
14
  from pathlib import Path
15
- from typing import Union
15
+ from typing import IO, Any, Union
16
16
 
17
17
  from jinja2 import TemplateRuntimeError, TemplateSyntaxError, UndefinedError
18
18
 
@@ -60,19 +60,19 @@ from .util.files import adjust_regex, ensure_mode_int, get_timestamp, sed_replac
60
60
 
61
61
  @operation()
62
62
  def download(
63
- src,
64
- dest,
65
- user=None,
66
- group=None,
67
- mode=None,
68
- cache_time=None,
63
+ src: str,
64
+ dest: str,
65
+ user: str | None = None,
66
+ group: str | None = None,
67
+ mode: str | None = None,
68
+ cache_time: int | None = None,
69
69
  force=False,
70
- sha256sum=None,
71
- sha1sum=None,
72
- md5sum=None,
73
- headers=None,
70
+ sha256sum: str | None = None,
71
+ sha1sum: str | None = None,
72
+ md5sum: str | None = None,
73
+ headers: dict[str, str] | None = None,
74
74
  insecure=False,
75
- proxy=None,
75
+ proxy: str | None = None,
76
76
  ):
77
77
  """
78
78
  Download files from remote locations using ``curl`` or ``wget``.
@@ -123,8 +123,8 @@ def download(
123
123
  if cache_time:
124
124
  # Time on files is not tz-aware, and will be the same tz as the server's time,
125
125
  # so we can safely remove the tzinfo from the Date fact before comparison.
126
- cache_time = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
127
- if info["mtime"] and info["mtime"] < cache_time:
126
+ ctime = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
127
+ if info["mtime"] and info["mtime"] < ctime:
128
128
  download = True
129
129
 
130
130
  if sha1sum:
@@ -227,11 +227,11 @@ def download(
227
227
 
228
228
  @operation()
229
229
  def line(
230
- path,
231
- line,
230
+ path: str,
231
+ line: str,
232
232
  present=True,
233
- replace=None,
234
- flags=None,
233
+ replace: str | None = None,
234
+ flags: list[str] | None = None,
235
235
  backup=False,
236
236
  interpolate_variables=False,
237
237
  escape_regex_characters=False,
@@ -269,7 +269,7 @@ def line(
269
269
  it will be append to the end of the file.
270
270
 
271
271
  Ensure new line:
272
- This will ensure that the ``line`` being appended is always on a seperate new
272
+ This will ensure that the ``line`` being appended is always on a separate new
273
273
  line in case the file doesn't end with a newline character.
274
274
 
275
275
 
@@ -427,10 +427,10 @@ def line(
427
427
 
428
428
  @operation()
429
429
  def replace(
430
- path,
431
- text=None,
432
- replace=None,
433
- flags=None,
430
+ path: str,
431
+ text: str | None = None,
432
+ replace: str | None = None,
433
+ flags: list[str] | None = None,
434
434
  backup=False,
435
435
  interpolate_variables=False,
436
436
  match=None, # deprecated
@@ -501,15 +501,15 @@ def replace(
501
501
 
502
502
  @operation()
503
503
  def sync(
504
- src,
505
- dest,
506
- user=None,
507
- group=None,
508
- mode=None,
509
- dir_mode=None,
504
+ src: str,
505
+ dest: str,
506
+ user: str | None = None,
507
+ group: str | None = None,
508
+ mode: str | None = None,
509
+ dir_mode: str | None = None,
510
510
  delete=False,
511
- exclude=None,
512
- exclude_dir=None,
511
+ exclude: str | list[str] | tuple[str] | None = None,
512
+ exclude_dir: str | list[str] | tuple[str] | None = None,
513
513
  add_deploy_dir=True,
514
514
  ):
515
515
  """
@@ -652,7 +652,7 @@ def show_rsync_warning():
652
652
 
653
653
 
654
654
  @operation(is_idempotent=False)
655
- def rsync(src, dest, flags=["-ax", "--delete"]):
655
+ def rsync(src: str, dest: str, flags: list[str] | None = None):
656
656
  """
657
657
  Use ``rsync`` to sync a local directory to the remote system. This operation will actually call
658
658
  the ``rsync`` binary on your system.
@@ -667,6 +667,8 @@ def rsync(src, dest, flags=["-ax", "--delete"]):
667
667
  global arguments.
668
668
  """
669
669
 
670
+ if flags is None:
671
+ flags = ["-ax", "--delete"]
670
672
  show_rsync_warning()
671
673
 
672
674
  try:
@@ -696,8 +698,8 @@ def _create_remote_dir(remote_filename, user, group):
696
698
  is_idempotent=False,
697
699
  )
698
700
  def get(
699
- src,
700
- dest,
701
+ src: str,
702
+ dest: str,
701
703
  add_deploy_dir=True,
702
704
  create_local_dir=False,
703
705
  force=False,
@@ -756,11 +758,11 @@ def get(
756
758
 
757
759
  @operation()
758
760
  def put(
759
- src,
760
- dest,
761
- user=None,
762
- group=None,
763
- mode=None,
761
+ src: str | IO[Any],
762
+ dest: str,
763
+ user: str | None = None,
764
+ group: str | None = None,
765
+ mode: int | str | bool | None = None,
764
766
  add_deploy_dir=True,
765
767
  create_remote_dir=True,
766
768
  force=False,
@@ -821,6 +823,7 @@ def put(
821
823
 
822
824
  # Assume string filename
823
825
  else:
826
+ assert isinstance(src, (str, Path))
824
827
  # Add deploy directory?
825
828
  if add_deploy_dir and state.cwd:
826
829
  src = os.path.join(state.cwd, src)
@@ -835,7 +838,7 @@ def put(
835
838
  raise IOError("No such file: {0}".format(local_file))
836
839
 
837
840
  if mode is True:
838
- if os.path.isfile(local_file):
841
+ if isinstance(local_file, str) and os.path.isfile(local_file):
839
842
  mode = get_path_permissions_mode(local_file)
840
843
  else:
841
844
  logger.warning(
@@ -849,6 +852,7 @@ def put(
849
852
  remote_file = host.get_fact(File, path=dest)
850
853
 
851
854
  if not remote_file and bool(host.get_fact(Directory, path=dest)):
855
+ assert isinstance(src, str)
852
856
  dest = unix_path_join(dest, os.path.basename(src))
853
857
  remote_file = host.get_fact(File, path=dest)
854
858
 
@@ -905,7 +909,15 @@ def put(
905
909
 
906
910
 
907
911
  @operation()
908
- def template(src, dest, user=None, group=None, mode=None, create_remote_dir=True, **data):
912
+ def template(
913
+ src: str | IO[Any],
914
+ dest: str,
915
+ user: str | None = None,
916
+ group: str | None = None,
917
+ mode: str | None = None,
918
+ create_remote_dir=True,
919
+ **data,
920
+ ):
909
921
  '''
910
922
  Generate a template using jinja2 and write it to the remote system.
911
923
 
@@ -1055,16 +1067,16 @@ def _raise_or_remove_invalid_path(fs_type, path, force, force_backup, force_back
1055
1067
 
1056
1068
  @operation()
1057
1069
  def link(
1058
- path,
1059
- target=None,
1070
+ path: str,
1071
+ target: str | None = None,
1060
1072
  present=True,
1061
- user=None,
1062
- group=None,
1073
+ user: str | None = None,
1074
+ group: str | None = None,
1063
1075
  symbolic=True,
1064
1076
  create_remote_dir=True,
1065
1077
  force=False,
1066
1078
  force_backup=True,
1067
- force_backup_dir=None,
1079
+ force_backup_dir: str | None = None,
1068
1080
  ):
1069
1081
  """
1070
1082
  Add/remove/update links.
@@ -1162,16 +1174,16 @@ def link(
1162
1174
 
1163
1175
  @operation()
1164
1176
  def file(
1165
- path,
1177
+ path: str,
1166
1178
  present=True,
1167
- user=None,
1168
- group=None,
1169
- mode=None,
1179
+ user: str | None = None,
1180
+ group: str | None = None,
1181
+ mode: int | str | None = None,
1170
1182
  touch=False,
1171
1183
  create_remote_dir=True,
1172
1184
  force=False,
1173
1185
  force_backup=True,
1174
- force_backup_dir=None,
1186
+ force_backup_dir: str | None = None,
1175
1187
  ):
1176
1188
  """
1177
1189
  Add/remove/update files.
@@ -1264,15 +1276,15 @@ def file(
1264
1276
 
1265
1277
  @operation()
1266
1278
  def directory(
1267
- path,
1279
+ path: str,
1268
1280
  present=True,
1269
- user=None,
1270
- group=None,
1271
- mode=None,
1281
+ user: str | None = None,
1282
+ group: str | None = None,
1283
+ mode: int | str | None = None,
1272
1284
  recursive=False,
1273
1285
  force=False,
1274
1286
  force_backup=True,
1275
- force_backup_dir=None,
1287
+ force_backup_dir: str | None = None,
1276
1288
  _no_check_owner_mode=False,
1277
1289
  _no_fail_on_link=False,
1278
1290
  ):
@@ -1365,7 +1377,7 @@ def directory(
1365
1377
 
1366
1378
 
1367
1379
  @operation()
1368
- def flags(path, flags=None, present=True):
1380
+ def flags(path: str, flags: list[str] | None = None, present=True):
1369
1381
  """
1370
1382
  Set/clear file flags.
1371
1383
 
@@ -1414,18 +1426,18 @@ def flags(path, flags=None, present=True):
1414
1426
 
1415
1427
  @operation()
1416
1428
  def block(
1417
- path,
1418
- content=None,
1429
+ path: str,
1430
+ content: str | list[str] | None = None,
1419
1431
  present=True,
1420
- line=None,
1432
+ line: str | None = None,
1421
1433
  backup=False,
1422
1434
  escape_regex_characters=False,
1423
1435
  try_prevent_shell_expansion=False,
1424
1436
  before=False,
1425
1437
  after=False,
1426
- marker=None,
1427
- begin=None,
1428
- end=None,
1438
+ marker: str | None = None,
1439
+ begin: str | None = None,
1440
+ end: str | None = None,
1429
1441
  ):
1430
1442
  """
1431
1443
  Ensure content, surrounded by the appropriate markers, is present (or not) in the file.
@@ -1573,6 +1585,7 @@ def block(
1573
1585
  f"\n{the_block}\n{here}",
1574
1586
  )
1575
1587
  elif current == []: # markers not found and have a pattern to match (not start or end)
1588
+ assert isinstance(line, str)
1576
1589
  regex = adjust_regex(line, escape_regex_characters)
1577
1590
  print_before = "{ print }" if before else ""
1578
1591
  print_after = "{ print }" if after else ""
@@ -1601,9 +1614,11 @@ def block(
1601
1614
  out_prep,
1602
1615
  prog,
1603
1616
  q_path,
1604
- '"' + "\n".join(content) + '"'
1605
- if not try_prevent_shell_expansion
1606
- else "'" + "\n".join(content) + "'",
1617
+ (
1618
+ '"' + "\n".join(content) + '"'
1619
+ if not try_prevent_shell_expansion
1620
+ else "'" + "\n".join(content) + "'"
1621
+ ),
1607
1622
  "> $OUT &&",
1608
1623
  real_out,
1609
1624
  )
pyinfra/operations/gem.py CHANGED
@@ -2,6 +2,8 @@
2
2
  Manage Ruby gem packages. (see https://rubygems.org/ )
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.gem import GemPackages
@@ -10,7 +12,7 @@ from .util.packaging import ensure_packages
10
12
 
11
13
 
12
14
  @operation()
13
- def packages(packages=None, present=True, latest=False):
15
+ def packages(packages: str | list[str] | None = None, present=True, latest=False):
14
16
  """
15
17
  Add/remove/update gem packages.
16
18