flatdir 0.0.6__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.
- flatdir/__init__.py +1 -0
- flatdir/__main__.py +89 -0
- flatdir/listing.py +66 -0
- flatdir/plugins/__init__.py +0 -0
- flatdir/plugins/defaults.py +38 -0
- flatdir/plugins/filename_length.py +8 -0
- flatdir/plugins_loader.py +52 -0
- flatdir-0.0.6.dist-info/METADATA +48 -0
- flatdir-0.0.6.dist-info/RECORD +12 -0
- flatdir-0.0.6.dist-info/WHEEL +5 -0
- flatdir-0.0.6.dist-info/licenses/LICENSE +21 -0
- flatdir-0.0.6.dist-info/top_level.txt +1 -0
flatdir/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.6"
|
flatdir/__main__.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Command-line entrypoint for flatdir: print directory listing as JSON.
|
|
2
|
+
|
|
3
|
+
Usage: python -m flatdir [--limit N] [--depth N] [--output FILE] [--fields FILE] [path]
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .listing import list_entries
|
|
13
|
+
from .plugins_loader import load_fields_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None) -> int:
|
|
17
|
+
argv = argv if argv is not None else sys.argv[1:]
|
|
18
|
+
|
|
19
|
+
# parse --limit flag if present
|
|
20
|
+
limit: int | None = None
|
|
21
|
+
if "--limit" in argv:
|
|
22
|
+
try:
|
|
23
|
+
idx = argv.index("--limit")
|
|
24
|
+
limit = int(argv[idx + 1])
|
|
25
|
+
argv = argv[:idx] + argv[idx + 2 :]
|
|
26
|
+
except (IndexError, ValueError):
|
|
27
|
+
print("error: --limit requires a valid integer argument", file=sys.stderr)
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
# parse --depth flag if present
|
|
31
|
+
depth: int | None = None
|
|
32
|
+
if "--depth" in argv:
|
|
33
|
+
try:
|
|
34
|
+
idx = argv.index("--depth")
|
|
35
|
+
depth = int(argv[idx + 1])
|
|
36
|
+
argv = argv[:idx] + argv[idx + 2 :]
|
|
37
|
+
except (IndexError, ValueError):
|
|
38
|
+
print("error: --depth requires a valid integer argument", file=sys.stderr)
|
|
39
|
+
return 1
|
|
40
|
+
|
|
41
|
+
# parse --output flag if present
|
|
42
|
+
output: str | None = None
|
|
43
|
+
if "--output" in argv:
|
|
44
|
+
try:
|
|
45
|
+
idx = argv.index("--output")
|
|
46
|
+
output = argv[idx + 1]
|
|
47
|
+
argv = argv[:idx] + argv[idx + 2 :]
|
|
48
|
+
except (IndexError, ValueError):
|
|
49
|
+
print("error: --output requires a file path argument", file=sys.stderr)
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
# parse --fields flag if present
|
|
53
|
+
fields = None
|
|
54
|
+
if "--fields" in argv:
|
|
55
|
+
try:
|
|
56
|
+
idx = argv.index("--fields")
|
|
57
|
+
fields_path = argv[idx + 1]
|
|
58
|
+
fields = load_fields_file(fields_path)
|
|
59
|
+
argv = argv[:idx] + argv[idx + 2 :]
|
|
60
|
+
except IndexError:
|
|
61
|
+
print("error: --fields requires a file path argument", file=sys.stderr)
|
|
62
|
+
return 1
|
|
63
|
+
except (FileNotFoundError, ImportError) as exc:
|
|
64
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
65
|
+
return 1
|
|
66
|
+
|
|
67
|
+
path = Path(argv[0]) if argv else Path(".")
|
|
68
|
+
|
|
69
|
+
# error in case of missing path or path is not a directory
|
|
70
|
+
if not path.exists() or not path.is_dir():
|
|
71
|
+
print(f"path is not a directory: {path}", file=sys.stderr)
|
|
72
|
+
return 2
|
|
73
|
+
|
|
74
|
+
# generate the actual list of entries to be returned as JSON
|
|
75
|
+
entries = list_entries(path, limit=limit, depth=depth, fields=fields)
|
|
76
|
+
|
|
77
|
+
# write JSON to output file or stdout
|
|
78
|
+
if output is not None:
|
|
79
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
80
|
+
json.dump(entries, f, ensure_ascii=False, indent=4)
|
|
81
|
+
f.write("\n")
|
|
82
|
+
else:
|
|
83
|
+
json.dump(entries, sys.stdout, ensure_ascii=False, indent=4)
|
|
84
|
+
_ = sys.stdout.write("\n")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
raise SystemExit(main())
|
flatdir/listing.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""List entries in a directory and return metadata as a list of dict.
|
|
2
|
+
|
|
3
|
+
Each entry is a dict with keys determined by field plugins (defaults: name, type, mtime, size).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .plugins import defaults as _defaults
|
|
12
|
+
from .plugins_loader import load_fields_file
|
|
13
|
+
|
|
14
|
+
# built-in default fields, loaded once
|
|
15
|
+
DEFAULT_FIELDS = load_fields_file(_defaults.__file__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_entries(
|
|
19
|
+
root: Path,
|
|
20
|
+
limit: int | None = None,
|
|
21
|
+
depth: int | None = None,
|
|
22
|
+
fields: dict[str, object] | None = None,
|
|
23
|
+
) -> list[dict[str, object]]:
|
|
24
|
+
entries: list[dict[str, object]] = []
|
|
25
|
+
root = root.resolve()
|
|
26
|
+
|
|
27
|
+
# merge default fields with custom fields (custom can override defaults)
|
|
28
|
+
all_fields = dict(DEFAULT_FIELDS)
|
|
29
|
+
if fields:
|
|
30
|
+
all_fields.update(fields)
|
|
31
|
+
|
|
32
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
33
|
+
base = Path(dirpath)
|
|
34
|
+
# check depth constraint
|
|
35
|
+
current_depth = len(base.relative_to(root).parts)
|
|
36
|
+
if depth is not None and depth >= 0 and current_depth > depth:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
# list subdirectories at this level
|
|
40
|
+
for dirname in dirnames:
|
|
41
|
+
p = (base / dirname).resolve()
|
|
42
|
+
entry: dict[str, object] = {}
|
|
43
|
+
for field_name, func in all_fields.items():
|
|
44
|
+
value = func(p, root)
|
|
45
|
+
if value is not None:
|
|
46
|
+
entry[field_name] = value
|
|
47
|
+
entries.append(entry)
|
|
48
|
+
|
|
49
|
+
# list files at this level
|
|
50
|
+
for filename in filenames:
|
|
51
|
+
p = (base / filename).resolve()
|
|
52
|
+
entry = {}
|
|
53
|
+
for field_name, func in all_fields.items():
|
|
54
|
+
value = func(p, root)
|
|
55
|
+
if value is not None:
|
|
56
|
+
entry[field_name] = value
|
|
57
|
+
entries.append(entry)
|
|
58
|
+
|
|
59
|
+
# return a sorted list of entries
|
|
60
|
+
entries.sort(key=lambda e: str(e.get("name", "")))
|
|
61
|
+
|
|
62
|
+
# apply limit if provided
|
|
63
|
+
if limit is not None and limit >= 0:
|
|
64
|
+
entries = entries[:limit]
|
|
65
|
+
|
|
66
|
+
return entries
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Default field plugins for flatdir.
|
|
2
|
+
|
|
3
|
+
These functions produce the built-in fields: name, type, mtime, size.
|
|
4
|
+
Each function receives the file path and the root directory path.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def name(path: Path, root: Path) -> str:
|
|
15
|
+
"""Relative path from the listing root."""
|
|
16
|
+
try:
|
|
17
|
+
return str(path.relative_to(root))
|
|
18
|
+
except ValueError:
|
|
19
|
+
return str(os.path.relpath(str(path), str(root)))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def type(path: Path, root: Path) -> str:
|
|
23
|
+
"""Entry type: 'file' or 'directory'."""
|
|
24
|
+
return "directory" if path.is_dir() else "file"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def mtime(path: Path, root: Path) -> str:
|
|
28
|
+
"""Last modification time in HTTP-date format."""
|
|
29
|
+
st = path.stat()
|
|
30
|
+
t = time.gmtime(st.st_mtime)
|
|
31
|
+
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", t)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def size(path: Path, root: Path) -> int | None:
|
|
35
|
+
"""File size in bytes. Returns None for directories (omitted from output)."""
|
|
36
|
+
if path.is_dir():
|
|
37
|
+
return None
|
|
38
|
+
return int(path.stat().st_size)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Load custom field functions from a user-provided Python file.
|
|
2
|
+
|
|
3
|
+
A fields file is a plain Python module where each **public** function
|
|
4
|
+
(i.e. whose name does not start with ``_``) is treated as a field provider.
|
|
5
|
+
|
|
6
|
+
Each function must accept a single :class:`pathlib.Path` argument (the file
|
|
7
|
+
being listed) and return a JSON-serialisable value. The function name becomes
|
|
8
|
+
the field key in the output.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import inspect
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Callable
|
|
18
|
+
|
|
19
|
+
FieldFunc = Callable[[Path], object]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_fields_file(filepath: str) -> dict[str, FieldFunc]:
|
|
23
|
+
"""Import *filepath* as a module and return its public callables.
|
|
24
|
+
|
|
25
|
+
Returns a ``{name: func}`` mapping for every public, non-class callable
|
|
26
|
+
defined in the file.
|
|
27
|
+
"""
|
|
28
|
+
path = Path(filepath).resolve()
|
|
29
|
+
if not path.is_file():
|
|
30
|
+
raise FileNotFoundError(f"fields file not found: {filepath}")
|
|
31
|
+
|
|
32
|
+
# import the file as a temporary module
|
|
33
|
+
spec = importlib.util.spec_from_file_location("_flatdir_fields", str(path))
|
|
34
|
+
if spec is None or spec.loader is None:
|
|
35
|
+
raise ImportError(f"cannot load fields file: {filepath}")
|
|
36
|
+
module = importlib.util.module_from_spec(spec)
|
|
37
|
+
sys.modules["_flatdir_fields"] = module
|
|
38
|
+
spec.loader.exec_module(module)
|
|
39
|
+
|
|
40
|
+
fields: dict[str, FieldFunc] = {}
|
|
41
|
+
for name, obj in inspect.getmembers(module, callable):
|
|
42
|
+
# skip private names, classes, and imported builtins
|
|
43
|
+
if name.startswith("_"):
|
|
44
|
+
continue
|
|
45
|
+
if inspect.isclass(obj):
|
|
46
|
+
continue
|
|
47
|
+
# only keep functions actually defined in this file
|
|
48
|
+
if getattr(obj, "__module__", None) != "_flatdir_fields":
|
|
49
|
+
continue
|
|
50
|
+
fields[name] = obj
|
|
51
|
+
|
|
52
|
+
return fields
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flatdir
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: A Python library to create a flat JSON index of files and directories.
|
|
5
|
+
Author: Your Name
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/romsson/flatdir
|
|
8
|
+
Project-URL: Repository, https://github.com/romsson/flatdir
|
|
9
|
+
Classifier: Natural Language :: English
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: <4,>=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# flatdir
|
|
23
|
+
|
|
24
|
+
`flatdir` scans a directory tree and generates a flat JSON file with metadata for each entry.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
# flatdir
|
|
29
|
+
|
|
30
|
+
`flatdir` scans a directory tree and generates a flat JSON file with metadata for each entry.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or from PyPI:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install flatdir
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m flatdir .
|
|
48
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
flatdir/__init__.py,sha256=QiiYsv0kcJaB8wCWyT-FnI2b6be87HA-CrrIUn8LQhg,22
|
|
2
|
+
flatdir/__main__.py,sha256=aEzAZ2EG_TiQwf8ZXeG-T8imTp2z1z92kzQwpmlVL1Y,2896
|
|
3
|
+
flatdir/listing.py,sha256=IANCsiryDUuuMfXjZOREbIQY_ePhwd44aT5Bsp_vOLg,2043
|
|
4
|
+
flatdir/plugins_loader.py,sha256=O8RI2au3oQFMM3olKF4JXMredTGVoQRmhxM4NvAfUhM,1778
|
|
5
|
+
flatdir/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
flatdir/plugins/defaults.py,sha256=SiOgZoBrD_qD0VxkqSthFAcvDnBWZwL1uArFQS22bh8,1045
|
|
7
|
+
flatdir/plugins/filename_length.py,sha256=g8DsDODnKxrhAghOpz7DyYERXQlXxHk7nENBAHbW6Yk,256
|
|
8
|
+
flatdir-0.0.6.dist-info/licenses/LICENSE,sha256=6naVCeRSIEXqwijKGxc74qCmX9xZr_sVWdmSAFSWIdI,1073
|
|
9
|
+
flatdir-0.0.6.dist-info/METADATA,sha256=E1Yaj9IvYfsddwjpm3xqpmSeRIJO5k2xy_vjUVD6Ki8,1140
|
|
10
|
+
flatdir-0.0.6.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
11
|
+
flatdir-0.0.6.dist-info/top_level.txt,sha256=_xeO7mmnryAGKUeKuTunqj9gy3duEVHLQrmpkU7i7nw,8
|
|
12
|
+
flatdir-0.0.6.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Romain Vuillemot
|
|
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
|
+
flatdir
|