fpathlib 0.1.0__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 ADDED
@@ -0,0 +1,15 @@
1
+ from fpathlib.path import (
2
+ Path,
3
+ FPath,
4
+ ExpandedFPath,
5
+ expand_fpath,
6
+ expand_fpath_decorator,
7
+ )
8
+
9
+ __all__ = [
10
+ "Path",
11
+ "FPath",
12
+ "ExpandedFPath",
13
+ "expand_fpath",
14
+ "expand_fpath_decorator",
15
+ ]
fpathlib/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
File without changes
fpathlib/ext/polars.py ADDED
@@ -0,0 +1,116 @@
1
+ from functools import wraps
2
+ from polars import *
3
+ import polars as pl
4
+ from fpathlib import expand_fpath_decorator
5
+
6
+
7
+ def join_metadata(df, expanded_fpath):
8
+ return df.join(
9
+ expanded_fpath.to_polars(lazy=isinstance(df, pl.LazyFrame)),
10
+ on="fname",
11
+ )
12
+
13
+
14
+ @expand_fpath_decorator(post_process=join_metadata)
15
+ def scan_csv(expanded_fpath, *args, **kwargs):
16
+ """
17
+ Scan the paths in the collection as CSV files, and return a
18
+ :obj:`polars.LazyFrame` along with the metadata captured from the path
19
+ variables.
20
+
21
+ Parameters
22
+ ----------
23
+ expanded_fpath : :obj:`fpathlib.ExpandedFPath`
24
+ An expanded f-string path.
25
+ *args
26
+ Positional arguments to pass to :meth:`polars.scan_csv`.
27
+ **kwargs
28
+ Keyword arguments to pass to :meth:`polars.scan_csv`.
29
+
30
+ Returns
31
+ -------
32
+ :obj:`polars.LazyFrame`
33
+ """
34
+
35
+ lf = pl.scan_csv(
36
+ expanded_fpath,
37
+ include_file_paths="fname",
38
+ *args,
39
+ **kwargs,
40
+ )
41
+
42
+ return lf
43
+
44
+
45
+ @expand_fpath_decorator(post_process=join_metadata)
46
+ def scan_txt(
47
+ expanded_fpath,
48
+ filter_expr=None,
49
+ separator=None,
50
+ new_columns=None,
51
+ has_header=False,
52
+ *args,
53
+ **kwargs,
54
+ ):
55
+ """
56
+ Scan the paths in the collection as text files, where each line is a
57
+ record, and return a :obj:`polars.LazyFrame` along with the metadata
58
+ captured from the path. The text files can also be delimited by
59
+ `separator`, in which case the lines are split by the separator and each
60
+ field is a record. The names of these field can be set by `new_columns`.
61
+
62
+ Parameters
63
+ ----------
64
+ expanded_fpath : :obj:`fpathlib.ExpandedFPath`
65
+ An expanded f-string path.
66
+ filter_expr : :obj:`polars.Expr`, optional
67
+ Filter the lines before splitting by the separator (if provided).
68
+ separator : :obj:`str`, optional
69
+ Deliminatorg to split each line into fields.
70
+ new_columns : :obj:`list`[:obj:`str`], optional
71
+ List of new column names to rename the fields after splitting by the separator.
72
+ If not provided, the fields are named as `field_0`, `field_1`, etc.
73
+ has_header : :obj:`bool`
74
+ Whether the text files have a header line that should be skipped. The header
75
+ must have the same delimiter as the separator provided in `separator`.
76
+ (Default: False)
77
+ *args
78
+ Positional arguments to pass to :meth:`polars.scan_csv`.
79
+ **kwargs
80
+ Keyword arguments to pass to :meth:`polars.scan_csv`.
81
+
82
+ Returns
83
+ -------
84
+ :obj:`polars.LazyFrame`
85
+ """
86
+
87
+ # TODO there are forbidden variables that should not be in expanded_fpath
88
+ # such as 'line' and 'fields' and 'fname'
89
+
90
+ lf = pl.scan_csv(
91
+ expanded_fpath,
92
+ include_file_paths="fname",
93
+ separator="\n",
94
+ new_columns=["line"],
95
+ has_header=False,
96
+ **kwargs,
97
+ )
98
+
99
+ if filter_expr is not None:
100
+ lf = lf.filter(filter_expr)
101
+
102
+ if separator is not None:
103
+ lf = lf.with_columns(fields=pl.col("line").str.split(separator, literal=False))
104
+ n_fields = lf.select(pl.col("fields").list.len().unique()).collect()
105
+ if n_fields.shape[0] > 1:
106
+ msg = "number of fields must be consistent across all lines", n_fields
107
+ raise ValueError(msg)
108
+ n_fields = n_fields.item()
109
+ lf = lf.with_columns(
110
+ [pl.col("fields").list.get(i).alias(f"field_{i}") for i in range(n_fields)]
111
+ ).drop(["fields", "line"])
112
+
113
+ if new_columns is not None:
114
+ lf = lf.rename({f"field_{i}": col for i, col in enumerate(new_columns)})
115
+
116
+ return lf
fpathlib/path.py ADDED
@@ -0,0 +1,309 @@
1
+ from collections.abc import Sequence
2
+ from functools import wraps
3
+ from glob import glob
4
+ import parse
5
+ import pathlib
6
+ import re
7
+
8
+ from .utils import import_optional_dependency
9
+
10
+
11
+ class Path(pathlib.Path):
12
+ """
13
+ :obj:`Path` is simply :obj:`pathlib.Path` with metadata.
14
+
15
+ Parameters
16
+ ----------
17
+ *pathsegments
18
+ """
19
+
20
+ def __init__(self, *pathsegments, metadata=None):
21
+ super().__init__(*pathsegments)
22
+ self.metadata = metadata
23
+
24
+ def match(self, path_patterns, case_sensitive=None):
25
+ """
26
+ Perform :meth:`pathlib.Path.match` on several `path_patterns`.
27
+
28
+ Parameters
29
+ ----------
30
+ path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
31
+ One or more path patterns to match against. (Default: None).
32
+ case_sensitive : :obj:`bool`
33
+ Whether to perform case-sensitive matching. If None, the default, then the
34
+ behavior is determined by the operating system. (Default: None).
35
+
36
+ Returns
37
+ -------
38
+ :obj:`generator`[:obj:`bool`]
39
+ """
40
+
41
+ if isinstance(path_patterns, str):
42
+ path_patterns = [path_patterns]
43
+
44
+ for path_pattern in path_patterns:
45
+ yield super().match(path_pattern, case_sensitive=case_sensitive)
46
+
47
+ def match_all(self, path_patterns, case_sensitive=None):
48
+ """
49
+ Perform :meth:`pathlib.Path.match` on several `path_patterns`. All patterns
50
+ must match.
51
+
52
+ Parameters
53
+ ----------
54
+ path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
55
+ One or more path patterns to match against. (Default: None).
56
+ case_sensitive : :obj:`bool`
57
+ Whether to perform case-sensitive matching. If None, the default, then the
58
+ behavior is determined by the operating system. (Default: None).
59
+
60
+ Returns
61
+ -------
62
+ :obj:`bool`
63
+ """
64
+
65
+ for match in self.match(path_patterns, case_sensitive=case_sensitive):
66
+ if not match:
67
+ return False
68
+
69
+ return True
70
+
71
+ def match_any(self, path_patterns, case_sensitive=None):
72
+ """
73
+ Perform :meth:`pathlib.Path.match` on several `path_patterns`. Any pattern
74
+ must match.
75
+
76
+ Parameters
77
+ ----------
78
+ path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
79
+ One or more path patterns to match against. (Default: None).
80
+ case_sensitive : :obj:`bool`
81
+ Whether to perform case-sensitive matching. If None, the default, then the
82
+ behavior is determined by the operating system. (Default: None).
83
+
84
+ Returns
85
+ -------
86
+ :obj:`bool`
87
+ """
88
+
89
+ for match in self.match(path_patterns):
90
+ if match:
91
+ return True
92
+
93
+ 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 = fpath
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, 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
+ exclude_path_patterns = kwargs.pop("exclude_path_patterns", None)
293
+ require_metadata = kwargs.pop("require_metadata", True)
294
+ expanded_fpath = expand_fpath(
295
+ fpath,
296
+ exclude_path_patterns=exclude_path_patterns,
297
+ require_metadata=require_metadata,
298
+ )
299
+ result = f(expanded_fpath, *args, **kwargs)
300
+ if post_process is not None:
301
+ result = post_process(result, expanded_fpath)
302
+ return result
303
+
304
+ return wrapper
305
+
306
+ if f is None:
307
+ return decorator
308
+ else:
309
+ return decorator(f)
fpathlib/utils.py ADDED
@@ -0,0 +1,22 @@
1
+ from importlib import import_module
2
+
3
+
4
+ def import_optional_dependency(package):
5
+ """
6
+ Try to import `package`. If it is not installed, raise an ImportError.
7
+
8
+ Parameters
9
+ ----------
10
+ package : :obj:`str`
11
+ The name of the package to import.
12
+
13
+ Returns
14
+ -------
15
+ :obj:`module`
16
+ """
17
+
18
+ try:
19
+ return import_module(package)
20
+ except ModuleNotFoundError:
21
+ msg = f"optional dependency '{package}' is not installed; please install it and try again."
22
+ raise ModuleNotFoundError(msg) from None
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: fpathlib
3
+ Version: 0.1.0
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.
@@ -0,0 +1,16 @@
1
+ fpathlib/__init__.py,sha256=BQhOA7UY0wS0OfGlw5T9bbNaYZlcb7nKiA0q5F89ofo,227
2
+ fpathlib/_version.py,sha256=n_5vdJsPNu7wZ57LGuRL585uvll-hiuvZUBWzdG0RQU,520
3
+ fpathlib/path.py,sha256=J9T0exhefoIUywynkOZGLnpArOERVVwA7KfaYinRxD4,9505
4
+ fpathlib/utils.py,sha256=os9ggFllqOZXYcjfKcxPIy92LuRfvt6jWL0gNfet1N0,545
5
+ fpathlib/__pycache__/__init__.cpython-313.pyc,sha256=YqTERPaAJJgHJ5kCpVw6c92d1Me7gfudZZ8rUAxKo50,321
6
+ fpathlib/__pycache__/path.cpython-313.pyc,sha256=qlcwnudDL4jkFkCOVHdV14W-NTByOnHWdjvV5iIjc4I,11533
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=_TBjWt61e8miV5W_5UXcjdm67PvPnndWnFKa9K8dfTM,3633
10
+ fpathlib/ext/__pycache__/__init__.cpython-313.pyc,sha256=lrgZbujuH_tlWGfe_Huo46srJdYJd1NQ6ZQFWamc6tc,153
11
+ fpathlib/ext/__pycache__/polars.cpython-313.pyc,sha256=_Xz06TsHan10-xO87r7-AeT2S7OEKB-GTjIFnuXIuJA,4626
12
+ fpathlib-0.1.0.dist-info/licenses/LICENSE,sha256=76rZ9i4ZumZ1M4jlYM9ZDOTYEeyzuxdsTKjF2qha3Lc,1069
13
+ fpathlib-0.1.0.dist-info/METADATA,sha256=7bPl-Pu-RKuUVRophpn8LdoVxzZ_0ARwISM0Q9PcnQY,482
14
+ fpathlib-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
15
+ fpathlib-0.1.0.dist-info/top_level.txt,sha256=QT5pDgO4--AJn7X4RB51xpNTt0pD5_U2mCjsydpQAQw,9
16
+ fpathlib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lockhart Lab
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 @@
1
+ fpathlib