pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__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.
- pyinfra/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +18 -3
- pyinfra/api/arguments.py +406 -0
- pyinfra/api/arguments_typed.py +79 -0
- pyinfra/api/command.py +274 -0
- pyinfra/api/config.py +222 -28
- pyinfra/api/connect.py +33 -13
- pyinfra/api/connectors.py +27 -0
- pyinfra/api/deploy.py +65 -66
- pyinfra/api/exceptions.py +67 -18
- pyinfra/api/facts.py +253 -202
- pyinfra/api/host.py +413 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +291 -368
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +381 -0
- pyinfra/connectors/dockerssh.py +297 -0
- pyinfra/connectors/local.py +238 -0
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +670 -0
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +309 -0
- pyinfra/connectors/sshuserclient/config.py +102 -0
- pyinfra/connectors/terraform.py +135 -0
- pyinfra/connectors/util.py +410 -0
- pyinfra/connectors/vagrant.py +183 -0
- pyinfra/context.py +145 -0
- pyinfra/facts/__init__.py +7 -6
- pyinfra/facts/apk.py +22 -7
- pyinfra/facts/apt.py +117 -60
- pyinfra/facts/brew.py +100 -15
- pyinfra/facts/bsdinit.py +23 -0
- pyinfra/facts/cargo.py +37 -0
- pyinfra/facts/choco.py +47 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +94 -0
- pyinfra/facts/dnf.py +48 -0
- pyinfra/facts/docker.py +96 -23
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +630 -58
- pyinfra/facts/flatpak.py +77 -0
- pyinfra/facts/freebsd.py +70 -0
- pyinfra/facts/gem.py +19 -6
- pyinfra/facts/git.py +59 -14
- pyinfra/facts/gpg.py +150 -0
- pyinfra/facts/hardware.py +313 -167
- pyinfra/facts/iptables.py +72 -62
- pyinfra/facts/launchd.py +44 -0
- pyinfra/facts/lxd.py +17 -4
- pyinfra/facts/mysql.py +122 -86
- pyinfra/facts/npm.py +17 -9
- pyinfra/facts/openrc.py +71 -0
- pyinfra/facts/opkg.py +246 -0
- pyinfra/facts/pacman.py +50 -7
- pyinfra/facts/pip.py +24 -7
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +15 -6
- pyinfra/facts/pkgin.py +35 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +178 -0
- pyinfra/facts/postgresql.py +6 -147
- pyinfra/facts/rpm.py +105 -0
- pyinfra/facts/runit.py +77 -0
- pyinfra/facts/selinux.py +161 -0
- pyinfra/facts/server.py +746 -285
- pyinfra/facts/snap.py +88 -0
- pyinfra/facts/systemd.py +139 -0
- pyinfra/facts/sysvinit.py +59 -0
- pyinfra/facts/upstart.py +35 -0
- pyinfra/facts/util/__init__.py +17 -0
- pyinfra/facts/util/databases.py +4 -6
- pyinfra/facts/util/packaging.py +37 -6
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/util/win_files.py +99 -0
- pyinfra/facts/vzctl.py +20 -13
- pyinfra/facts/xbps.py +35 -0
- pyinfra/facts/yum.py +34 -40
- pyinfra/facts/zfs.py +77 -0
- pyinfra/facts/zypper.py +42 -0
- pyinfra/local.py +45 -83
- pyinfra/operations/__init__.py +12 -0
- pyinfra/operations/apk.py +98 -0
- pyinfra/operations/apt.py +488 -0
- pyinfra/operations/brew.py +231 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +191 -0
- pyinfra/operations/dnf.py +210 -0
- pyinfra/operations/docker.py +446 -0
- pyinfra/operations/files.py +1939 -0
- pyinfra/operations/flatpak.py +94 -0
- pyinfra/operations/freebsd/__init__.py +12 -0
- pyinfra/operations/freebsd/freebsd_update.py +70 -0
- pyinfra/operations/freebsd/pkg.py +219 -0
- pyinfra/operations/freebsd/service.py +116 -0
- pyinfra/operations/freebsd/sysrc.py +92 -0
- pyinfra/operations/gem.py +47 -0
- pyinfra/operations/git.py +419 -0
- pyinfra/operations/iptables.py +311 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +68 -0
- pyinfra/operations/mysql.py +609 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pacman.py +81 -0
- pyinfra/operations/pip.py +205 -0
- pyinfra/operations/pipx.py +102 -0
- pyinfra/operations/pkg.py +70 -0
- pyinfra/operations/pkgin.py +91 -0
- pyinfra/operations/postgres.py +436 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +40 -0
- pyinfra/operations/python.py +72 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +189 -0
- pyinfra/operations/server.py +1099 -0
- pyinfra/operations/snap.py +117 -0
- pyinfra/operations/ssh.py +216 -0
- pyinfra/operations/systemd.py +149 -0
- pyinfra/operations/sysvinit.py +141 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +251 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +336 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +77 -0
- pyinfra/operations/yum.py +210 -0
- pyinfra/operations/zfs.py +175 -0
- pyinfra/operations/zypper.py +192 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.5.1.dist-info/METADATA +141 -0
- pyinfra-3.5.1.dist-info/RECORD +159 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
- pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +780 -0
- pyinfra_cli/commands.py +66 -0
- pyinfra_cli/exceptions.py +155 -65
- pyinfra_cli/inventory.py +233 -89
- pyinfra_cli/log.py +39 -43
- pyinfra_cli/main.py +26 -495
- pyinfra_cli/prints.py +215 -156
- pyinfra_cli/util.py +172 -105
- pyinfra_cli/virtualenv.py +25 -20
- pyinfra/api/connectors/__init__.py +0 -21
- pyinfra/api/connectors/ansible.py +0 -99
- pyinfra/api/connectors/docker.py +0 -178
- pyinfra/api/connectors/local.py +0 -169
- pyinfra/api/connectors/ssh.py +0 -402
- pyinfra/api/connectors/sshuserclient/client.py +0 -105
- pyinfra/api/connectors/sshuserclient/config.py +0 -90
- pyinfra/api/connectors/util.py +0 -63
- pyinfra/api/connectors/vagrant.py +0 -155
- pyinfra/facts/init.py +0 -176
- pyinfra/facts/util/files.py +0 -102
- pyinfra/hook.py +0 -41
- pyinfra/modules/__init__.py +0 -11
- pyinfra/modules/apk.py +0 -64
- pyinfra/modules/apt.py +0 -272
- pyinfra/modules/brew.py +0 -122
- pyinfra/modules/files.py +0 -711
- pyinfra/modules/gem.py +0 -30
- pyinfra/modules/git.py +0 -115
- pyinfra/modules/init.py +0 -344
- pyinfra/modules/iptables.py +0 -271
- pyinfra/modules/lxd.py +0 -45
- pyinfra/modules/mysql.py +0 -347
- pyinfra/modules/npm.py +0 -47
- pyinfra/modules/pacman.py +0 -60
- pyinfra/modules/pip.py +0 -99
- pyinfra/modules/pkg.py +0 -43
- pyinfra/modules/postgresql.py +0 -245
- pyinfra/modules/puppet.py +0 -20
- pyinfra/modules/python.py +0 -37
- pyinfra/modules/server.py +0 -524
- pyinfra/modules/ssh.py +0 -150
- pyinfra/modules/util/files.py +0 -52
- pyinfra/modules/util/packaging.py +0 -118
- pyinfra/modules/vzctl.py +0 -133
- pyinfra/modules/yum.py +0 -171
- pyinfra/pseudo_modules.py +0 -64
- pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
- pyinfra-0.11.dev3.dist-info/METADATA +0 -135
- pyinfra-0.11.dev3.dist-info/RECORD +0 -95
- pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
- pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
- pyinfra_cli/__main__.py +0 -40
- pyinfra_cli/config.py +0 -92
- /pyinfra/{modules/util → connectors}/__init__.py +0 -0
- /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
from pyinfra.api import OperationError
|
|
5
|
+
|
|
6
|
+
|
|
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))
|
|
20
|
+
|
|
21
|
+
for port in self.ports:
|
|
22
|
+
args.append("-p {0}".format(port))
|
|
23
|
+
|
|
24
|
+
for volume in self.volumes:
|
|
25
|
+
args.append("-v {0}".format(volume))
|
|
26
|
+
|
|
27
|
+
for env_var in self.env_vars:
|
|
28
|
+
args.append("-e {0}".format(env_var))
|
|
29
|
+
|
|
30
|
+
if self.pull_always:
|
|
31
|
+
args.append("--pull always")
|
|
32
|
+
|
|
33
|
+
args.append(self.image)
|
|
34
|
+
|
|
35
|
+
return args
|
|
36
|
+
|
|
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'")
|
|
49
|
+
|
|
50
|
+
spec = kwargs["spec"]
|
|
51
|
+
|
|
52
|
+
if not spec.image:
|
|
53
|
+
raise OperationError("Docker image not specified")
|
|
54
|
+
|
|
55
|
+
command = [
|
|
56
|
+
"docker container create --name {0}".format(kwargs["container"])
|
|
57
|
+
] + spec.container_create_args()
|
|
58
|
+
|
|
59
|
+
return " ".join(command)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _remove_container(**kwargs):
|
|
63
|
+
return "docker container rm -f {0}".format(kwargs["container"])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _start_container(**kwargs):
|
|
67
|
+
return "docker container start {0}".format(kwargs["container"])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _stop_container(**kwargs):
|
|
71
|
+
return "docker container stop {0}".format(kwargs["container"])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _pull_image(**kwargs):
|
|
75
|
+
return "docker image pull {0}".format(kwargs["image"])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _remove_image(**kwargs):
|
|
79
|
+
return "docker image rm {0}".format(kwargs["image"])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _prune_command(**kwargs):
|
|
83
|
+
command = ["docker system prune"]
|
|
84
|
+
|
|
85
|
+
if kwargs["all"]:
|
|
86
|
+
command.append("-a")
|
|
87
|
+
|
|
88
|
+
if kwargs["filter"] != "":
|
|
89
|
+
command.append("--filter={0}".format(kwargs["filter"]))
|
|
90
|
+
|
|
91
|
+
if kwargs["volumes"]:
|
|
92
|
+
command.append("--volumes")
|
|
93
|
+
|
|
94
|
+
command.append("-f")
|
|
95
|
+
|
|
96
|
+
return " ".join(command)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _create_volume(**kwargs):
|
|
100
|
+
command = []
|
|
101
|
+
labels = kwargs["labels"] if kwargs["labels"] else []
|
|
102
|
+
|
|
103
|
+
command.append("docker volume create {0}".format(kwargs["volume"]))
|
|
104
|
+
|
|
105
|
+
if kwargs["driver"] != "":
|
|
106
|
+
command.append("-d {0}".format(kwargs["driver"]))
|
|
107
|
+
|
|
108
|
+
for label in labels:
|
|
109
|
+
command.append("--label {0}".format(label))
|
|
110
|
+
|
|
111
|
+
return " ".join(command)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _remove_volume(**kwargs):
|
|
115
|
+
return "docker image rm {0}".format(kwargs["volume"])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _create_network(**kwargs):
|
|
119
|
+
command = []
|
|
120
|
+
aux_addresses = kwargs["aux_addresses"] if kwargs["aux_addresses"] else {}
|
|
121
|
+
opts = kwargs["opts"] if kwargs["opts"] else []
|
|
122
|
+
ipam_opts = kwargs["ipam_opts"] if kwargs["ipam_opts"] else []
|
|
123
|
+
labels = kwargs["labels"] if kwargs["labels"] else []
|
|
124
|
+
|
|
125
|
+
command.append("docker network create {0}".format(kwargs["network"]))
|
|
126
|
+
if kwargs["driver"] != "":
|
|
127
|
+
command.append("-d {0}".format(kwargs["driver"]))
|
|
128
|
+
|
|
129
|
+
if kwargs["gateway"] != "":
|
|
130
|
+
command.append("--gateway {0}".format(kwargs["gateway"]))
|
|
131
|
+
|
|
132
|
+
if kwargs["ip_range"] != "":
|
|
133
|
+
command.append("--ip-range {0}".format(kwargs["ip_range"]))
|
|
134
|
+
|
|
135
|
+
if kwargs["ipam_driver"] != "":
|
|
136
|
+
command.append("--ipam-driver {0}".format(kwargs["ipam_driver"]))
|
|
137
|
+
|
|
138
|
+
if kwargs["subnet"] != "":
|
|
139
|
+
command.append("--subnet {0}".format(kwargs["subnet"]))
|
|
140
|
+
|
|
141
|
+
if kwargs["scope"] != "":
|
|
142
|
+
command.append("--scope {0}".format(kwargs["scope"]))
|
|
143
|
+
|
|
144
|
+
if kwargs["ingress"]:
|
|
145
|
+
command.append("--ingress")
|
|
146
|
+
|
|
147
|
+
if kwargs["attachable"]:
|
|
148
|
+
command.append("--attachable")
|
|
149
|
+
|
|
150
|
+
for host, address in aux_addresses.items():
|
|
151
|
+
command.append("--aux-address '{0}={1}'".format(host, address))
|
|
152
|
+
|
|
153
|
+
for opt in opts:
|
|
154
|
+
command.append("--opt {0}".format(opt))
|
|
155
|
+
|
|
156
|
+
for opt in ipam_opts:
|
|
157
|
+
command.append("--ipam-opt {0}".format(opt))
|
|
158
|
+
|
|
159
|
+
for label in labels:
|
|
160
|
+
command.append("--label {0}".format(label))
|
|
161
|
+
return " ".join(command)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _remove_network(**kwargs):
|
|
165
|
+
return "docker network rm {0}".format(kwargs["network"])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _install_plugin(**kwargs):
|
|
169
|
+
command = ["docker plugin install {0} --grant-all-permissions".format(kwargs["plugin"])]
|
|
170
|
+
|
|
171
|
+
plugin_options = kwargs["plugin_options"] if kwargs["plugin_options"] else {}
|
|
172
|
+
|
|
173
|
+
if kwargs["alias"]:
|
|
174
|
+
command.append("--alias {0}".format(kwargs["alias"]))
|
|
175
|
+
|
|
176
|
+
if not kwargs["enabled"]:
|
|
177
|
+
command.append("--disable")
|
|
178
|
+
|
|
179
|
+
for option, value in plugin_options.items():
|
|
180
|
+
command.append("{0}={1}".format(option, value))
|
|
181
|
+
|
|
182
|
+
return " ".join(command)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _remove_plugin(**kwargs):
|
|
186
|
+
return "docker plugin rm -f {0}".format(kwargs["plugin"])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _enable_plugin(**kwargs):
|
|
190
|
+
return "docker plugin enable {0}".format(kwargs["plugin"])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _disable_plugin(**kwargs):
|
|
194
|
+
return "docker plugin disable {0}".format(kwargs["plugin"])
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _set_plugin_options(**kwargs):
|
|
198
|
+
command = ["docker plugin set {0}".format(kwargs["plugin"])]
|
|
199
|
+
existent_options = kwargs.get("existing_options", {})
|
|
200
|
+
required_options = kwargs.get("required_options", {})
|
|
201
|
+
options_to_set = existent_options | required_options
|
|
202
|
+
for option, value in options_to_set.items():
|
|
203
|
+
command.append("{0}={1}".format(option, value))
|
|
204
|
+
return " ".join(command)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def handle_docker(resource: str, command: str, **kwargs):
|
|
208
|
+
container_commands = {
|
|
209
|
+
"create": _create_container,
|
|
210
|
+
"remove": _remove_container,
|
|
211
|
+
"start": _start_container,
|
|
212
|
+
"stop": _stop_container,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
image_commands = {
|
|
216
|
+
"pull": _pull_image,
|
|
217
|
+
"remove": _remove_image,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
volume_commands = {
|
|
221
|
+
"create": _create_volume,
|
|
222
|
+
"remove": _remove_volume,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
network_commands = {
|
|
226
|
+
"create": _create_network,
|
|
227
|
+
"remove": _remove_network,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
system_commands = {
|
|
231
|
+
"prune": _prune_command,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
plugin_commands = {
|
|
235
|
+
"install": _install_plugin,
|
|
236
|
+
"remove": _remove_plugin,
|
|
237
|
+
"enable": _enable_plugin,
|
|
238
|
+
"disable": _disable_plugin,
|
|
239
|
+
"set": _set_plugin_options,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
docker_commands = {
|
|
243
|
+
"container": container_commands,
|
|
244
|
+
"image": image_commands,
|
|
245
|
+
"volume": volume_commands,
|
|
246
|
+
"network": network_commands,
|
|
247
|
+
"system": system_commands,
|
|
248
|
+
"plugin": plugin_commands,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return docker_commands[resource][command](**kwargs)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Callable, Generator
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from pyinfra.api import QuoteString, StringCommand
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MetadataTimeField(Enum):
|
|
15
|
+
ATIME = "atime"
|
|
16
|
+
MTIME = "mtime"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def unix_path_join(*parts) -> str:
|
|
20
|
+
part_list = list(parts)
|
|
21
|
+
part_list[0:-1] = [part.rstrip("/") for part in part_list[0:-1]]
|
|
22
|
+
return "/".join(part_list)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ensure_mode_int(mode: str | int | None) -> int | str | None:
|
|
26
|
+
# Already an int (/None)?
|
|
27
|
+
if isinstance(mode, int) or mode is None:
|
|
28
|
+
return mode
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
# Try making an int ('700' -> 700)
|
|
32
|
+
return int(mode)
|
|
33
|
+
|
|
34
|
+
except (TypeError, ValueError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
# Return as-is (ie +x which we don't need to normalise, it always gets run)
|
|
38
|
+
return mode
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_timestamp() -> str:
|
|
42
|
+
return datetime.now().strftime("%y%m%d%H%M")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_sed_ignore_case = re.compile("[iI]")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sed_delete_builder(line: str, replace: str, flags: str, interpolate_variables: bool) -> str:
|
|
49
|
+
return (
|
|
50
|
+
'"/{0}/{1}d"'
|
|
51
|
+
if interpolate_variables
|
|
52
|
+
# fmt: skip
|
|
53
|
+
else "'/{0}/{1}d'"
|
|
54
|
+
).format(line, "I" if _sed_ignore_case.search(flags) else "")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def sed_delete(
|
|
58
|
+
filename: str,
|
|
59
|
+
line: str,
|
|
60
|
+
replace: str,
|
|
61
|
+
flags: list[str] | None = None,
|
|
62
|
+
backup=False,
|
|
63
|
+
interpolate_variables=False,
|
|
64
|
+
) -> StringCommand:
|
|
65
|
+
return _sed_command(**locals(), sed_script_builder=_sed_delete_builder)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _sed_replace_builder(line: str, replace: str, flags: str, interpolate_variables: bool) -> str:
|
|
69
|
+
return (
|
|
70
|
+
'"s/{0}/{1}/{2}"'
|
|
71
|
+
if interpolate_variables
|
|
72
|
+
# fmt: skip
|
|
73
|
+
else "'s/{0}/{1}/{2}'"
|
|
74
|
+
).format(line, replace, flags)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def sed_replace(
|
|
78
|
+
filename: str,
|
|
79
|
+
line: str,
|
|
80
|
+
replace: str,
|
|
81
|
+
flags: list[str] | None = None,
|
|
82
|
+
backup=False,
|
|
83
|
+
interpolate_variables=False,
|
|
84
|
+
) -> StringCommand:
|
|
85
|
+
return _sed_command(**locals(), sed_script_builder=_sed_replace_builder)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _sed_command(
|
|
89
|
+
filename: str,
|
|
90
|
+
line: str,
|
|
91
|
+
replace: str,
|
|
92
|
+
flags: list[str] | None = None,
|
|
93
|
+
backup=False,
|
|
94
|
+
interpolate_variables=False,
|
|
95
|
+
# Python requires a default value here, so use _sed_replace_builder for
|
|
96
|
+
# backwards compatibility.
|
|
97
|
+
sed_script_builder: Callable[[str, str, str, bool], str] = _sed_replace_builder,
|
|
98
|
+
) -> StringCommand:
|
|
99
|
+
flags_str = "".join(flags) if flags else ""
|
|
100
|
+
|
|
101
|
+
line = line.replace("/", r"\/")
|
|
102
|
+
replace = str(replace)
|
|
103
|
+
replace = replace.replace("/", r"\/")
|
|
104
|
+
replace = replace.replace("&", r"\&")
|
|
105
|
+
backup_extension = get_timestamp()
|
|
106
|
+
|
|
107
|
+
if interpolate_variables:
|
|
108
|
+
line = line.replace('"', '\\"')
|
|
109
|
+
replace = replace.replace('"', '\\"')
|
|
110
|
+
else:
|
|
111
|
+
# Single quotes cannot contain other single quotes, even when escaped , so turn
|
|
112
|
+
# each ' into '"'"' (end string, double quote the single quote, (re)start string)
|
|
113
|
+
line = line.replace("'", "'\"'\"'")
|
|
114
|
+
replace = replace.replace("'", "'\"'\"'")
|
|
115
|
+
|
|
116
|
+
sed_script = sed_script_builder(line, replace, flags_str, interpolate_variables)
|
|
117
|
+
|
|
118
|
+
sed_command = StringCommand(
|
|
119
|
+
"sed",
|
|
120
|
+
"-i.{0}".format(backup_extension),
|
|
121
|
+
sed_script,
|
|
122
|
+
QuoteString(filename),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if not backup: # if we're not backing up, remove the file *if* sed succeeds
|
|
126
|
+
backup_filename = "{0}.{1}".format(filename, backup_extension)
|
|
127
|
+
sed_command = StringCommand(sed_command, "&&", "rm", "-f", QuoteString(backup_filename))
|
|
128
|
+
|
|
129
|
+
return sed_command
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def chmod(target: str, mode: str | int, recursive=False) -> StringCommand:
|
|
133
|
+
args = ["chmod"]
|
|
134
|
+
if recursive:
|
|
135
|
+
args.append("-R")
|
|
136
|
+
|
|
137
|
+
args.append("{0}".format(mode))
|
|
138
|
+
|
|
139
|
+
return StringCommand(" ".join(args), QuoteString(target))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def chown(
|
|
143
|
+
target: str,
|
|
144
|
+
user: str | None = None,
|
|
145
|
+
group: str | None = None,
|
|
146
|
+
recursive=False,
|
|
147
|
+
dereference=True,
|
|
148
|
+
) -> StringCommand:
|
|
149
|
+
command = "chown"
|
|
150
|
+
user_group = None
|
|
151
|
+
|
|
152
|
+
if user and group:
|
|
153
|
+
user_group = "{0}:{1}".format(user, group)
|
|
154
|
+
|
|
155
|
+
elif user:
|
|
156
|
+
user_group = user
|
|
157
|
+
|
|
158
|
+
elif group:
|
|
159
|
+
command = "chgrp"
|
|
160
|
+
user_group = group
|
|
161
|
+
|
|
162
|
+
args = [command]
|
|
163
|
+
if recursive:
|
|
164
|
+
args.append("-R")
|
|
165
|
+
|
|
166
|
+
if not dereference:
|
|
167
|
+
args.append("-h")
|
|
168
|
+
|
|
169
|
+
return StringCommand(" ".join(args), user_group, QuoteString(target))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# like the touch command, but only supports setting one field at a time, and expects any
|
|
173
|
+
# reference times to have been read from the reference file metadata and turned into
|
|
174
|
+
# aware datetimes
|
|
175
|
+
def touch(
|
|
176
|
+
target: str,
|
|
177
|
+
timefield: MetadataTimeField,
|
|
178
|
+
timesrc: datetime,
|
|
179
|
+
dereference=True,
|
|
180
|
+
) -> StringCommand:
|
|
181
|
+
args = ["touch"]
|
|
182
|
+
|
|
183
|
+
if timefield is MetadataTimeField.ATIME:
|
|
184
|
+
args.append("-a")
|
|
185
|
+
else:
|
|
186
|
+
args.append("-m")
|
|
187
|
+
|
|
188
|
+
if not dereference:
|
|
189
|
+
args.append("-h")
|
|
190
|
+
|
|
191
|
+
# don't reinvent the wheel; use isoformat()
|
|
192
|
+
timestr = timesrc.astimezone(timezone.utc).isoformat()
|
|
193
|
+
# but replace the ISO format TZ offset with "Z" for BSD
|
|
194
|
+
timestr = timestr.replace("+00:00", "Z")
|
|
195
|
+
args.extend(["-d", timestr])
|
|
196
|
+
|
|
197
|
+
return StringCommand(" ".join(args), QuoteString(target))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def adjust_regex(line: str, escape_regex_characters: bool) -> str:
|
|
201
|
+
"""
|
|
202
|
+
Ensure the regex starts with '^' and ends with '$' and escape regex characters if requested
|
|
203
|
+
"""
|
|
204
|
+
match_line = line
|
|
205
|
+
|
|
206
|
+
if escape_regex_characters:
|
|
207
|
+
match_line = re.sub(r"([\.\\\+\*\?\[\^\]\$\(\)\{\}\-])", r"\\\1", match_line)
|
|
208
|
+
|
|
209
|
+
# Ensure we're matching a whole line, note: match may be a partial line so we
|
|
210
|
+
# put any matches on either side.
|
|
211
|
+
if not match_line.startswith("^"):
|
|
212
|
+
match_line = "^.*{0}".format(match_line)
|
|
213
|
+
if not match_line.endswith("$"):
|
|
214
|
+
match_line = "{0}.*$".format(match_line)
|
|
215
|
+
|
|
216
|
+
return match_line
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def generate_color_diff(
|
|
220
|
+
current_lines: list[str], desired_lines: list[str]
|
|
221
|
+
) -> Generator[str, None, None]:
|
|
222
|
+
def _format_range_unified(start: int, stop: int) -> str:
|
|
223
|
+
beginning = start + 1 # lines start numbering with one
|
|
224
|
+
length = stop - start
|
|
225
|
+
if length == 1:
|
|
226
|
+
return "{}".format(beginning)
|
|
227
|
+
if not length:
|
|
228
|
+
beginning -= 1 # empty ranges begin at line just before the range
|
|
229
|
+
return "{},{}".format(beginning, length)
|
|
230
|
+
|
|
231
|
+
for group in difflib.SequenceMatcher(None, current_lines, desired_lines).get_grouped_opcodes(2):
|
|
232
|
+
first, last = group[0], group[-1]
|
|
233
|
+
file1_range = _format_range_unified(first[1], last[2])
|
|
234
|
+
file2_range = _format_range_unified(first[3], last[4])
|
|
235
|
+
yield "@@ -{} +{} @@".format(file1_range, file2_range)
|
|
236
|
+
|
|
237
|
+
for tag, i1, i2, j1, j2 in group:
|
|
238
|
+
if tag == "equal":
|
|
239
|
+
for line in current_lines[i1:i2]:
|
|
240
|
+
yield " " + line.rstrip()
|
|
241
|
+
continue
|
|
242
|
+
if tag in {"replace", "delete"}:
|
|
243
|
+
for line in current_lines[i1:i2]:
|
|
244
|
+
yield click.style("- " + line.rstrip(), "red")
|
|
245
|
+
if tag in {"replace", "insert"}:
|
|
246
|
+
for line in desired_lines[j1:j2]:
|
|
247
|
+
yield click.style("+ " + line.rstrip(), "green")
|