pathlib-next 0.1.7__tar.gz → 0.2.0__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.1.7 → pathlib_next-0.2.0}/PKG-INFO +7 -1
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/pyproject.toml +3 -2
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/example.py +11 -5
- pathlib_next-0.2.0/src/pathlib_next/__init__.py +10 -0
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/fspath.py +6 -5
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/path.py +1 -2
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/uri/__init__.py +19 -14
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/uri/query.py +3 -2
- pathlib_next-0.2.0/src/pathlib_next/uri/schemes/__init__.py +10 -0
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/uri/schemes/file.py +3 -2
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/uri/schemes/http.py +10 -9
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/uri/schemes/sftp.py +10 -5
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/uri/source.py +4 -3
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/utils/__init__.py +8 -8
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/utils/stat.py +68 -2
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/utils/sync.py +26 -13
- pathlib_next-0.2.0/tests/test_uri.py +99 -0
- pathlib_next-0.1.7/src/pathlib_next/__init__.py +0 -7
- pathlib_next-0.1.7/src/pathlib_next/uri/schemes/__init__.py +0 -10
- pathlib_next-0.1.7/tests/test_uri.py +0 -75
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/.gitignore +0 -0
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/LICENSE +0 -0
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/README.md +0 -0
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/mempath.py +0 -0
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/src/pathlib_next/utils/glob.py +0 -0
- {pathlib_next-0.1.7 → pathlib_next-0.2.0}/tests/test_local.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pathlib_next
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Generic Path Protocol based pathlib
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/pathlib_next/
|
|
6
6
|
Project-URL: Issues, https://github.com/jose-pr/pathlib_next/issues
|
|
@@ -10,7 +10,13 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.11
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: build; extra == 'dev'
|
|
15
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
16
|
+
Requires-Dist: twine; extra == 'dev'
|
|
13
17
|
Provides-Extra: http
|
|
18
|
+
Requires-Dist: bs4; extra == 'http'
|
|
19
|
+
Requires-Dist: htmllistparse; extra == 'http'
|
|
14
20
|
Requires-Dist: requests; extra == 'http'
|
|
15
21
|
Requires-Dist: uritools; extra == 'http'
|
|
16
22
|
Provides-Extra: sftp
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pathlib_next"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
authors = [{ name = "Jose A" }]
|
|
9
9
|
description = "Generic Path Protocol based pathlib"
|
|
10
10
|
readme = "README.md"
|
|
@@ -17,8 +17,9 @@ classifiers = [
|
|
|
17
17
|
dependencies = []
|
|
18
18
|
[project.optional-dependencies]
|
|
19
19
|
uri = ["uritools"]
|
|
20
|
-
http = ["requests", "pathlib_next[uri]"]
|
|
20
|
+
http = ["requests", "pathlib_next[uri]", 'htmllistparse', 'bs4']
|
|
21
21
|
sftp = ["paramiko", "pathlib_next[uri]"]
|
|
22
|
+
dev = ['build', 'twine', 'hatchling']
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
[project.urls]
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
1
3
|
from pathlib_next import Path, glob
|
|
2
4
|
from pathlib_next.mempath import MemPath
|
|
3
|
-
from pathlib_next.uri import Query, Source, UriPath
|
|
4
|
-
from pathlib_next.uri.schemes import *
|
|
5
|
+
from pathlib_next.uri import Query, Source, Uri, UriPath
|
|
5
6
|
from pathlib_next.utils.sync import PathSyncer
|
|
6
7
|
|
|
8
|
+
rootless = Uri("sftp://root@sftpexample")
|
|
9
|
+
rootless.source
|
|
10
|
+
authkeys = rootless / "root/.ssh/authorized_keys"
|
|
11
|
+
keys = authkeys.as_uri()
|
|
12
|
+
|
|
13
|
+
local = Path("./_ssh")
|
|
14
|
+
|
|
7
15
|
mempath = MemPath("test/test3") / "subpath"
|
|
8
16
|
mempath.parent.mkdir(parents=True, exist_ok=True)
|
|
9
17
|
mempath.write_text("test")
|
|
10
18
|
check = mempath.read_text()
|
|
11
19
|
mempath.parent.rm(recursive=True)
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
local = Path("./_ssh")
|
|
21
|
+
test = list(os.scandir(local))
|
|
16
22
|
print(list(local.iterdir()))
|
|
17
23
|
query = Query({"test": "://$#!1", "test2&": [1, 2]})
|
|
18
24
|
q2 = Query(str(query)).to_dict()
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import pathlib as _path
|
|
2
|
-
import os as _os
|
|
3
1
|
import functools as _func
|
|
2
|
+
import os as _os
|
|
3
|
+
import pathlib as _path
|
|
4
|
+
import re as _re
|
|
4
5
|
import typing as _ty
|
|
6
|
+
|
|
5
7
|
from . import path as _proto
|
|
6
|
-
import re as _re
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
@_func.cache
|
|
@@ -32,7 +33,7 @@ class _BaseFSPathname(_path.PurePath, _proto.Pathname):
|
|
|
32
33
|
@property
|
|
33
34
|
def segments(self):
|
|
34
35
|
return self.parts
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
def with_segments(self, *args: str | _proto.FsPathLike):
|
|
37
38
|
return type(self)(*args)
|
|
38
39
|
|
|
@@ -79,4 +80,4 @@ class LocalPath(
|
|
|
79
80
|
include_hidden=include_hidden,
|
|
80
81
|
recursive=recursive,
|
|
81
82
|
dironly=dironly,
|
|
82
|
-
)
|
|
83
|
+
)
|
|
@@ -177,7 +177,7 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
177
177
|
def __truediv__(self, key: _ty.Self | str) -> _ty.Self:
|
|
178
178
|
try:
|
|
179
179
|
return type(self)(self, key)
|
|
180
|
-
except (TypeError, NotImplementedError):
|
|
180
|
+
except (TypeError, NotImplementedError) as _err:
|
|
181
181
|
return NotImplemented
|
|
182
182
|
|
|
183
183
|
@property
|
|
@@ -208,7 +208,6 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
208
208
|
path_pattern = _glob.compile_pattern(path_pattern, case_sensitive)
|
|
209
209
|
return path_pattern.match(path) is not None
|
|
210
210
|
|
|
211
|
-
@_utils.notimplemented
|
|
212
211
|
def as_posix(self) -> str:
|
|
213
212
|
return "/".join(self.segments)
|
|
214
213
|
|
|
@@ -137,6 +137,13 @@ class Uri(Pathname):
|
|
|
137
137
|
if _path.startswith("/"):
|
|
138
138
|
break
|
|
139
139
|
|
|
140
|
+
if (
|
|
141
|
+
(source.host or source.userinfo or source.port)
|
|
142
|
+
and _path
|
|
143
|
+
and not _path.startswith("/")
|
|
144
|
+
):
|
|
145
|
+
_path = "/" + _path
|
|
146
|
+
|
|
140
147
|
self._init(source, _path, query, fragment)
|
|
141
148
|
|
|
142
149
|
def _init(self, source: Source, path: str, query: str, fragment: str, **kwargs):
|
|
@@ -201,7 +208,10 @@ class Uri(Pathname):
|
|
|
201
208
|
raise NotImplementedError(f"fspath for {self.source.scheme}")
|
|
202
209
|
|
|
203
210
|
def __repr__(self):
|
|
204
|
-
|
|
211
|
+
if self._initiated:
|
|
212
|
+
return "{}({!r})".format(type(self).__name__, str(self))
|
|
213
|
+
else:
|
|
214
|
+
return super().__repr__()
|
|
205
215
|
|
|
206
216
|
def as_uri(self, /, sanitize=False):
|
|
207
217
|
if self._uri is None or sanitize:
|
|
@@ -398,6 +408,13 @@ class UriPath(Uri, Path):
|
|
|
398
408
|
inst._init(uri.source, uri.path, uri.query, uri.fragment, **kwargs)
|
|
399
409
|
else:
|
|
400
410
|
inst = Uri.__new__(cls, *args, **kwargs)
|
|
411
|
+
backend = kwargs.get("backend", None)
|
|
412
|
+
if backend is None:
|
|
413
|
+
for segment in reversed(args):
|
|
414
|
+
if isinstance(segment, cls):
|
|
415
|
+
backend = segment.backend
|
|
416
|
+
break
|
|
417
|
+
inst._backend = backend
|
|
401
418
|
return inst
|
|
402
419
|
|
|
403
420
|
def _initbackend(self):
|
|
@@ -433,18 +450,6 @@ class UriPath(Uri, Path):
|
|
|
433
450
|
def with_backend(self, backend):
|
|
434
451
|
return self._from_parsed_parts(*self.parts, backend=backend)
|
|
435
452
|
|
|
436
|
-
def _load_parts(self):
|
|
437
|
-
super()._load_parts()
|
|
438
|
-
if self.source and self.source.scheme and self._backend is None:
|
|
439
|
-
for uri in reversed(self._raw_uris):
|
|
440
|
-
if (
|
|
441
|
-
isinstance(uri, UriPath)
|
|
442
|
-
and uri.source.scheme == self.source.scheme
|
|
443
|
-
and uri._backend
|
|
444
|
-
):
|
|
445
|
-
self._backend = uri._backend
|
|
446
|
-
break
|
|
447
|
-
|
|
448
453
|
def __truediv__(self, key: str | Uri | os.PathLike):
|
|
449
454
|
try:
|
|
450
455
|
return type(self)(self, key, findclass=True)
|
|
@@ -456,7 +461,7 @@ class UriPath(Uri, Path):
|
|
|
456
461
|
if not source:
|
|
457
462
|
inst = Uri.__new__(UriPath)
|
|
458
463
|
elif source.scheme not in cls._schemes():
|
|
459
|
-
inst =
|
|
464
|
+
inst = cls.__new__(cls, source.scheme + ":", findclass=True)
|
|
460
465
|
else:
|
|
461
466
|
inst = cls.__new__(cls, backend=self._backend)
|
|
462
467
|
inst._init(source, self.path, self.query, self.fragment)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import typing as _ty
|
|
2
|
+
|
|
2
3
|
import uritools as _uritools
|
|
3
4
|
|
|
4
5
|
|
|
@@ -16,7 +17,7 @@ class Query(str):
|
|
|
16
17
|
),
|
|
17
18
|
*,
|
|
18
19
|
encoding=ENCODING,
|
|
19
|
-
separator=SEPARATOR
|
|
20
|
+
separator=SEPARATOR,
|
|
20
21
|
):
|
|
21
22
|
if isinstance(query, Query):
|
|
22
23
|
_encoding = query._encoding
|
|
@@ -44,7 +45,7 @@ class Query(str):
|
|
|
44
45
|
return _uritools.SplitResultString("", "", "", str(query), "").getquerylist(
|
|
45
46
|
query._separator, query._encoding
|
|
46
47
|
)
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
def __iter__(self):
|
|
49
50
|
return iter(self.decode())
|
|
50
51
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
from ...utils.stat import FileStat
|
|
2
|
-
from .. import UriPath
|
|
3
|
-
from ... import utils as _utils
|
|
4
1
|
import io as _io
|
|
5
|
-
import requests as _req
|
|
6
|
-
import typing as _ty
|
|
7
|
-
import bs4 as _bs4
|
|
8
2
|
import time as _time
|
|
3
|
+
import typing as _ty
|
|
9
4
|
|
|
5
|
+
import bs4 as _bs4
|
|
6
|
+
import requests as _req
|
|
10
7
|
from htmllistparse import parse as _htmlparse
|
|
11
8
|
|
|
9
|
+
from ... import utils as _utils
|
|
10
|
+
from ...utils.stat import FileStat
|
|
11
|
+
from .. import UriPath
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
class _FileEntry(_ty.NamedTuple):
|
|
14
15
|
name: str
|
|
@@ -42,7 +43,7 @@ class HttpPath(UriPath):
|
|
|
42
43
|
return HttpBackend(_req.Session(), {})
|
|
43
44
|
|
|
44
45
|
def _listdir(self) -> list[_FileEntry]:
|
|
45
|
-
req = self.backend.
|
|
46
|
+
req = self.backend.request("GET", self)
|
|
46
47
|
req.raise_for_status()
|
|
47
48
|
soup = _bs4.BeautifulSoup(req.content, "html5lib")
|
|
48
49
|
_, listing = _htmlparse(soup)
|
|
@@ -64,7 +65,7 @@ class HttpPath(UriPath):
|
|
|
64
65
|
or resp.url.endswith("/.")
|
|
65
66
|
)
|
|
66
67
|
|
|
67
|
-
def stat(self):
|
|
68
|
+
def stat(self, *, follow_symlinks=True, walk_up_last_modified=False):
|
|
68
69
|
check = (
|
|
69
70
|
[self.with_path(self.path.removesuffix("/")), self]
|
|
70
71
|
if self.path.endswith("/")
|
|
@@ -90,7 +91,7 @@ class HttpPath(UriPath):
|
|
|
90
91
|
|
|
91
92
|
st_size = 0 if self._isdir else int(resp.headers.get("Content-Length", 0))
|
|
92
93
|
lm = resp.headers.get("Last-Modified")
|
|
93
|
-
if lm is None:
|
|
94
|
+
if lm is None and walk_up_last_modified:
|
|
94
95
|
parent = self.parent
|
|
95
96
|
if self != parent:
|
|
96
97
|
try:
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import typing as _ty
|
|
2
|
-
from .. import UriPath, Source
|
|
3
|
-
from ... import utils as _utils
|
|
4
1
|
import threading as _thread
|
|
2
|
+
import typing as _ty
|
|
3
|
+
|
|
5
4
|
import paramiko as _paramiko
|
|
6
5
|
|
|
6
|
+
from ... import utils as _utils
|
|
7
|
+
from .. import Source, UriPath
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
class BaseSftpBackend(object):
|
|
9
11
|
__slots__ = ()
|
|
@@ -77,8 +79,11 @@ class SftpPath(UriPath):
|
|
|
77
79
|
for path in self._sftpclient.listdir(self.path):
|
|
78
80
|
yield path
|
|
79
81
|
|
|
80
|
-
def stat(self):
|
|
81
|
-
|
|
82
|
+
def stat(self, *, follow_symlinks=True):
|
|
83
|
+
if follow_symlinks:
|
|
84
|
+
return self._sftpclient.stat(self.path)
|
|
85
|
+
else:
|
|
86
|
+
return self._sftpclient.lstat(self.path)
|
|
82
87
|
|
|
83
88
|
def _open(self, mode="r", buffering=-1):
|
|
84
89
|
return self._sftpclient.open(self.path, mode, buffering)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import ipaddress as _ip
|
|
2
|
+
import socket as _socket
|
|
1
3
|
import typing as _ty
|
|
4
|
+
|
|
2
5
|
import uritools as _uritools
|
|
3
|
-
import socket as _socket
|
|
4
|
-
import ipaddress as _ip
|
|
5
6
|
|
|
6
7
|
from .. import utils as _utils
|
|
7
8
|
|
|
@@ -19,7 +20,7 @@ class Source(_ty.NamedTuple):
|
|
|
19
20
|
|
|
20
21
|
def __bool__(self):
|
|
21
22
|
for val in self:
|
|
22
|
-
if val ==
|
|
23
|
+
if val == "" or val is None:
|
|
23
24
|
continue
|
|
24
25
|
return True
|
|
25
26
|
return False
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import collections
|
|
1
2
|
import functools as _functools
|
|
2
|
-
from threading import RLock
|
|
3
|
-
import typing as _ty
|
|
4
3
|
import time as _time
|
|
4
|
+
import typing as _ty
|
|
5
5
|
from email.utils import parsedate as _parsedate
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import collections
|
|
6
|
+
from threading import RLock
|
|
9
7
|
|
|
10
8
|
K = _ty.ParamSpec("K")
|
|
11
9
|
V = _ty.TypeVar("V")
|
|
@@ -22,7 +20,7 @@ class LRU(_ty.Generic[K, V]):
|
|
|
22
20
|
@property
|
|
23
21
|
def maxsize(self):
|
|
24
22
|
return self._maxsize
|
|
25
|
-
|
|
23
|
+
|
|
26
24
|
@maxsize.setter
|
|
27
25
|
def maxsize(self, maxsize: int):
|
|
28
26
|
cache = self.cache
|
|
@@ -78,11 +76,13 @@ def notimplemented(method):
|
|
|
78
76
|
|
|
79
77
|
return _notimplemented
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
|
|
82
80
|
import ipaddress as _ip
|
|
81
|
+
import socket as _socket
|
|
82
|
+
|
|
83
83
|
|
|
84
84
|
def get_machine_ips():
|
|
85
|
-
ips:list[_ip.IPv4Address|_ip.IPv6Address] = list()
|
|
85
|
+
ips: list[_ip.IPv4Address | _ip.IPv6Address] = list()
|
|
86
86
|
for item in _socket.getaddrinfo(_socket.gethostname(), None):
|
|
87
87
|
protocol, *_, (ip, *_) = item
|
|
88
88
|
if protocol == _socket.AddressFamily.AF_INET:
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import abc as _abc
|
|
1
2
|
import stat as _stat
|
|
2
3
|
import typing as _ty
|
|
3
|
-
|
|
4
|
+
|
|
4
5
|
from .. import utils as _utils
|
|
5
6
|
|
|
7
|
+
if _ty.TYPE_CHECKING:
|
|
8
|
+
from os import stat_result as _os_stat
|
|
9
|
+
|
|
10
|
+
from ..path import Path
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
class FileStatLike(_ty.Protocol):
|
|
7
14
|
__slots__ = ()
|
|
8
15
|
|
|
@@ -16,6 +23,7 @@ class FileStatLike(_ty.Protocol):
|
|
|
16
23
|
@_abc.abstractmethod
|
|
17
24
|
def st_mtime(self) -> int: ...
|
|
18
25
|
|
|
26
|
+
|
|
19
27
|
class FileStat(FileStatLike):
|
|
20
28
|
__slots__ = (
|
|
21
29
|
"st_mode",
|
|
@@ -74,4 +82,62 @@ class FileStat(FileStatLike):
|
|
|
74
82
|
|
|
75
83
|
def __str__(self):
|
|
76
84
|
props = [f"{k}={v}" for k, v in self.items()]
|
|
77
|
-
return "<%s %s>" % (type(self).__name__, ", ".join(props))
|
|
85
|
+
return "<%s %s>" % (type(self).__name__, ", ".join(props))
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_path(cls, path: "Path", *, follow_symlink=True):
|
|
89
|
+
try:
|
|
90
|
+
stat: _os_stat = path.stat(follow_symlinks=follow_symlink)
|
|
91
|
+
if isinstance(stat, FileStat):
|
|
92
|
+
return stat
|
|
93
|
+
else:
|
|
94
|
+
result = FileStat.__new__(FileStat)
|
|
95
|
+
for prop in FileStat.__slots__:
|
|
96
|
+
val = getattr(stat, prop, 0)
|
|
97
|
+
setattr(result, prop, val)
|
|
98
|
+
return result
|
|
99
|
+
except FileNotFoundError:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def is_dir(self):
|
|
103
|
+
"""
|
|
104
|
+
Whether this path is a directory.
|
|
105
|
+
"""
|
|
106
|
+
return _stat.S_ISDIR(self.st_mode)
|
|
107
|
+
|
|
108
|
+
def is_file(self):
|
|
109
|
+
"""
|
|
110
|
+
Whether this path is a regular file (also True for symlinks pointing
|
|
111
|
+
to regular files).
|
|
112
|
+
"""
|
|
113
|
+
return _stat.S_ISREG(self.st_mode)
|
|
114
|
+
|
|
115
|
+
def is_symlink(self):
|
|
116
|
+
"""
|
|
117
|
+
Whether this path is a symbolic link.
|
|
118
|
+
"""
|
|
119
|
+
return _stat.S_ISLNK(self.st_mode)
|
|
120
|
+
|
|
121
|
+
def is_block_device(self):
|
|
122
|
+
"""
|
|
123
|
+
Whether this path is a block device.
|
|
124
|
+
"""
|
|
125
|
+
return _stat.S_ISBLK(self.st_mode)
|
|
126
|
+
|
|
127
|
+
def is_char_device(self):
|
|
128
|
+
"""
|
|
129
|
+
Whether this path is a character device.
|
|
130
|
+
"""
|
|
131
|
+
return _stat.S_ISCHR(self.st_mode)
|
|
132
|
+
|
|
133
|
+
def is_fifo(self):
|
|
134
|
+
"""
|
|
135
|
+
Whether this path is a FIFO.
|
|
136
|
+
"""
|
|
137
|
+
return _stat.S_ISFIFO(self.st_mode)
|
|
138
|
+
|
|
139
|
+
def is_socket(self):
|
|
140
|
+
"""
|
|
141
|
+
Whether this path is a socket.
|
|
142
|
+
"""
|
|
143
|
+
return _stat.S_ISSOCK(self.st_mode)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import enum as _enum
|
|
1
2
|
import typing as _ty
|
|
3
|
+
|
|
2
4
|
from ..path import Path
|
|
3
|
-
|
|
5
|
+
from ..utils.stat import FileStat
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class SyncEvent(_enum.Enum):
|
|
@@ -12,7 +14,7 @@ class SyncEvent(_enum.Enum):
|
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class PathSyncer(object):
|
|
15
|
-
__slots__ = ("checksum", "_hook", "remove_missing")
|
|
17
|
+
__slots__ = ("checksum", "_hook", "remove_missing", "follow_symlinks")
|
|
16
18
|
EVENT_LOG_FORMAT = "[{event}] Source:{source} Target:{target} DryRun:{dry_run}"
|
|
17
19
|
|
|
18
20
|
def __init__(
|
|
@@ -20,11 +22,13 @@ class PathSyncer(object):
|
|
|
20
22
|
checksum: _ty.Callable[[Path], int],
|
|
21
23
|
/,
|
|
22
24
|
remove_missing: bool = False,
|
|
25
|
+
follow_symlinks: bool = True,
|
|
23
26
|
hook: _ty.Callable[[Path, Path, SyncEvent, bool], None] = None,
|
|
24
27
|
) -> None:
|
|
25
28
|
self.checksum = checksum
|
|
26
29
|
self.remove_missing = remove_missing
|
|
27
30
|
self._hook = hook
|
|
31
|
+
self.follow_symlinks = follow_symlinks
|
|
28
32
|
|
|
29
33
|
def log(self, msg: str, **kwargs: str):
|
|
30
34
|
print(msg.format_map(kwargs))
|
|
@@ -46,34 +50,43 @@ class PathSyncer(object):
|
|
|
46
50
|
dry_run=dry_run,
|
|
47
51
|
)
|
|
48
52
|
|
|
49
|
-
def sync(
|
|
50
|
-
self, source: Path, target: Path, /, dry_run: bool = False
|
|
51
|
-
):
|
|
53
|
+
def sync(self, source: Path, target: Path, /, dry_run: bool = False):
|
|
52
54
|
checksum = self.checksum
|
|
53
55
|
self.hook(source, target, SyncEvent.SyncStart, dry_run)
|
|
54
56
|
|
|
55
|
-
|
|
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:
|
|
56
61
|
if self.remove_missing:
|
|
57
62
|
if not dry_run:
|
|
58
63
|
target.rm(recursive=True, missing_ok=True)
|
|
59
64
|
self.hook(source, target, SyncEvent.RemovedMissing, dry_run)
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
elif src_stat.is_symlink():
|
|
66
|
+
raise NotImplementedError("symlink sync not implemented yet")
|
|
67
|
+
elif src_stat.is_file():
|
|
62
68
|
synced = False
|
|
63
|
-
if
|
|
69
|
+
if tgt_stat and tgt_stat.is_file():
|
|
64
70
|
if checksum(target) == checksum(source):
|
|
65
71
|
synced = True
|
|
66
72
|
if not synced:
|
|
67
73
|
if not dry_run:
|
|
68
|
-
|
|
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())
|
|
69
80
|
source.copy(target)
|
|
70
81
|
self.hook(source, target, SyncEvent.Copy, dry_run)
|
|
71
82
|
else:
|
|
72
|
-
|
|
83
|
+
t_exists = tgt_stat != None
|
|
84
|
+
if tgt_stat and tgt_stat.is_file():
|
|
73
85
|
if not dry_run:
|
|
74
86
|
target.unlink()
|
|
87
|
+
t_exists = False
|
|
75
88
|
|
|
76
|
-
if not
|
|
89
|
+
if not t_exists:
|
|
77
90
|
if not dry_run:
|
|
78
91
|
target.mkdir()
|
|
79
92
|
self.hook(source, target, SyncEvent.CreatedDirectory, dry_run)
|
|
@@ -84,7 +97,7 @@ class PathSyncer(object):
|
|
|
84
97
|
if not dry_run:
|
|
85
98
|
child.rm(recursive=True)
|
|
86
99
|
self.hook(source, target, SyncEvent.RemovedMissing, dry_run)
|
|
87
|
-
|
|
100
|
+
|
|
88
101
|
for child in source.iterdir():
|
|
89
102
|
self.sync(child, target / child.name, dry_run)
|
|
90
103
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import src.pathlib_next as pathlib_next
|
|
6
|
+
from src.pathlib_next.uri import Uri
|
|
7
|
+
|
|
8
|
+
test_uris = ["http://user:pass@google.com:80"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# @pytest.mark.parametrize("_uri", test_uris)
|
|
12
|
+
def parse_uri(_uri: str):
|
|
13
|
+
uri = pathlib_next.Uri(_uri)
|
|
14
|
+
assert uri.as_uri() == _uri
|
|
15
|
+
return uri
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_source():
|
|
19
|
+
uri = parse_uri(test_uris[0])
|
|
20
|
+
assert uri.source.scheme == "http"
|
|
21
|
+
assert uri.source.host == "google.com"
|
|
22
|
+
assert uri.source.port == 80
|
|
23
|
+
assert uri.source.parsed_userinfo() == ("user", "pass")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_no_scheme():
|
|
27
|
+
uri = parse_uri("//user:pass@google.com:80/")
|
|
28
|
+
assert uri.source.scheme == None
|
|
29
|
+
assert uri.source.host == "google.com"
|
|
30
|
+
assert uri.source.port == 80
|
|
31
|
+
assert uri.source.parsed_userinfo() == ("user", "pass")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_no_scheme_with_host_no_pass():
|
|
35
|
+
uri = parse_uri("//user@google.com:80/")
|
|
36
|
+
assert uri.source.scheme == None
|
|
37
|
+
assert uri.source.host == "google.com"
|
|
38
|
+
assert uri.source.port == 80
|
|
39
|
+
assert uri.source.parsed_userinfo() == ("user", "")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_no_scheme_no_host():
|
|
43
|
+
uri = parse_uri("//user@:80/")
|
|
44
|
+
assert uri.source.scheme == None
|
|
45
|
+
assert uri.source.host == ""
|
|
46
|
+
assert uri.source.port == 80
|
|
47
|
+
assert uri.source.parsed_userinfo() == ("user", "")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_no_scheme_no_netloc():
|
|
51
|
+
uri = parse_uri("//user@")
|
|
52
|
+
assert uri.source.scheme == None
|
|
53
|
+
assert uri.source.host == ""
|
|
54
|
+
assert uri.source.port == None
|
|
55
|
+
assert uri.source.parsed_userinfo() == ("user", "")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_path():
|
|
59
|
+
uri = parse_uri("http://google.com/root/subroot/filename.ext")
|
|
60
|
+
assert uri.source.scheme == "http"
|
|
61
|
+
assert uri.source.host == "google.com"
|
|
62
|
+
assert uri.source.port == None
|
|
63
|
+
assert uri.source.parsed_userinfo() == ("", "")
|
|
64
|
+
assert uri.path == "/root/subroot/filename.ext"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_encoded_path():
|
|
68
|
+
uri = pathlib_next.Uri(
|
|
69
|
+
"http://goog%2Fe.com/root/subroot/%3Fquery/%23fragment/%2Fencoded%2Ffilename.ext"
|
|
70
|
+
)
|
|
71
|
+
assert uri.source.scheme == "http"
|
|
72
|
+
assert uri.source.host == "goog/e.com"
|
|
73
|
+
assert uri.source.port == None
|
|
74
|
+
assert uri.source.parsed_userinfo() == ("", "")
|
|
75
|
+
assert uri.path == "/root/subroot/?query/#fragment//encoded/filename.ext"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_child():
|
|
79
|
+
sftp_root = Uri("sftp://root@sftpexample/")
|
|
80
|
+
authkeys = sftp_root / "root/.ssh/authorized_keys"
|
|
81
|
+
uri = authkeys.as_uri()
|
|
82
|
+
assert uri == "sftp://root@sftpexample/root/.ssh/authorized_keys"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_truediv_pathlib():
|
|
86
|
+
sftp_root = Uri("sftp://root@sftpexample/")
|
|
87
|
+
authkeys = sftp_root / pathlib.PurePosixPath("root/.ssh/authorized_keys")
|
|
88
|
+
uri = authkeys.as_uri()
|
|
89
|
+
assert uri == "sftp://root@sftpexample/root/.ssh/authorized_keys"
|
|
90
|
+
|
|
91
|
+
authkeys = sftp_root / pathlib.Path("root/.ssh/authorized_keys")
|
|
92
|
+
uri = authkeys.as_uri()
|
|
93
|
+
assert uri == "file:/root/.ssh/authorized_keys"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_join_without_root():
|
|
97
|
+
authkeys = Uri("sftp://root@sftpexample") / "root/.ssh/authorized_keys"
|
|
98
|
+
uri = authkeys.as_uri()
|
|
99
|
+
assert uri == "sftp://root@sftpexample/root/.ssh/authorized_keys"
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
import src.pathlib_next as pathlib_next
|
|
3
|
-
from src.pathlib_next.uri import Uri
|
|
4
|
-
|
|
5
|
-
import pathlib
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_source():
|
|
9
|
-
uri = pathlib_next.Uri("http://user:pass@google.com:80")
|
|
10
|
-
assert uri.source.scheme == "http"
|
|
11
|
-
assert uri.source.host == "google.com"
|
|
12
|
-
assert uri.source.port == 80
|
|
13
|
-
assert uri.source.parsed_userinfo() == ('user','pass')
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_no_scheme():
|
|
17
|
-
uri = pathlib_next.Uri("//user:pass@google.com:80/")
|
|
18
|
-
assert uri.source.scheme == None
|
|
19
|
-
assert uri.source.host == "google.com"
|
|
20
|
-
assert uri.source.port == 80
|
|
21
|
-
assert uri.source.parsed_userinfo() == ('user','pass')
|
|
22
|
-
|
|
23
|
-
def test_no_scheme_with_host_no_pass():
|
|
24
|
-
uri = pathlib_next.Uri("//user@google.com:80/")
|
|
25
|
-
assert uri.source.scheme == None
|
|
26
|
-
assert uri.source.host == "google.com"
|
|
27
|
-
assert uri.source.port == 80
|
|
28
|
-
assert uri.source.parsed_userinfo() == ('user', '')
|
|
29
|
-
|
|
30
|
-
def test_no_scheme_no_host():
|
|
31
|
-
uri = pathlib_next.Uri("//user@:80/")
|
|
32
|
-
assert uri.source.scheme == None
|
|
33
|
-
assert uri.source.host == ""
|
|
34
|
-
assert uri.source.port == 80
|
|
35
|
-
assert uri.source.parsed_userinfo() == ('user', '')
|
|
36
|
-
|
|
37
|
-
def test_no_scheme_no_netloc():
|
|
38
|
-
uri = pathlib_next.Uri("//user@")
|
|
39
|
-
assert uri.source.scheme == None
|
|
40
|
-
assert uri.source.host == ""
|
|
41
|
-
assert uri.source.port == None
|
|
42
|
-
assert uri.source.parsed_userinfo() == ('user', '')
|
|
43
|
-
|
|
44
|
-
def test_path():
|
|
45
|
-
uri = pathlib_next.Uri("http://google.com/root/subroot/filename.ext")
|
|
46
|
-
assert uri.source.scheme == "http"
|
|
47
|
-
assert uri.source.host == "google.com"
|
|
48
|
-
assert uri.source.port == None
|
|
49
|
-
assert uri.source.parsed_userinfo() == ('','')
|
|
50
|
-
assert uri.path == '/root/subroot/filename.ext'
|
|
51
|
-
|
|
52
|
-
def test_encoded_path():
|
|
53
|
-
uri = pathlib_next.Uri("http://goog%2Fe.com/root/subroot/%3Fquery/%23fragment/%2Fencoded%2Ffilename.ext")
|
|
54
|
-
assert uri.source.scheme == "http"
|
|
55
|
-
assert uri.source.host == "goog/e.com"
|
|
56
|
-
assert uri.source.port == None
|
|
57
|
-
assert uri.source.parsed_userinfo() == ('','')
|
|
58
|
-
assert uri.path == '/root/subroot/?query/#fragment//encoded/filename.ext'
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_child():
|
|
62
|
-
sftp_root = Uri('sftp://root@sftpexample/')
|
|
63
|
-
authkeys = sftp_root / 'root/.ssh/authorized_keys'
|
|
64
|
-
uri = authkeys.as_uri()
|
|
65
|
-
assert uri == 'sftp://root@sftpexample/root/.ssh/authorized_keys'
|
|
66
|
-
|
|
67
|
-
def test_truediv_pathlib():
|
|
68
|
-
sftp_root = Uri('sftp://root@sftpexample/')
|
|
69
|
-
authkeys = sftp_root / pathlib.PurePosixPath('root/.ssh/authorized_keys')
|
|
70
|
-
uri = authkeys.as_uri()
|
|
71
|
-
assert uri == 'sftp://root@sftpexample/root/.ssh/authorized_keys'
|
|
72
|
-
authkeys = sftp_root / pathlib.Path('root/.ssh/authorized_keys')
|
|
73
|
-
uri = authkeys.as_uri()
|
|
74
|
-
assert uri == 'file:/root/.ssh/authorized_keys'
|
|
75
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|