treefs 2.0.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.
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: treefs
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Filesystem generator/importer using tree, YAML, JSON, TOML and tree format. Deluxe edition.
|
|
5
|
+
Author: Marcelo
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/marcelodevops/treefspy
|
|
8
|
+
Project-URL: Repository, https://github.com/marcelodevops/treefspy
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: PyYAML>=6.0
|
|
18
|
+
Requires-Dist: toml>=0.10.2
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# ๐งฐ TreeFS
|
|
22
|
+
|
|
23
|
+
A powerful CLI toolkit to build and export directory structures from **tree text**, **YAML**, **JSON**, or **TOML** configs โ with support for templates, Git initialization, and cross-platform release binaries.
|
|
24
|
+
|
|
25
|
+
Originally inspired by a tiny Bash script, TreeFS has grown into a full-featured Python project that helps automate project scaffolding and structure generation.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Build directory trees** from:
|
|
32
|
+
- Tree-formatted text files
|
|
33
|
+
- YAML / JSON / TOML config files
|
|
34
|
+
- Support for **file templates** and content templates
|
|
35
|
+
- **Export** file systems into tree text or structured configs
|
|
36
|
+
- **Git integration**
|
|
37
|
+
- `git init` with optional `.gitignore` and hooks
|
|
38
|
+
- **Cross-platform binaries**
|
|
39
|
+
- Build single-file executables via PyInstaller
|
|
40
|
+
- GitHub Actions workflow included to auto-build release binaries
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## ๐งพ Installation
|
|
45
|
+
|
|
46
|
+
Install locally using `pip`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
- Build from a tree file
|
|
54
|
+
```bash
|
|
55
|
+
treefs build tree.txt myproject/
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
- Add optional templates and git setup
|
|
59
|
+
```bash
|
|
60
|
+
treefs build project.yaml myproj/ \
|
|
61
|
+
--templates ./templates \
|
|
62
|
+
--init-git \
|
|
63
|
+
--gitignore ./common_gitignore \
|
|
64
|
+
--git-hooks ./my-hooks
|
|
65
|
+
```
|
|
66
|
+
- Preview what would be created without writing to disk:
|
|
67
|
+
```bash
|
|
68
|
+
treefs build project.yaml myproj/ --dry-run
|
|
69
|
+
```
|
|
70
|
+
### Export project structure
|
|
71
|
+
- Export a folder to a tree layout text:
|
|
72
|
+
```bash
|
|
73
|
+
treefs export-tree myproj/ tree.txt
|
|
74
|
+
```
|
|
75
|
+
- Or export to structured configs:
|
|
76
|
+
```bash
|
|
77
|
+
treefs export-config myproj/ project.yaml
|
|
78
|
+
treefs export-config myproj/ project.json
|
|
79
|
+
treefs export-config myproj/ project.toml
|
|
80
|
+
```
|
|
81
|
+
### Bundling a binary
|
|
82
|
+
- If PyInstaller is installed, you can auto-bundle TreeFS:
|
|
83
|
+
```bash
|
|
84
|
+
treefs bundle treefs.py --name treefs
|
|
85
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
treefs.py,sha256=E14e8H7hBI-mXoEOsbpxZezhWhwr15ZzQckYTL5SUDo,8342
|
|
2
|
+
treefs-2.0.0.dist-info/licenses/LICENSE,sha256=ROTPTYx7JJRaA9YLznUjB7n3do98U_pe1HPNkpnzVsQ,1070
|
|
3
|
+
treefs-2.0.0.dist-info/METADATA,sha256=s0ddALhh0MJwBlUObRjJBEURBU_DqbMvBMu2Bn8wrrg,2449
|
|
4
|
+
treefs-2.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
treefs-2.0.0.dist-info/entry_points.txt,sha256=gyCMZB6z6vv3jZiLVCDrJ4Rs7syrjDep-EHf4UPK4TI,39
|
|
6
|
+
treefs-2.0.0.dist-info/top_level.txt,sha256=7-rwtd18TzaXyD45zjo21h9QUWKb6kILLjSMXnaoTF8,7
|
|
7
|
+
treefs-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 marcelodevops
|
|
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
|
+
treefs
|
treefs.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TreeFS โ deluxe edition (fixed parser version)
|
|
4
|
+
|
|
5
|
+
Changes:
|
|
6
|
+
- Replaced fragile ASCII-tree stack parser
|
|
7
|
+
- Introduced AST-based tree parser (tree โ dict)
|
|
8
|
+
- build_from_tree now reuses build_from_dict safely
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, Optional
|
|
19
|
+
|
|
20
|
+
# optional deps: pyyaml, toml
|
|
21
|
+
try:
|
|
22
|
+
import yaml
|
|
23
|
+
except Exception:
|
|
24
|
+
yaml = None
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import toml
|
|
28
|
+
except Exception:
|
|
29
|
+
toml = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------- Utilities ----------
|
|
33
|
+
|
|
34
|
+
# 4-character indentation units a tree line's prefix is built from (one per
|
|
35
|
+
# ancestor depth level that is *not* the immediate parent connector).
|
|
36
|
+
_INDENT_UNITS = ("โ ", " ", "| ")
|
|
37
|
+
|
|
38
|
+
# 4-character connectors that introduce an entry's own name.
|
|
39
|
+
_CONNECTORS = ("โโโ ", "โโโ ", "|-- ", "`-- ")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_tree_line(line: str) -> tuple[int, str]:
|
|
43
|
+
"""Split a single tree line into (depth, name).
|
|
44
|
+
|
|
45
|
+
This walks the line manually instead of relying on trailing slashes or
|
|
46
|
+
fragile character-replacement math, so it works whether or not the tree
|
|
47
|
+
file marks directories with a trailing "/" (the stock `tree` command does
|
|
48
|
+
not do so unless run with `-F`).
|
|
49
|
+
|
|
50
|
+
A bare root line (no leading indent units and no connector, e.g. the
|
|
51
|
+
"myproj" line at the very top of `tree` output) is one level *above* its
|
|
52
|
+
connector-prefixed children, even though they share the same count of
|
|
53
|
+
indent units, so it gets depth -1 relative to them.
|
|
54
|
+
"""
|
|
55
|
+
i = 0
|
|
56
|
+
depth = 0
|
|
57
|
+
while line[i:i + 4] in _INDENT_UNITS:
|
|
58
|
+
depth += 1
|
|
59
|
+
i += 4
|
|
60
|
+
|
|
61
|
+
has_connector = line[i:i + 4] in _CONNECTORS
|
|
62
|
+
if has_connector:
|
|
63
|
+
i += 4
|
|
64
|
+
else:
|
|
65
|
+
depth -= 1
|
|
66
|
+
|
|
67
|
+
name = line[i:].strip()
|
|
68
|
+
return depth, name
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_tty() -> bool:
|
|
72
|
+
return sys.stdout.isatty()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def colorize(text: str, color: str) -> str:
|
|
76
|
+
if not is_tty():
|
|
77
|
+
return text
|
|
78
|
+
codes = {
|
|
79
|
+
"green": "\033[92m",
|
|
80
|
+
"yellow": "\033[93m",
|
|
81
|
+
"blue": "\033[94m",
|
|
82
|
+
"red": "\033[91m",
|
|
83
|
+
"reset": "\033[0m",
|
|
84
|
+
}
|
|
85
|
+
return f"{codes.get(color, '')}{text}{codes['reset']}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def safe_print(msg: str = "", c: str = "green"):
|
|
89
|
+
print(colorize(msg, c))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def ensure_parent(path: Path):
|
|
93
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------- FIXED: TREE PARSER (AST BUILDER) ----------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def tree_to_dict(tree_file: Path) -> dict:
|
|
100
|
+
"""
|
|
101
|
+
Converts ASCII tree file into nested dictionary (AST).
|
|
102
|
+
|
|
103
|
+
Directories are detected either by an explicit trailing "/" on the name,
|
|
104
|
+
or (since the stock `tree` command doesn't add one) by the presence of a
|
|
105
|
+
following line indented one level deeper.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
entries = [] # (depth, name, has_trailing_slash)
|
|
109
|
+
|
|
110
|
+
with tree_file.open("r", encoding="utf-8") as f:
|
|
111
|
+
for raw in f:
|
|
112
|
+
line = raw.rstrip()
|
|
113
|
+
|
|
114
|
+
if not line or line.strip() == "โ":
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
if line.lower().startswith("project"):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
depth, name = _parse_tree_line(line)
|
|
121
|
+
if not name:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
has_slash = name.endswith("/")
|
|
125
|
+
entries.append((depth, name.rstrip("/"), has_slash))
|
|
126
|
+
|
|
127
|
+
root: dict = {}
|
|
128
|
+
stack = [(-2, root)] # (depth, node); -2 is below any real/root depth
|
|
129
|
+
|
|
130
|
+
for idx, (depth, name, has_slash) in enumerate(entries):
|
|
131
|
+
is_dir = has_slash
|
|
132
|
+
if not is_dir and idx + 1 < len(entries):
|
|
133
|
+
is_dir = entries[idx + 1][0] > depth
|
|
134
|
+
|
|
135
|
+
# move to correct parent
|
|
136
|
+
while stack and stack[-1][0] >= depth:
|
|
137
|
+
stack.pop()
|
|
138
|
+
|
|
139
|
+
parent = stack[-1][1]
|
|
140
|
+
|
|
141
|
+
if is_dir:
|
|
142
|
+
node: dict = {}
|
|
143
|
+
parent[name] = node
|
|
144
|
+
stack.append((depth, node))
|
|
145
|
+
else:
|
|
146
|
+
parent[name] = ""
|
|
147
|
+
|
|
148
|
+
return root
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------- FIXED: BUILD FROM TREE ----------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_from_tree(
|
|
155
|
+
tree_file: Path,
|
|
156
|
+
root: Path,
|
|
157
|
+
force: bool,
|
|
158
|
+
dry_run: bool,
|
|
159
|
+
templates_dir: Optional[Path] = None,
|
|
160
|
+
):
|
|
161
|
+
structure = tree_to_dict(tree_file)
|
|
162
|
+
|
|
163
|
+
# unwrap single-root tree (platform/ etc.)
|
|
164
|
+
if len(structure) == 1:
|
|
165
|
+
structure = list(structure.values())[0]
|
|
166
|
+
|
|
167
|
+
return build_from_dict(
|
|
168
|
+
root=root,
|
|
169
|
+
structure=structure,
|
|
170
|
+
force=force,
|
|
171
|
+
dry_run=dry_run,
|
|
172
|
+
templates_dir=templates_dir,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------- BUILD FROM DICT (UNCHANGED CORE) ----------
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def build_from_dict(
|
|
180
|
+
root: Path,
|
|
181
|
+
structure: Dict[str, Any],
|
|
182
|
+
force: bool,
|
|
183
|
+
dry_run: bool,
|
|
184
|
+
templates_dir: Optional[Path] = None,
|
|
185
|
+
):
|
|
186
|
+
created = []
|
|
187
|
+
|
|
188
|
+
def recurse(base: Path, node: Dict[str, Any]):
|
|
189
|
+
for name, val in node.items():
|
|
190
|
+
path = base / name
|
|
191
|
+
|
|
192
|
+
if isinstance(val, dict):
|
|
193
|
+
if dry_run:
|
|
194
|
+
created.append(str(path) + "/ (would create)")
|
|
195
|
+
else:
|
|
196
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
created.append(str(path) + "/")
|
|
198
|
+
|
|
199
|
+
recurse(path, val)
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
ensure_parent(path)
|
|
203
|
+
|
|
204
|
+
if path.exists() and not force:
|
|
205
|
+
created.append(f"{path} (exists, kept)")
|
|
206
|
+
else:
|
|
207
|
+
if dry_run:
|
|
208
|
+
created.append(f"{path} (would create file)")
|
|
209
|
+
else:
|
|
210
|
+
path.write_text(val or "", encoding="utf-8")
|
|
211
|
+
created.append(str(path))
|
|
212
|
+
|
|
213
|
+
recurse(root, structure)
|
|
214
|
+
return created
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------- EXPORT TREE ----------
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def export_tree(root: Path, output: Path):
|
|
221
|
+
lines = [root.name + "/"]
|
|
222
|
+
|
|
223
|
+
def walk(p: Path, prefix: str = ""):
|
|
224
|
+
entries = sorted(p.iterdir(), key=lambda x: x.name)
|
|
225
|
+
for i, entry in enumerate(entries):
|
|
226
|
+
connector = "โโโ " if i == len(entries) - 1 else "โโโ "
|
|
227
|
+
lines.append(prefix + connector + entry.name)
|
|
228
|
+
|
|
229
|
+
if entry.is_dir():
|
|
230
|
+
ext = " " if i == len(entries) - 1 else "โ "
|
|
231
|
+
walk(entry, prefix + ext)
|
|
232
|
+
|
|
233
|
+
walk(root)
|
|
234
|
+
output.write_text("\n".join(lines), encoding="utf-8")
|
|
235
|
+
return str(output)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------- EXPORT CONFIG ----------
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def export_dict(root: Path) -> Dict[str, Any]:
|
|
242
|
+
def recurse(p: Path):
|
|
243
|
+
data = {}
|
|
244
|
+
for child in sorted(p.iterdir(), key=lambda x: x.name):
|
|
245
|
+
if child.is_dir():
|
|
246
|
+
data[child.name] = recurse(child)
|
|
247
|
+
else:
|
|
248
|
+
data[child.name] = child.read_text(encoding="utf-8")
|
|
249
|
+
return data
|
|
250
|
+
|
|
251
|
+
return {root.name: recurse(root)}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ---------- CLI ----------
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main(argv=None):
|
|
258
|
+
parser = argparse.ArgumentParser("treefs")
|
|
259
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
260
|
+
|
|
261
|
+
b = sub.add_parser("build")
|
|
262
|
+
b.add_argument("input")
|
|
263
|
+
b.add_argument("root")
|
|
264
|
+
b.add_argument("--force", action="store_true")
|
|
265
|
+
b.add_argument("--dry-run", action="store_true")
|
|
266
|
+
|
|
267
|
+
t = sub.add_parser("export-tree")
|
|
268
|
+
t.add_argument("root")
|
|
269
|
+
t.add_argument("output")
|
|
270
|
+
|
|
271
|
+
c = sub.add_parser("export-config")
|
|
272
|
+
c.add_argument("root")
|
|
273
|
+
c.add_argument("output")
|
|
274
|
+
|
|
275
|
+
args = parser.parse_args(argv)
|
|
276
|
+
|
|
277
|
+
if args.cmd == "build":
|
|
278
|
+
inp = Path(args.input)
|
|
279
|
+
root = Path(args.root)
|
|
280
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
|
|
282
|
+
created = build_from_tree(
|
|
283
|
+
inp,
|
|
284
|
+
root,
|
|
285
|
+
force=args.force,
|
|
286
|
+
dry_run=args.dry_run,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
safe_print("\nCreated / Verified:", "yellow")
|
|
290
|
+
for cstr in created:
|
|
291
|
+
safe_print(" - " + cstr)
|
|
292
|
+
|
|
293
|
+
elif args.cmd == "export-tree":
|
|
294
|
+
out = Path(args.output)
|
|
295
|
+
safe_print("Exporting tree...", "blue")
|
|
296
|
+
export_tree(Path(args.root), out)
|
|
297
|
+
safe_print("Saved", "yellow")
|
|
298
|
+
|
|
299
|
+
elif args.cmd == "export-config":
|
|
300
|
+
root = Path(args.root)
|
|
301
|
+
out = Path(args.output)
|
|
302
|
+
struct = export_dict(root)
|
|
303
|
+
|
|
304
|
+
fmt = out.suffix.lower().lstrip(".")
|
|
305
|
+
if fmt == "json":
|
|
306
|
+
out.write_text(json.dumps(struct, indent=2))
|
|
307
|
+
elif fmt == "yaml" and yaml:
|
|
308
|
+
out.write_text(yaml.safe_dump(struct))
|
|
309
|
+
elif fmt == "toml" and toml:
|
|
310
|
+
out.write_text(toml.dumps(struct))
|
|
311
|
+
else:
|
|
312
|
+
raise RuntimeError("Unsupported format")
|
|
313
|
+
|
|
314
|
+
safe_print("Saved", "yellow")
|
|
315
|
+
|
|
316
|
+
else:
|
|
317
|
+
parser.print_help()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
main()
|