pathlib-next 0.3.0__tar.gz → 0.3.5__tar.gz
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.
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/.gitignore +1 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/PKG-INFO +1 -1
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/pyproject.toml +1 -1
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/example.py +4 -6
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/uri/schemes/http.py +7 -2
- pathlib_next-0.3.5/src/pathlib_next/utils/sync.py +258 -0
- pathlib_next-0.3.0/src/pathlib_next/utils/sync.py +0 -104
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/LICENSE +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/README.md +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/__init__.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/fspath.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/mempath.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/path.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/protocols/__init__.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/protocols/fs.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/protocols/io.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/uri/__init__.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/uri/query.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/uri/schemes/__init__.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/uri/schemes/file.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/uri/schemes/sftp.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/uri/source.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/utils/__init__.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/utils/glob.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/src/pathlib_next/utils/stat.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/tests/test_local.py +0 -0
- {pathlib_next-0.3.0 → pathlib_next-0.3.5}/tests/test_uri.py +0 -0
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
from pathlib_next import Path, glob
|
|
4
4
|
from pathlib_next.mempath import MemPath
|
|
5
5
|
from pathlib_next.uri import Query, Source, Uri, UriPath
|
|
6
|
-
from pathlib_next.utils.sync import PathSyncer
|
|
6
|
+
from pathlib_next.utils.sync import PathAndStat, PathSyncer
|
|
7
7
|
|
|
8
8
|
rootless = Uri("sftp://root@sftpexample")
|
|
9
9
|
rootless.source
|
|
@@ -18,8 +18,6 @@ mempath.write_text("test")
|
|
|
18
18
|
check = mempath.read_text()
|
|
19
19
|
mempath.parent.rm(recursive=True)
|
|
20
20
|
|
|
21
|
-
test = list(os.scandir(local))
|
|
22
|
-
print(list(local.iterdir()))
|
|
23
21
|
query = Query({"test": "://$#!1", "test2&": [1, 2]})
|
|
24
22
|
q2 = Query(str(query)).to_dict()
|
|
25
23
|
for name, value in query:
|
|
@@ -53,13 +51,13 @@ authkeys = sftp_root / "root/.ssh/authorized_keys"
|
|
|
53
51
|
print(authkeys.as_posix())
|
|
54
52
|
|
|
55
53
|
|
|
56
|
-
def checksum(uri:
|
|
57
|
-
stat = uri.stat
|
|
54
|
+
def checksum(uri: PathAndStat):
|
|
55
|
+
stat = uri.stat
|
|
58
56
|
return hash(stat.st_size)
|
|
59
57
|
|
|
60
58
|
|
|
61
59
|
syncer = PathSyncer(checksum, remove_missing=False)
|
|
62
|
-
syncer.sync((sftp_root / "root/.ssh"), dest, dry_run=
|
|
60
|
+
syncer.sync((sftp_root / "root/.ssh"), dest, dry_run=False)
|
|
63
61
|
|
|
64
62
|
rocky_repo = UriPath("http://dl.rockylinux.org/pub")
|
|
65
63
|
|
|
@@ -6,6 +6,9 @@ import bs4 as _bs4
|
|
|
6
6
|
import requests as _req
|
|
7
7
|
from htmllistparse import parse as _htmlparse
|
|
8
8
|
|
|
9
|
+
if _ty.TYPE_CHECKING:
|
|
10
|
+
from urllib3.response import HTTPResponse
|
|
11
|
+
|
|
9
12
|
from ... import utils as _utils
|
|
10
13
|
from ...utils.stat import FileStat
|
|
11
14
|
from .. import UriPath
|
|
@@ -119,10 +122,12 @@ class HttpPath(UriPath):
|
|
|
119
122
|
raise NotImplementedError(mode)
|
|
120
123
|
buffer_size = _io.DEFAULT_BUFFER_SIZE if buffering < 0 else buffering
|
|
121
124
|
req = self.backend.request("GET", self.as_uri(), stream=True)
|
|
125
|
+
resp: "HTTPResponse" = req.raw
|
|
126
|
+
resp.auto_close = False
|
|
122
127
|
return (
|
|
123
|
-
|
|
128
|
+
resp
|
|
124
129
|
if buffer_size == 0
|
|
125
|
-
else _io.BufferedReader(
|
|
130
|
+
else _io.BufferedReader(resp, buffer_size=buffer_size)
|
|
126
131
|
)
|
|
127
132
|
|
|
128
133
|
def is_dir(self):
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import enum as _enum
|
|
2
|
+
import typing as _ty
|
|
3
|
+
|
|
4
|
+
from ..path import Path
|
|
5
|
+
from ..utils.stat import FileStat
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SyncEvent(_enum.Enum):
|
|
9
|
+
Copy = 1
|
|
10
|
+
RemovedMissing = 2
|
|
11
|
+
TypeMismatch = 6
|
|
12
|
+
SyncStart = 5
|
|
13
|
+
Synced = 3
|
|
14
|
+
CreatedDirectory = 4
|
|
15
|
+
CheckTargetChild = _enum.auto()
|
|
16
|
+
CheckTargetChildren = _enum.auto()
|
|
17
|
+
SyncChild = _enum.auto()
|
|
18
|
+
SyncChildren = _enum.auto()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PathAndStat(object):
|
|
22
|
+
__slots__ = ("_path", "_stat")
|
|
23
|
+
|
|
24
|
+
def __init__(self, path: Path, *, follow_symlink=None) -> None:
|
|
25
|
+
self._path = path
|
|
26
|
+
self.refresh(follow_symlink)
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return str(self.path)
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return str((self.path, self._stat))
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def path(self):
|
|
36
|
+
return self._path
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def stat(self):
|
|
40
|
+
return self._stat
|
|
41
|
+
|
|
42
|
+
def exists(self):
|
|
43
|
+
return self.stat != None
|
|
44
|
+
|
|
45
|
+
def refresh(self, follow_symlink: bool):
|
|
46
|
+
self._stat = FileStat.from_path(self.path, follow_symlink=follow_symlink)
|
|
47
|
+
|
|
48
|
+
def __getattr__(self, name: str):
|
|
49
|
+
if name.startswith("is_"):
|
|
50
|
+
if self.stat:
|
|
51
|
+
return getattr(self.stat, name)
|
|
52
|
+
else:
|
|
53
|
+
return lambda *args, **kwargs: False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if _ty.TYPE_CHECKING:
|
|
57
|
+
|
|
58
|
+
class PathAndStat(PathAndStat, FileStat): ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _OnPathSyncerError(_ty.Protocol):
|
|
62
|
+
def __call__(
|
|
63
|
+
self,
|
|
64
|
+
error: Exception,
|
|
65
|
+
source: PathAndStat,
|
|
66
|
+
target: PathAndStat,
|
|
67
|
+
event: SyncEvent,
|
|
68
|
+
) -> bool: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PathSyncer(object):
|
|
72
|
+
__slots__ = (
|
|
73
|
+
"checksum",
|
|
74
|
+
"_hook",
|
|
75
|
+
"remove_missing",
|
|
76
|
+
"follow_symlinks",
|
|
77
|
+
"ignore_error",
|
|
78
|
+
)
|
|
79
|
+
EVENT_LOG_FORMAT = "[{event}] Source:{source} Target:{target} DryRun:{dry_run}"
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
checksum: _ty.Callable[[PathAndStat], int],
|
|
84
|
+
/,
|
|
85
|
+
remove_missing: bool = False,
|
|
86
|
+
follow_symlinks: bool = True,
|
|
87
|
+
hook: _ty.Callable[[PathAndStat, PathAndStat, SyncEvent, bool], None] = None,
|
|
88
|
+
ignore_error: _OnPathSyncerError | bool = False,
|
|
89
|
+
) -> None:
|
|
90
|
+
self.checksum = checksum
|
|
91
|
+
self.remove_missing = remove_missing
|
|
92
|
+
self._hook = hook
|
|
93
|
+
self.follow_symlinks = follow_symlinks
|
|
94
|
+
if not callable(ignore_error):
|
|
95
|
+
_ignore_error = lambda *args, **kwargs: bool(ignore_error)
|
|
96
|
+
else:
|
|
97
|
+
_ignore_error = ignore_error
|
|
98
|
+
self.ignore_error = _ty.cast(_OnPathSyncerError, _ignore_error)
|
|
99
|
+
|
|
100
|
+
def log(self, msg: str, **kwargs: str):
|
|
101
|
+
print(msg.format_map(kwargs))
|
|
102
|
+
|
|
103
|
+
def hook(
|
|
104
|
+
self,
|
|
105
|
+
source: PathAndStat,
|
|
106
|
+
target: PathAndStat,
|
|
107
|
+
event: SyncEvent,
|
|
108
|
+
dry_run: bool,
|
|
109
|
+
do: _ty.Callable[[], None] = None,
|
|
110
|
+
):
|
|
111
|
+
if not dry_run and do:
|
|
112
|
+
try:
|
|
113
|
+
do()
|
|
114
|
+
except Exception as e:
|
|
115
|
+
if self.ignore_error(e, source, target, event):
|
|
116
|
+
return e
|
|
117
|
+
raise
|
|
118
|
+
if self._hook:
|
|
119
|
+
self._hook(source, target, event, dry_run)
|
|
120
|
+
self.log(
|
|
121
|
+
self.EVENT_LOG_FORMAT,
|
|
122
|
+
event=event,
|
|
123
|
+
source=source,
|
|
124
|
+
target=target,
|
|
125
|
+
dry_run=dry_run,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def sync(
|
|
129
|
+
self,
|
|
130
|
+
source: Path | PathAndStat,
|
|
131
|
+
target: Path | PathAndStat,
|
|
132
|
+
/,
|
|
133
|
+
dry_run: bool = False,
|
|
134
|
+
ignore_error: (
|
|
135
|
+
bool | _ty.Callable[[Exception, PathAndStat, PathAndStat], None]
|
|
136
|
+
) = False,
|
|
137
|
+
):
|
|
138
|
+
checksum = self.checksum
|
|
139
|
+
|
|
140
|
+
def start():
|
|
141
|
+
nonlocal source, target
|
|
142
|
+
source = (
|
|
143
|
+
PathAndStat(source, follow_symlink=self.follow_symlinks)
|
|
144
|
+
if not isinstance(source, PathAndStat)
|
|
145
|
+
else source
|
|
146
|
+
)
|
|
147
|
+
target = (
|
|
148
|
+
PathAndStat(target, follow_symlink=self.follow_symlinks)
|
|
149
|
+
if not isinstance(target, PathAndStat)
|
|
150
|
+
else target
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if self.hook(source, target, SyncEvent.SyncStart, False, start):
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if not source.exists():
|
|
157
|
+
if self.remove_missing:
|
|
158
|
+
if self.hook(
|
|
159
|
+
source,
|
|
160
|
+
target,
|
|
161
|
+
SyncEvent.RemovedMissing,
|
|
162
|
+
dry_run,
|
|
163
|
+
lambda: target.path.rm(recursive=True, missing_ok=True),
|
|
164
|
+
):
|
|
165
|
+
return
|
|
166
|
+
elif source.is_symlink():
|
|
167
|
+
error = NotImplementedError("symlink sync not implemented yet")
|
|
168
|
+
if not ignore_error(error, source, target, None):
|
|
169
|
+
raise error
|
|
170
|
+
return
|
|
171
|
+
elif source.is_file():
|
|
172
|
+
synced = False
|
|
173
|
+
if target.is_file():
|
|
174
|
+
if checksum(target) == checksum(source):
|
|
175
|
+
synced = True
|
|
176
|
+
if not synced:
|
|
177
|
+
|
|
178
|
+
def copy():
|
|
179
|
+
if target.is_file() or target.is_symlink():
|
|
180
|
+
target.path.unlink()
|
|
181
|
+
else:
|
|
182
|
+
if target.exists():
|
|
183
|
+
target.path.rm(recursive=target.is_dir())
|
|
184
|
+
source.path.copy(target.path)
|
|
185
|
+
|
|
186
|
+
if self.hook(source, target, SyncEvent.Copy, dry_run, copy):
|
|
187
|
+
return
|
|
188
|
+
else:
|
|
189
|
+
if target.is_file():
|
|
190
|
+
if self.hook(
|
|
191
|
+
source,
|
|
192
|
+
target,
|
|
193
|
+
SyncEvent.TypeMismatch,
|
|
194
|
+
dry_run,
|
|
195
|
+
lambda: target.path.unlink(),
|
|
196
|
+
):
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
target._stat = None
|
|
200
|
+
|
|
201
|
+
if not target.exists():
|
|
202
|
+
if self.hook(
|
|
203
|
+
source,
|
|
204
|
+
target,
|
|
205
|
+
SyncEvent.CreatedDirectory,
|
|
206
|
+
dry_run,
|
|
207
|
+
lambda: target.path.mkdir(),
|
|
208
|
+
):
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if self.remove_missing:
|
|
212
|
+
|
|
213
|
+
def checkchildren():
|
|
214
|
+
for child in target.path.iterdir():
|
|
215
|
+
|
|
216
|
+
def checkchild():
|
|
217
|
+
if not (source.path / child.name).exists():
|
|
218
|
+
self.hook(
|
|
219
|
+
source,
|
|
220
|
+
target,
|
|
221
|
+
SyncEvent.RemovedMissing,
|
|
222
|
+
dry_run,
|
|
223
|
+
lambda: child.rm(recursive=True),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
self.hook(
|
|
227
|
+
source,
|
|
228
|
+
target,
|
|
229
|
+
SyncEvent.CheckTargetChild,
|
|
230
|
+
False,
|
|
231
|
+
checkchild,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.hook(
|
|
235
|
+
source,
|
|
236
|
+
target,
|
|
237
|
+
SyncEvent.CheckTargetChildren,
|
|
238
|
+
False,
|
|
239
|
+
checkchildren,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def sync_children():
|
|
243
|
+
for child in source.path.iterdir():
|
|
244
|
+
self.hook(
|
|
245
|
+
source,
|
|
246
|
+
target,
|
|
247
|
+
SyncEvent.SyncChild,
|
|
248
|
+
False,
|
|
249
|
+
lambda: self.sync(
|
|
250
|
+
child,
|
|
251
|
+
target.path / (child.name or child.parent.name),
|
|
252
|
+
dry_run,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self.hook(source, target, SyncEvent.SyncChildren, False, sync_children)
|
|
257
|
+
|
|
258
|
+
self.hook(source, target, SyncEvent.Synced, dry_run)
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import enum as _enum
|
|
2
|
-
import typing as _ty
|
|
3
|
-
|
|
4
|
-
from ..path import Path
|
|
5
|
-
from ..utils.stat import FileStat
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SyncEvent(_enum.Enum):
|
|
9
|
-
Copy = 1
|
|
10
|
-
RemovedMissing = 2
|
|
11
|
-
SyncStart = 5
|
|
12
|
-
Synced = 3
|
|
13
|
-
CreatedDirectory = 4
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class PathSyncer(object):
|
|
17
|
-
__slots__ = ("checksum", "_hook", "remove_missing", "follow_symlinks")
|
|
18
|
-
EVENT_LOG_FORMAT = "[{event}] Source:{source} Target:{target} DryRun:{dry_run}"
|
|
19
|
-
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
checksum: _ty.Callable[[Path], int],
|
|
23
|
-
/,
|
|
24
|
-
remove_missing: bool = False,
|
|
25
|
-
follow_symlinks: bool = True,
|
|
26
|
-
hook: _ty.Callable[[Path, Path, SyncEvent, bool], None] = None,
|
|
27
|
-
) -> None:
|
|
28
|
-
self.checksum = checksum
|
|
29
|
-
self.remove_missing = remove_missing
|
|
30
|
-
self._hook = hook
|
|
31
|
-
self.follow_symlinks = follow_symlinks
|
|
32
|
-
|
|
33
|
-
def log(self, msg: str, **kwargs: str):
|
|
34
|
-
print(msg.format_map(kwargs))
|
|
35
|
-
|
|
36
|
-
def hook(
|
|
37
|
-
self,
|
|
38
|
-
source: Path,
|
|
39
|
-
target: Path,
|
|
40
|
-
event: SyncEvent,
|
|
41
|
-
dry_run: bool,
|
|
42
|
-
):
|
|
43
|
-
if self._hook:
|
|
44
|
-
self._hook(source, target, event, dry_run)
|
|
45
|
-
self.log(
|
|
46
|
-
self.EVENT_LOG_FORMAT,
|
|
47
|
-
event=event,
|
|
48
|
-
source=source,
|
|
49
|
-
target=target,
|
|
50
|
-
dry_run=dry_run,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
def sync(self, source: Path, target: Path, /, dry_run: bool = False):
|
|
54
|
-
checksum = self.checksum
|
|
55
|
-
self.hook(source, target, SyncEvent.SyncStart, dry_run)
|
|
56
|
-
|
|
57
|
-
src_stat = FileStat.from_path(source, follow_symlink=self.follow_symlinks)
|
|
58
|
-
tgt_stat = FileStat.from_path(target, follow_symlink=self.follow_symlinks)
|
|
59
|
-
|
|
60
|
-
if src_stat is None:
|
|
61
|
-
if self.remove_missing:
|
|
62
|
-
if not dry_run:
|
|
63
|
-
target.rm(recursive=True, missing_ok=True)
|
|
64
|
-
self.hook(source, target, SyncEvent.RemovedMissing, dry_run)
|
|
65
|
-
elif src_stat.is_symlink():
|
|
66
|
-
raise NotImplementedError("symlink sync not implemented yet")
|
|
67
|
-
elif src_stat.is_file():
|
|
68
|
-
synced = False
|
|
69
|
-
if tgt_stat and tgt_stat.is_file():
|
|
70
|
-
if checksum(target) == checksum(source):
|
|
71
|
-
synced = True
|
|
72
|
-
if not synced:
|
|
73
|
-
if not dry_run:
|
|
74
|
-
if tgt_stat:
|
|
75
|
-
if tgt_stat.is_file() or tgt_stat.is_symlink():
|
|
76
|
-
target.unlink()
|
|
77
|
-
else:
|
|
78
|
-
if target:
|
|
79
|
-
target.rm(recursive=tgt_stat.is_dir())
|
|
80
|
-
source.copy(target)
|
|
81
|
-
self.hook(source, target, SyncEvent.Copy, dry_run)
|
|
82
|
-
else:
|
|
83
|
-
t_exists = tgt_stat != None
|
|
84
|
-
if tgt_stat and tgt_stat.is_file():
|
|
85
|
-
if not dry_run:
|
|
86
|
-
target.unlink()
|
|
87
|
-
t_exists = False
|
|
88
|
-
|
|
89
|
-
if not t_exists:
|
|
90
|
-
if not dry_run:
|
|
91
|
-
target.mkdir()
|
|
92
|
-
self.hook(source, target, SyncEvent.CreatedDirectory, dry_run)
|
|
93
|
-
|
|
94
|
-
if self.remove_missing:
|
|
95
|
-
for child in target.iterdir():
|
|
96
|
-
if not (source / child.name).exists():
|
|
97
|
-
if not dry_run:
|
|
98
|
-
child.rm(recursive=True)
|
|
99
|
-
self.hook(source, target, SyncEvent.RemovedMissing, dry_run)
|
|
100
|
-
|
|
101
|
-
for child in source.iterdir():
|
|
102
|
-
self.sync(child, target / child.name, dry_run)
|
|
103
|
-
|
|
104
|
-
self.hook(source, target, SyncEvent.Synced, dry_run)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|