superpathlib 2.0.14__tar.gz → 2.1.1__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.
- {superpathlib-2.0.14/src/superpathlib.egg-info → superpathlib-2.1.1}/PKG-INFO +18 -16
- {superpathlib-2.0.14 → superpathlib-2.1.1}/README.md +13 -8
- {superpathlib-2.0.14 → superpathlib-2.1.1}/pyproject.toml +8 -21
- superpathlib-2.1.1/src/superpathlib/archive.py +71 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/common_folders.py +3 -8
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/content_properties.py +38 -25
- superpathlib-2.1.1/src/superpathlib/encrypted.py +79 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/extra_functionality.py +63 -133
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/metadata_properties.py +10 -52
- superpathlib-2.1.1/src/superpathlib/override.py +82 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/utils.py +17 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1/src/superpathlib.egg-info}/PKG-INFO +18 -16
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib.egg-info/SOURCES.txt +2 -5
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib.egg-info/requires.txt +3 -6
- {superpathlib-2.0.14 → superpathlib-2.1.1}/tests/test_common_folders.py +3 -3
- superpathlib-2.1.1/tests/test_content.py +141 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/tests/test_functionality.py +40 -93
- {superpathlib-2.0.14 → superpathlib-2.1.1}/tests/test_inheritance.py +3 -2
- {superpathlib-2.0.14 → superpathlib-2.1.1}/tests/test_metadata.py +3 -29
- superpathlib-2.0.14/src/superpathlib/cached_content.py +0 -63
- superpathlib-2.0.14/src/superpathlib/encryption.py +0 -73
- superpathlib-2.0.14/src/superpathlib/override.py +0 -114
- superpathlib-2.0.14/src/superpathlib/tags.py +0 -53
- superpathlib-2.0.14/tests/test_cached_content.py +0 -61
- superpathlib-2.0.14/tests/test_content.py +0 -101
- superpathlib-2.0.14/tests/test_encrypted_content.py +0 -66
- {superpathlib-2.0.14 → superpathlib-2.1.1}/LICENSE +0 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/setup.cfg +0 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/__init__.py +0 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/base.py +0 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/path.py +0 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib/py.typed +0 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib.egg-info/dependency_links.txt +0 -0
- {superpathlib-2.0.14 → superpathlib-2.1.1}/src/superpathlib.egg-info/top_level.txt +0 -0
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: superpathlib
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: Extended Pathlib
|
|
5
5
|
Author-email: Quinten Roets <qdr2104@columbia.edu>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Source Code, https://github.com/quintenroets/superpathlib
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: simple-classproperty<5,>=4.0.2
|
|
12
|
-
Requires-Dist: typing_extensions<5,>=4.0
|
|
13
12
|
Provides-Extra: full
|
|
14
13
|
Requires-Dist: dirhash<1,>=0.2.1; extra == "full"
|
|
15
14
|
Requires-Dist: numpy<3,>=1.26.4; extra == "full"
|
|
16
|
-
Requires-Dist: package-utils<1,>=0.
|
|
15
|
+
Requires-Dist: package-utils<1,>=0.9.0; extra == "full"
|
|
17
16
|
Requires-Dist: PyYaml<7,>=6.0.1; extra == "full"
|
|
18
|
-
Requires-Dist: xattr<2,>=0.10.1; extra == "full"
|
|
19
17
|
Provides-Extra: dev
|
|
20
18
|
Requires-Dist: hypothesis<7,>=6.97.1; extra == "dev"
|
|
21
|
-
Requires-Dist: package-dev-tools<1,>=0.8.
|
|
19
|
+
Requires-Dist: package-dev-tools<1,>=0.8.6; extra == "dev"
|
|
22
20
|
Requires-Dist: types-PyYaml<7,>=6.0.12.12; extra == "dev"
|
|
23
21
|
Requires-Dist: dirhash<1,>=0.2.1; extra == "dev"
|
|
24
22
|
Requires-Dist: numpy<3,>=1.26.4; extra == "dev"
|
|
25
|
-
Requires-Dist: package-utils<1,>=0.
|
|
23
|
+
Requires-Dist: package-utils<1,>=0.9.0; extra == "dev"
|
|
26
24
|
Requires-Dist: PyYaml<7,>=6.0.1; extra == "dev"
|
|
27
|
-
Requires-Dist: xattr<2,>=0.10.1; extra == "dev"
|
|
28
25
|
Dynamic: license-file
|
|
29
26
|
|
|
30
27
|
# Superpathlib
|
|
31
28
|
[](https://badge.fury.io/py/superpathlib)
|
|
32
29
|

|
|
33
|
-

|
|
34
31
|

|
|
35
32
|

|
|
36
33
|
|
|
@@ -42,13 +39,13 @@ For custom operations, the package supports subclassing, which has been added to
|
|
|
42
39
|
|
|
43
40
|
## Key features:
|
|
44
41
|
* Read and write content in various formats such as text, bytes, YAML, JSON, and even NumPy arrays
|
|
45
|
-
* Access and modify file metadata like file size
|
|
42
|
+
* Access and modify file metadata like file size and modification time
|
|
46
43
|
* Utility methods:
|
|
47
44
|
* `rmtree` to remove directories recursively
|
|
48
45
|
* `copy_to` to copy content
|
|
49
46
|
* `tempfile` to create and use temporary files
|
|
50
47
|
* `find` for recursive search
|
|
51
|
-
* `
|
|
48
|
+
* `unpack_if_archive` for unpacking archives
|
|
52
49
|
|
|
53
50
|
## Usage
|
|
54
51
|
|
|
@@ -66,10 +63,17 @@ path = Path(filename)
|
|
|
66
63
|
* json
|
|
67
64
|
* numpy
|
|
68
65
|
|
|
66
|
+
read-only:
|
|
67
|
+
* cached_yaml: yaml content, cached as json next to the file
|
|
68
|
+
|
|
69
|
+
gpg-encrypted on disk:
|
|
70
|
+
* encrypted: prefix any of the above to read & write plaintext (e.g. `path.encrypted.json`)
|
|
71
|
+
|
|
69
72
|
examples:
|
|
70
73
|
|
|
71
74
|
```shell
|
|
72
75
|
path.json = {key: value}
|
|
76
|
+
path.encrypted.json = {key: secret}
|
|
73
77
|
|
|
74
78
|
for line in path.lines:
|
|
75
79
|
if interesting(line):
|
|
@@ -85,14 +89,13 @@ for line in path.lines:
|
|
|
85
89
|
* content_hash: a hash of the complete substructure found in a folder
|
|
86
90
|
* get & set:
|
|
87
91
|
* mtime: modified time
|
|
88
|
-
* tag: can be used for alternative ordering or metadata
|
|
89
92
|
|
|
90
93
|
examples:
|
|
91
94
|
|
|
92
95
|
```shell
|
|
93
96
|
path_new.mtime = path_old.mtime
|
|
94
97
|
|
|
95
|
-
if path.
|
|
98
|
+
if path.filetype == "video":
|
|
96
99
|
process(path)
|
|
97
100
|
```
|
|
98
101
|
### 3) Use class properties to access commonly used folders:
|
|
@@ -110,11 +113,10 @@ names = names_path.lines
|
|
|
110
113
|
* find(): recursively find all paths under a root that match a condition (extra options available for performance optimization)
|
|
111
114
|
* rmtree(): remove directory recursively
|
|
112
115
|
* copy_to(dest): copy content to dest
|
|
113
|
-
*
|
|
116
|
+
* copy_mtime_to(dest): recursively copy modified time to all n-level children of dest
|
|
114
117
|
* tempfile(): create temporary file that can be used as context manager
|
|
115
|
-
*
|
|
118
|
+
* unpack_if_archive(): extract an archive(zip, tar, ..) file to desired folder, recursively
|
|
116
119
|
* pop_parent(): remove first parent from path in filesystem
|
|
117
|
-
* from_uri(uri): create path object from uri string
|
|
118
120
|
|
|
119
121
|
examples:
|
|
120
122
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Superpathlib
|
|
2
2
|
[](https://badge.fury.io/py/superpathlib)
|
|
3
3
|

|
|
4
|
-

|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
@@ -13,13 +13,13 @@ For custom operations, the package supports subclassing, which has been added to
|
|
|
13
13
|
|
|
14
14
|
## Key features:
|
|
15
15
|
* Read and write content in various formats such as text, bytes, YAML, JSON, and even NumPy arrays
|
|
16
|
-
* Access and modify file metadata like file size
|
|
16
|
+
* Access and modify file metadata like file size and modification time
|
|
17
17
|
* Utility methods:
|
|
18
18
|
* `rmtree` to remove directories recursively
|
|
19
19
|
* `copy_to` to copy content
|
|
20
20
|
* `tempfile` to create and use temporary files
|
|
21
21
|
* `find` for recursive search
|
|
22
|
-
* `
|
|
22
|
+
* `unpack_if_archive` for unpacking archives
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
@@ -37,10 +37,17 @@ path = Path(filename)
|
|
|
37
37
|
* json
|
|
38
38
|
* numpy
|
|
39
39
|
|
|
40
|
+
read-only:
|
|
41
|
+
* cached_yaml: yaml content, cached as json next to the file
|
|
42
|
+
|
|
43
|
+
gpg-encrypted on disk:
|
|
44
|
+
* encrypted: prefix any of the above to read & write plaintext (e.g. `path.encrypted.json`)
|
|
45
|
+
|
|
40
46
|
examples:
|
|
41
47
|
|
|
42
48
|
```shell
|
|
43
49
|
path.json = {key: value}
|
|
50
|
+
path.encrypted.json = {key: secret}
|
|
44
51
|
|
|
45
52
|
for line in path.lines:
|
|
46
53
|
if interesting(line):
|
|
@@ -56,14 +63,13 @@ for line in path.lines:
|
|
|
56
63
|
* content_hash: a hash of the complete substructure found in a folder
|
|
57
64
|
* get & set:
|
|
58
65
|
* mtime: modified time
|
|
59
|
-
* tag: can be used for alternative ordering or metadata
|
|
60
66
|
|
|
61
67
|
examples:
|
|
62
68
|
|
|
63
69
|
```shell
|
|
64
70
|
path_new.mtime = path_old.mtime
|
|
65
71
|
|
|
66
|
-
if path.
|
|
72
|
+
if path.filetype == "video":
|
|
67
73
|
process(path)
|
|
68
74
|
```
|
|
69
75
|
### 3) Use class properties to access commonly used folders:
|
|
@@ -81,11 +87,10 @@ names = names_path.lines
|
|
|
81
87
|
* find(): recursively find all paths under a root that match a condition (extra options available for performance optimization)
|
|
82
88
|
* rmtree(): remove directory recursively
|
|
83
89
|
* copy_to(dest): copy content to dest
|
|
84
|
-
*
|
|
90
|
+
* copy_mtime_to(dest): recursively copy modified time to all n-level children of dest
|
|
85
91
|
* tempfile(): create temporary file that can be used as context manager
|
|
86
|
-
*
|
|
92
|
+
* unpack_if_archive(): extract an archive(zip, tar, ..) file to desired folder, recursively
|
|
87
93
|
* pop_parent(): remove first parent from path in filesystem
|
|
88
|
-
* from_uri(uri): create path object from uri string
|
|
89
94
|
|
|
90
95
|
examples:
|
|
91
96
|
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "superpathlib"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.1.1"
|
|
4
4
|
description = "Extended Pathlib"
|
|
5
5
|
authors = [{name = "Quinten Roets", email = "qdr2104@columbia.edu"}]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
requires-python = ">=3.
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
9
|
dependencies = [
|
|
10
10
|
"simple-classproperty >=4.0.2, <5",
|
|
11
|
-
"typing_extensions >=4.0, <5",
|
|
12
11
|
]
|
|
13
12
|
|
|
14
13
|
[project.optional-dependencies]
|
|
15
14
|
full = [
|
|
16
15
|
"dirhash >=0.2.1, <1",
|
|
17
16
|
"numpy >=1.26.4, <3",
|
|
18
|
-
"package-utils >=0.
|
|
17
|
+
"package-utils >=0.9.0, <1",
|
|
19
18
|
"PyYaml >=6.0.1, <7",
|
|
20
|
-
"xattr >=0.10.1, <2",
|
|
21
19
|
]
|
|
22
20
|
dev = [
|
|
23
21
|
"hypothesis >=6.97.1, <7",
|
|
24
|
-
"package-dev-tools >=0.8.
|
|
22
|
+
"package-dev-tools >=0.8.6, <1",
|
|
25
23
|
|
|
26
24
|
# types
|
|
27
25
|
"types-PyYaml >=6.0.12.12, <7",
|
|
@@ -29,9 +27,8 @@ dev = [
|
|
|
29
27
|
# full
|
|
30
28
|
"dirhash >=0.2.1, <1",
|
|
31
29
|
"numpy >=1.26.4, <3",
|
|
32
|
-
"package-utils >=0.
|
|
30
|
+
"package-utils >=0.9.0, <1",
|
|
33
31
|
"PyYaml >=6.0.1, <7",
|
|
34
|
-
"xattr >=0.10.1, <2",
|
|
35
32
|
]
|
|
36
33
|
|
|
37
34
|
[project.urls]
|
|
@@ -43,6 +40,7 @@ build-backend = "setuptools.build_meta"
|
|
|
43
40
|
|
|
44
41
|
[tool.coverage.run]
|
|
45
42
|
command_line = "-m pytest tests"
|
|
43
|
+
relative_files = true
|
|
46
44
|
|
|
47
45
|
[tool.coverage.report]
|
|
48
46
|
precision = 4
|
|
@@ -55,7 +53,6 @@ no_implicit_reexport = false
|
|
|
55
53
|
[[tool.mypy.overrides]]
|
|
56
54
|
module = [
|
|
57
55
|
"dirhash.*",
|
|
58
|
-
"xattr.*",
|
|
59
56
|
]
|
|
60
57
|
ignore_missing_imports = true
|
|
61
58
|
|
|
@@ -71,6 +68,7 @@ fix = true
|
|
|
71
68
|
select = ["ALL"]
|
|
72
69
|
ignore = [
|
|
73
70
|
"ANN401", # annotated with Any
|
|
71
|
+
"CPY001", # missing copyright notice
|
|
74
72
|
"D1", # missing docstrings
|
|
75
73
|
"D200", # one-line docstring
|
|
76
74
|
"D203", # conflicts with D211
|
|
@@ -78,6 +76,7 @@ ignore = [
|
|
|
78
76
|
"D212", # conflicts with D213
|
|
79
77
|
"D401", # imperative first line
|
|
80
78
|
"G004", # logging f-string
|
|
79
|
+
"PLC0415", # lazy imports for import performance and optional dependencies
|
|
81
80
|
]
|
|
82
81
|
|
|
83
82
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -87,18 +86,6 @@ ignore = [
|
|
|
87
86
|
"tests/*" = [
|
|
88
87
|
"S101", # assert used
|
|
89
88
|
]
|
|
90
|
-
"src/superpathlib/cached_content.py" = [
|
|
91
|
-
"PLC0415", # lazy imports for optional dependency
|
|
92
|
-
]
|
|
93
|
-
"src/superpathlib/content_properties.py" = [
|
|
94
|
-
"PLC0415", # lazy imports for optional dependencies
|
|
95
|
-
]
|
|
96
|
-
"src/superpathlib/extra_functionality.py" = [
|
|
97
|
-
"PLC0415", # lazy imports for optional dependencies
|
|
98
|
-
]
|
|
99
|
-
"src/superpathlib/metadata_properties.py" = [
|
|
100
|
-
"PLC0415", # lazy imports for optional dependencies
|
|
101
|
-
]
|
|
102
89
|
|
|
103
90
|
[tool.setuptools.package-data]
|
|
104
91
|
superpathlib = ["py.typed"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from typing import Generic, TypeVar, cast
|
|
4
|
+
|
|
5
|
+
from .extra_functionality import Path
|
|
6
|
+
|
|
7
|
+
P = TypeVar("P", bound=Path)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Archive(Generic[P]):
|
|
11
|
+
def __init__(self, path: P) -> None:
|
|
12
|
+
self.path = path
|
|
13
|
+
|
|
14
|
+
@cached_property
|
|
15
|
+
def format_(self) -> str | None:
|
|
16
|
+
# noinspection PyProtectedMember
|
|
17
|
+
format_ = shutil._find_unpack_format(str(self.path)) # type: ignore[attr-defined] # noqa: SLF001
|
|
18
|
+
return cast("str | None", format_)
|
|
19
|
+
|
|
20
|
+
def unpack( # noqa: PLR0913
|
|
21
|
+
self,
|
|
22
|
+
extraction_directory: P | None = None,
|
|
23
|
+
*,
|
|
24
|
+
remove_existing: bool = True,
|
|
25
|
+
preserve_mtime: bool = True,
|
|
26
|
+
remove_original: bool = True,
|
|
27
|
+
format_: str | None = None,
|
|
28
|
+
recursive: bool = True,
|
|
29
|
+
) -> None:
|
|
30
|
+
format_ = cast("str", self.format_) if format_ is None else format_
|
|
31
|
+
extraction_directory = (
|
|
32
|
+
self.create_extraction_directory(format_)
|
|
33
|
+
if extraction_directory is None
|
|
34
|
+
else extraction_directory
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if remove_existing:
|
|
38
|
+
extraction_directory.remove()
|
|
39
|
+
|
|
40
|
+
shutil.unpack_archive(
|
|
41
|
+
self.path,
|
|
42
|
+
extract_dir=extraction_directory,
|
|
43
|
+
format=format_,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self.cleanup(extraction_directory)
|
|
47
|
+
if preserve_mtime:
|
|
48
|
+
self.path.copy_mtime_to(extraction_directory)
|
|
49
|
+
|
|
50
|
+
if remove_original:
|
|
51
|
+
self.path.unlink()
|
|
52
|
+
|
|
53
|
+
if recursive:
|
|
54
|
+
for path in extraction_directory.find():
|
|
55
|
+
path.unpack_if_archive()
|
|
56
|
+
|
|
57
|
+
def create_extraction_directory(self, format_: str) -> P:
|
|
58
|
+
# noinspection PyProtectedMember
|
|
59
|
+
unpack_formats = shutil._UNPACK_FORMATS # type: ignore[attr-defined] # noqa: SLF001
|
|
60
|
+
extensions, *_ = unpack_formats[format_]
|
|
61
|
+
extract_name = self.path.name
|
|
62
|
+
for archive_extension in extensions:
|
|
63
|
+
extract_name = extract_name.removesuffix(archive_extension)
|
|
64
|
+
return self.path.with_name(extract_name)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def cleanup(extraction_directory: P) -> None:
|
|
68
|
+
(extraction_directory / "__MACOSX").rmtree(missing_ok=True)
|
|
69
|
+
subfolder = extraction_directory / extraction_directory.name
|
|
70
|
+
if subfolder.exists() and extraction_directory.number_of_children == 1:
|
|
71
|
+
subfolder.pop_parent() # pragma: nocover
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import sys
|
|
3
3
|
import typing
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Self
|
|
5
5
|
|
|
6
6
|
from simple_classproperty import classproperty
|
|
7
|
-
from typing_extensions import Self
|
|
8
7
|
|
|
9
8
|
from . import base
|
|
10
9
|
|
|
11
|
-
T = TypeVar("T", bound="Path")
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
def enable_classproperties(cls: type[T]) -> None: # pragma: nocover
|
|
11
|
+
def enable_classproperties(cls: type) -> None: # pragma: nocover
|
|
15
12
|
for name, method in vars(cls).items():
|
|
16
13
|
if isinstance(method, classmethod):
|
|
17
14
|
wrapped_method = method.__func__
|
|
@@ -28,9 +25,7 @@ class PropertyMeta(abc.ABCMeta):
|
|
|
28
25
|
) -> "PropertyMeta":
|
|
29
26
|
meta_class = super().__new__(cls, name, bases, attributes)
|
|
30
27
|
if sys.version_info >= (3, 13):
|
|
31
|
-
enable_classproperties(
|
|
32
|
-
meta_class, # type: ignore[arg-type]
|
|
33
|
-
) # pragma: nocover
|
|
28
|
+
enable_classproperties(meta_class) # pragma: nocover
|
|
34
29
|
return meta_class
|
|
35
30
|
|
|
36
31
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import typing
|
|
5
4
|
from typing import Any
|
|
6
5
|
|
|
7
|
-
from . import
|
|
6
|
+
from . import metadata_properties
|
|
7
|
+
from .utils import catch_missing
|
|
8
8
|
|
|
9
9
|
if typing.TYPE_CHECKING: # pragma: nocover
|
|
10
10
|
from collections.abc import Iterable
|
|
@@ -12,12 +12,7 @@ if typing.TYPE_CHECKING: # pragma: nocover
|
|
|
12
12
|
from numpy.typing import NDArray
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
# - performance optimization
|
|
17
|
-
# - enabling optional dependencies
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class Path(base.Path):
|
|
15
|
+
class Path(metadata_properties.Path):
|
|
21
16
|
"""
|
|
22
17
|
Properties to read & write content in different formats.
|
|
23
18
|
"""
|
|
@@ -31,11 +26,12 @@ class Path(base.Path):
|
|
|
31
26
|
self.write_bytes(value)
|
|
32
27
|
|
|
33
28
|
@property
|
|
29
|
+
@catch_missing(default="")
|
|
34
30
|
def text(self) -> str:
|
|
35
31
|
return self.read_text()
|
|
36
32
|
|
|
37
33
|
@text.setter
|
|
38
|
-
def text(self, value:
|
|
34
|
+
def text(self, value: object) -> None:
|
|
39
35
|
self.write_text(str(value))
|
|
40
36
|
|
|
41
37
|
@property
|
|
@@ -56,31 +52,48 @@ class Path(base.Path):
|
|
|
56
52
|
self.lines = typing.cast("list[str]", lines)
|
|
57
53
|
|
|
58
54
|
@property
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
@catch_missing(default=None)
|
|
56
|
+
def json(self) -> Any:
|
|
57
|
+
with self.open("rb") as fp:
|
|
58
|
+
import json
|
|
59
|
+
|
|
60
|
+
return json.load(fp)
|
|
62
61
|
|
|
63
62
|
@json.setter
|
|
64
|
-
def json(self, content:
|
|
65
|
-
|
|
63
|
+
def json(self, content: Any) -> None:
|
|
64
|
+
import json
|
|
65
|
+
|
|
66
|
+
with self.open("w") as fp:
|
|
67
|
+
json.dump(content, fp)
|
|
66
68
|
|
|
67
69
|
@property
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
@catch_missing(default=None)
|
|
71
|
+
def yaml(self) -> Any:
|
|
72
|
+
with self.open("rb") as fp:
|
|
73
|
+
import yaml
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
yaml.
|
|
74
|
-
)
|
|
75
|
-
return yaml.load(self.text, Loader=Loader) or {} # noqa: S506
|
|
75
|
+
# C implementation much faster
|
|
76
|
+
loader = yaml.CSafeLoader if yaml.__with_libyaml__ else yaml.SafeLoader
|
|
77
|
+
return yaml.load(fp, Loader=loader) # noqa: S506
|
|
76
78
|
|
|
77
79
|
@yaml.setter
|
|
78
|
-
def yaml(self, value:
|
|
80
|
+
def yaml(self, value: Any) -> None:
|
|
79
81
|
import yaml
|
|
80
82
|
|
|
81
|
-
# C implementation much faster
|
|
82
|
-
|
|
83
|
-
self.
|
|
83
|
+
# C implementation much faster
|
|
84
|
+
dumper = yaml.CSafeDumper if yaml.__with_libyaml__ else yaml.SafeDumper
|
|
85
|
+
with self.open("w") as fp:
|
|
86
|
+
yaml.dump(value, fp, Dumper=dumper, width=1024)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def cached_yaml(self) -> Any:
|
|
90
|
+
cache_file = self.with_name(self.name + ".cache")
|
|
91
|
+
mtime = self.mtime
|
|
92
|
+
exists = mtime > 0
|
|
93
|
+
if exists and cache_file.mtime != mtime:
|
|
94
|
+
cache_file.json = self.yaml
|
|
95
|
+
cache_file.mtime = mtime
|
|
96
|
+
return cache_file.json if exists else None
|
|
84
97
|
|
|
85
98
|
@property
|
|
86
99
|
def numpy(self) -> NDArray[Any]:
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import subprocess
|
|
3
|
+
from functools import cache
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import IO, Any, cast
|
|
6
|
+
|
|
7
|
+
from package_utils.secrets_ import load_secret
|
|
8
|
+
|
|
9
|
+
from .path import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EncryptedPath(Path):
|
|
13
|
+
def open( # type: ignore[override]
|
|
14
|
+
self,
|
|
15
|
+
mode: str = "r",
|
|
16
|
+
buffering: int = -1, # noqa: ARG002
|
|
17
|
+
encoding: str | None = None,
|
|
18
|
+
errors: str | None = None,
|
|
19
|
+
newline: str | None = None,
|
|
20
|
+
) -> IO[Any]:
|
|
21
|
+
access = mode.replace("b", "").replace("t", "")
|
|
22
|
+
writing = {"r": False, "w": True}[access]
|
|
23
|
+
buffer = EncryptedFile(Path(self), writing=writing)
|
|
24
|
+
return (
|
|
25
|
+
buffer
|
|
26
|
+
if "b" in mode
|
|
27
|
+
else EncryptedTextFile(buffer, encoding, errors, newline)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EncryptedFile(io.BytesIO):
|
|
32
|
+
def __init__(self, path: Path, *, writing: bool) -> None:
|
|
33
|
+
self.path = path
|
|
34
|
+
self.writing = writing
|
|
35
|
+
plaintext = b"" if writing else run_gpg(path.read_bytes(), "--decrypt")
|
|
36
|
+
super().__init__(plaintext)
|
|
37
|
+
|
|
38
|
+
def __exit__(
|
|
39
|
+
self,
|
|
40
|
+
exception_type: type[BaseException] | None,
|
|
41
|
+
exception: BaseException | None,
|
|
42
|
+
traceback: TracebackType | None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.cancel_write_if_failed(exception_type)
|
|
45
|
+
super().__exit__(exception_type, exception, traceback)
|
|
46
|
+
|
|
47
|
+
def cancel_write_if_failed(
|
|
48
|
+
self,
|
|
49
|
+
exception_type: type[BaseException] | None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.writing &= exception_type is None
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
if not self.closed and self.writing:
|
|
55
|
+
self.path.write_bytes(run_gpg(self.getvalue(), "--symmetric"))
|
|
56
|
+
super().close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EncryptedTextFile(io.TextIOWrapper):
|
|
60
|
+
def __exit__(
|
|
61
|
+
self,
|
|
62
|
+
exception_type: type[BaseException] | None,
|
|
63
|
+
exception: BaseException | None,
|
|
64
|
+
traceback: TracebackType | None,
|
|
65
|
+
) -> None:
|
|
66
|
+
cast("EncryptedFile", self.buffer).cancel_write_if_failed(exception_type)
|
|
67
|
+
super().__exit__(exception_type, exception, traceback)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def run_gpg(data: bytes, *options: str) -> bytes:
|
|
71
|
+
command = ("gpg", "--passphrase-fd", "0", "--batch", "--quiet", *options)
|
|
72
|
+
input_ = f"{load_passphrase()}\n".encode() + data
|
|
73
|
+
process = subprocess.run(command, input=input_, stdout=subprocess.PIPE, check=True) # noqa: S603
|
|
74
|
+
return process.stdout
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@cache
|
|
78
|
+
def load_passphrase() -> str:
|
|
79
|
+
return load_secret("file encryption password")
|