ftxt 0.1.0__tar.gz

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.
ftxt-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: ftxt
3
+ Version: 0.1.0
4
+ Summary: Strict codebase-to-text bundler with in-place updates and ACID guarantees.
5
+ Author: germanter
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Environment :: Console
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Build Tools
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # ftxt
15
+
16
+ Strict, minimal CLI codebase-to-text packaging tool.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install ftxt
ftxt-0.1.0/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # ftxt
2
+
3
+ Strict, minimal CLI codebase-to-text packaging tool.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install ftxt
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ftxt"
7
+ version = "0.1.0"
8
+ description = "Strict codebase-to-text bundler with in-place updates and ACID guarantees."
9
+ readme = "README.md"
10
+ authors = [{ name = "germanter" }]
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.10"
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Environment :: Console",
16
+ "Operating System :: OS Independent",
17
+ "Topic :: Software Development :: Build Tools",
18
+ ]
19
+
20
+ [project.scripts]
21
+ ftxt = "ftxt.cli:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
ftxt-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,370 @@
1
+ import os
2
+ import shlex
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ ROOT_DIR = Path.cwd().resolve()
7
+ active_folder: Path = ROOT_DIR
8
+ active_file: Path | None = None
9
+
10
+
11
+ def parse_cli_input(raw: str) -> list[str]:
12
+ raw_tokens = shlex.split(raw, posix=False)
13
+ cleaned = []
14
+ for t in raw_tokens:
15
+ if len(t) >= 2 and ((t.startswith('"') and t.endswith('"')) or (t.startswith("'") and t.endswith("'"))):
16
+ cleaned.append(t[1:-1])
17
+ else:
18
+ cleaned.append(t)
19
+ return cleaned
20
+
21
+
22
+ def resolve_folder_target(name: str) -> Path:
23
+ normalized = name.replace("\\", "/")
24
+ p = Path(normalized)
25
+ if "/" in normalized:
26
+ return (ROOT_DIR / p).resolve()
27
+ return (active_folder / p).resolve()
28
+
29
+
30
+ def resolve_target_file(name: str, must_be_txt: bool = True) -> Path:
31
+ normalized = name.replace("\\", "/")
32
+ p = Path(normalized)
33
+
34
+ if must_be_txt:
35
+ if p.suffix and p.suffix != ".txt":
36
+ raise ValueError(f"target is not a .txt file: {name}")
37
+ if not p.suffix:
38
+ normalized += ".txt"
39
+ p = Path(normalized)
40
+
41
+ if "/" in normalized:
42
+ return (ROOT_DIR / p).resolve()
43
+ return (active_folder / p).resolve()
44
+
45
+
46
+ def get_next_available_dir(base_dir: Path) -> Path:
47
+ if not base_dir.exists():
48
+ return base_dir
49
+ idx = 1
50
+ while True:
51
+ candidate = base_dir.parent / f"{base_dir.name}_{idx}"
52
+ if not candidate.exists():
53
+ return candidate
54
+ idx += 1
55
+
56
+
57
+ def get_next_available_file(base_file: Path) -> Path:
58
+ if not base_file.exists():
59
+ return base_file
60
+ stem = base_file.stem
61
+ idx = 1
62
+ while True:
63
+ candidate = base_file.parent / f"{stem}_{idx}.txt"
64
+ if not candidate.exists():
65
+ return candidate
66
+ idx += 1
67
+
68
+
69
+ def is_path_header(line: str) -> bool:
70
+ clean = line.strip().replace("\\", "/")
71
+ if not clean or any(c in clean for c in ' \t":{}()<>;$!'):
72
+ return False
73
+
74
+ try:
75
+ target_path = (ROOT_DIR / clean).resolve()
76
+ if target_path.is_file() and ROOT_DIR in target_path.parents:
77
+ return True
78
+ except Exception:
79
+ pass
80
+
81
+ parts = clean.split("/")
82
+ if len(parts) > 1 and "." in parts[-1] and not parts[-1].startswith("."):
83
+ return True
84
+
85
+ return False
86
+
87
+
88
+ def parse_out_file(target: Path) -> list[tuple[str, str]]:
89
+ if not target.exists():
90
+ raise FileNotFoundError(f"target file not found: {target.name}")
91
+
92
+ raw_text = target.read_text(encoding="utf-8")
93
+ if not raw_text.strip():
94
+ return []
95
+
96
+ lines = raw_text.splitlines()
97
+ entries: list[tuple[str, list[str]]] = []
98
+ current_path: str | None = None
99
+ current_lines: list[str] = []
100
+
101
+ for line in lines:
102
+ trimmed = line.strip()
103
+ if is_path_header(trimmed):
104
+ if current_path is not None:
105
+ entries.append((current_path, current_lines))
106
+ current_path = trimmed.replace("\\", "/")
107
+ current_lines = []
108
+ else:
109
+ if current_path is not None:
110
+ current_lines.append(line)
111
+
112
+ if current_path is not None:
113
+ entries.append((current_path, current_lines))
114
+
115
+ result: list[tuple[str, str]] = []
116
+ for path_str, body_lines in entries:
117
+ body = "\n".join(body_lines).rstrip("\r\n")
118
+ result.append((path_str, body))
119
+
120
+ return result
121
+
122
+
123
+ def resolve_source_files(source_str: str) -> list[Path]:
124
+ normalized = source_str.replace("\\", "/")
125
+ p = Path(normalized)
126
+ resolved = p if p.is_absolute() else (ROOT_DIR / p).resolve()
127
+
128
+ if not resolved.exists():
129
+ raise FileNotFoundError(f"path does not exist: {source_str}")
130
+
131
+ if resolved.is_file():
132
+ return [resolved]
133
+
134
+ if resolved.is_dir():
135
+ files = sorted([f for f in resolved.rglob("*") if f.is_file()])
136
+ if not files:
137
+ raise FileNotFoundError(f"directory contains no files: {source_str}")
138
+ return files
139
+
140
+ raise ValueError(f"unsupported path type: {source_str}")
141
+
142
+
143
+ def commit_write(target: Path, source_args: list[str]) -> None:
144
+ global active_file
145
+
146
+ if not target.parent.exists():
147
+ raise FileNotFoundError(f"destination directory does not exist: {target.parent}")
148
+
149
+ target_files: list[Path] = []
150
+ for src in source_args:
151
+ target_files.extend(resolve_source_files(src))
152
+
153
+ incoming_payloads: list[tuple[str, str]] = []
154
+ for f in target_files:
155
+ try:
156
+ content = f.read_text(encoding="utf-8")
157
+ except Exception as exc:
158
+ raise RuntimeError(f"failed to read '{f}': {exc}") from exc
159
+
160
+ try:
161
+ rel_path = f.relative_to(ROOT_DIR).as_posix()
162
+ except ValueError:
163
+ rel_path = f.as_posix()
164
+
165
+ incoming_payloads.append((rel_path, content.rstrip("\r\n")))
166
+
167
+ existing_entries: list[tuple[str, str]] = []
168
+ if target.exists() and target.stat().st_size > 0:
169
+ existing_entries = parse_out_file(target)
170
+
171
+ entry_index_map = {path: idx for idx, (path, _) in enumerate(existing_entries)}
172
+
173
+ for rel_path, content in incoming_payloads:
174
+ if rel_path in entry_index_map:
175
+ idx = entry_index_map[rel_path]
176
+ existing_entries[idx] = (rel_path, content)
177
+ else:
178
+ entry_index_map[rel_path] = len(existing_entries)
179
+ existing_entries.append((rel_path, content))
180
+
181
+ with open(target, "w", encoding="utf-8") as out:
182
+ for rel_path, content in existing_entries:
183
+ out.write(f"{rel_path}\n{content}\n\n")
184
+
185
+ active_file = target
186
+ print("roger")
187
+
188
+
189
+ def cmd_createfolder(args: list[str]) -> None:
190
+ if len(args) != 1:
191
+ raise ValueError("createfolder requires exactly 1 argument")
192
+
193
+ target = resolve_folder_target(args[0])
194
+ final_dir = get_next_available_dir(target)
195
+ final_dir.mkdir(parents=True, exist_ok=False)
196
+ print("roger")
197
+
198
+
199
+ def cmd_selectfolder(args: list[str]) -> None:
200
+ global active_folder
201
+
202
+ if len(args) != 1:
203
+ raise ValueError("selectfolder requires exactly 1 argument")
204
+
205
+ target = resolve_folder_target(args[0])
206
+ if not target.is_dir():
207
+ raise FileNotFoundError(f"directory not found: {args[0]}")
208
+
209
+ active_folder = target
210
+ print("roger")
211
+
212
+
213
+ def cmd_openfile(args: list[str]) -> None:
214
+ global active_file
215
+
216
+ if len(args) != 1:
217
+ raise ValueError("openfile requires exactly 1 argument")
218
+
219
+ target = resolve_target_file(args[0], must_be_txt=True)
220
+ if not target.parent.exists():
221
+ raise FileNotFoundError(f"parent directory does not exist: {target.parent}")
222
+
223
+ final_file = get_next_available_file(target)
224
+ final_file.touch(exist_ok=False)
225
+
226
+ active_file = final_file
227
+ print("roger")
228
+
229
+
230
+ def cmd_delfile(args: list[str]) -> None:
231
+ global active_file
232
+
233
+ if len(args) != 1:
234
+ raise ValueError("delfile requires exactly 1 argument")
235
+
236
+ target = resolve_target_file(args[0], must_be_txt=True)
237
+
238
+ if not target.exists():
239
+ raise FileNotFoundError(f"file not found: {args[0]}")
240
+ if target.is_dir():
241
+ raise ValueError(f"refused: target is a directory: {args[0]}")
242
+
243
+ target.unlink()
244
+ if active_file == target:
245
+ active_file = None
246
+
247
+ print("roger")
248
+
249
+
250
+ def cmd_inspect(args: list[str]) -> None:
251
+ global active_file
252
+
253
+ if len(args) > 1:
254
+ raise ValueError("inspect accepts at most 1 argument")
255
+
256
+ if len(args) == 1:
257
+ target = resolve_target_file(args[0], must_be_txt=True)
258
+ else:
259
+ if active_file is None:
260
+ raise RuntimeError("no active file. use 'inspect <target>'")
261
+ target = active_file
262
+
263
+ entries = parse_out_file(target)
264
+ if not entries:
265
+ raise ValueError(f"file '{target.name}' contains no records")
266
+
267
+ active_file = target
268
+ for idx, (path_str, _) in enumerate(entries, 1):
269
+ print(f"{idx}. {path_str}")
270
+
271
+
272
+ def cmd_writo(args: list[str]) -> None:
273
+ global active_file
274
+
275
+ if not args:
276
+ raise ValueError("writo requires target file")
277
+
278
+ target = resolve_target_file(args[0], must_be_txt=True)
279
+ if not target.parent.exists():
280
+ raise FileNotFoundError(f"parent directory does not exist: {target.parent}")
281
+
282
+ if not target.exists():
283
+ target.touch()
284
+
285
+ active_file = target
286
+
287
+ if len(args) == 1:
288
+ print("roger")
289
+ return
290
+
291
+ commit_write(target, args[1:])
292
+
293
+
294
+ def cmd_write(args: list[str]) -> None:
295
+ global active_file
296
+
297
+ if not args:
298
+ raise ValueError("missing arguments for write")
299
+
300
+ if args[0].lower() == "to":
301
+ cmd_writo(args[1:])
302
+ return
303
+
304
+ if active_file is None:
305
+ raise RuntimeError("no active file. use 'writo <target>' or 'openfile'")
306
+
307
+ commit_write(active_file, args)
308
+
309
+
310
+ def cmd_help() -> None:
311
+ help_text = (
312
+ "commands:\n"
313
+ " createfolder <name> Create directory with auto-indexing (alias: mkdir)\n"
314
+ " selectfolder <name> Select active directory (alias: cd)\n"
315
+ " openfile <name> Create indexed .txt file\n"
316
+ " delfile <target> Delete .txt file (directories forbidden)\n"
317
+ " writo <target> [paths...] Select .txt file or write/update files inplace\n"
318
+ " write to <target> [paths..] Equivalent to writo\n"
319
+ " write <path...> Write/update files into current active .txt\n"
320
+ " inspect [target] List files contained inside target .txt\n"
321
+ " help Show manual\n"
322
+ " exit Terminate session (alias: quit)"
323
+ )
324
+ print(help_text)
325
+
326
+
327
+ def main() -> None:
328
+ while True:
329
+ try:
330
+ raw = input("ftxt> ").strip()
331
+ if not raw:
332
+ continue
333
+
334
+ tokens = parse_cli_input(raw)
335
+ if not tokens:
336
+ continue
337
+
338
+ cmd = tokens[0].lower()
339
+ args = tokens[1:]
340
+
341
+ if cmd in ("exit", "quit"):
342
+ sys.exit(0)
343
+ elif cmd in ("createfolder", "mkdir"):
344
+ cmd_createfolder(args)
345
+ elif cmd in ("selectfolder", "cd"):
346
+ cmd_selectfolder(args)
347
+ elif cmd in ("openfile", "open"):
348
+ cmd_openfile(args)
349
+ elif cmd == "delfile":
350
+ cmd_delfile(args)
351
+ elif cmd == "writo":
352
+ cmd_writo(args)
353
+ elif cmd == "write":
354
+ cmd_write(args)
355
+ elif cmd == "inspect":
356
+ cmd_inspect(args)
357
+ elif cmd == "help":
358
+ cmd_help()
359
+ else:
360
+ raise ValueError(f"unknown command: {cmd}")
361
+
362
+ except (ValueError, FileNotFoundError, RuntimeError) as err:
363
+ print(f"fail: {err}")
364
+ except (KeyboardInterrupt, EOFError):
365
+ print("\nroger")
366
+ sys.exit(0)
367
+
368
+
369
+ if __name__ == "__main__":
370
+ main()
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: ftxt
3
+ Version: 0.1.0
4
+ Summary: Strict codebase-to-text bundler with in-place updates and ACID guarantees.
5
+ Author: germanter
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Environment :: Console
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Build Tools
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # ftxt
15
+
16
+ Strict, minimal CLI codebase-to-text packaging tool.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install ftxt
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/ftxt/__init__.py
4
+ src/ftxt/cli.py
5
+ src/ftxt.egg-info/PKG-INFO
6
+ src/ftxt.egg-info/SOURCES.txt
7
+ src/ftxt.egg-info/dependency_links.txt
8
+ src/ftxt.egg-info/entry_points.txt
9
+ src/ftxt.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ftxt = ftxt.cli:main
@@ -0,0 +1 @@
1
+ ftxt