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 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,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.40.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1 @@
1
+ plib