pyinfra 3.2__py2.py3-none-any.whl → 3.3.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 (89) hide show
  1. pyinfra/api/arguments_typed.py +4 -5
  2. pyinfra/api/command.py +22 -3
  3. pyinfra/api/config.py +5 -2
  4. pyinfra/api/facts.py +3 -0
  5. pyinfra/api/host.py +10 -4
  6. pyinfra/api/operation.py +2 -1
  7. pyinfra/api/state.py +1 -1
  8. pyinfra/connectors/base.py +34 -8
  9. pyinfra/connectors/chroot.py +7 -2
  10. pyinfra/connectors/docker.py +7 -2
  11. pyinfra/connectors/dockerssh.py +7 -2
  12. pyinfra/connectors/local.py +7 -2
  13. pyinfra/connectors/ssh.py +9 -2
  14. pyinfra/connectors/sshuserclient/client.py +18 -2
  15. pyinfra/connectors/sshuserclient/config.py +2 -0
  16. pyinfra/connectors/terraform.py +1 -1
  17. pyinfra/connectors/util.py +13 -9
  18. pyinfra/context.py +9 -2
  19. pyinfra/facts/apk.py +5 -0
  20. pyinfra/facts/apt.py +9 -1
  21. pyinfra/facts/brew.py +13 -0
  22. pyinfra/facts/bsdinit.py +3 -0
  23. pyinfra/facts/cargo.py +5 -0
  24. pyinfra/facts/choco.py +6 -0
  25. pyinfra/facts/crontab.py +6 -1
  26. pyinfra/facts/deb.py +10 -0
  27. pyinfra/facts/dnf.py +5 -0
  28. pyinfra/facts/docker.py +10 -0
  29. pyinfra/facts/efibootmgr.py +5 -0
  30. pyinfra/facts/files.py +19 -1
  31. pyinfra/facts/flatpak.py +7 -0
  32. pyinfra/facts/freebsd.py +75 -0
  33. pyinfra/facts/gem.py +5 -0
  34. pyinfra/facts/git.py +9 -0
  35. pyinfra/facts/gpg.py +7 -0
  36. pyinfra/facts/hardware.py +13 -0
  37. pyinfra/facts/iptables.py +9 -1
  38. pyinfra/facts/launchd.py +5 -0
  39. pyinfra/facts/lxd.py +5 -0
  40. pyinfra/facts/mysql.py +8 -0
  41. pyinfra/facts/npm.py +5 -0
  42. pyinfra/facts/openrc.py +8 -0
  43. pyinfra/facts/opkg.py +12 -0
  44. pyinfra/facts/pacman.py +9 -1
  45. pyinfra/facts/pip.py +5 -0
  46. pyinfra/facts/pipx.py +8 -0
  47. pyinfra/facts/pkg.py +4 -0
  48. pyinfra/facts/pkgin.py +5 -0
  49. pyinfra/facts/podman.py +7 -0
  50. pyinfra/facts/postgres.py +8 -2
  51. pyinfra/facts/rpm.py +11 -0
  52. pyinfra/facts/runit.py +7 -0
  53. pyinfra/facts/selinux.py +16 -0
  54. pyinfra/facts/server.py +49 -3
  55. pyinfra/facts/snap.py +7 -0
  56. pyinfra/facts/systemd.py +5 -0
  57. pyinfra/facts/sysvinit.py +4 -0
  58. pyinfra/facts/upstart.py +5 -0
  59. pyinfra/facts/util/__init__.py +4 -1
  60. pyinfra/facts/vzctl.py +5 -0
  61. pyinfra/facts/xbps.py +6 -1
  62. pyinfra/facts/yum.py +5 -0
  63. pyinfra/facts/zfs.py +19 -2
  64. pyinfra/facts/zypper.py +5 -0
  65. pyinfra/operations/apt.py +10 -3
  66. pyinfra/operations/docker.py +48 -44
  67. pyinfra/operations/files.py +47 -1
  68. pyinfra/operations/freebsd/__init__.py +12 -0
  69. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  70. pyinfra/operations/freebsd/pkg.py +219 -0
  71. pyinfra/operations/freebsd/service.py +116 -0
  72. pyinfra/operations/freebsd/sysrc.py +92 -0
  73. pyinfra/operations/opkg.py +5 -5
  74. pyinfra/operations/postgres.py +99 -16
  75. pyinfra/operations/server.py +6 -4
  76. pyinfra/operations/util/docker.py +44 -22
  77. {pyinfra-3.2.dist-info → pyinfra-3.3.1.dist-info}/LICENSE.md +1 -1
  78. {pyinfra-3.2.dist-info → pyinfra-3.3.1.dist-info}/METADATA +25 -24
  79. {pyinfra-3.2.dist-info → pyinfra-3.3.1.dist-info}/RECORD +89 -83
  80. pyinfra_cli/exceptions.py +5 -0
  81. pyinfra_cli/log.py +3 -0
  82. pyinfra_cli/main.py +9 -8
  83. pyinfra_cli/prints.py +1 -1
  84. pyinfra_cli/virtualenv.py +1 -1
  85. tests/test_connectors/test_ssh.py +302 -182
  86. tests/test_connectors/test_sshuserclient.py +10 -5
  87. {pyinfra-3.2.dist-info → pyinfra-3.3.1.dist-info}/WHEEL +0 -0
  88. {pyinfra-3.2.dist-info → pyinfra-3.3.1.dist-info}/entry_points.txt +0 -0
  89. {pyinfra-3.2.dist-info → pyinfra-3.3.1.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,10 @@
1
1
  """
2
- Manage packages on OpenWrt using opkg
3
- + ``update`` - update local copy of package information
4
- + ``packages`` - install and remove packages
2
+ Manage packages on OpenWrt using opkg
3
+ + ``update`` - update local copy of package information
4
+ + ``packages`` - install and remove packages
5
5
 
6
- see https://openwrt.org/docs/guide-user/additional-software/opkg
7
- OpenWrt recommends against upgrading all packages thus there is no ``opkg.upgrade`` function
6
+ see https://openwrt.org/docs/guide-user/additional-software/opkg
7
+ OpenWrt recommends against upgrading all packages thus there is no ``opkg.upgrade`` function
8
8
  """
9
9
 
10
10
  from typing import List, Union
@@ -56,14 +56,14 @@ def sql(
56
56
  @operation()
57
57
  def role(
58
58
  role: str,
59
- present=True,
59
+ present: bool = True,
60
60
  password: str | None = None,
61
- login=True,
62
- superuser=False,
63
- inherit=False,
64
- createdb=False,
65
- createrole=False,
66
- replication=False,
61
+ login: bool = True,
62
+ superuser: bool = False,
63
+ inherit: bool = False,
64
+ createdb: bool = False,
65
+ createrole: bool = False,
66
+ replication: bool = False,
67
67
  connection_limit: int | None = None,
68
68
  # Details for speaking to PostgreSQL via `psql` CLI
69
69
  psql_user: str | None = None,
@@ -102,7 +102,7 @@ def role(
102
102
  password="somepassword",
103
103
  superuser=True,
104
104
  login=True,
105
- sudo_user="postgres",
105
+ _sudo_user="postgres",
106
106
  )
107
107
 
108
108
  """
@@ -163,7 +163,47 @@ def role(
163
163
  database=psql_database,
164
164
  )
165
165
  else:
166
- host.noop("postgresql role {0} exists".format(role))
166
+ # Check if any attributes need updating
167
+ current_role = roles[role]
168
+ should_execute = False
169
+ sql_bits = ['ALTER ROLE "{0}"'.format(role)]
170
+ if login and "login" in current_role and current_role["login"] != login:
171
+ sql_bits.append("LOGIN")
172
+ should_execute = True
173
+ if superuser and "superuser" in current_role and current_role["superuser"] != superuser:
174
+ sql_bits.append("SUPERUSER")
175
+ should_execute = True
176
+ if inherit and "inherit" in current_role and current_role["inherit"] != inherit:
177
+ sql_bits.append("INHERIT")
178
+ should_execute = True
179
+ if createdb and "createdb" in current_role and current_role["createdb"] != createdb:
180
+ sql_bits.append("CREATEDB")
181
+ should_execute = True
182
+ if createrole and "createrole" in current_role and current_role["createrole"] != createrole:
183
+ sql_bits.append("CREATEROLE")
184
+ should_execute = True
185
+ if (
186
+ connection_limit
187
+ and "connection_limit" in current_role
188
+ and roles[role]["connection_limit"] != connection_limit
189
+ ):
190
+ sql_bits.append("CONNECTION LIMIT {0}".format(connection_limit))
191
+ should_execute = True
192
+ if password:
193
+ sql_bits.append(MaskString("PASSWORD '{0}'".format(password)))
194
+ should_execute = True
195
+
196
+ if should_execute:
197
+ yield make_execute_psql_command(
198
+ StringCommand(*sql_bits),
199
+ user=psql_user,
200
+ password=psql_password,
201
+ host=psql_host,
202
+ port=psql_port,
203
+ database=psql_database,
204
+ )
205
+ else:
206
+ host.noop("postgresql role {0} exists and does not need updates".format(role))
167
207
 
168
208
 
169
209
  @operation()
@@ -199,9 +239,8 @@ def database(
199
239
  + psql_*: global module arguments, see above
200
240
 
201
241
  Updates:
202
- pyinfra will not attempt to change existing databases - it will either
203
- create or drop databases, but not alter them (if the db exists this
204
- operation will make no changes).
242
+ pyinfra will change existing databases - but some parameters are not
243
+ changeable (template, encoding, lc_collate and lc_ctype).
205
244
 
206
245
  **Example:**
207
246
 
@@ -212,7 +251,7 @@ def database(
212
251
  database="pyinfra_stuff",
213
252
  owner="pyinfra",
214
253
  encoding="UTF8",
215
- sudo_user="postgres",
254
+ _sudo_user="postgres",
216
255
  )
217
256
 
218
257
  """
@@ -250,8 +289,8 @@ def database(
250
289
  ("OWNER", '"{0}"'.format(owner) if owner else owner),
251
290
  ("TEMPLATE", template),
252
291
  ("ENCODING", encoding),
253
- ("LC_COLLATE", lc_collate),
254
- ("LC_CTYPE", lc_ctype),
292
+ ("LC_COLLATE", "'{0}'".format(lc_collate) if lc_collate else lc_collate),
293
+ ("LC_CTYPE", "'{0}'".format(lc_ctype) if lc_ctype else lc_ctype),
255
294
  ("TABLESPACE", tablespace),
256
295
  ("CONNECTION LIMIT", connection_limit),
257
296
  ):
@@ -267,7 +306,51 @@ def database(
267
306
  database=psql_database,
268
307
  )
269
308
  else:
270
- host.noop("postgresql database {0} exists".format(database))
309
+ current_db = current_databases[database]
310
+
311
+ for key, value, current_value in (
312
+ ("TEMPLATE", template, current_db.get("istemplate")),
313
+ ("ENCODING", encoding, current_db.get("encoding")),
314
+ ("LC_COLLATE", lc_collate, None),
315
+ ("LC_CTYPE", lc_ctype, None),
316
+ ):
317
+ if value and (current_value is None or current_value != value):
318
+ host.noop(
319
+ "postgresql database {0} already exists, skipping {1}".format(database, key)
320
+ )
321
+
322
+ sql_bits = []
323
+
324
+ if owner and "owner" in current_db and current_db["owner"] != owner:
325
+ sql_bits.append('ALTER DATABASE "{0}" OWNER TO "{1}";'.format(database, owner))
326
+
327
+ if tablespace and "tablespace" in current_db and current_db["tablespace"] != tablespace:
328
+ sql_bits.append(
329
+ 'ALTER DATABASE "{0}" SET TABLESPACE "{1}";'.format(database, tablespace)
330
+ )
331
+
332
+ if (
333
+ connection_limit
334
+ and "connlimit" in current_db
335
+ and current_db["connlimit"] != connection_limit
336
+ ):
337
+ sql_bits.append(
338
+ 'ALTER DATABASE "{0}" CONNECTION LIMIT {1};'.format(database, connection_limit)
339
+ )
340
+
341
+ if len(sql_bits) > 0:
342
+ yield make_execute_psql_command(
343
+ StringCommand(*sql_bits),
344
+ user=psql_user,
345
+ password=psql_password,
346
+ host=psql_host,
347
+ port=psql_port,
348
+ database=psql_database,
349
+ )
350
+ else:
351
+ host.noop(
352
+ "postgresql database {0} already exists with the same parameters".format(database)
353
+ )
271
354
 
272
355
 
273
356
  @operation(is_idempotent=False)
@@ -437,7 +437,7 @@ def sysctl(
437
437
  existing_sysctls = host.get_fact(Sysctl, keys=[key])
438
438
  existing_value = existing_sysctls.get(key)
439
439
 
440
- if not existing_value or existing_value != value:
440
+ if existing_value != value:
441
441
  yield "sysctl {0}='{1}'".format(key, string_value)
442
442
  else:
443
443
  host.noop("sysctl {0} is set to {1}".format(key, string_value))
@@ -764,6 +764,7 @@ def user(
764
764
  shell: str | None = None,
765
765
  group: str | None = None,
766
766
  groups: list[str] | None = None,
767
+ append=False,
767
768
  public_keys: str | list[str] | None = None,
768
769
  delete_keys=False,
769
770
  ensure_home=True,
@@ -771,7 +772,6 @@ def user(
771
772
  system=False,
772
773
  uid: int | None = None,
773
774
  comment: str | None = None,
774
- add_deploy_dir=True,
775
775
  unique=True,
776
776
  password: str | None = None,
777
777
  ):
@@ -784,14 +784,14 @@ def user(
784
784
  + shell: the users shell
785
785
  + group: the users primary group
786
786
  + groups: the users secondary groups
787
+ + append: whether to add `user` to `groups`, w/o losing membership of other groups
787
788
  + public_keys: list of public keys to attach to this user, ``home`` must be specified
788
789
  + delete_keys: whether to remove any keys not specified in ``public_keys``
789
790
  + ensure_home: whether to ensure the ``home`` directory exists
790
- + create_home: whether to new user create home directories from the system skeleton
791
+ + create_home: whether user create new user home directories from the system skeleton
791
792
  + system: whether to create a system account
792
793
  + uid: use a specific userid number
793
794
  + comment: the user GECOS comment
794
- + add_deploy_dir: any public_key filenames are relative to the deploy directory
795
795
  + unique: prevent creating users with duplicate UID
796
796
  + password: set the encrypted password for the user
797
797
 
@@ -931,6 +931,8 @@ def user(
931
931
 
932
932
  # Check secondary groups, if defined
933
933
  if groups and set(existing_user["groups"]) != set(groups):
934
+ if append:
935
+ args.append("-a")
934
936
  args.append("-G {0}".format(",".join(groups)))
935
937
 
936
938
  if comment and existing_user["comment"] != comment:
@@ -1,38 +1,60 @@
1
+ import dataclasses
2
+ from typing import Any, Dict, List
3
+
1
4
  from pyinfra.api import OperationError
2
5
 
3
6
 
4
- def _create_container(**kwargs):
5
- command = []
7
+ @dataclasses.dataclass
8
+ class ContainerSpec:
9
+ image: str = ""
10
+ ports: List[str] = dataclasses.field(default_factory=list)
11
+ networks: List[str] = dataclasses.field(default_factory=list)
12
+ volumes: List[str] = dataclasses.field(default_factory=list)
13
+ env_vars: List[str] = dataclasses.field(default_factory=list)
14
+ pull_always: bool = False
15
+
16
+ def container_create_args(self):
17
+ args = []
18
+ for network in self.networks:
19
+ args.append("--network {0}".format(network))
6
20
 
7
- networks = kwargs["networks"] if kwargs["networks"] else []
8
- ports = kwargs["ports"] if kwargs["ports"] else []
9
- volumes = kwargs["volumes"] if kwargs["volumes"] else []
10
- env_vars = kwargs["env_vars"] if kwargs["env_vars"] else []
21
+ for port in self.ports:
22
+ args.append("-p {0}".format(port))
11
23
 
12
- if kwargs["image"] == "":
13
- raise OperationError("missing 1 required argument: 'image'")
24
+ for volume in self.volumes:
25
+ args.append("-v {0}".format(volume))
14
26
 
15
- command.append("docker container create --name {0}".format(kwargs["container"]))
27
+ for env_var in self.env_vars:
28
+ args.append("-e {0}".format(env_var))
16
29
 
17
- for network in networks:
18
- command.append("--network {0}".format(network))
30
+ if self.pull_always:
31
+ args.append("--pull always")
19
32
 
20
- for port in ports:
21
- command.append("-p {0}".format(port))
33
+ args.append(self.image)
22
34
 
23
- for volume in volumes:
24
- command.append("-v {0}".format(volume))
35
+ return args
25
36
 
26
- for env_var in env_vars:
27
- command.append("-e {0}".format(env_var))
37
+ def diff_from_inspect(self, inspect_dict: Dict[str, Any]) -> List[str]:
38
+ # TODO(@minor-fixes): Diff output of "docker inspect" against this spec
39
+ # to determine if the container needs to be recreated. Currently, this
40
+ # function will never recreate when attributes change, which is
41
+ # consistent with prior behavior.
42
+ del inspect_dict
43
+ return []
44
+
45
+
46
+ def _create_container(**kwargs):
47
+ if "spec" not in kwargs:
48
+ raise OperationError("missing 1 required argument: 'spec'")
28
49
 
29
- if kwargs["pull_always"]:
30
- command.append("--pull always")
50
+ spec = kwargs["spec"]
31
51
 
32
- command.append(kwargs["image"])
52
+ if not spec.image:
53
+ raise OperationError("Docker image not specified")
33
54
 
34
- if kwargs["start"]:
35
- command.append("; {0}".format(_start_container(container=kwargs["container"])))
55
+ command = [
56
+ "docker container create --name {0}".format(kwargs["container"])
57
+ ] + spec.container_create_args()
36
58
 
37
59
  return " ".join(command)
38
60
 
@@ -1,4 +1,4 @@
1
- Copyright (C) 2020 Nick Barrett <nick@fizzadar.com>
1
+ Copyright (C) 2025 Nick Barrett <nick@fizzadar.com>
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyinfra
3
- Version: 3.2
3
+ Version: 3.3.1
4
4
  Summary: pyinfra automates/provisions/manages/deploys infrastructure.
5
5
  Home-page: https://pyinfra.com
6
6
  Author: Nick / Fizzadar
@@ -16,7 +16,6 @@ Classifier: Intended Audience :: Information Technology
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.8
20
19
  Classifier: Programming Language :: Python :: 3.9
21
20
  Classifier: Programming Language :: Python :: 3.10
22
21
  Classifier: Programming Language :: Python :: 3.11
@@ -24,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.12
24
23
  Classifier: Topic :: System :: Systems Administration
25
24
  Classifier: Topic :: System :: Installation/Setup
26
25
  Classifier: Topic :: Utilities
27
- Requires-Python: >=3.8
26
+ Requires-Python: >=3.9
28
27
  Description-Content-Type: text/markdown
29
28
  License-File: LICENSE.md
30
29
  Requires-Dist: gevent >=1.5
@@ -41,43 +40,45 @@ Requires-Dist: importlib-metadata >=3.6 ; python_version < "3.10"
41
40
  Requires-Dist: typing-extensions ; python_version < "3.11"
42
41
  Requires-Dist: graphlib-backport ; python_version < "3.9"
43
42
  Provides-Extra: dev
44
- Requires-Dist: pytest ==8.3.3 ; extra == 'dev'
45
- Requires-Dist: coverage ==7.6.1 ; extra == 'dev'
46
- Requires-Dist: pytest-cov ==5.0.0 ; extra == 'dev'
47
- Requires-Dist: black ==24.8.0 ; extra == 'dev'
48
- Requires-Dist: isort ==5.13.2 ; extra == 'dev'
49
- Requires-Dist: flake8 ==7.1.1 ; extra == 'dev'
43
+ Requires-Dist: pytest ==8.3.5 ; extra == 'dev'
44
+ Requires-Dist: coverage ==7.7.1 ; extra == 'dev'
45
+ Requires-Dist: pytest-cov ==6.0.0 ; extra == 'dev'
46
+ Requires-Dist: black ==25.1.0 ; extra == 'dev'
47
+ Requires-Dist: isort ==6.0.1 ; extra == 'dev'
48
+ Requires-Dist: flake8 ==7.1.2 ; extra == 'dev'
50
49
  Requires-Dist: flake8-black ==0.3.6 ; extra == 'dev'
51
- Requires-Dist: flake8-isort ==6.1.1 ; extra == 'dev'
50
+ Requires-Dist: flake8-isort ==6.1.2 ; extra == 'dev'
51
+ Requires-Dist: pyyaml ==6.0.2 ; extra == 'dev'
52
52
  Requires-Dist: mypy ; extra == 'dev'
53
53
  Requires-Dist: types-cryptography ; extra == 'dev'
54
54
  Requires-Dist: types-paramiko ; extra == 'dev'
55
55
  Requires-Dist: types-python-dateutil ; extra == 'dev'
56
56
  Requires-Dist: types-PyYAML ; extra == 'dev'
57
57
  Requires-Dist: types-setuptools ; extra == 'dev'
58
- Requires-Dist: pyinfra-guzzle-sphinx-theme ==0.16 ; extra == 'dev'
59
- Requires-Dist: myst-parser ==3.0.1 ; extra == 'dev'
60
- Requires-Dist: sphinx ==6.2.1 ; extra == 'dev'
58
+ Requires-Dist: pyinfra-guzzle-sphinx-theme ==0.17 ; extra == 'dev'
59
+ Requires-Dist: myst-parser ==4.0.1 ; extra == 'dev'
60
+ Requires-Dist: sphinx ==8.2.3 ; extra == 'dev'
61
61
  Requires-Dist: wheel ; extra == 'dev'
62
62
  Requires-Dist: twine ; extra == 'dev'
63
63
  Requires-Dist: ipython ; extra == 'dev'
64
64
  Requires-Dist: ipdb ; extra == 'dev'
65
65
  Requires-Dist: ipdbplugin ; extra == 'dev'
66
- Requires-Dist: flake8-spellcheck ==0.12.1 ; extra == 'dev'
66
+ Requires-Dist: flake8-spellcheck ==0.28.0 ; extra == 'dev'
67
67
  Requires-Dist: redbaron ; extra == 'dev'
68
68
  Provides-Extra: docs
69
- Requires-Dist: pyinfra-guzzle-sphinx-theme ==0.16 ; extra == 'docs'
70
- Requires-Dist: myst-parser ==3.0.1 ; extra == 'docs'
71
- Requires-Dist: sphinx ==6.2.1 ; extra == 'docs'
69
+ Requires-Dist: pyinfra-guzzle-sphinx-theme ==0.17 ; extra == 'docs'
70
+ Requires-Dist: myst-parser ==4.0.1 ; extra == 'docs'
71
+ Requires-Dist: sphinx ==8.2.3 ; extra == 'docs'
72
72
  Provides-Extra: test
73
- Requires-Dist: pytest ==8.3.3 ; extra == 'test'
74
- Requires-Dist: coverage ==7.6.1 ; extra == 'test'
75
- Requires-Dist: pytest-cov ==5.0.0 ; extra == 'test'
76
- Requires-Dist: black ==24.8.0 ; extra == 'test'
77
- Requires-Dist: isort ==5.13.2 ; extra == 'test'
78
- Requires-Dist: flake8 ==7.1.1 ; extra == 'test'
73
+ Requires-Dist: pytest ==8.3.5 ; extra == 'test'
74
+ Requires-Dist: coverage ==7.7.1 ; extra == 'test'
75
+ Requires-Dist: pytest-cov ==6.0.0 ; extra == 'test'
76
+ Requires-Dist: black ==25.1.0 ; extra == 'test'
77
+ Requires-Dist: isort ==6.0.1 ; extra == 'test'
78
+ Requires-Dist: flake8 ==7.1.2 ; extra == 'test'
79
79
  Requires-Dist: flake8-black ==0.3.6 ; extra == 'test'
80
- Requires-Dist: flake8-isort ==6.1.1 ; extra == 'test'
80
+ Requires-Dist: flake8-isort ==6.1.2 ; extra == 'test'
81
+ Requires-Dist: pyyaml ==6.0.2 ; extra == 'test'
81
82
  Requires-Dist: mypy ; extra == 'test'
82
83
  Requires-Dist: types-cryptography ; extra == 'test'
83
84
  Requires-Dist: types-paramiko ; extra == 'test'