pathlib-next 0.1.0__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.
@@ -0,0 +1,136 @@
1
+ from ...utils.stat import FileStat
2
+ from .. import UriPath
3
+ from ... import utils as _utils
4
+ import io as _io
5
+ import requests as _req
6
+ import typing as _ty
7
+ import bs4 as _bs4
8
+ import time as _time
9
+
10
+ from htmllistparse import parse as _htmlparse
11
+
12
+
13
+ class _FileEntry(_ty.NamedTuple):
14
+ name: str
15
+ modified: _ty.Optional[_time.struct_time]
16
+ size: _ty.Optional[int]
17
+ description: _ty.Optional[str]
18
+
19
+
20
+ class HttpBackend(_ty.NamedTuple):
21
+ session: _req.Session
22
+ requests_args: dict
23
+
24
+ def request(self, method, uri: "HttpPath|str", **kwargs):
25
+ return self.session.request(
26
+ **self.requests_args,
27
+ **kwargs,
28
+ method=method,
29
+ url=uri if isinstance(uri, str) else uri.as_uri(False),
30
+ )
31
+
32
+
33
+ class HttpPath(UriPath):
34
+ __SCHEMES = ("http", "https")
35
+ __slots__ = ("_isdir", "_session", "_requests_args")
36
+ _isdir: bool
37
+
38
+ if _ty.TYPE_CHECKING:
39
+ backend: HttpBackend
40
+
41
+ def _initbackend(self):
42
+ return HttpBackend(_req.Session(), {})
43
+
44
+ def _listdir(self) -> list[_FileEntry]:
45
+ req = self.backend.session.request("GET", self)
46
+ req.raise_for_status()
47
+ soup = _bs4.BeautifulSoup(req.content, "html5lib")
48
+ _, listing = _htmlparse(soup)
49
+ return listing
50
+
51
+ def iterdir(self):
52
+ _self = self.path.removesuffix("/")
53
+ cls = type(self)
54
+ for path in self._listdir():
55
+ inst = type(self).__new__(cls, backend=self.backend)
56
+ inst._init(self.source, f"{_self}/{path.name}", "", "")
57
+ yield inst
58
+
59
+ def _is_dir(self, resp: _req.Response):
60
+ return (
61
+ resp.is_redirect
62
+ or resp.url.endswith("/")
63
+ or resp.url.endswith("/..")
64
+ or resp.url.endswith("/.")
65
+ )
66
+
67
+ def stat(self):
68
+ check = (
69
+ [self.with_path(self.path.removesuffix("/")), self]
70
+ if self.path.endswith("/")
71
+ else [self]
72
+ )
73
+ for uri in check:
74
+ resp = self.backend.request("HEAD", uri, allow_redirects=False)
75
+ resp.close()
76
+ if resp.status_code < 400:
77
+ break
78
+
79
+ if self._isdir is None:
80
+ self._isdir = self._is_dir(resp)
81
+
82
+ if resp.is_redirect:
83
+ resp = self.backend.request("HEAD", uri)
84
+ if resp.status_code == 404:
85
+ raise FileNotFoundError(self)
86
+ elif resp.status_code == 403:
87
+ raise PermissionError(self)
88
+ else:
89
+ resp.raise_for_status()
90
+
91
+ st_size = 0 if self._isdir else int(resp.headers.get("Content-Length", 0))
92
+ lm = resp.headers.get("Last-Modified")
93
+ if lm is None:
94
+ parent = self.parent
95
+ if self != parent:
96
+ try:
97
+ entry = next(
98
+ filter(
99
+ lambda p: p.name.removesuffix("/") == self.name,
100
+ parent._listdir(),
101
+ )
102
+ )
103
+ if entry and entry.modified:
104
+ lm = entry.modified
105
+ except:
106
+ pass
107
+
108
+ return FileStat(
109
+ st_size=st_size, st_mtime=_utils.parsedate(lm), is_dir=self._isdir
110
+ )
111
+
112
+ def _open(
113
+ self,
114
+ mode="r",
115
+ buffering=-1,
116
+ ):
117
+ if mode != "r":
118
+ raise NotImplementedError(mode)
119
+ buffer_size = _io.DEFAULT_BUFFER_SIZE if buffering < 0 else buffering
120
+ req = self.backend.request("GET", self.as_uri(), stream=True)
121
+ return (
122
+ req.raw
123
+ if buffer_size == 0
124
+ else _io.BufferedReader(req.raw, buffer_size=buffer_size)
125
+ )
126
+
127
+ def is_dir(self):
128
+ if self._isdir is None:
129
+ self.stat()
130
+ return self._is_dir is not None and self._isdir
131
+
132
+ def is_file(self):
133
+ return self.is_dir is not None and not self.is_dir()
134
+
135
+ def with_session(self, session: _req.Session, **requests_args):
136
+ return type(self)(self, backend=HttpBackend(session, requests_args))
@@ -0,0 +1,101 @@
1
+ import typing as _ty
2
+ from .. import UriPath, Source
3
+ from ... import utils as _utils
4
+ import threading as _thread
5
+ import paramiko as _paramiko
6
+
7
+
8
+ class BaseSftpBackend(object):
9
+ __slots__ = ()
10
+
11
+ @_utils.notimplemented
12
+ def client(self, source: Source) -> _paramiko.SFTPClient: ...
13
+
14
+
15
+ class SftpBackend(BaseSftpBackend):
16
+ __slots__ = ("connect_opts", "hostkeypolicy")
17
+ connect_opts: dict[str, str]
18
+ hostkeypolicy: _paramiko.MissingHostKeyPolicy
19
+
20
+ def __init__(self, connect_opts, hostkeypolicy) -> None:
21
+ self.connect_opts = connect_opts
22
+ self.hostkeypolicy = hostkeypolicy
23
+
24
+ def opts(self, source: Source):
25
+ connect_ops = {
26
+ **self.connect_opts,
27
+ "hostname": str(source.host),
28
+ "port": source.port or 22,
29
+ }
30
+ user, password = source.parsed_userinfo()
31
+ if user:
32
+ connect_ops["username"] = user
33
+ if password:
34
+ connect_ops["password"] = password
35
+ return connect_ops
36
+
37
+ def transport(self, source: Source) -> _paramiko.Transport:
38
+ client = _paramiko.SSHClient()
39
+ client.set_missing_host_key_policy(self.hostkeypolicy)
40
+ client.connect(**self.opts(source))
41
+ transport = client.get_transport()
42
+ if not transport:
43
+ raise Exception()
44
+ return transport
45
+
46
+ def client(self, source: Source):
47
+ return self.transport(source).open_sftp_client()
48
+
49
+
50
+ def _create_sftpclient(backend: BaseSftpBackend, source: Source, thread_id: int):
51
+ return backend.client(source)
52
+
53
+
54
+ _CACHED_CLIENTS = _utils.LRU(_create_sftpclient, maxsize=128)
55
+
56
+
57
+ class SftpPath(UriPath):
58
+
59
+ __SCHEMES = ("sftp",)
60
+ __slots__ = ()
61
+
62
+ if _ty.TYPE_CHECKING:
63
+ backend: BaseSftpBackend
64
+
65
+ def _initbackend(self):
66
+ return SftpBackend({}, _paramiko.MissingHostKeyPolicy)
67
+
68
+ @property
69
+ def _sftpclient(self):
70
+ thead_id = _thread.get_ident()
71
+ client = _CACHED_CLIENTS(self.backend, self.source, thead_id)
72
+ if client is None or not client.sock.active:
73
+ client = _CACHED_CLIENTS.invalidate(self.backend, self.source, thead_id)
74
+ return client
75
+
76
+ def _listdir(self):
77
+ for path in self._sftpclient.listdir(self.path):
78
+ yield path
79
+
80
+ def stat(self):
81
+ return self._sftpclient.stat(self.path)
82
+
83
+ def _open(self, mode="r", buffering=-1):
84
+ return self._sftpclient.open(self.path, mode, buffering)
85
+
86
+ def _mkdir(self, mode):
87
+ return self._sftpclient.mkdir(self.path, mode)
88
+
89
+ def chmod(self, mode):
90
+ return self._sftpclient.chmod(self.path, mode)
91
+
92
+ def unlink(self, missing_ok=False):
93
+ if missing_ok and not self.exists():
94
+ return
95
+ return self._sftpclient.remove(self.path)
96
+
97
+ def rmdir(self):
98
+ return self._sftpclient.rmdir(self.path)
99
+
100
+ def _rename(self, target):
101
+ return self._sftpclient.rename(self.path, target.as_posix())
@@ -0,0 +1,73 @@
1
+ import typing as _ty
2
+ import uritools as _uritools
3
+ import socket as _socket
4
+ import ipaddress as _ip
5
+
6
+ from .. import utils as _utils
7
+
8
+ _IPAddress = _ip.IPv4Address | _ip.IPv6Address
9
+
10
+ if _ty.TYPE_CHECKING:
11
+ from . import UriPath
12
+
13
+
14
+ class Source(_ty.NamedTuple):
15
+ scheme: str | None
16
+ userinfo: str | None
17
+ host: str | _IPAddress | None
18
+ port: int | None
19
+
20
+ def __bool__(self):
21
+ for val in self:
22
+ if val == '' or val is None:
23
+ continue
24
+ return True
25
+ return False
26
+
27
+ def __str__(self) -> str:
28
+ return _uritools.uricompose(
29
+ scheme=self.scheme, userinfo=self.userinfo, host=self.host, port=self.port
30
+ )
31
+
32
+ @classmethod
33
+ def from_str(cls, source: str, strict=True):
34
+ uri = _uritools.urisplit(source)
35
+ if strict and (uri.path or uri.fragment or uri.query):
36
+ raise ValueError(source)
37
+ return cls(uri.getscheme(), uri.getuserinfo(), uri.gethost(), uri.getport())
38
+
39
+ def keys(self):
40
+ return self._asdict().keys()
41
+
42
+ def __getitem__(self, key: int | str):
43
+ if not isinstance(key, str):
44
+ key = self._fields[key]
45
+ return getattr(self, key)
46
+
47
+ def parsed_userinfo(self):
48
+ parts = []
49
+ if self.userinfo:
50
+ parts = self.userinfo.split(":", maxsplit=1)
51
+ parts = parts + ["", ""]
52
+ return parts[0], parts[1]
53
+
54
+ def get_scheme_cls(self, schemesmap: _ty.Mapping[str, type["UriPath"]] = None):
55
+ from . import UriPath
56
+
57
+ if self.scheme:
58
+ if schemesmap is None:
59
+ schemesmap = UriPath._schemesmap()
60
+ _cls = schemesmap.get(self.scheme, None)
61
+ return _cls if _cls else UriPath
62
+ return UriPath
63
+
64
+ def is_local(self):
65
+ host = self.host
66
+ if not host or host == "localhost":
67
+ return True
68
+ if isinstance(host, str):
69
+ host = _ip.ip_address(_socket.gethostbyname(host))
70
+ return host.is_loopback or host in _utils.get_machine_ips()
71
+
72
+
73
+ _NOSOURCE = Source(None, None, None, None)
@@ -0,0 +1,93 @@
1
+ import functools as _functools
2
+ from threading import RLock
3
+ import typing as _ty
4
+ import time as _time
5
+ from email.utils import parsedate as _parsedate
6
+
7
+
8
+ import collections
9
+
10
+ K = _ty.ParamSpec("K")
11
+ V = _ty.TypeVar("V")
12
+
13
+
14
+ class LRU(_ty.Generic[K, V]):
15
+
16
+ def __init__(self, func: _ty.Callable[K, V], maxsize=128):
17
+ self.cache = collections.OrderedDict()
18
+ self.func = func
19
+ self._maxsize = maxsize
20
+ self.lock = RLock()
21
+
22
+ @property
23
+ def maxsize(self):
24
+ return self._maxsize
25
+
26
+ @maxsize.setter
27
+ def maxsize(self, maxsize: int):
28
+ cache = self.cache
29
+ with self.lock:
30
+ self._maxsize = maxsize
31
+ while len(cache) > maxsize:
32
+ cache.pop(last=False)
33
+
34
+ def __call__(self, *args: K.args) -> V:
35
+ cache = self.cache
36
+ with self.lock:
37
+ if args in cache:
38
+ cache.move_to_end(args)
39
+ return cache[args]
40
+ result = self.func(*args)
41
+ with self.lock:
42
+ cache[args] = result
43
+ if len(cache) > self._maxsize:
44
+ cache.popitem(last=False)
45
+ return result
46
+
47
+ def invalidate(self, *args: K.args) -> V:
48
+ with self.lock():
49
+ if args in self.cache:
50
+ self.cache.pop(args, None)
51
+
52
+ return self(*args)
53
+
54
+
55
+ def parsedate(date: _ty.Union[str, _time.struct_time, tuple, float]):
56
+ if date is None:
57
+ return _time.time()
58
+ if isinstance(date, str):
59
+ date = _parsedate(date)
60
+ return _time.mktime(date)
61
+
62
+
63
+ def sizeof_fmt(num: _ty.Union[int, float]):
64
+ for unit in ("", "K", "M", "G", "T", "P", "E", "Z"):
65
+ if abs(num) < 1024:
66
+ if unit:
67
+ return "%3.1f%s" % (num, unit)
68
+ else:
69
+ return int(num)
70
+ num /= 1024.0
71
+ return "%.1f%s" % (num, "Y")
72
+
73
+
74
+ def notimplemented(method):
75
+ @_functools.wraps(method)
76
+ def _notimplemented(*args, **kwargs):
77
+ raise NotImplementedError(f"Not implement method {method.__name__}")
78
+
79
+ return _notimplemented
80
+
81
+ import socket as _socket
82
+ import ipaddress as _ip
83
+
84
+ def get_machine_ips():
85
+ ips:list[_ip.IPv4Address|_ip.IPv6Address] = list()
86
+ for item in _socket.getaddrinfo(_socket.gethostname(), None):
87
+ protocol, *_, (ip, *_) = item
88
+ if protocol == _socket.AddressFamily.AF_INET:
89
+ ips.append(_ip.ip_address(ip))
90
+ elif protocol == _socket.AddressFamily.AF_INET6:
91
+ ips.append(_ip.ip_address(ip))
92
+
93
+ return ips
@@ -0,0 +1,145 @@
1
+ # Based on glob built-in on python modified to work with Uri/anything that implemetns fspath/iterdir that is similar to pathlib.Path
2
+
3
+
4
+ """Filename globbing utility."""
5
+
6
+ import fnmatch as _fnmatch
7
+ import typing as _ty
8
+ import functools as _func
9
+ import re as _re
10
+
11
+
12
+ RECURSIVE = "**"
13
+ ANY_PATTERN = _re.compile(_fnmatch.translate("*"))
14
+ WILCARD_PATTERN = _re.compile("([*?[])")
15
+
16
+ if _ty.TYPE_CHECKING:
17
+ from ..path import P as _Globable
18
+ else:
19
+
20
+ class _Globable(_ty.Protocol): ...
21
+
22
+
23
+ @_func.lru_cache(maxsize=256, typed=True)
24
+ def compile_pattern(pat: str, case_sensitive: bool):
25
+ flags = _re.NOFLAG if case_sensitive else _re.IGNORECASE
26
+ return _re.compile(_fnmatch.translate(pat), flags)
27
+
28
+
29
+ def glob(
30
+ path: _Globable,
31
+ *,
32
+ dironly: bool = False,
33
+ root_dir: _Globable | None = None,
34
+ recursive: bool = False,
35
+ include_hidden: bool = False,
36
+ case_sensitive: bool | None = None
37
+ ) -> _ty.Iterable[_Globable]:
38
+ """Return an iterator which yields the paths matching a pathname pattern.
39
+
40
+ The pattern may contain simple shell-style wildcards a la
41
+ fnmatch. However, unlike fnmatch, filenames starting with a
42
+ dot are special cases that are not matched by '*' and '?'
43
+ patterns.
44
+
45
+ If recursive is true, the pattern '**' will match any files and
46
+ zero or more directories and subdirectories.
47
+ """
48
+ if case_sensitive is None:
49
+ case_sensitive = path._is_case_sensitive
50
+
51
+ include_hidden = include_hidden or path.is_hidden()
52
+ pattern = compile_pattern(path.name, case_sensitive) if path.name else ANY_PATTERN
53
+
54
+ name_is_pattern = WILCARD_PATTERN.match(path.name) != None
55
+ wildcard_in_path = name_is_pattern
56
+ parent = next(iter(path.parents), None)
57
+ for segment in path.segments:
58
+ if WILCARD_PATTERN.match(segment) != None:
59
+ wildcard_in_path = True
60
+ break
61
+
62
+ root: _Globable = (
63
+ (root_dir or parent) if not root_dir or not parent else (root_dir / parent)
64
+ )
65
+
66
+ if recursive and path.name == RECURSIVE:
67
+ globber = _glob_recursive
68
+ else:
69
+ globber = _glob_with_pattern
70
+
71
+ if not parent or not wildcard_in_path:
72
+ yield from globber(
73
+ root or path,
74
+ pattern,
75
+ dironly,
76
+ include_hidden=include_hidden,
77
+ )
78
+ return
79
+
80
+ if parent and name_is_pattern:
81
+ dirs = glob(
82
+ parent,
83
+ root_dir=root_dir,
84
+ recursive=recursive,
85
+ dironly=True,
86
+ include_hidden=include_hidden,
87
+ case_sensitive=case_sensitive,
88
+ )
89
+ else:
90
+ dirs = [parent]
91
+
92
+ for parent in dirs:
93
+ yield from globber(parent, pattern, dironly, include_hidden)
94
+
95
+
96
+ def _glob_with_pattern(
97
+ parent: _Globable, pattern: _re.Pattern, dironly: bool, include_hidden=False
98
+ ) -> _ty.Iterable[_Globable]:
99
+ if not include_hidden:
100
+
101
+ def _filter(p: _Globable):
102
+ return not p.is_hidden()
103
+
104
+ else:
105
+
106
+ def _filter(p: _Globable):
107
+ return True
108
+
109
+ for path in _iterdir(parent, dironly):
110
+ if _filter(path) and pattern.match(path.name):
111
+ yield path
112
+
113
+
114
+ # This helper function recursively yields relative pathnames inside a literal
115
+ # directory.
116
+
117
+
118
+ def _glob_recursive(
119
+ parent: _Globable, pattern: _re.Pattern, dironly: bool, include_hidden=False
120
+ ):
121
+ if parent and parent.is_dir():
122
+ yield parent
123
+ yield from _rlistdir(parent, dironly, include_hidden=include_hidden)
124
+
125
+
126
+ # If dironly is false, yields all file names inside a directory.
127
+ # If dironly is true, yields only directory names.
128
+ def _iterdir(path: _Globable, dironly: bool):
129
+ for entry in path.iterdir():
130
+ try:
131
+ if not dironly or entry.is_dir():
132
+ yield entry
133
+ except OSError:
134
+ pass
135
+
136
+
137
+ # Recursively yields relative pathnames inside a literal directory.
138
+ def _rlistdir(
139
+ dirname: _Globable, dironly: bool, include_hidden=False
140
+ ) -> _ty.Iterable[_Globable]:
141
+ for path in _iterdir(dirname, dironly):
142
+ if include_hidden or not path.is_hidden():
143
+ yield path
144
+ for y in _rlistdir(path, dironly, include_hidden):
145
+ yield y
@@ -0,0 +1,77 @@
1
+ import stat as _stat
2
+ import typing as _ty
3
+ import abc as _abc
4
+ from .. import utils as _utils
5
+
6
+ class FileStatLike(_ty.Protocol):
7
+ __slots__ = ()
8
+
9
+ @property
10
+ @_abc.abstractmethod
11
+ def st_mode(self) -> int: ...
12
+ @property
13
+ @_abc.abstractmethod
14
+ def st_size(self) -> int: ...
15
+ @property
16
+ @_abc.abstractmethod
17
+ def st_mtime(self) -> int: ...
18
+
19
+ class FileStat(FileStatLike):
20
+ __slots__ = (
21
+ "st_mode",
22
+ "st_nlink",
23
+ "st_uid",
24
+ "st_gid",
25
+ "st_size",
26
+ "st_atime",
27
+ "st_mtime",
28
+ "st_ctime",
29
+ )
30
+
31
+ def __init__(
32
+ self,
33
+ st_mode: int = None,
34
+ st_size: int = 0,
35
+ st_mtime: int = 0,
36
+ is_dir: bool = False,
37
+ ):
38
+ self.st_mode = st_mode or (
39
+ _stat.S_IFDIR | 0o555 if is_dir else _stat.S_IFREG | 0o444
40
+ )
41
+ self.st_nlink = 1
42
+ self.st_uid = 0
43
+ self.st_gid = 0
44
+ self.st_size = st_size
45
+ self.st_atime = 0
46
+ self.st_mtime = st_mtime
47
+ self.st_ctime = 0
48
+
49
+ def settime(self, value):
50
+ self.st_atime = self.st_mtime = self.st_ctime = value
51
+
52
+ def setmode(self, value, isdir=None):
53
+ if isdir is None:
54
+ isdir = self.st_mode & _stat.S_IFDIR
55
+ if isdir:
56
+ self.st_mode = _stat.S_IFDIR | value
57
+ else:
58
+ self.st_mode = _stat.S_IFREG | value
59
+
60
+ def __getitem__(self, key):
61
+ return getattr(self, key)
62
+
63
+ def items(self):
64
+ for key in self.__slots__:
65
+ yield key, getattr(self, key)
66
+
67
+ def __repr__(self):
68
+ return "<%s mode=%o, size=%s, mtime=%d>" % (
69
+ type(self).__name__,
70
+ self.st_mode,
71
+ _utils.sizeof_fmt(self.st_size),
72
+ self.st_mtime,
73
+ )
74
+
75
+ def __str__(self):
76
+ props = [f"{k}={v}" for k, v in self.items()]
77
+ return "<%s %s>" % (type(self).__name__, ", ".join(props))