superpathlib 1.0.1__py2.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.
- plib/__init__.py +1 -0
- plib/plib.py +529 -0
- plib/tags.py +39 -0
- plib/utils.py +25 -0
- superpathlib-1.0.1.dist-info/LICENSE +21 -0
- superpathlib-1.0.1.dist-info/METADATA +115 -0
- superpathlib-1.0.1.dist-info/RECORD +9 -0
- superpathlib-1.0.1.dist-info/WHEEL +6 -0
- superpathlib-1.0.1.dist-info/top_level.txt +1 -0
plib/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .plib import Path
|
plib/plib.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
from __future__ import annotations # https://www.python.org/dev/peps/pep-0563/
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import mimetypes
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from functools import cached_property, wraps
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .utils import find_first_match
|
|
17
|
+
|
|
18
|
+
# Long import times relative to their usage frequency: lazily imported
|
|
19
|
+
# import yaml
|
|
20
|
+
# from datatime import datetime
|
|
21
|
+
# from .tags import XDGTags
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def catch_missing(default=None):
|
|
25
|
+
def wrap_function(func):
|
|
26
|
+
@wraps(func)
|
|
27
|
+
def wrap_args(*args, **kwargs):
|
|
28
|
+
try:
|
|
29
|
+
res = func(*args, **kwargs)
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
res = default
|
|
32
|
+
return res
|
|
33
|
+
|
|
34
|
+
return wrap_args
|
|
35
|
+
|
|
36
|
+
return wrap_function
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_parent_on_missing(func):
|
|
40
|
+
@wraps(func)
|
|
41
|
+
def wrapper(*args, **kwargs):
|
|
42
|
+
try:
|
|
43
|
+
res = func(*args, **kwargs)
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
path = args[0]
|
|
46
|
+
path.create_parent()
|
|
47
|
+
res = func(*args, **kwargs)
|
|
48
|
+
return res
|
|
49
|
+
|
|
50
|
+
return wrapper
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_parent(func):
|
|
54
|
+
@wraps(func)
|
|
55
|
+
def wrapper(*args, **kwargs):
|
|
56
|
+
path = func(*args, **kwargs)
|
|
57
|
+
path.create_parent()
|
|
58
|
+
return path
|
|
59
|
+
|
|
60
|
+
return wrapper
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Path(pathlib.Path):
|
|
64
|
+
"""Extend pathlib functionality and enable further extensions by
|
|
65
|
+
inheriting.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
_flavour = (
|
|
69
|
+
pathlib._windows_flavour if os.name == "nt" else pathlib._posix_flavour
|
|
70
|
+
) # _flavour attribute needs to inherited explicitely from pathlib
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
Overwrite existing methods with exception handling and default values
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@create_parent_on_missing
|
|
77
|
+
def touch(self, mode=0o666, exist_ok=True, mtime=None):
|
|
78
|
+
super().touch(mode=mode, exist_ok=exist_ok)
|
|
79
|
+
if mtime is not None:
|
|
80
|
+
self.mtime = mtime # set time after touch or it is immediately overwritten
|
|
81
|
+
|
|
82
|
+
@catch_missing(default=0)
|
|
83
|
+
def rmdir(self):
|
|
84
|
+
return super().rmdir()
|
|
85
|
+
|
|
86
|
+
def iterdir(self, missing_ok=True):
|
|
87
|
+
if self.exists() or not missing_ok:
|
|
88
|
+
yield from super().iterdir()
|
|
89
|
+
|
|
90
|
+
def rename(self, target):
|
|
91
|
+
target = Path(target)
|
|
92
|
+
target.create_parent()
|
|
93
|
+
try:
|
|
94
|
+
target = super().rename(target)
|
|
95
|
+
except OSError:
|
|
96
|
+
target = shutil.move(self, target)
|
|
97
|
+
return target
|
|
98
|
+
|
|
99
|
+
def create_parent(self):
|
|
100
|
+
self.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
return self.parent
|
|
102
|
+
|
|
103
|
+
def with_nonexistent_name(self):
|
|
104
|
+
path = self
|
|
105
|
+
if path.exists():
|
|
106
|
+
stem = path.stem
|
|
107
|
+
|
|
108
|
+
def with_number(i: int):
|
|
109
|
+
return path.with_stem(f"{stem} ({i})")
|
|
110
|
+
|
|
111
|
+
def nonexistent(i):
|
|
112
|
+
return not with_number(i).exists()
|
|
113
|
+
|
|
114
|
+
first_free_number = find_first_match(nonexistent)
|
|
115
|
+
path = with_number(first_free_number)
|
|
116
|
+
|
|
117
|
+
return path
|
|
118
|
+
|
|
119
|
+
def with_timestamp(self):
|
|
120
|
+
from datetime import datetime # noqa: autoimport
|
|
121
|
+
|
|
122
|
+
timestamp = datetime.fromtimestamp(int(time.time())) # precision up to second
|
|
123
|
+
return self.with_stem(f"{self.stem} {timestamp}")
|
|
124
|
+
|
|
125
|
+
def open(self, mode="r", **kwargs):
|
|
126
|
+
try:
|
|
127
|
+
res = super().open(mode, **kwargs)
|
|
128
|
+
except FileNotFoundError:
|
|
129
|
+
if "w" in mode or "a" in mode:
|
|
130
|
+
# exist_ok=True: catch race conditions when calling multiple times
|
|
131
|
+
self.create_parent()
|
|
132
|
+
res = super().open(mode, **kwargs)
|
|
133
|
+
else:
|
|
134
|
+
encrypted = self.encrypted.exists()
|
|
135
|
+
if "b" in mode:
|
|
136
|
+
byte_content = self.encrypted.byte_content if encrypted else b""
|
|
137
|
+
res = io.BytesIO(byte_content)
|
|
138
|
+
else:
|
|
139
|
+
text = self.encrypted.text if encrypted else ""
|
|
140
|
+
res = io.StringIO(text)
|
|
141
|
+
return res
|
|
142
|
+
return res
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
Properties to read & write content in different formats
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def byte_content(self) -> bytes:
|
|
150
|
+
return self.read_bytes()
|
|
151
|
+
|
|
152
|
+
@byte_content.setter
|
|
153
|
+
def byte_content(self, value: bytes) -> None:
|
|
154
|
+
self.write_bytes(value)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def text(self) -> str:
|
|
158
|
+
return self.read_text()
|
|
159
|
+
|
|
160
|
+
@text.setter
|
|
161
|
+
def text(self, value: Any) -> None:
|
|
162
|
+
self.write_text(str(value))
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def lines(self) -> list[str]:
|
|
166
|
+
lines = self.text.strip().splitlines()
|
|
167
|
+
lines = [line for line in lines if line]
|
|
168
|
+
return lines
|
|
169
|
+
|
|
170
|
+
@lines.setter
|
|
171
|
+
def lines(self, lines: Iterable[Any]) -> None:
|
|
172
|
+
self.text = "\n".join(str(line) for line in lines)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def json(self):
|
|
176
|
+
return json.loads(self.text or "{}")
|
|
177
|
+
|
|
178
|
+
@json.setter
|
|
179
|
+
def json(self, content):
|
|
180
|
+
self.text = json.dumps(content)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def yaml(self):
|
|
184
|
+
import yaml # noqa: autoimport
|
|
185
|
+
|
|
186
|
+
# C implementation much faster but only supported on Linux
|
|
187
|
+
Loader = yaml.CFullLoader if hasattr(yaml, "CFullLoader") else yaml.FullLoader
|
|
188
|
+
return yaml.load(self.text, Loader=Loader) or {}
|
|
189
|
+
|
|
190
|
+
@yaml.setter
|
|
191
|
+
def yaml(self, value):
|
|
192
|
+
import yaml # noqa: autoimport
|
|
193
|
+
|
|
194
|
+
# C implementation much faster but only supported on Linux
|
|
195
|
+
Dumper = yaml.CDumper if hasattr(yaml, "CDumper") else yaml.Dumper
|
|
196
|
+
self.text = yaml.dump(value, Dumper=Dumper, width=1024)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def encrypted(self):
|
|
200
|
+
path = self
|
|
201
|
+
encryption_suffix = ".gpg"
|
|
202
|
+
if path.suffix != encryption_suffix:
|
|
203
|
+
path = path.with_suffix(path.suffix + encryption_suffix)
|
|
204
|
+
return EncryptedPath(path)
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
Properties to read & write metadata
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
@catch_missing(default=0.0)
|
|
212
|
+
def mtime(self):
|
|
213
|
+
return self.stat().st_mtime
|
|
214
|
+
|
|
215
|
+
@mtime.setter
|
|
216
|
+
def mtime(self, time: float):
|
|
217
|
+
os.utime(self, (time, time)) # set create time as well
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
subprocess.run(("touch", "-d", "@%f" % time, self))
|
|
221
|
+
except subprocess.CalledProcessError:
|
|
222
|
+
pass # Doesn't work on Windows
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def tags(self):
|
|
226
|
+
from .tags import XDGTags # noqa: autoimport
|
|
227
|
+
|
|
228
|
+
return XDGTags(self).get()
|
|
229
|
+
|
|
230
|
+
@tags.setter
|
|
231
|
+
def tags(self, *values):
|
|
232
|
+
from .tags import XDGTags # noqa: autoimport
|
|
233
|
+
|
|
234
|
+
if len(values) == 1 and values[0] in (None, []):
|
|
235
|
+
XDGTags(self).clear()
|
|
236
|
+
else:
|
|
237
|
+
XDGTags(self).set(*values)
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def tag(self):
|
|
241
|
+
return self.tags[0] if self.tags else None
|
|
242
|
+
|
|
243
|
+
@tag.setter
|
|
244
|
+
def tag(self, value):
|
|
245
|
+
self.tags = value
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
@catch_missing(default=0)
|
|
249
|
+
def size(self):
|
|
250
|
+
return self.stat().st_size
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def is_root(self):
|
|
254
|
+
path = self
|
|
255
|
+
while not path.exists():
|
|
256
|
+
path = path.parent
|
|
257
|
+
return path.owner() == "root"
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def has_children(self):
|
|
261
|
+
return next(self.iterdir(), None) is not None if self.is_dir() else False
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def number_of_children(self):
|
|
265
|
+
return sum(1 for _ in self.iterdir()) if self.is_dir() else 0
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def filetype(self):
|
|
269
|
+
filetype = mimetypes.guess_type(self)[0]
|
|
270
|
+
if filetype:
|
|
271
|
+
filetype = filetype.split("/")[0]
|
|
272
|
+
return filetype
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
Additional functionality
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def copy_to(self, dest: Path, include_properties=True):
|
|
279
|
+
dest.byte_content = self.byte_content
|
|
280
|
+
if include_properties:
|
|
281
|
+
self.copy_properties_to(dest)
|
|
282
|
+
|
|
283
|
+
def copy_properties_to(self, dest: Path):
|
|
284
|
+
for path in dest.find():
|
|
285
|
+
path.tag = self.tag
|
|
286
|
+
path.mtime = self.mtime
|
|
287
|
+
|
|
288
|
+
@cached_property
|
|
289
|
+
def archive_format(self):
|
|
290
|
+
return shutil._find_unpack_format(str(self))
|
|
291
|
+
|
|
292
|
+
def unpack_if_archive(self, extract_dir: Path = None, recursive=True):
|
|
293
|
+
if self.archive_format is not None:
|
|
294
|
+
self.unpack(extract_dir, recursive=recursive)
|
|
295
|
+
|
|
296
|
+
def unpack(
|
|
297
|
+
self,
|
|
298
|
+
extract_dir: Path | None = None,
|
|
299
|
+
remove_existing: bool = True,
|
|
300
|
+
preserve_properties: bool = True,
|
|
301
|
+
remove_original: bool = True,
|
|
302
|
+
format: str = None,
|
|
303
|
+
recursive: bool = True,
|
|
304
|
+
):
|
|
305
|
+
def cleanup(path: Path):
|
|
306
|
+
(path / "__MACOSX").rmtree()
|
|
307
|
+
subfolder = path / path.name
|
|
308
|
+
if subfolder.exists() and path.number_of_children == 1:
|
|
309
|
+
subfolder.pop_parent()
|
|
310
|
+
|
|
311
|
+
if format is None:
|
|
312
|
+
format = self.archive_format
|
|
313
|
+
|
|
314
|
+
if extract_dir is None:
|
|
315
|
+
extract_name = self.name
|
|
316
|
+
unpack_info = shutil._UNPACK_FORMATS[format]
|
|
317
|
+
for archive_ext in unpack_info[0]:
|
|
318
|
+
if extract_name.endswith(archive_ext):
|
|
319
|
+
extract_name = extract_name.replace(archive_ext, "")
|
|
320
|
+
extract_dir = self.with_name(extract_name)
|
|
321
|
+
|
|
322
|
+
if remove_existing:
|
|
323
|
+
extract_dir.rmtree()
|
|
324
|
+
|
|
325
|
+
shutil.unpack_archive(self, extract_dir=extract_dir, format=format)
|
|
326
|
+
|
|
327
|
+
cleanup(extract_dir)
|
|
328
|
+
if preserve_properties:
|
|
329
|
+
self.copy_properties_to(extract_dir)
|
|
330
|
+
if remove_original:
|
|
331
|
+
self.unlink()
|
|
332
|
+
|
|
333
|
+
if recursive:
|
|
334
|
+
for path in extract_dir.find():
|
|
335
|
+
path.unpack_if_archive()
|
|
336
|
+
|
|
337
|
+
def pop_parent(self):
|
|
338
|
+
"""Remove first parent from path in filesystem."""
|
|
339
|
+
dest = self.parent.parent / self.name
|
|
340
|
+
parent = self.parent
|
|
341
|
+
temp_dest = dest.with_nonexistent_name() # can only move to nonexisting path
|
|
342
|
+
|
|
343
|
+
self.rename(temp_dest)
|
|
344
|
+
|
|
345
|
+
if not parent.has_children:
|
|
346
|
+
parent.rmdir()
|
|
347
|
+
if not parent.exists():
|
|
348
|
+
temp_dest.rename(dest)
|
|
349
|
+
else:
|
|
350
|
+
# merge in existing folder
|
|
351
|
+
shutil.copytree(temp_dest, dest, dirs_exist_ok=True)
|
|
352
|
+
temp_dest.rmtree()
|
|
353
|
+
|
|
354
|
+
def is_empty(self):
|
|
355
|
+
return (
|
|
356
|
+
not self.exists()
|
|
357
|
+
or (self.is_dir() and next(self.iterdir(), None) is None)
|
|
358
|
+
or (self.is_file() and self.size == 0)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def load_yaml(self, trusted=False):
|
|
362
|
+
"""
|
|
363
|
+
:param trusted: if the path is trusted, an unsafe loader
|
|
364
|
+
can be used to instantiate any object
|
|
365
|
+
:return: Content in path that contains yaml format
|
|
366
|
+
"""
|
|
367
|
+
import yaml # noqa: autoimport
|
|
368
|
+
|
|
369
|
+
loader = yaml.CUnsafeLoader if trusted else yaml.CFullLoader
|
|
370
|
+
return yaml.load(self.text, Loader=loader) or {}
|
|
371
|
+
|
|
372
|
+
def update(self, value):
|
|
373
|
+
# only read and write if value to add not empty
|
|
374
|
+
if value:
|
|
375
|
+
updated_content = self.yaml | value
|
|
376
|
+
self.yaml = updated_content
|
|
377
|
+
return updated_content
|
|
378
|
+
|
|
379
|
+
def find(
|
|
380
|
+
self,
|
|
381
|
+
condition=None,
|
|
382
|
+
exclude=None,
|
|
383
|
+
recurse_on_match=False,
|
|
384
|
+
follow_symlinks=False,
|
|
385
|
+
only_folders=False,
|
|
386
|
+
):
|
|
387
|
+
"""
|
|
388
|
+
Find all subpaths under path that match condition.
|
|
389
|
+
|
|
390
|
+
only_folders option can be used for efficiency reasons
|
|
391
|
+
"""
|
|
392
|
+
if condition is None:
|
|
393
|
+
recurse_on_match = True
|
|
394
|
+
|
|
395
|
+
def condition(_):
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
if exclude is None:
|
|
399
|
+
|
|
400
|
+
def exclude(_):
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
to_traverse = [self] if self.exists() else []
|
|
404
|
+
while to_traverse:
|
|
405
|
+
path = to_traverse.pop(0)
|
|
406
|
+
|
|
407
|
+
if not exclude(path):
|
|
408
|
+
match = condition(path)
|
|
409
|
+
if match:
|
|
410
|
+
yield path
|
|
411
|
+
|
|
412
|
+
if not match or recurse_on_match:
|
|
413
|
+
if only_folders or path.is_dir():
|
|
414
|
+
try:
|
|
415
|
+
for child in path.iterdir():
|
|
416
|
+
if follow_symlinks or not child.is_symlink():
|
|
417
|
+
if not only_folders or child.is_dir():
|
|
418
|
+
to_traverse.append(child)
|
|
419
|
+
except PermissionError:
|
|
420
|
+
pass # skip folders that do not allow listing
|
|
421
|
+
|
|
422
|
+
def rmtree(self, missing_ok=False, remove_root=True):
|
|
423
|
+
for path in self.iterdir():
|
|
424
|
+
if path.is_dir():
|
|
425
|
+
path.rmtree()
|
|
426
|
+
else:
|
|
427
|
+
path.unlink()
|
|
428
|
+
if remove_root:
|
|
429
|
+
self.rmdir()
|
|
430
|
+
|
|
431
|
+
def subpath(self, *parts):
|
|
432
|
+
path = self
|
|
433
|
+
for part in parts:
|
|
434
|
+
path /= part.replace(self._flavour.sep, "_")
|
|
435
|
+
return path
|
|
436
|
+
|
|
437
|
+
@classmethod
|
|
438
|
+
def tempfile(cls, **kwargs) -> Path:
|
|
439
|
+
"""
|
|
440
|
+
Usage:
|
|
441
|
+
with Path.tempfile() as tmp:
|
|
442
|
+
run_command(log_file=tmp)
|
|
443
|
+
logs = tmp.text
|
|
444
|
+
process_logs(logs)
|
|
445
|
+
"""
|
|
446
|
+
_, path = tempfile.mkstemp(**kwargs)
|
|
447
|
+
return cls(path)
|
|
448
|
+
|
|
449
|
+
def __enter__(self):
|
|
450
|
+
return self
|
|
451
|
+
|
|
452
|
+
def __exit__(self, *_):
|
|
453
|
+
self.unlink(missing_ok=True)
|
|
454
|
+
|
|
455
|
+
"""
|
|
456
|
+
Common folders: use properties and classmethods to guarantee
|
|
457
|
+
the same behavior for all derived classes
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
@classmethod
|
|
461
|
+
@property
|
|
462
|
+
def HOME(cls) -> Path:
|
|
463
|
+
return cls.home()
|
|
464
|
+
|
|
465
|
+
@classmethod
|
|
466
|
+
@property
|
|
467
|
+
def docs(cls) -> Path:
|
|
468
|
+
return cls.HOME / "Documents"
|
|
469
|
+
|
|
470
|
+
@classmethod
|
|
471
|
+
@property
|
|
472
|
+
def scripts(cls) -> Path:
|
|
473
|
+
return cls.docs / "Scripts"
|
|
474
|
+
|
|
475
|
+
@classmethod
|
|
476
|
+
@property
|
|
477
|
+
def script_assets(cls) -> Path:
|
|
478
|
+
return cls.scripts / "assets"
|
|
479
|
+
|
|
480
|
+
@classmethod
|
|
481
|
+
@property
|
|
482
|
+
def assets(cls) -> Path:
|
|
483
|
+
"""Often overwritten by child classes for specific project."""
|
|
484
|
+
return cls.script_assets
|
|
485
|
+
|
|
486
|
+
@classmethod
|
|
487
|
+
@property
|
|
488
|
+
def draft(cls) -> Path:
|
|
489
|
+
return cls.docs / "draft.txt"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class EncryptedPath(Path):
|
|
493
|
+
@cached_property
|
|
494
|
+
def password(self):
|
|
495
|
+
command = 'ksshaskpass -- "Enter passphrase for file encryption: "'
|
|
496
|
+
return subprocess.getoutput(command)
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def encryption_command(self):
|
|
500
|
+
return *self.decryption_command, "-c"
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def decryption_command(self):
|
|
504
|
+
return "gpg", "--passphrase", self.password, "--batch", "--quiet", "--yes"
|
|
505
|
+
|
|
506
|
+
def read_bytes(self) -> bytes:
|
|
507
|
+
encrypted_bytes = super().read_bytes()
|
|
508
|
+
if encrypted_bytes:
|
|
509
|
+
process = subprocess.Popen(
|
|
510
|
+
self.decryption_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
|
511
|
+
)
|
|
512
|
+
decrypted_bytes, _ = process.communicate(input=encrypted_bytes)
|
|
513
|
+
else:
|
|
514
|
+
decrypted_bytes = encrypted_bytes
|
|
515
|
+
return decrypted_bytes
|
|
516
|
+
|
|
517
|
+
def write_bytes(self, data: bytes) -> int:
|
|
518
|
+
process = subprocess.Popen(
|
|
519
|
+
self.encryption_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
|
520
|
+
)
|
|
521
|
+
encrypted_data = process.communicate(input=data)[0]
|
|
522
|
+
return super().write_bytes(encrypted_data)
|
|
523
|
+
|
|
524
|
+
def read_text(self, encoding: str | None = ..., errors: str | None = ...) -> str:
|
|
525
|
+
return self.read_bytes().decode()
|
|
526
|
+
|
|
527
|
+
def write_text(self, data: str, **_) -> int:
|
|
528
|
+
data = data.encode()
|
|
529
|
+
return self.write_bytes(data)
|
plib/tags.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import xattr
|
|
3
|
+
|
|
4
|
+
except:
|
|
5
|
+
xattr = None # Don't fail if xattr not supported (Windows)
|
|
6
|
+
|
|
7
|
+
delim = ","
|
|
8
|
+
default_tag_name = "user.xdg.tags"
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
More user-friendly way to interact with tags.
|
|
12
|
+
- Work with strings and integers instead of bytes
|
|
13
|
+
- Use xdg tags by default -> useful for filemanager that can order according to this tag
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class XDGTags:
|
|
18
|
+
def __init__(self, path, name=default_tag_name):
|
|
19
|
+
self.tags = xattr.xattr(path) if xattr is not None else None
|
|
20
|
+
self.name = name
|
|
21
|
+
|
|
22
|
+
def get(self):
|
|
23
|
+
tags = set({})
|
|
24
|
+
if self.tags and self.tags.has_key(self.name):
|
|
25
|
+
tags = self.tags[self.name].decode().strip().split(delim)
|
|
26
|
+
return tags
|
|
27
|
+
|
|
28
|
+
def set(self, *values, name=default_tag_name):
|
|
29
|
+
"""
|
|
30
|
+
:param values: tag values to set
|
|
31
|
+
"""
|
|
32
|
+
if self.tags is not None:
|
|
33
|
+
values = {str(v).zfill(4) if isinstance(v, int) else str(v) for v in values}
|
|
34
|
+
values = delim.join(values).encode()
|
|
35
|
+
self.tags.set(self.name, values)
|
|
36
|
+
|
|
37
|
+
def clear(self):
|
|
38
|
+
if self.tags is not None and self.tags.has_key(self.name):
|
|
39
|
+
self.tags.remove(self.name)
|
plib/utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def find_first_match(condition: Callable) -> int:
|
|
5
|
+
"""
|
|
6
|
+
:param condition: Condition that number needs to match.
|
|
7
|
+
The condition is assumed to be valid for all integers staring from an initial value.
|
|
8
|
+
:return: First integer for which condition is valid.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# exponential increase for logarithmic search
|
|
12
|
+
upper_bound = 1
|
|
13
|
+
while not condition(upper_bound):
|
|
14
|
+
upper_bound *= 2
|
|
15
|
+
|
|
16
|
+
# narrow down range of first match = [lower_bound + 1, upper_bound] until one value left
|
|
17
|
+
lower_bound = upper_bound // 2
|
|
18
|
+
while lower_bound + 1 < upper_bound:
|
|
19
|
+
middle = (upper_bound + lower_bound) // 2
|
|
20
|
+
if condition(middle):
|
|
21
|
+
upper_bound = middle
|
|
22
|
+
else:
|
|
23
|
+
lower_bound = middle
|
|
24
|
+
|
|
25
|
+
return upper_bound
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 quintenroets
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: superpathlib
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: extended pathlib
|
|
5
|
+
Home-page: https://github.com/quintenroets/superpathlib
|
|
6
|
+
Author: Quinten Roets
|
|
7
|
+
Author-email: quinten.roets@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: PyYAML
|
|
13
|
+
|
|
14
|
+
# Superpathlib
|
|
15
|
+
Superpathlib offers Path objects with functionality extended from pathlib to maximize your productivity with a minimal amount of code.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
from plib import Path
|
|
21
|
+
path = Path(filename)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 1) Use properties to read & write path content in different formats
|
|
25
|
+
* text
|
|
26
|
+
* byte_content
|
|
27
|
+
* lines
|
|
28
|
+
* yaml
|
|
29
|
+
* json
|
|
30
|
+
|
|
31
|
+
examples:
|
|
32
|
+
|
|
33
|
+
```shell
|
|
34
|
+
path.json = {key: value}
|
|
35
|
+
|
|
36
|
+
for line in path.lines:
|
|
37
|
+
if interesting(line):
|
|
38
|
+
process(line)
|
|
39
|
+
```
|
|
40
|
+
### 2) Use instance properties to get/set file metadata:
|
|
41
|
+
* get:
|
|
42
|
+
* size: filesize
|
|
43
|
+
* is_root: whether the owner of the file is a root user
|
|
44
|
+
* has_children: whether a path has children
|
|
45
|
+
* number_of_children: number of children in a folder
|
|
46
|
+
* filetype: content type of a file
|
|
47
|
+
* get & set:
|
|
48
|
+
* mtime: modified time
|
|
49
|
+
* tag: can be used for alternative ordering or metadata
|
|
50
|
+
|
|
51
|
+
examples:
|
|
52
|
+
|
|
53
|
+
```shell
|
|
54
|
+
path_new.mtime = path_old.mtime
|
|
55
|
+
|
|
56
|
+
if path.tag != skip_keyword and path.filetype == "video":
|
|
57
|
+
process(path)
|
|
58
|
+
```
|
|
59
|
+
### 3) Use class properties to access commonly used folders:
|
|
60
|
+
* docs
|
|
61
|
+
* assets
|
|
62
|
+
* ..
|
|
63
|
+
|
|
64
|
+
example:
|
|
65
|
+
|
|
66
|
+
```shell
|
|
67
|
+
names_path = Path.assets / 'names'
|
|
68
|
+
names = names_path.lines
|
|
69
|
+
```
|
|
70
|
+
### 4) Use additional functionality
|
|
71
|
+
* find(): recursively find all paths under a root that match a condition (extra options available for performance optimization)
|
|
72
|
+
* rmtree(): remove directory recursively
|
|
73
|
+
* copy_to(dest): copy content to dest
|
|
74
|
+
* copy_properties_to(dest): recursively copy path properties (mtime, tag) to all n-level children of dest
|
|
75
|
+
* tempfile(): create temporary file that can be used as context manager
|
|
76
|
+
* unpack(): extract archive(zip, tar, ..) file to desired folder
|
|
77
|
+
* pop_parent(): remove first parent from path in filesystem
|
|
78
|
+
|
|
79
|
+
examples:
|
|
80
|
+
|
|
81
|
+
```shell
|
|
82
|
+
with Path.tempfile() as tmp:
|
|
83
|
+
do_work(logfile=tmp)
|
|
84
|
+
log = tmp.text
|
|
85
|
+
process(log)
|
|
86
|
+
|
|
87
|
+
condition = lambda p: (p / '.git').exists()
|
|
88
|
+
for git_path in root.find(condition):
|
|
89
|
+
process_git(git_path)
|
|
90
|
+
```
|
|
91
|
+
### 5) Enhance existing functionality
|
|
92
|
+
* Automatically create parents when writing files, creating new files, renaming files, ..
|
|
93
|
+
* Return default values when path does not exist (e.g. size = 0, lines=[])
|
|
94
|
+
|
|
95
|
+
### 6) Inherit from Path to define your own additional functionality:
|
|
96
|
+
|
|
97
|
+
example:
|
|
98
|
+
|
|
99
|
+
```shell
|
|
100
|
+
from plib import Path as BasePath
|
|
101
|
+
|
|
102
|
+
class Path(BasePath):
|
|
103
|
+
def count_children(self):
|
|
104
|
+
return sum(1 for _ in self.iterdir())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This only works if you inherit from plib and not from the builtin pathlib
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Installation
|
|
111
|
+
|
|
112
|
+
```shell
|
|
113
|
+
pip install superpathlib
|
|
114
|
+
requires python version >= 3.9
|
|
115
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
plib/__init__.py,sha256=ibGdCuiCzlbTOWSQKvy6O1DnBkSiRBLqdlW0dPnFQAg,23
|
|
2
|
+
plib/plib.py,sha256=0spcnXus4C45hKeqpbf4QZji0fWtATK3HVCVMc1bx-E,15026
|
|
3
|
+
plib/tags.py,sha256=xXovH46eRim-JSM0b2MAHY-uhqg9VXofJeqyHiR7I1s,1152
|
|
4
|
+
plib/utils.py,sha256=bcAbMWUnmAdLdJ0bNhKKwYmH0QRlrbO_jb3J8IBIy_Y,808
|
|
5
|
+
superpathlib-1.0.1.dist-info/LICENSE,sha256=coy5XgzwxQweNZLEXayFiokEKOaK232vkE8TFfBDbFw,1069
|
|
6
|
+
superpathlib-1.0.1.dist-info/METADATA,sha256=9M_6w4l152AuYr0hM18DccvQisQezMZOELX04COBeLE,2830
|
|
7
|
+
superpathlib-1.0.1.dist-info/WHEEL,sha256=a-zpFRIJzOq5QfuhBzbhiA1eHTzNCJn8OdRvhdNX0Rk,110
|
|
8
|
+
superpathlib-1.0.1.dist-info/top_level.txt,sha256=-V_xRFwJBbUgjbv9DhvFKSZizOCXvDIGZcwqg-jGvks,5
|
|
9
|
+
superpathlib-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
plib
|