pathlibutil 0.2.1__tar.gz → 0.3.2__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.
- {pathlibutil-0.2.1 → pathlibutil-0.3.2}/PKG-INFO +24 -11
- {pathlibutil-0.2.1 → pathlibutil-0.3.2}/README.md +21 -8
- pathlibutil-0.3.2/pathlibutil/base.py +44 -0
- {pathlibutil-0.2.1 → pathlibutil-0.3.2}/pathlibutil/path.py +48 -1
- pathlibutil-0.3.2/pathlibutil/urlpath.py +404 -0
- {pathlibutil-0.2.1 → pathlibutil-0.3.2}/pyproject.toml +18 -22
- pathlibutil-0.2.1/pathlibutil/base.py +0 -19
- {pathlibutil-0.2.1 → pathlibutil-0.3.2}/LICENSE +0 -0
- {pathlibutil-0.2.1 → pathlibutil-0.3.2}/pathlibutil/__init__.py +0 -0
- {pathlibutil-0.2.1 → pathlibutil-0.3.2}/pathlibutil/json.py +0 -0
- {pathlibutil-0.2.1 → pathlibutil-0.3.2}/pathlibutil/types.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pathlibutil
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: inherits from pathlib.Path with methods for hashing, copying, deleting and more
|
|
5
5
|
Home-page: https://d-chris.github.io
|
|
6
6
|
License: MIT
|
|
7
|
-
Keywords: pathlib,hashlib,shutil
|
|
7
|
+
Keywords: pathlib,hashlib,shutil,urllib.parse
|
|
8
8
|
Author: Christoph Dörrer
|
|
9
9
|
Author-email: d-chris@web.de
|
|
10
|
-
Requires-Python: >=3.8.1,<
|
|
10
|
+
Requires-Python: >=3.8.1,<4.0.0
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.8
|
|
19
20
|
Provides-Extra: 7z
|
|
20
21
|
Requires-Dist: py7zr (>=0.20.2,<0.21.0) ; extra == "7z"
|
|
@@ -22,17 +23,21 @@ Project-URL: Documentation, https://d-chris.github.io/pathlibutil
|
|
|
22
23
|
Project-URL: Repository, https://github.com/d-chris/pathlibutil
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
|
|
26
|
+
<!--
|
|
27
|
+
filename: ./README.md
|
|
28
|
+
-->
|
|
29
|
+
|
|
25
30
|
# pathlibutil
|
|
26
31
|
|
|
27
32
|
[](https://pypi.org/project/pathlibutil/)
|
|
28
|
-
[](https://pypi.org/project/pathlibutil/)
|
|
33
|
+
[](https://pypi.org/project/pathlibutil/)
|
|
29
34
|
[](https://pypi.org/project/pathlibutil/)
|
|
30
35
|
[](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
|
|
31
|
-
[](https://
|
|
36
|
+
[](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
|
|
37
|
+
[](https://d-chris.github.io/pathlibutil)
|
|
38
|
+
[](https://github.com/d-chris/pathlibutil)
|
|
39
|
+
[](https://codecov.io/gh/d-chris/pathlibutil)
|
|
40
|
+
[](https://raw.githubusercontent.com/d-chris/pathlibutil/main/.pre-commit-config.yaml)
|
|
36
41
|
|
|
37
42
|
---
|
|
38
43
|
|
|
@@ -58,11 +63,20 @@ Description-Content-Type: text/markdown
|
|
|
58
63
|
- `Path.resolve()` to resolve a unc path to a mapped windows drive.
|
|
59
64
|
- `Path.walk()` to walk over a directory tree like `os.walk()`
|
|
60
65
|
- `Path.iterdir()` with `recursive` all files from the directory tree will be yielded and `exclude_dirs` via callable.
|
|
66
|
+
- `Path.is_expired()` to check if a file is expired by a given `datetime.timedelta`
|
|
67
|
+
- `Path.expand()` yields file paths for multiple file patterns if they exsits.
|
|
61
68
|
|
|
62
69
|
JSON serialization of `Path` objects is supported in `pathlibutil.json`.
|
|
63
70
|
|
|
64
71
|
- `pathlibutil.json.dumps()` and `pathlibutil.json.dump()` to serialize `Path` objects as posix paths.
|
|
65
72
|
|
|
73
|
+
Parse and modify URLs with `pathlibutil.urlpath`.
|
|
74
|
+
|
|
75
|
+
- `pathlibutil.urlpath.UrlPath()` modify URL and easy access the `path` of the url like a `pathlib.PurePosixPath` object.
|
|
76
|
+
- `pathlibutil.urlpath.UrlNetloc()` to parse and modify the `netloc` part of a URL.
|
|
77
|
+
- `pathlibutil.urlpath.normalize_url()` to normalize a URL string.
|
|
78
|
+
|
|
79
|
+
|
|
66
80
|
## Installation
|
|
67
81
|
|
|
68
82
|
```bash
|
|
@@ -123,7 +137,7 @@ file = Path("pathlibutil.md5")
|
|
|
123
137
|
|
|
124
138
|
with file.open("w") as f:
|
|
125
139
|
f.write(
|
|
126
|
-
|
|
140
|
+
"# MD5 checksums generated with pathlibutil "
|
|
127
141
|
"(https://pypi.org/project/pathlibutil/)\n\n"
|
|
128
142
|
)
|
|
129
143
|
|
|
@@ -308,4 +322,3 @@ Path.cwd(frozen=True) is K:/pathlibutil/examples
|
|
|
308
322
|
Path.cwd(frozen=False) is K:/pathlibutil
|
|
309
323
|
Path.cwd(frozen=_MEIPASS) is C:/Users/CHRIST~1.DOE/AppData/Local/Temp/_MEI106042
|
|
310
324
|
```
|
|
311
|
-
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
filename: ./README.md
|
|
3
|
+
-->
|
|
4
|
+
|
|
1
5
|
# pathlibutil
|
|
2
6
|
|
|
3
7
|
[](https://pypi.org/project/pathlibutil/)
|
|
4
|
-
[](https://pypi.org/project/pathlibutil/)
|
|
8
|
+
[](https://pypi.org/project/pathlibutil/)
|
|
5
9
|
[](https://pypi.org/project/pathlibutil/)
|
|
6
10
|
[](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
|
|
7
|
-
[](https://
|
|
11
|
+
[](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
|
|
12
|
+
[](https://d-chris.github.io/pathlibutil)
|
|
13
|
+
[](https://github.com/d-chris/pathlibutil)
|
|
14
|
+
[](https://codecov.io/gh/d-chris/pathlibutil)
|
|
15
|
+
[](https://raw.githubusercontent.com/d-chris/pathlibutil/main/.pre-commit-config.yaml)
|
|
12
16
|
|
|
13
17
|
---
|
|
14
18
|
|
|
@@ -34,11 +38,20 @@
|
|
|
34
38
|
- `Path.resolve()` to resolve a unc path to a mapped windows drive.
|
|
35
39
|
- `Path.walk()` to walk over a directory tree like `os.walk()`
|
|
36
40
|
- `Path.iterdir()` with `recursive` all files from the directory tree will be yielded and `exclude_dirs` via callable.
|
|
41
|
+
- `Path.is_expired()` to check if a file is expired by a given `datetime.timedelta`
|
|
42
|
+
- `Path.expand()` yields file paths for multiple file patterns if they exsits.
|
|
37
43
|
|
|
38
44
|
JSON serialization of `Path` objects is supported in `pathlibutil.json`.
|
|
39
45
|
|
|
40
46
|
- `pathlibutil.json.dumps()` and `pathlibutil.json.dump()` to serialize `Path` objects as posix paths.
|
|
41
47
|
|
|
48
|
+
Parse and modify URLs with `pathlibutil.urlpath`.
|
|
49
|
+
|
|
50
|
+
- `pathlibutil.urlpath.UrlPath()` modify URL and easy access the `path` of the url like a `pathlib.PurePosixPath` object.
|
|
51
|
+
- `pathlibutil.urlpath.UrlNetloc()` to parse and modify the `netloc` part of a URL.
|
|
52
|
+
- `pathlibutil.urlpath.normalize_url()` to normalize a URL string.
|
|
53
|
+
|
|
54
|
+
|
|
42
55
|
## Installation
|
|
43
56
|
|
|
44
57
|
```bash
|
|
@@ -99,7 +112,7 @@ file = Path("pathlibutil.md5")
|
|
|
99
112
|
|
|
100
113
|
with file.open("w") as f:
|
|
101
114
|
f.write(
|
|
102
|
-
|
|
115
|
+
"# MD5 checksums generated with pathlibutil "
|
|
103
116
|
"(https://pypi.org/project/pathlibutil/)\n\n"
|
|
104
117
|
)
|
|
105
118
|
|
|
@@ -283,4 +296,4 @@ os.getcwd is K:/pathlibutil
|
|
|
283
296
|
Path.cwd(frozen=True) is K:/pathlibutil/examples
|
|
284
297
|
Path.cwd(frozen=False) is K:/pathlibutil
|
|
285
298
|
Path.cwd(frozen=_MEIPASS) is C:/Users/CHRIST~1.DOE/AppData/Local/Temp/_MEI106042
|
|
286
|
-
```
|
|
299
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Generator, TypeVar
|
|
5
|
+
|
|
6
|
+
_Path = TypeVar("_Path", bound=pathlib.Path)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BasePath(pathlib.Path):
|
|
10
|
+
"""
|
|
11
|
+
Baseclass to inherit from `pathlib.Path`.
|
|
12
|
+
|
|
13
|
+
This class is only needed for python versions < 3.12.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
if sys.version_info < (3, 12):
|
|
17
|
+
_flavour = (
|
|
18
|
+
pathlib._windows_flavour if os.name == "nt" else pathlib._posix_flavour
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def expand(cls, file: str) -> Generator[_Path, None, None]:
|
|
23
|
+
"""
|
|
24
|
+
yields only Path object of file names that exists. Supports glob patterns in
|
|
25
|
+
filename as wildcards.
|
|
26
|
+
|
|
27
|
+
>>> list(Path.expand(__file__))
|
|
28
|
+
[BasePath('pathlibutil/base.py')]
|
|
29
|
+
|
|
30
|
+
>>> list(Path.expand("pathlibutil/*.py")
|
|
31
|
+
[BasePath('pathlibutil/base.py'), BasePath('pathlibutil/json.py'),
|
|
32
|
+
BasePath('pathlibutil/path.py'), BasePath('pathlibutil/types.py'),
|
|
33
|
+
BasePath('pathlibutil/__init__.py')]
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
file = cls(file)
|
|
37
|
+
try:
|
|
38
|
+
file.resolve(True)
|
|
39
|
+
except (OSError, FileNotFoundError):
|
|
40
|
+
parent, pattern = file.parent, file.name
|
|
41
|
+
|
|
42
|
+
yield from parent.glob(pattern)
|
|
43
|
+
else:
|
|
44
|
+
yield file
|
|
@@ -6,10 +6,11 @@ import re
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
+
from datetime import datetime, timedelta
|
|
9
10
|
from typing import Callable, Dict, Generator, List, Literal, Set, Tuple, Union
|
|
10
11
|
|
|
11
12
|
from pathlibutil.base import BasePath, _Path
|
|
12
|
-
from pathlibutil.types import ByteInt, StatResult, _stat_result, byteint
|
|
13
|
+
from pathlibutil.types import ByteInt, StatResult, TimeInt, _stat_result, byteint
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class Path(BasePath):
|
|
@@ -635,6 +636,52 @@ class Path(BasePath):
|
|
|
635
636
|
else:
|
|
636
637
|
yield from super().iterdir()
|
|
637
638
|
|
|
639
|
+
def is_expired(self, *, stat="st_mtime", **kwargs) -> bool:
|
|
640
|
+
"""
|
|
641
|
+
Returns `True` if the time of the file is greater than a given threshold.
|
|
642
|
+
|
|
643
|
+
For `**kwargs` see `datetime.timedelta`.
|
|
644
|
+
|
|
645
|
+
>>> Path("README.md").is_expired(weeks=9999)
|
|
646
|
+
False
|
|
647
|
+
"""
|
|
648
|
+
try:
|
|
649
|
+
attr: TimeInt = getattr(self.stat(), stat)
|
|
650
|
+
diff = datetime.now() - attr.datetime
|
|
651
|
+
except AttributeError as e:
|
|
652
|
+
stats = [attr for attr in dir(os.stat_result) if attr.endswith("time")]
|
|
653
|
+
|
|
654
|
+
raise ValueError(f"{stat=} is not from {stats}") from e
|
|
655
|
+
|
|
656
|
+
return diff > timedelta(**kwargs)
|
|
657
|
+
|
|
658
|
+
@classmethod
|
|
659
|
+
def expand(
|
|
660
|
+
cls,
|
|
661
|
+
*files: str,
|
|
662
|
+
duplicates: bool = True,
|
|
663
|
+
) -> Generator[_Path, None, None]:
|
|
664
|
+
"""
|
|
665
|
+
Yields only Path object of file names that exists. Supports glob patterns in
|
|
666
|
+
filename as wildcards.
|
|
667
|
+
|
|
668
|
+
If `duplicates` is `False` only one instance of each file is yielded.
|
|
669
|
+
|
|
670
|
+
>>> list(Path.expand("README.md", "*.md", duplicates=False))
|
|
671
|
+
[Path('README.md')]
|
|
672
|
+
"""
|
|
673
|
+
if duplicates:
|
|
674
|
+
for file in files:
|
|
675
|
+
yield from super().expand(file)
|
|
676
|
+
else:
|
|
677
|
+
seen = set()
|
|
678
|
+
|
|
679
|
+
for file in files:
|
|
680
|
+
for item in super().expand(file):
|
|
681
|
+
if item not in seen:
|
|
682
|
+
seen.add(item)
|
|
683
|
+
yield item
|
|
684
|
+
|
|
638
685
|
|
|
639
686
|
class Register7zFormat(Path, archive="7z"):
|
|
640
687
|
"""
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import pathlib
|
|
3
|
+
import re
|
|
4
|
+
import urllib.parse as up
|
|
5
|
+
import urllib.request
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from functools import cached_property, wraps
|
|
8
|
+
from typing import Any, Dict, Optional, Tuple, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class UrlNetloc:
|
|
13
|
+
"""
|
|
14
|
+
A dataclass to represent the netloc part of a URL.
|
|
15
|
+
|
|
16
|
+
>>> url = UrlNetloc.from_netloc("www.example.com:443")
|
|
17
|
+
>>> url.port = None
|
|
18
|
+
>>> str(url)
|
|
19
|
+
'www.example.com'
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
hostname: str
|
|
23
|
+
port: Optional[int] = field(default=None)
|
|
24
|
+
username: Optional[str] = field(default=None)
|
|
25
|
+
password: Optional[str] = field(default=None)
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return self.netloc
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def netloc(self) -> str:
|
|
32
|
+
"""netloc string representation of the `dataclass`"""
|
|
33
|
+
|
|
34
|
+
netloc = ""
|
|
35
|
+
|
|
36
|
+
if self.username:
|
|
37
|
+
netloc += self.username
|
|
38
|
+
|
|
39
|
+
if self.password:
|
|
40
|
+
netloc += f":{self.password}"
|
|
41
|
+
|
|
42
|
+
netloc += "@"
|
|
43
|
+
|
|
44
|
+
if ":" in self.hostname:
|
|
45
|
+
netloc += f"[{self.hostname}]"
|
|
46
|
+
else:
|
|
47
|
+
netloc += self.hostname
|
|
48
|
+
|
|
49
|
+
if self.port:
|
|
50
|
+
netloc += f":{self.port:d}"
|
|
51
|
+
|
|
52
|
+
return netloc
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_netloc(cls, netloc: str, normalize: bool = False) -> "UrlNetloc":
|
|
56
|
+
"""Parse a netloc string into a `UrlNetloc` object"""
|
|
57
|
+
|
|
58
|
+
if not netloc.startswith("//"):
|
|
59
|
+
netloc = f"//{netloc}"
|
|
60
|
+
|
|
61
|
+
url = up.urlparse(netloc)
|
|
62
|
+
|
|
63
|
+
hostname = url.hostname
|
|
64
|
+
|
|
65
|
+
if normalize is False:
|
|
66
|
+
try:
|
|
67
|
+
pattern = re.escape(url.hostname)
|
|
68
|
+
hostname = re.search(pattern, netloc, re.IGNORECASE).group()
|
|
69
|
+
except AttributeError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
return cls(
|
|
73
|
+
hostname=hostname,
|
|
74
|
+
port=url.port,
|
|
75
|
+
username=url.username,
|
|
76
|
+
password=url.password,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def to_dict(self, prune: bool = False) -> Dict[str, Any]:
|
|
80
|
+
"""
|
|
81
|
+
Convert the `UrlNetloc` object to a dictionary
|
|
82
|
+
|
|
83
|
+
If `prune` is `True`, remove all key-value pairs from the dict where the value
|
|
84
|
+
is `None`.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
data = asdict(self)
|
|
88
|
+
|
|
89
|
+
if not prune:
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
_UrlPath = TypeVar("_UrlPath", bound="UrlPath")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def normalize_url(
|
|
99
|
+
url: str,
|
|
100
|
+
port: bool = False,
|
|
101
|
+
sort: bool = True,
|
|
102
|
+
) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Function to normalize a URL by converting the scheme and host to lowercase, removing
|
|
105
|
+
port if present, and sorting the query parameters.
|
|
106
|
+
|
|
107
|
+
>>> normalize_url("https://www.ExamplE.com:443/Path?b=2&a=1")
|
|
108
|
+
'https://www.example.com/Path?a=1&b=2'
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
url = UrlPath(url)
|
|
112
|
+
|
|
113
|
+
if port is False:
|
|
114
|
+
ports = {url.scheme.lower(): url.port}
|
|
115
|
+
else:
|
|
116
|
+
ports = {}
|
|
117
|
+
|
|
118
|
+
return url.normalize(sort=sort, ports=ports)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def urlpath(func):
|
|
122
|
+
"""
|
|
123
|
+
decorator to return a `UrlPath` object from a `urllib.parse.ParseResult` object.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
@wraps(func)
|
|
127
|
+
def wrapper(self, *args, **kwargs) -> _UrlPath:
|
|
128
|
+
result = func(self, *args, **kwargs)
|
|
129
|
+
|
|
130
|
+
return self.__class__(result.geturl(), **self._kwargs)
|
|
131
|
+
|
|
132
|
+
return wrapper
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class UrlPath(up.ParseResult):
|
|
136
|
+
"""
|
|
137
|
+
Class to manipulate URLs to change the scheme, netloc, path, query, and fragment.
|
|
138
|
+
|
|
139
|
+
Wrap the `pathlib.PurePosixPath` methods to return a new `UrlPath` object
|
|
140
|
+
|
|
141
|
+
>>> url = UrlPath("https://www.example.com/path/to/file").with_suffix(".txt")
|
|
142
|
+
>>> str(url)
|
|
143
|
+
'https://www.example.com/path/to/file.txt'
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
_default_ports = {
|
|
148
|
+
"http": 80,
|
|
149
|
+
"https": 443,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def __new__(cls, url, **kwargs) -> _UrlPath:
|
|
153
|
+
parsed_url = up.urlparse(url, **kwargs)
|
|
154
|
+
return super().__new__(cls, *parsed_url)
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
url: str,
|
|
159
|
+
scheme: str = "",
|
|
160
|
+
allow_fragments: bool = True,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Initialize the `UrlPath` object with a URL string.
|
|
164
|
+
|
|
165
|
+
A `ValueError` is raised if the URL is not valid.
|
|
166
|
+
"""
|
|
167
|
+
self._url = url
|
|
168
|
+
self._kwargs = {
|
|
169
|
+
"scheme": scheme,
|
|
170
|
+
"allow_fragments": allow_fragments,
|
|
171
|
+
}
|
|
172
|
+
self._path = pathlib.PurePosixPath(up.unquote(self.path))
|
|
173
|
+
|
|
174
|
+
def __str__(self) -> str:
|
|
175
|
+
return self.normalize()
|
|
176
|
+
|
|
177
|
+
def geturl(self, normalize: bool = False) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Return a re-combined version of the URL.
|
|
180
|
+
|
|
181
|
+
If `normalize` is `True` scheme and netloc is converted to lowercase,
|
|
182
|
+
default ports are removed and query parameters are sorted.
|
|
183
|
+
"""
|
|
184
|
+
if normalize:
|
|
185
|
+
return self.normalize()
|
|
186
|
+
|
|
187
|
+
return super().geturl()
|
|
188
|
+
|
|
189
|
+
def normalize(self, sort: bool = True, **kwargs) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Normalize the URL by converting the scheme and host to lowercase, removing the
|
|
192
|
+
default port if present, and sorting the query parameters.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
ports = kwargs.get("ports", self._default_ports)
|
|
196
|
+
|
|
197
|
+
scheme = self.scheme.lower()
|
|
198
|
+
netloc = UrlNetloc.from_netloc(self.netloc, normalize=True)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if ports[scheme] == netloc.port:
|
|
202
|
+
netloc.port = None
|
|
203
|
+
except KeyError:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
path = up.quote(up.unquote(self.path))
|
|
207
|
+
query = up.urlencode(sorted(up.parse_qsl(self.query))) if sort else self.query
|
|
208
|
+
|
|
209
|
+
return up.urlunparse(
|
|
210
|
+
(
|
|
211
|
+
scheme,
|
|
212
|
+
str(netloc),
|
|
213
|
+
path,
|
|
214
|
+
self.params,
|
|
215
|
+
query,
|
|
216
|
+
self.fragment,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def __getattr__(self, attr: str) -> Any:
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
attr = getattr(self._path, attr)
|
|
224
|
+
except AttributeError as e:
|
|
225
|
+
raise AttributeError(
|
|
226
|
+
f"'{self.__class__.__name__}' object has no attribute '{attr}'"
|
|
227
|
+
) from e
|
|
228
|
+
|
|
229
|
+
if not callable(attr):
|
|
230
|
+
return attr
|
|
231
|
+
|
|
232
|
+
@wraps(attr)
|
|
233
|
+
def wrapper(*args, **kwargs) -> _UrlPath:
|
|
234
|
+
result = attr(*args, **kwargs)
|
|
235
|
+
|
|
236
|
+
return self.with_path(result)
|
|
237
|
+
|
|
238
|
+
return wrapper
|
|
239
|
+
|
|
240
|
+
@urlpath
|
|
241
|
+
def with_scheme(self, scheme: str) -> _UrlPath:
|
|
242
|
+
"""
|
|
243
|
+
Change the scheme of the URL.
|
|
244
|
+
"""
|
|
245
|
+
return self._replace(scheme=scheme)
|
|
246
|
+
|
|
247
|
+
@urlpath
|
|
248
|
+
def with_netloc(self, netloc: Union[str, UrlNetloc]) -> _UrlPath:
|
|
249
|
+
"""
|
|
250
|
+
Change the netloc of the URL.
|
|
251
|
+
"""
|
|
252
|
+
return self._replace(netloc=str(netloc))
|
|
253
|
+
|
|
254
|
+
@urlpath
|
|
255
|
+
def with_path(self, path: Union[str, pathlib.PurePosixPath]) -> _UrlPath:
|
|
256
|
+
"""
|
|
257
|
+
Change the path of the URL.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
path = path.as_posix()
|
|
262
|
+
except AttributeError as e:
|
|
263
|
+
if not isinstance(path, str):
|
|
264
|
+
raise TypeError(
|
|
265
|
+
f"Expected str or PurePosixPath, got {type(path)}"
|
|
266
|
+
) from e
|
|
267
|
+
|
|
268
|
+
return self._replace(path=path)
|
|
269
|
+
|
|
270
|
+
@urlpath
|
|
271
|
+
def with_params(self, params: str) -> _UrlPath:
|
|
272
|
+
"""
|
|
273
|
+
Change the parameters of the URL.
|
|
274
|
+
"""
|
|
275
|
+
return self._replace(params=params)
|
|
276
|
+
|
|
277
|
+
@urlpath
|
|
278
|
+
def with_query(self, query: str) -> _UrlPath:
|
|
279
|
+
"""
|
|
280
|
+
Change the query of the URL.
|
|
281
|
+
"""
|
|
282
|
+
return self._replace(query=query)
|
|
283
|
+
|
|
284
|
+
@urlpath
|
|
285
|
+
def with_fragment(self, fragment: str) -> _UrlPath:
|
|
286
|
+
"""
|
|
287
|
+
Change the fragment of the URL.
|
|
288
|
+
"""
|
|
289
|
+
return self._replace(fragment=fragment)
|
|
290
|
+
|
|
291
|
+
def with_port(self, port: int) -> _UrlPath:
|
|
292
|
+
"""
|
|
293
|
+
change the port in the netloc of the URL.
|
|
294
|
+
|
|
295
|
+
If `port` is `None`, the port is removed.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
netloc = UrlNetloc.from_netloc(self.netloc)
|
|
299
|
+
netloc.port = port
|
|
300
|
+
|
|
301
|
+
return self.with_netloc(netloc)
|
|
302
|
+
|
|
303
|
+
def with_hostname(self, hostname: str) -> _UrlPath:
|
|
304
|
+
"""
|
|
305
|
+
change the hostname in the netloc of the URL
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
netloc = UrlNetloc.from_netloc(self.netloc)
|
|
309
|
+
netloc.hostname = hostname
|
|
310
|
+
|
|
311
|
+
return self.with_netloc(netloc)
|
|
312
|
+
|
|
313
|
+
def with_credentials(self, username: str, password: str = None) -> _UrlPath:
|
|
314
|
+
"""
|
|
315
|
+
change the username and password in the netloc of the URL
|
|
316
|
+
|
|
317
|
+
to change only `username` the `password` must also be provided.
|
|
318
|
+
|
|
319
|
+
If `username` is `None`, the credentials are removed.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
netloc = UrlNetloc.from_netloc(self.netloc)
|
|
323
|
+
netloc.username = username
|
|
324
|
+
netloc.password = password
|
|
325
|
+
|
|
326
|
+
return self.with_netloc(netloc)
|
|
327
|
+
|
|
328
|
+
@cached_property
|
|
329
|
+
def parts(self) -> Tuple[str, ...]:
|
|
330
|
+
"""
|
|
331
|
+
return the parts of the path without any '/'.
|
|
332
|
+
"""
|
|
333
|
+
return tuple(part for part in self._path.parts if not part.startswith("/"))
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def anchor(self) -> str:
|
|
337
|
+
"""
|
|
338
|
+
The concatenation of the netloc and root of the path.
|
|
339
|
+
|
|
340
|
+
>>> UrlPath("//server/root/path/file.txt").anchor
|
|
341
|
+
'//server/root'
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
root = self.parts[0]
|
|
345
|
+
except IndexError:
|
|
346
|
+
root = ""
|
|
347
|
+
|
|
348
|
+
return f"//{self.netloc}/{root}"
|
|
349
|
+
|
|
350
|
+
def with_anchor(self, anchor: str, root: bool = False, **kwargs) -> _UrlPath:
|
|
351
|
+
"""
|
|
352
|
+
Change the anchor of the URL.
|
|
353
|
+
|
|
354
|
+
If `root` is `True`, the root of the path will not be removed.
|
|
355
|
+
|
|
356
|
+
>>> url = UrlPath("//server/root/path/file.txt")
|
|
357
|
+
>>> url.with_anchor("https://www.server.com").geturl()
|
|
358
|
+
'https://www.server.com/path/file.txt'
|
|
359
|
+
"""
|
|
360
|
+
anchor = self.__class__(anchor, **kwargs)
|
|
361
|
+
|
|
362
|
+
url = self.with_netloc(anchor.netloc)
|
|
363
|
+
|
|
364
|
+
if anchor.scheme != url.scheme:
|
|
365
|
+
url = url.with_scheme(anchor.scheme)
|
|
366
|
+
|
|
367
|
+
if root is False:
|
|
368
|
+
parts = url.parts[1:]
|
|
369
|
+
else:
|
|
370
|
+
parts = url.parts
|
|
371
|
+
|
|
372
|
+
# if anchor has a path, anchor and url path are concatenated
|
|
373
|
+
if any(anchor.parts):
|
|
374
|
+
return url.with_path("/".join(itertools.chain(anchor.parts, parts)))
|
|
375
|
+
|
|
376
|
+
# if root is False, the root of the path is removed
|
|
377
|
+
if root is False:
|
|
378
|
+
return url.with_path("/".join(parts))
|
|
379
|
+
|
|
380
|
+
return url
|
|
381
|
+
|
|
382
|
+
def exists(self, errors: bool = False, **kwargs) -> bool:
|
|
383
|
+
"""
|
|
384
|
+
Check if the URL returns a 200 status code.
|
|
385
|
+
|
|
386
|
+
If `errors` is `False`, exceptions are suppressed and `False` is returned.
|
|
387
|
+
|
|
388
|
+
For `kwargs` see `urllib.request.urlopen`.
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
with urllib.request.urlopen(self.normalize(False), **kwargs) as response:
|
|
392
|
+
return response.status == 200
|
|
393
|
+
except Exception as e:
|
|
394
|
+
if errors is not False:
|
|
395
|
+
raise e
|
|
396
|
+
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
__all__ = [
|
|
401
|
+
"UrlNetloc",
|
|
402
|
+
"UrlPath",
|
|
403
|
+
"normalize_url",
|
|
404
|
+
]
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
build-backend = "poetry.core.masonry.api"
|
|
3
3
|
|
|
4
|
-
requires = [
|
|
5
|
-
"poetry-core",
|
|
6
|
-
]
|
|
4
|
+
requires = [ "poetry-core" ]
|
|
7
5
|
|
|
8
6
|
[tool.poetry]
|
|
9
7
|
name = "pathlibutil"
|
|
10
|
-
version = "v0.2
|
|
8
|
+
version = "v0.3.2"
|
|
11
9
|
description = "inherits from pathlib.Path with methods for hashing, copying, deleting and more"
|
|
12
10
|
authors = [ "Christoph Dörrer <d-chris@web.de>" ]
|
|
13
11
|
readme = "README.md"
|
|
@@ -18,40 +16,36 @@ classifiers = [
|
|
|
18
16
|
"Programming Language :: Python :: 3.10",
|
|
19
17
|
"Programming Language :: Python :: 3.11",
|
|
20
18
|
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
21
20
|
"License :: OSI Approved :: MIT License",
|
|
22
21
|
"Operating System :: OS Independent",
|
|
23
22
|
]
|
|
24
|
-
keywords = [ "pathlib", "hashlib", "shutil" ]
|
|
23
|
+
keywords = [ "pathlib", "hashlib", "shutil", "urllib.parse" ]
|
|
25
24
|
homepage = "https://d-chris.github.io"
|
|
26
25
|
repository = "https://github.com/d-chris/pathlibutil"
|
|
27
26
|
documentation = "https://d-chris.github.io/pathlibutil"
|
|
28
|
-
include = [ "LICENSE" ]
|
|
29
27
|
|
|
30
28
|
[tool.poetry.dependencies]
|
|
31
|
-
python = "
|
|
29
|
+
python = "^3.8.1"
|
|
32
30
|
py7zr = { version = "^0.20.2", optional = true }
|
|
33
31
|
|
|
34
32
|
[tool.poetry.extras]
|
|
35
33
|
7z = [ "py7zr" ]
|
|
36
34
|
|
|
37
35
|
[tool.poetry.group.dev.dependencies]
|
|
38
|
-
pytest = "^7.4.3"
|
|
39
36
|
tox = "^4.11.4"
|
|
37
|
+
pyinstaller = { version = "^6.10.0", python = "<3.14" }
|
|
38
|
+
|
|
39
|
+
[tool.poetry.group.test.dependencies]
|
|
40
|
+
pytest = "^8.3.3"
|
|
40
41
|
pytest-random-order = "^1.1.0"
|
|
41
42
|
pytest-cov = "^4.1.0"
|
|
42
43
|
pytest-mock = "^3.12.0"
|
|
43
|
-
exrex = "
|
|
44
|
+
exrex = { git = "https://github.com/asciimoo/exrex", rev = "1c22c70" }
|
|
44
45
|
|
|
45
|
-
[tool.poetry.group.
|
|
46
|
-
flake8 = "^7.0.0"
|
|
47
|
-
black = "^23.12.1"
|
|
48
|
-
docformatter = "^1.7.5"
|
|
49
|
-
|
|
50
|
-
[tool.poetry.group.doc.dependencies]
|
|
51
|
-
jinja2-pdoc = "^0.2.0"
|
|
46
|
+
[tool.poetry.group.docs.dependencies]
|
|
52
47
|
pdoc = "^14.3.0"
|
|
53
|
-
|
|
54
|
-
pyinstaller = "^6.5.0"
|
|
48
|
+
jinja2-pdoc = "^1.1.0"
|
|
55
49
|
|
|
56
50
|
[[tool.poetry.source]]
|
|
57
51
|
name = "PyPI"
|
|
@@ -62,6 +56,9 @@ name = "testpypi"
|
|
|
62
56
|
url = "https://test.pypi.org/legacy/"
|
|
63
57
|
priority = "explicit"
|
|
64
58
|
|
|
59
|
+
[tool.isort]
|
|
60
|
+
profile = "black"
|
|
61
|
+
|
|
65
62
|
[tool.pytest.ini_options]
|
|
66
63
|
minversion = "6.0"
|
|
67
64
|
testpaths = "tests"
|
|
@@ -69,8 +66,7 @@ addopts = [
|
|
|
69
66
|
"--random-order",
|
|
70
67
|
"--color=yes",
|
|
71
68
|
"-s",
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# "--cov-report=html",
|
|
69
|
+
"--cov=pathlibutil",
|
|
70
|
+
"--cov-report=term-missing:skip-covered",
|
|
71
|
+
"--cov-report=xml",
|
|
76
72
|
]
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import pathlib
|
|
3
|
-
import sys
|
|
4
|
-
from typing import TypeVar
|
|
5
|
-
|
|
6
|
-
_Path = TypeVar("_Path", bound=pathlib.Path)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class BasePath(pathlib.Path):
|
|
10
|
-
"""
|
|
11
|
-
Baseclass to inherit from `pathlib.Path`.
|
|
12
|
-
|
|
13
|
-
This class is only needed for python versions < 3.12.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
if sys.version_info < (3, 12):
|
|
17
|
-
_flavour = (
|
|
18
|
-
pathlib._windows_flavour if os.name == "nt" else pathlib._posix_flavour
|
|
19
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|