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 CHANGED
@@ -1,14 +1,11 @@
1
- from fpathlib.path import (
2
- Path,
3
- FPath,
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.2.dev0'
22
- __version_tuple__ = version_tuple = (0, 1, 2, 'dev0')
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
- from polars import *
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, pl.LazyFrame)),
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 = pl.scan_csv(
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 = pl.scan_parquet(
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 = pl.scan_csv(
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
- pl.col("line").str.split(separator, literal=False).alias("fields")
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
- # Count the number of fields
242
- n_fields = (
243
- lf.head(1)
244
- .select(pl.col("fields").list.len().unique())
245
- .collect(engine="streaming")
246
- .item()
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
- lf = lf.with_columns(
254
- [pl.col("fields").list.get(i).alias(field) for i, field in fields.items()]
255
- ).drop("fields")
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(pl.col(fields)).to_dict(as_series=False)
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
- lf = lf.rename(
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 kwargs.get("infer_schema", True):
280
- sample = (
281
- lf.head(kwargs.get("infer_schema_length", 100))
282
- .collect(engine="streaming")
283
- .write_csv()
284
- .encode()
285
- )
286
- inferred_schema = pl.read_csv(sample).schema
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,,