fpathlib 0.1.2.dev0__py3-none-any.whl → 0.1.3.dev0__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.
- fpathlib/__init__.py +3 -7
- fpathlib/_version.py +2 -2
- fpathlib/expand.py +106 -0
- fpathlib/ext/polars.py +70 -34
- fpathlib/fpath.py +160 -0
- fpathlib/path.py +0 -252
- fpathlib-0.1.3.dev0.dist-info/METADATA +78 -0
- fpathlib-0.1.3.dev0.dist-info/RECORD +18 -0
- fpathlib-0.1.2.dev0.dist-info/METADATA +0 -19
- fpathlib-0.1.2.dev0.dist-info/RECORD +0 -16
- {fpathlib-0.1.2.dev0.dist-info → fpathlib-0.1.3.dev0.dist-info}/WHEEL +0 -0
- {fpathlib-0.1.2.dev0.dist-info → fpathlib-0.1.3.dev0.dist-info}/licenses/LICENSE +0 -0
- {fpathlib-0.1.2.dev0.dist-info → fpathlib-0.1.3.dev0.dist-info}/top_level.txt +0 -0
fpathlib/__init__.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
from fpathlib.path import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ExpandedFPath,
|
|
1
|
+
from fpathlib.path import Path
|
|
2
|
+
from fpathlib.fpath import FPath, ExpandedFPath
|
|
3
|
+
from fpathlib.expand import (
|
|
5
4
|
expand_fpath,
|
|
6
5
|
expand_fpath_decorator,
|
|
7
6
|
is_expandable,
|
|
8
7
|
)
|
|
9
8
|
|
|
10
|
-
from fpathlib.ext import polars
|
|
11
|
-
|
|
12
9
|
__all__ = [
|
|
13
10
|
"Path",
|
|
14
11
|
"FPath",
|
|
@@ -16,5 +13,4 @@ __all__ = [
|
|
|
16
13
|
"expand_fpath",
|
|
17
14
|
"expand_fpath_decorator",
|
|
18
15
|
"is_expandable",
|
|
19
|
-
"polars",
|
|
20
16
|
]
|
fpathlib/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.3.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 3, 'dev0')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
fpathlib/expand.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
import parse
|
|
3
|
+
|
|
4
|
+
from .fpath import ExpandedFPath, FPath
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def expand_fpath(fpath, *, exclude_path_patterns=None, require_metadata=True):
|
|
8
|
+
"""
|
|
9
|
+
Use an f-string to extract out a collection of paths, where the f-string variables
|
|
10
|
+
are captured and stored along the path name. This is a convenience function that
|
|
11
|
+
simply creates an :obj:`FPath` and calls its :meth:`.expand` method.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
fpath : :obj:`str`
|
|
16
|
+
An f-string path, where the variables are captured and stored along the path
|
|
17
|
+
name.
|
|
18
|
+
exclude_path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
|
|
19
|
+
Exclude paths that match the supplied pattern. (Default: None).
|
|
20
|
+
require_metadata : :obj:`bool`
|
|
21
|
+
Require that all paths identified must have found metadata. (Default: True).
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
:obj:`.ExpandedFPath`
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
return FPath(fpath).expand(
|
|
29
|
+
exclude_path_patterns=exclude_path_patterns,
|
|
30
|
+
require_metadata=require_metadata,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def expand_fpath_decorator(f=None, require_expandable=True, post_process=None):
|
|
35
|
+
"""
|
|
36
|
+
Decorator for :func:`.expand_fpath`.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
f : :obj:`callable`
|
|
41
|
+
A function that takes an :obj:`.ExpandedFPath` as its first argument.
|
|
42
|
+
post_process : :obj:`callable`
|
|
43
|
+
A function that takes the output of `f` and the :obj:`.ExpandedFPath`.
|
|
44
|
+
(Default: None).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def decorator(f):
|
|
48
|
+
@wraps(f)
|
|
49
|
+
def wrapper(fpath, *args, **kwargs):
|
|
50
|
+
# If fpath is an :obj:`ExpandedFPath`, just call f with it.
|
|
51
|
+
if isinstance(fpath, ExpandedFPath):
|
|
52
|
+
expanded_fpath = fpath
|
|
53
|
+
result = f(fpath, *args, **kwargs)
|
|
54
|
+
|
|
55
|
+
# Otherwise, expand fpath and call f with the result.
|
|
56
|
+
else:
|
|
57
|
+
# What happens if fpath is not expandable?
|
|
58
|
+
# If require_expandable is True, raise an error.
|
|
59
|
+
# Otherwise, just call f with the original fpath.
|
|
60
|
+
if not is_expandable(fpath):
|
|
61
|
+
if require_expandable:
|
|
62
|
+
msg = f"fpath not expandable: '{fpath}'"
|
|
63
|
+
raise ValueError(msg)
|
|
64
|
+
return f(fpath, *args, **kwargs)
|
|
65
|
+
|
|
66
|
+
# We know fpath is expandable, so we can expand it and call f
|
|
67
|
+
exclude_path_patterns = kwargs.pop("exclude_path_patterns", None)
|
|
68
|
+
require_metadata = kwargs.pop("require_metadata", True)
|
|
69
|
+
expanded_fpath = expand_fpath(
|
|
70
|
+
fpath,
|
|
71
|
+
exclude_path_patterns=exclude_path_patterns,
|
|
72
|
+
require_metadata=require_metadata,
|
|
73
|
+
)
|
|
74
|
+
result = f(expanded_fpath, *args, **kwargs)
|
|
75
|
+
|
|
76
|
+
# If post_process is provided, call it with the result and the expanded_fpath.
|
|
77
|
+
if post_process is not None:
|
|
78
|
+
result = post_process(result, expanded_fpath)
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
return wrapper
|
|
83
|
+
|
|
84
|
+
if f is None:
|
|
85
|
+
return decorator
|
|
86
|
+
else:
|
|
87
|
+
return decorator(f)
|
|
88
|
+
|
|
89
|
+
def is_expandable(fpath):
|
|
90
|
+
"""
|
|
91
|
+
Check if expandable.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
fpath : :obj:`str`
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
:obj:`bool`
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
parser = parse.compile(str(fpath))
|
|
104
|
+
return bool(parser.named_fields)
|
|
105
|
+
except TypeError:
|
|
106
|
+
return False
|
fpathlib/ext/polars.py
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
from functools import wraps
|
|
2
|
-
|
|
3
|
-
import polars as pl
|
|
2
|
+
import polars as _polars
|
|
4
3
|
from fpathlib import expand_fpath_decorator, ExpandedFPath
|
|
5
4
|
|
|
6
5
|
|
|
6
|
+
def __getattr__(name):
|
|
7
|
+
# Delegates any attribute this module doesn't define itself to the real
|
|
8
|
+
# polars module, so `fpathlib.ext.polars` remains a drop-in replacement for
|
|
9
|
+
# `import polars` (pl.DataFrame, pl.col, pl.List, etc. all still resolve).
|
|
10
|
+
# Unlike `from polars import *`, this only kicks in for names Python didn't
|
|
11
|
+
# already find defined here, so it can't silently shadow builtins like
|
|
12
|
+
# `list` or `len` for this module's own implementation code.
|
|
13
|
+
return getattr(_polars, name)
|
|
14
|
+
|
|
15
|
+
|
|
7
16
|
def join_metadata(df, expanded_fpath):
|
|
8
17
|
return df.join(
|
|
9
|
-
expanded_fpath.to_polars(lazy=isinstance(df,
|
|
18
|
+
expanded_fpath.to_polars(lazy=isinstance(df, _polars.LazyFrame)),
|
|
10
19
|
on="fname",
|
|
11
20
|
)
|
|
12
21
|
|
|
@@ -109,7 +118,7 @@ def scan_csv(expanded_fpath, *args, **kwargs):
|
|
|
109
118
|
:obj:`polars.LazyFrame`
|
|
110
119
|
"""
|
|
111
120
|
|
|
112
|
-
lf =
|
|
121
|
+
lf = _polars.scan_csv(
|
|
113
122
|
expanded_fpath,
|
|
114
123
|
include_file_paths="fname",
|
|
115
124
|
*args,
|
|
@@ -140,7 +149,7 @@ def scan_parquet(expanded_fpath, *args, **kwargs):
|
|
|
140
149
|
:obj:`polars.LazyFrame`
|
|
141
150
|
"""
|
|
142
151
|
|
|
143
|
-
lf =
|
|
152
|
+
lf = _polars.scan_parquet(
|
|
144
153
|
expanded_fpath,
|
|
145
154
|
include_file_paths="fname",
|
|
146
155
|
*args,
|
|
@@ -151,8 +160,6 @@ def scan_parquet(expanded_fpath, *args, **kwargs):
|
|
|
151
160
|
|
|
152
161
|
|
|
153
162
|
# TODO rename expanded_fpath as source
|
|
154
|
-
# TODO this gets very slow when expanded_fpath contains thousands of files
|
|
155
|
-
# I turned on streaming to fix this, but streaming is also slow. expanded_fpath[0] is the answer
|
|
156
163
|
@expand_fpath_decorator(require_expandable=False, post_process=join_metadata)
|
|
157
164
|
def scan_txt(
|
|
158
165
|
expanded_fpath,
|
|
@@ -206,7 +213,7 @@ def scan_txt(
|
|
|
206
213
|
|
|
207
214
|
# TODO schema and schema_overrides is probably broken
|
|
208
215
|
|
|
209
|
-
lf =
|
|
216
|
+
lf = _polars.scan_csv(
|
|
210
217
|
expanded_fpath,
|
|
211
218
|
include_file_paths="fname",
|
|
212
219
|
separator="\n",
|
|
@@ -224,12 +231,36 @@ def scan_txt(
|
|
|
224
231
|
if separator is not None:
|
|
225
232
|
# Separate line into fields by separator
|
|
226
233
|
lf = lf.with_columns(
|
|
227
|
-
|
|
234
|
+
_polars.col("line").str.split(separator, literal=False).alias("fields")
|
|
228
235
|
)
|
|
229
236
|
|
|
230
237
|
if not keep_line:
|
|
231
238
|
lf = lf.drop("line")
|
|
232
239
|
|
|
240
|
+
# With many matched files, inferring the field count/dtypes directly
|
|
241
|
+
# against the full glob is extremely slow (every file has to be opened
|
|
242
|
+
# before a `.head()` takes effect). Instead, recurse on a single
|
|
243
|
+
# representative file -- expanded_fpath[0] -- and reuse its already-cheap
|
|
244
|
+
# (single-file) inference below instead of duplicating it here. Computed
|
|
245
|
+
# once here since it's needed by both the field-count branch below and
|
|
246
|
+
# the dtype-inference branch further down.
|
|
247
|
+
infer_schema = kwargs.get("infer_schema", True)
|
|
248
|
+
sample_schema = None
|
|
249
|
+
if (
|
|
250
|
+
isinstance(expanded_fpath, ExpandedFPath)
|
|
251
|
+
and len(expanded_fpath) > 1
|
|
252
|
+
and (usecols is None or infer_schema)
|
|
253
|
+
):
|
|
254
|
+
sample_schema = scan_txt(
|
|
255
|
+
expanded_fpath[0],
|
|
256
|
+
filter_expr=filter_expr,
|
|
257
|
+
separator=separator,
|
|
258
|
+
new_columns=new_columns,
|
|
259
|
+
has_header=has_header,
|
|
260
|
+
usecols=usecols,
|
|
261
|
+
**kwargs,
|
|
262
|
+
).collect_schema()
|
|
263
|
+
|
|
233
264
|
# Set the columns to use for the fields.
|
|
234
265
|
# Either specify a subset of columns to use, or use all columns
|
|
235
266
|
if usecols is not None:
|
|
@@ -238,21 +269,23 @@ def scan_txt(
|
|
|
238
269
|
fields[col] = f"field_{col}"
|
|
239
270
|
|
|
240
271
|
else:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
272
|
+
if sample_schema is not None:
|
|
273
|
+
n_fields = len(sample_schema) - 1 # minus 'fname'
|
|
274
|
+
else:
|
|
275
|
+
n_fields = (
|
|
276
|
+
lf.head(1)
|
|
277
|
+
.select(_polars.col("fields").list.len().unique())
|
|
278
|
+
.collect()
|
|
279
|
+
.item()
|
|
280
|
+
)
|
|
248
281
|
|
|
249
282
|
# Initial field names, may be renamed later from header or by `new_columns`
|
|
250
283
|
fields = {i: f"field_{i}" for i in range(n_fields)}
|
|
251
284
|
|
|
252
285
|
# Add each field as a separate column
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
286
|
+
for i, field in fields.items():
|
|
287
|
+
lf = lf.with_columns(_polars.col("fields").list.get(i).alias(field))
|
|
288
|
+
lf = lf.drop("fields")
|
|
256
289
|
|
|
257
290
|
# LazyFrame does not guarantee order, so the header might not be the first row
|
|
258
291
|
# This can be fixed by scan_csv with include_row_index. But this seems clunky
|
|
@@ -261,29 +294,32 @@ def scan_txt(
|
|
|
261
294
|
first_row = lf.head(1).collect()
|
|
262
295
|
header.to_pandas().transpose()[0].to_dict()
|
|
263
296
|
lf = lf.slice(offset=1, length=None)
|
|
264
|
-
header = first_row.select(
|
|
297
|
+
header = first_row.select(_polars.col(fields)).to_dict(as_series=False)
|
|
265
298
|
lf = lf.rename({field: header[field][0] for field in fields})
|
|
266
299
|
"""
|
|
267
300
|
raise NotImplementedError
|
|
268
301
|
|
|
269
302
|
# Apply new column names if provided
|
|
270
303
|
if new_columns is not None:
|
|
271
|
-
|
|
272
|
-
{
|
|
273
|
-
field: new_column
|
|
274
|
-
for field, new_column in zip(fields.keys(), new_columns)
|
|
275
|
-
}
|
|
276
|
-
)
|
|
304
|
+
for field, new_column in zip(fields.values(), new_columns):
|
|
305
|
+
lf = lf.rename({field: new_column})
|
|
277
306
|
|
|
278
307
|
# Infer dtypes?
|
|
279
|
-
if
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
308
|
+
if infer_schema:
|
|
309
|
+
if sample_schema is not None:
|
|
310
|
+
inferred_schema = {
|
|
311
|
+
name: dtype
|
|
312
|
+
for name, dtype in sample_schema.items()
|
|
313
|
+
if name != "fname"
|
|
314
|
+
}
|
|
315
|
+
else:
|
|
316
|
+
sample = (
|
|
317
|
+
lf.head(kwargs.get("infer_schema_length", 100))
|
|
318
|
+
.collect()
|
|
319
|
+
.write_csv()
|
|
320
|
+
.encode()
|
|
321
|
+
)
|
|
322
|
+
inferred_schema = _polars.read_csv(sample).schema
|
|
287
323
|
lf = lf.cast(inferred_schema)
|
|
288
324
|
|
|
289
325
|
return lf
|
fpathlib/fpath.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from glob import glob
|
|
3
|
+
import parse
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .path import Path
|
|
7
|
+
from .utils import import_optional_dependency
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FPath:
|
|
11
|
+
"""
|
|
12
|
+
:obj:`FPath` is an f-string version of :obj:`Path`. This does not behave like a
|
|
13
|
+
typical Path, because typically there are several matches to the f-string Path.
|
|
14
|
+
Therefore, the :meth:`.FPath.expand` method must be used to create an
|
|
15
|
+
:obj:`ExpandedFPath` that lists all the possibilities.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
fpath : :obj:`str`
|
|
20
|
+
An f-string path, where the variables are captured and stored along the path
|
|
21
|
+
name.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
:obj:`.FPath`
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, fpath):
|
|
29
|
+
self.fpath = str(fpath) # must be string, not Path or something else
|
|
30
|
+
|
|
31
|
+
def __repr__(self):
|
|
32
|
+
return "FPath({!r})".format(self.fpath)
|
|
33
|
+
|
|
34
|
+
def expand(self, exclude_path_patterns=None, require_metadata=True, errors="raise"):
|
|
35
|
+
"""
|
|
36
|
+
Use an f-string to extract out a collection of paths, where the f-string
|
|
37
|
+
variables are captured and stored along the path name.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
exclude_path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
|
|
42
|
+
Exclude paths that match the supplied pattern. (Default: None).
|
|
43
|
+
require_metadata : :obj:`bool`
|
|
44
|
+
Require that all paths identified must have found metadata. (Default: True).
|
|
45
|
+
errors : :obj:`str`
|
|
46
|
+
How to handle errors. If "raise", then raise an error. If "warn", then warn
|
|
47
|
+
and return an empty collection. If "ignore", then ignore the error and
|
|
48
|
+
return an empty collection. (Default: "raise").
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
:obj:`.ExpandedFPath`
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if errors not in {"raise", "warn", "ignore"}:
|
|
56
|
+
msg = f"invalid value for 'errors': {errors}"
|
|
57
|
+
raise ValueError(msg)
|
|
58
|
+
|
|
59
|
+
parser = parse.compile(self.fpath)
|
|
60
|
+
|
|
61
|
+
paths = []
|
|
62
|
+
for fname in glob(re.sub(r"\{.*?\}", "*", self.fpath)):
|
|
63
|
+
path = Path(fname)
|
|
64
|
+
if exclude_path_patterns and path.match_any(exclude_path_patterns):
|
|
65
|
+
continue
|
|
66
|
+
path.metadata = getattr(parser.parse(fname), "named", None)
|
|
67
|
+
if require_metadata and path.metadata is None:
|
|
68
|
+
msg = f"metadata not found for '{fname}' with '{self.fpath}'"
|
|
69
|
+
raise AttributeError(msg)
|
|
70
|
+
paths.append(path)
|
|
71
|
+
|
|
72
|
+
if len(paths) == 0 and errors != "ignore":
|
|
73
|
+
msg = f"no paths found for {self.fpath.__repr__()}"
|
|
74
|
+
if errors == "raise":
|
|
75
|
+
raise IOError(msg)
|
|
76
|
+
elif errors == "warn":
|
|
77
|
+
import warnings
|
|
78
|
+
|
|
79
|
+
warnings.warn(msg)
|
|
80
|
+
|
|
81
|
+
return ExpandedFPath(paths=paths, fpath=self)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# TODO does scan_csv belong here?
|
|
85
|
+
class ExpandedFPath(Sequence):
|
|
86
|
+
"""
|
|
87
|
+
:obj:`ExpandedFPath` is the result of expanding an :obj:`FPath`. It is a
|
|
88
|
+
collection of :obj:`Path` objects that match the f-string pattern, and their
|
|
89
|
+
associated metadata.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
paths : :obj:`Iterable`[:obj:`Path`]
|
|
94
|
+
A collection of :obj:`Path` objects that match the f-string pattern, and their
|
|
95
|
+
associated metadata.
|
|
96
|
+
fpath : :obj:`FPath`
|
|
97
|
+
The original :obj:`FPath` that was expanded to create this
|
|
98
|
+
:obj:`ExpandedFPath`.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, paths, fpath):
|
|
102
|
+
if len(paths) != len(set(paths)):
|
|
103
|
+
raise AttributeError("paths are not unique")
|
|
104
|
+
self.paths = paths
|
|
105
|
+
self.fpath = fpath
|
|
106
|
+
|
|
107
|
+
def __getitem__(self, item):
|
|
108
|
+
return self.paths[item]
|
|
109
|
+
|
|
110
|
+
def __len__(self):
|
|
111
|
+
return len(self.paths)
|
|
112
|
+
|
|
113
|
+
def __repr__(self):
|
|
114
|
+
n = len(self)
|
|
115
|
+
|
|
116
|
+
txt = f"{self.fpath.__repr__()}\n\n"
|
|
117
|
+
txt += f"{n} matches found:\n\n"
|
|
118
|
+
|
|
119
|
+
for path in self.paths[:10]:
|
|
120
|
+
txt += f"{repr(path)}\n"
|
|
121
|
+
if n > 10:
|
|
122
|
+
txt += "...\n"
|
|
123
|
+
|
|
124
|
+
return txt
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def metadata(self):
|
|
128
|
+
"""
|
|
129
|
+
Return the metadata for each path in the collection.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
:obj:`dict`[:obj:`str`, :obj:`dict`]
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
metadata = {path: path.metadata for path in self.paths}
|
|
137
|
+
return metadata
|
|
138
|
+
|
|
139
|
+
# TODO fix value types?
|
|
140
|
+
def to_polars(self, lazy=False):
|
|
141
|
+
"""
|
|
142
|
+
Convert the metadata for each path in the collection to a polars DataFrame.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
lazy : :obj:`bool`
|
|
147
|
+
Return a lazy polars DataFrame? (Default: False).
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
:obj:`polars.DataFrame` or :obj:`polars.LazyFrame`
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
pl = import_optional_dependency("polars")
|
|
155
|
+
|
|
156
|
+
data = [{"fname": str(key), **value} for key, value in self.metadata.items()]
|
|
157
|
+
df = pl.DataFrame(data)
|
|
158
|
+
if lazy:
|
|
159
|
+
df = df.lazy()
|
|
160
|
+
return df
|
fpathlib/path.py
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
from collections.abc import Sequence
|
|
2
|
-
from functools import wraps
|
|
3
|
-
from glob import glob
|
|
4
|
-
import parse
|
|
5
1
|
import pathlib
|
|
6
|
-
import re
|
|
7
|
-
|
|
8
|
-
from .utils import import_optional_dependency
|
|
9
2
|
|
|
10
3
|
|
|
11
4
|
class Path(pathlib.Path):
|
|
@@ -91,248 +84,3 @@ class Path(pathlib.Path):
|
|
|
91
84
|
return True
|
|
92
85
|
|
|
93
86
|
return False
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class FPath:
|
|
97
|
-
"""
|
|
98
|
-
:obj:`FPath` is an f-string version of :obj:`Path`. This does not behave like a
|
|
99
|
-
typical Path, because typically there are several matches to the f-string Path.
|
|
100
|
-
Therefore, the :meth:`.FPath.expand` method must be used to create an
|
|
101
|
-
:obj:`ExpandedFPath` that lists all the possibilities.
|
|
102
|
-
|
|
103
|
-
Parameters
|
|
104
|
-
----------
|
|
105
|
-
fpath : :obj:`str`
|
|
106
|
-
An f-string path, where the variables are captured and stored along the path
|
|
107
|
-
name.
|
|
108
|
-
|
|
109
|
-
Returns
|
|
110
|
-
-------
|
|
111
|
-
:obj:`.FPath`
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
def __init__(self, fpath):
|
|
115
|
-
self.fpath = str(fpath) # must be string, not Path or something else
|
|
116
|
-
|
|
117
|
-
def __repr__(self):
|
|
118
|
-
return "FPath({!r})".format(self.fpath)
|
|
119
|
-
|
|
120
|
-
def expand(self, exclude_path_patterns=None, require_metadata=True, errors="raise"):
|
|
121
|
-
"""
|
|
122
|
-
Use an f-string to extract out a collection of paths, where the f-string
|
|
123
|
-
variables are captured and stored along the path name.
|
|
124
|
-
|
|
125
|
-
Parameters
|
|
126
|
-
----------
|
|
127
|
-
exclude_path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
|
|
128
|
-
Exclude paths that match the supplied pattern. (Default: None).
|
|
129
|
-
require_metadata : :obj:`bool`
|
|
130
|
-
Require that all paths identified must have found metadata. (Default: True).
|
|
131
|
-
errors : :obj:`str`
|
|
132
|
-
How to handle errors. If "raise", then raise an error. If "warn", then warn
|
|
133
|
-
and return an empty collection. If "ignore", then ignore the error and
|
|
134
|
-
return an empty collection. (Default: "raise").
|
|
135
|
-
|
|
136
|
-
Returns
|
|
137
|
-
-------
|
|
138
|
-
:obj:`.ExpandedFPath`
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
if errors not in {"raise", "warn", "ignore"}:
|
|
142
|
-
msg = f"invalid value for 'errors': {errors}"
|
|
143
|
-
raise ValueError(msg)
|
|
144
|
-
|
|
145
|
-
parser = parse.compile(self.fpath)
|
|
146
|
-
|
|
147
|
-
paths = []
|
|
148
|
-
for fname in glob(re.sub(r"\{.*?\}", "*", self.fpath)):
|
|
149
|
-
path = Path(fname)
|
|
150
|
-
if exclude_path_patterns and path.match_any(exclude_path_patterns):
|
|
151
|
-
continue
|
|
152
|
-
path.metadata = getattr(parser.parse(fname), "named", None)
|
|
153
|
-
if require_metadata and path.metadata is None:
|
|
154
|
-
msg = f"metadata not found for '{fname}' with '{self.fpath}'"
|
|
155
|
-
raise AttributeError(msg)
|
|
156
|
-
paths.append(path)
|
|
157
|
-
|
|
158
|
-
if len(paths) == 0 and errors != "ignore":
|
|
159
|
-
msg = f"no paths found for {self.fpath.__repr__()}"
|
|
160
|
-
if errors == "raise":
|
|
161
|
-
raise IOError(msg)
|
|
162
|
-
elif errors == "warn":
|
|
163
|
-
import warnings
|
|
164
|
-
|
|
165
|
-
warnings.warn(msg)
|
|
166
|
-
|
|
167
|
-
return ExpandedFPath(paths=paths, fpath=self)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
# TODO does scan_csv belong here?
|
|
171
|
-
class ExpandedFPath(Sequence):
|
|
172
|
-
"""
|
|
173
|
-
:obj:`ExpandedFPath` is the result of expanding an :obj:`FPath`. It is a
|
|
174
|
-
collection of :obj:`Path` objects that match the f-string pattern, and their
|
|
175
|
-
associated metadata.
|
|
176
|
-
|
|
177
|
-
Parameters
|
|
178
|
-
----------
|
|
179
|
-
paths : :obj:`Iterable`[:obj:`Path`]
|
|
180
|
-
A collection of :obj:`Path` objects that match the f-string pattern, and their
|
|
181
|
-
associated metadata.
|
|
182
|
-
fpath : :obj:`FPath`
|
|
183
|
-
The original :obj:`FPath` that was expanded to create this
|
|
184
|
-
:obj:`ExpandedFPath`.
|
|
185
|
-
"""
|
|
186
|
-
|
|
187
|
-
def __init__(self, paths, fpath):
|
|
188
|
-
if len(paths) != len(set(paths)):
|
|
189
|
-
raise AttributeError("paths are not unique")
|
|
190
|
-
self.paths = paths
|
|
191
|
-
self.fpath = fpath
|
|
192
|
-
|
|
193
|
-
def __getitem__(self, item):
|
|
194
|
-
return self.paths[item]
|
|
195
|
-
|
|
196
|
-
def __len__(self):
|
|
197
|
-
return len(self.paths)
|
|
198
|
-
|
|
199
|
-
def __repr__(self):
|
|
200
|
-
n = len(self)
|
|
201
|
-
|
|
202
|
-
txt = f"{self.fpath.__repr__()}\n\n"
|
|
203
|
-
txt += f"{n} matches found:\n\n"
|
|
204
|
-
|
|
205
|
-
for path in self.paths[:10]:
|
|
206
|
-
txt += f"{repr(path)}\n"
|
|
207
|
-
if n > 10:
|
|
208
|
-
txt += "...\n"
|
|
209
|
-
|
|
210
|
-
return txt
|
|
211
|
-
|
|
212
|
-
@property
|
|
213
|
-
def metadata(self):
|
|
214
|
-
"""
|
|
215
|
-
Return the metadata for each path in the collection.
|
|
216
|
-
|
|
217
|
-
Returns
|
|
218
|
-
-------
|
|
219
|
-
:obj:`dict`[:obj:`str`, :obj:`dict`]
|
|
220
|
-
"""
|
|
221
|
-
|
|
222
|
-
metadata = {path: path.metadata for path in self.paths}
|
|
223
|
-
return metadata
|
|
224
|
-
|
|
225
|
-
# TODO fix value types?
|
|
226
|
-
def to_polars(self, lazy=False):
|
|
227
|
-
"""
|
|
228
|
-
Convert the metadata for each path in the collection to a polars DataFrame.
|
|
229
|
-
|
|
230
|
-
Parameters
|
|
231
|
-
----------
|
|
232
|
-
lazy : :obj:`bool`
|
|
233
|
-
Return a lazy polars DataFrame? (Default: False).
|
|
234
|
-
|
|
235
|
-
Returns
|
|
236
|
-
-------
|
|
237
|
-
:obj:`polars.DataFrame` or :obj:`polars.LazyFrame`
|
|
238
|
-
"""
|
|
239
|
-
|
|
240
|
-
pl = import_optional_dependency("polars")
|
|
241
|
-
|
|
242
|
-
data = [{"fname": str(key), **value} for key, value in self.metadata.items()]
|
|
243
|
-
df = pl.DataFrame(data)
|
|
244
|
-
if lazy:
|
|
245
|
-
df = df.lazy()
|
|
246
|
-
return df
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def expand_fpath(fpath, *, exclude_path_patterns=None, require_metadata=True):
|
|
250
|
-
"""
|
|
251
|
-
Use an f-string to extract out a collection of paths, where the f-string variables
|
|
252
|
-
are captured and stored along the path name. This is a convenience function that
|
|
253
|
-
simply creates an :obj:`FPath` and calls its :meth:`.expand` method.
|
|
254
|
-
|
|
255
|
-
Parameters
|
|
256
|
-
----------
|
|
257
|
-
fpath : :obj:`str`
|
|
258
|
-
An f-string path, where the variables are captured and stored along the path
|
|
259
|
-
name.
|
|
260
|
-
exclude_path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
|
|
261
|
-
Exclude paths that match the supplied pattern. (Default: None).
|
|
262
|
-
require_metadata : :obj:`bool`
|
|
263
|
-
Require that all paths identified must have found metadata. (Default: True).
|
|
264
|
-
|
|
265
|
-
Returns
|
|
266
|
-
-------
|
|
267
|
-
:obj:`.ExpandedFPath`
|
|
268
|
-
"""
|
|
269
|
-
|
|
270
|
-
return FPath(fpath).expand(
|
|
271
|
-
exclude_path_patterns=exclude_path_patterns,
|
|
272
|
-
require_metadata=require_metadata,
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def expand_fpath_decorator(f=None, require_expandable=True, post_process=None):
|
|
277
|
-
"""
|
|
278
|
-
Decorator for :func:`.expand_fpath`.
|
|
279
|
-
|
|
280
|
-
Parameters
|
|
281
|
-
----------
|
|
282
|
-
f : :obj:`callable`
|
|
283
|
-
A function that takes an :ref:`.ExpandedFPath` as its first argument.
|
|
284
|
-
post_process : :obj:`callable`
|
|
285
|
-
A function that takes the output of `f` and the :ref:`.ExpandedFPath`.
|
|
286
|
-
(Default: None).
|
|
287
|
-
"""
|
|
288
|
-
|
|
289
|
-
def decorator(f):
|
|
290
|
-
@wraps(f)
|
|
291
|
-
def wrapper(fpath, *args, **kwargs):
|
|
292
|
-
# What happens if fpath is not expandable?
|
|
293
|
-
# If require_expandable is True, raise an error.
|
|
294
|
-
# Otherwise, just call f with the original fpath.
|
|
295
|
-
if not is_expandable(fpath):
|
|
296
|
-
if require_expandable:
|
|
297
|
-
msg = f"fpath '{fpath}' is not expandable"
|
|
298
|
-
raise ValueError(msg)
|
|
299
|
-
return f(fpath, *args, **kwargs)
|
|
300
|
-
|
|
301
|
-
# We know fpath is expandable, so we can expand it and call f
|
|
302
|
-
exclude_path_patterns = kwargs.pop("exclude_path_patterns", None)
|
|
303
|
-
require_metadata = kwargs.pop("require_metadata", True)
|
|
304
|
-
expanded_fpath = expand_fpath(
|
|
305
|
-
fpath,
|
|
306
|
-
exclude_path_patterns=exclude_path_patterns,
|
|
307
|
-
require_metadata=require_metadata,
|
|
308
|
-
)
|
|
309
|
-
result = f(expanded_fpath, *args, **kwargs)
|
|
310
|
-
if post_process is not None:
|
|
311
|
-
result = post_process(result, expanded_fpath)
|
|
312
|
-
return result
|
|
313
|
-
|
|
314
|
-
return wrapper
|
|
315
|
-
|
|
316
|
-
if f is None:
|
|
317
|
-
return decorator
|
|
318
|
-
else:
|
|
319
|
-
return decorator(f)
|
|
320
|
-
|
|
321
|
-
def is_expandable(fpath):
|
|
322
|
-
"""
|
|
323
|
-
Check if expandable.
|
|
324
|
-
|
|
325
|
-
Parameters
|
|
326
|
-
----------
|
|
327
|
-
fpath : :obj:`str`
|
|
328
|
-
|
|
329
|
-
Returns
|
|
330
|
-
-------
|
|
331
|
-
:obj:`bool`
|
|
332
|
-
"""
|
|
333
|
-
|
|
334
|
-
try:
|
|
335
|
-
parser = parse.compile(fpath)
|
|
336
|
-
return bool(parser.named_fields)
|
|
337
|
-
except TypeError:
|
|
338
|
-
return False
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fpathlib
|
|
3
|
+
Version: 0.1.3.dev0
|
|
4
|
+
Summary: A package to combine paths with metadata
|
|
5
|
+
Author-email: "C. Lockhart" <clockha2@gmu.edu>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: parse
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest; extra == "dev"
|
|
12
|
+
Requires-Dist: black; extra == "dev"
|
|
13
|
+
Requires-Dist: sphinx; extra == "dev"
|
|
14
|
+
Requires-Dist: furo; extra == "dev"
|
|
15
|
+
Requires-Dist: polars; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
fpathlib
|
|
19
|
+
========
|
|
20
|
+
|
|
21
|
+
A Python package for adding metadata to file paths.
|
|
22
|
+
|
|
23
|
+
`fpathlib` combines file paths with the metadata encoded in their names. It
|
|
24
|
+
does this with an f-string-like path pattern -- `FPath` -- whose
|
|
25
|
+
`{variable}` fields are captured out of every matching path on disk.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from fpathlib import expand_fpath
|
|
29
|
+
|
|
30
|
+
# Given files like data/tr1/output/0/job2.log, data/tr2/output/1/job0.log, ...
|
|
31
|
+
expanded = expand_fpath("data/tr{trajectory:d}/output/{replica:d}/job{job:d}.log")
|
|
32
|
+
|
|
33
|
+
for path in expanded:
|
|
34
|
+
print(path, path.metadata)
|
|
35
|
+
# data/tr1/output/0/job2.log {'trajectory': 1, 'replica': 0, 'job': 2}
|
|
36
|
+
# data/tr2/output/1/job0.log {'trajectory': 2, 'replica': 1, 'job': 0}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Combine the metadata for every matched path into a single table:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
df = expanded.to_polars()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`fpathlib.ext.polars` goes a step further: it wraps `polars`'s own
|
|
46
|
+
`read_csv`/`scan_csv`/`read_txt`/`scan_txt` so that an expandable path
|
|
47
|
+
pattern is accepted directly, and the resulting DataFrame or LazyFrame comes
|
|
48
|
+
back with the metadata already joined in.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import fpathlib.ext.polars as pl
|
|
52
|
+
|
|
53
|
+
df = pl.read_csv(
|
|
54
|
+
"data/tr{trajectory:d}/output/{replica:d}/job{job:d}.log",
|
|
55
|
+
has_header=False,
|
|
56
|
+
)
|
|
57
|
+
# df has columns "column_1", ..., plus "trajectory", "replica", "job"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Installation
|
|
61
|
+
------------
|
|
62
|
+
|
|
63
|
+
```shell
|
|
64
|
+
pip install fpathlib
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `fpathlib.ext.polars` extension additionally requires `polars`, which
|
|
68
|
+
is not installed by `fpathlib` itself:
|
|
69
|
+
|
|
70
|
+
```shell
|
|
71
|
+
pip install polars
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Documentation
|
|
75
|
+
-------------
|
|
76
|
+
|
|
77
|
+
Full API documentation is in `docs/`; build it locally with
|
|
78
|
+
`scripts/docs.sh` (requires the `dev` extras: `pip install -e .[dev]`).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
fpathlib/__init__.py,sha256=VniZjNqT5pxzj6ymcpAlhNoWC44MwkQLu9jssc7o13Q,308
|
|
2
|
+
fpathlib/_version.py,sha256=dxsNQZw4RtnKvRTjSi1emxt4lZsN7c7hbUngeAYGVZY,533
|
|
3
|
+
fpathlib/expand.py,sha256=M_K1nfRAD9c983d68kCRuzo7-zNaOY_3R9gn8aBritI,3393
|
|
4
|
+
fpathlib/fpath.py,sha256=okEgqpWQjearSyDTp6RdIWbFolijI3BrXR8F5t-9Was,4877
|
|
5
|
+
fpathlib/path.py,sha256=ME2jfMBCK1Emu35mu8wN-KvmPE4p8qQqX8q-uO5cDsI,2629
|
|
6
|
+
fpathlib/utils.py,sha256=os9ggFllqOZXYcjfKcxPIy92LuRfvt6jWL0gNfet1N0,545
|
|
7
|
+
fpathlib/__pycache__/__init__.cpython-313.pyc,sha256=clLXRuVcvX9dJt4CTvaUBR6vhAzg67Ex7gKRwtumJFY,428
|
|
8
|
+
fpathlib/__pycache__/path.cpython-313.pyc,sha256=mQVWw1qjmvUp3NflXzhC_Q654H4hFW29gLYHP9nxRPQ,12257
|
|
9
|
+
fpathlib/__pycache__/utils.cpython-313.pyc,sha256=V4KSHMjm3ePQfaOKWwAkmaocvYb4VOul6Zooh0EvLa0,772
|
|
10
|
+
fpathlib/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
fpathlib/ext/polars.py,sha256=r3-MoGfbB11cOrECGCEyFzdKXGGEI9DSV6Xfc8vHeJo,10994
|
|
12
|
+
fpathlib/ext/__pycache__/__init__.cpython-313.pyc,sha256=lrgZbujuH_tlWGfe_Huo46srJdYJd1NQ6ZQFWamc6tc,153
|
|
13
|
+
fpathlib/ext/__pycache__/polars.cpython-313.pyc,sha256=S0epPPABmHRj0IrCxyOC8hD2_g0hBKggFFWfsmXw4m0,7740
|
|
14
|
+
fpathlib-0.1.3.dev0.dist-info/licenses/LICENSE,sha256=76rZ9i4ZumZ1M4jlYM9ZDOTYEeyzuxdsTKjF2qha3Lc,1069
|
|
15
|
+
fpathlib-0.1.3.dev0.dist-info/METADATA,sha256=EqyLMoPqqdMHwjd0To4_dc-o-wW2B2t6HmHel926ueM,2141
|
|
16
|
+
fpathlib-0.1.3.dev0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
17
|
+
fpathlib-0.1.3.dev0.dist-info/top_level.txt,sha256=QT5pDgO4--AJn7X4RB51xpNTt0pD5_U2mCjsydpQAQw,9
|
|
18
|
+
fpathlib-0.1.3.dev0.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fpathlib
|
|
3
|
-
Version: 0.1.2.dev0
|
|
4
|
-
Summary: A package to combine paths with metadata
|
|
5
|
-
Author-email: "C. Lockhart" <clockha2@gmu.edu>
|
|
6
|
-
Requires-Python: >=3.9
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: parse
|
|
10
|
-
Provides-Extra: dev
|
|
11
|
-
Requires-Dist: pytest; extra == "dev"
|
|
12
|
-
Requires-Dist: black; extra == "dev"
|
|
13
|
-
Requires-Dist: sphinx; extra == "dev"
|
|
14
|
-
Dynamic: license-file
|
|
15
|
-
|
|
16
|
-
fpathlib
|
|
17
|
-
========
|
|
18
|
-
|
|
19
|
-
A Python package for adding metadata to file paths.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
fpathlib/__init__.py,sha256=EBSrXJL75V913iVAFnxN9-I2PptBoNvVJuz7a3ljPrA,314
|
|
2
|
-
fpathlib/_version.py,sha256=fsBXt_uGfzjmQP8ggFtdc8lndHuCWCLefeg9u0NFdb8,533
|
|
3
|
-
fpathlib/path.py,sha256=QognR9ULT7uxUBVJa2mXLAVSrdE5luX62DtMYStwuMY,10372
|
|
4
|
-
fpathlib/utils.py,sha256=os9ggFllqOZXYcjfKcxPIy92LuRfvt6jWL0gNfet1N0,545
|
|
5
|
-
fpathlib/__pycache__/__init__.cpython-313.pyc,sha256=clLXRuVcvX9dJt4CTvaUBR6vhAzg67Ex7gKRwtumJFY,428
|
|
6
|
-
fpathlib/__pycache__/path.cpython-313.pyc,sha256=mQVWw1qjmvUp3NflXzhC_Q654H4hFW29gLYHP9nxRPQ,12257
|
|
7
|
-
fpathlib/__pycache__/utils.cpython-313.pyc,sha256=V4KSHMjm3ePQfaOKWwAkmaocvYb4VOul6Zooh0EvLa0,772
|
|
8
|
-
fpathlib/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
fpathlib/ext/polars.py,sha256=viSPdThRZCDafN37YixntfwRzgqKfNB6TfqvlFFATK0,9262
|
|
10
|
-
fpathlib/ext/__pycache__/__init__.cpython-313.pyc,sha256=lrgZbujuH_tlWGfe_Huo46srJdYJd1NQ6ZQFWamc6tc,153
|
|
11
|
-
fpathlib/ext/__pycache__/polars.cpython-313.pyc,sha256=S0epPPABmHRj0IrCxyOC8hD2_g0hBKggFFWfsmXw4m0,7740
|
|
12
|
-
fpathlib-0.1.2.dev0.dist-info/licenses/LICENSE,sha256=76rZ9i4ZumZ1M4jlYM9ZDOTYEeyzuxdsTKjF2qha3Lc,1069
|
|
13
|
-
fpathlib-0.1.2.dev0.dist-info/METADATA,sha256=j8pkPrc9dnYU2tgvZiibJ1PFoM_I_U55vvFN6GPeKt4,487
|
|
14
|
-
fpathlib-0.1.2.dev0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
-
fpathlib-0.1.2.dev0.dist-info/top_level.txt,sha256=QT5pDgO4--AJn7X4RB51xpNTt0pD5_U2mCjsydpQAQw,9
|
|
16
|
-
fpathlib-0.1.2.dev0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|