dukatools 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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .dist/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .venv/
dukatools-0.1.0/DEV.md ADDED
@@ -0,0 +1,14 @@
1
+ # Developer Guide
2
+
3
+ ## Add a new CLI
4
+ 1) Create file: src/dukatools/<tool>.py with `def main(): ...`
5
+ 2) Register entry point in pyproject.toml under [project.scripts]: `<tool> = "dukatools.<tool>:main"`
6
+ 3) Bump version in pyproject.toml (e.g., 0.1.0 -> 0.2.0)
7
+ 4) Build & test locally
8
+ 5) Tag & push to GitHub (CI will publish to PyPI)
9
+
10
+ ## Local build & test
11
+ python -m pip install -U build twine
12
+ python -m build
13
+ pipx install dist/dukatools-<ver>-py3-none-any.whl
14
+ treex --help
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Your Name
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,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: dukatools
3
+ Version: 0.1.0
4
+ Summary: Small cross-platform CLI toolbelt: treex, dirproc, and more.
5
+ Project-URL: Homepage, https://github.com/<your-user>/dukatools
6
+ Project-URL: Issues, https://github.com/<your-user>/dukatools/issues
7
+ Author: rexologue
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: cli,filesystem,tree,utilities
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+
17
+ # dukatools
18
+
19
+ Cross-platform CLI toolbelt.
20
+
21
+ ## Install
22
+ uv tool install dukatools
23
+
24
+ ## Upgrade
25
+ uv tool upgrade dukatools
26
+
27
+ ## Commands
28
+ - treex — enhanced directory tree
29
+ - dirproc — directory batch processor
30
+
31
+ ## Usage
32
+ treex --help
33
+ dirproc --help
@@ -0,0 +1,17 @@
1
+ # dukatools
2
+
3
+ Cross-platform CLI toolbelt.
4
+
5
+ ## Install
6
+ uv tool install dukatools
7
+
8
+ ## Upgrade
9
+ uv tool upgrade dukatools
10
+
11
+ ## Commands
12
+ - treex — enhanced directory tree
13
+ - dirproc — directory batch processor
14
+
15
+ ## Usage
16
+ treex --help
17
+ dirproc --help
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dukatools"
7
+ version = "0.1.0"
8
+ description = "Small cross-platform CLI toolbelt: treex, dirproc, and more."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "rexologue"}]
13
+ keywords = ["cli", "utilities", "filesystem", "tree"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ # Здесь «магия» команд:
21
+ [project.scripts]
22
+ treex = "dukatools.treex:main"
23
+ dirproc = "dukatools.dirproc:main"
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/<your-user>/dukatools"
27
+ Issues = "https://github.com/<your-user>/dukatools/issues"
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+ import os
3
+ import re
4
+ import fnmatch # не используется, но оставлю на случай расширений
5
+ import chardet
6
+ from pathlib import Path
7
+ from argparse import ArgumentParser, Namespace
8
+ from typing import Iterable, List, Set, Optional
9
+
10
+
11
+ def detect_encoding(file_path: str) -> str:
12
+ with open(file_path, "rb") as file:
13
+ raw_data = file.read()
14
+ result = chardet.detect(raw_data)
15
+ return result.get("encoding") or "latin1"
16
+
17
+
18
+ def compile_patterns(patterns: Iterable[str]) -> List[re.Pattern]:
19
+ compiled = []
20
+ for pat in patterns:
21
+ try:
22
+ compiled.append(re.compile(pat))
23
+ except re.error as e:
24
+ raise SystemExit(f"Invalid regex in --exclude-pattern: {pat!r} -> {e}")
25
+ return compiled
26
+
27
+
28
+ def should_exclude(path: Path, rel_posix: str, exclude_names: Set[str], exclude_regex: List[re.Pattern]) -> bool:
29
+ # Точное совпадение имени (базовое имя, без пути)
30
+ if path.name in exclude_names:
31
+ return True
32
+
33
+ # Соответствие любому из regex по относительному POSIX-пути
34
+ for rx in exclude_regex:
35
+ if rx.search(rel_posix):
36
+ return True
37
+
38
+ return False
39
+
40
+
41
+ def process_directory(
42
+ root_dir: Path,
43
+ current_rel: Path,
44
+ out_fh, # текстовый файл или None (stdout)
45
+ recursive: bool,
46
+ exclude_names: Set[str],
47
+ exclude_regex: List[re.Pattern],
48
+ ) -> None:
49
+ base = root_dir / current_rel
50
+ try:
51
+ entries = list(os.scandir(base))
52
+ except FileNotFoundError:
53
+ msg = f"Path not found: {base}"
54
+ if out_fh:
55
+ print(msg, file=out_fh)
56
+ else:
57
+ print(msg)
58
+ return
59
+
60
+ for entry in entries:
61
+ p = Path(entry.path)
62
+ rel = current_rel / entry.name
63
+ rel_posix = rel.as_posix()
64
+
65
+ if should_exclude(p, rel_posix, exclude_names, exclude_regex):
66
+ # Если это директория — не заходим внутрь
67
+ continue
68
+
69
+ if entry.is_dir(follow_symlinks=False):
70
+ if recursive:
71
+ process_directory(root_dir, rel, out_fh, recursive, exclude_names, exclude_regex)
72
+ elif entry.is_file(follow_symlinks=False):
73
+ result = f"File: {rel_posix}\n\n"
74
+ try:
75
+ encoding = detect_encoding(str(p))
76
+ with open(p, "r", encoding=encoding, errors="replace") as f:
77
+ content = f.read()
78
+ result += f"Content:\n{content}\n\n"
79
+ except Exception as e:
80
+ result += f"Error reading file {rel_posix}: {e}\n\n"
81
+
82
+ if out_fh:
83
+ out_fh.write(result)
84
+ else:
85
+ print(result)
86
+
87
+
88
+ def main() -> None:
89
+ parser = ArgumentParser(description="Dump files of a directory; optionally save to a file. No auto-excludes.")
90
+ parser.add_argument("root_dir", type=str, help="Root directory to process.")
91
+ parser.add_argument("--output-file", type=str, default=None, help="Where to save output (UTF-8).")
92
+ parser.add_argument("-nR", "--non-recursive", action="store_true", help="Disable recursion.")
93
+ parser.add_argument(
94
+ "--exclude-name",
95
+ action="append",
96
+ default=[],
97
+ help="Exclude by exact base name (file or directory). Can be repeated.",
98
+ )
99
+ parser.add_argument(
100
+ "--exclude-pattern",
101
+ action="append",
102
+ default=[],
103
+ help="Exclude by regex applied to relative POSIX path. Can be repeated.",
104
+ )
105
+
106
+ args: Namespace = parser.parse_args()
107
+
108
+ root_dir = Path(args.root_dir).resolve()
109
+
110
+ # Собрать список паттернов (append -> может быть списком списков, нормализуем)
111
+ # Здесь action='append', так что это уже плоский список или None -> []
112
+ exclude_names: Set[str] = set(args.exclude_name or [])
113
+ exclude_regex: List[re.Pattern] = compile_patterns(args.exclude_pattern or [])
114
+
115
+ out_fh = None
116
+ try:
117
+ if args.output_file:
118
+ out_path = Path(args.output_file)
119
+ out_path.parent.mkdir(parents=True, exist_ok=True)
120
+ out_fh = open(out_path, "w", encoding="utf-8")
121
+
122
+ process_directory(
123
+ root_dir=root_dir,
124
+ current_rel=Path("."),
125
+ out_fh=out_fh,
126
+ recursive=not args.non_recursive,
127
+ exclude_names=exclude_names,
128
+ exclude_regex=exclude_regex,
129
+ )
130
+ finally:
131
+ if out_fh:
132
+ out_fh.close()
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
@@ -0,0 +1,50 @@
1
+ import os
2
+ import fnmatch
3
+ from argparse import ArgumentParser
4
+
5
+
6
+ def print_tree(directory: str, prefix: str = "", exclude_names: list[str] | None = None, exclude_patterns: list[str] | None = None) -> None:
7
+ exclude_names = exclude_names or []
8
+ exclude_patterns = exclude_patterns or []
9
+
10
+ try:
11
+ entries = os.listdir(directory)
12
+ except PermissionError:
13
+ print(f"{prefix}└── [Permission denied]")
14
+ return
15
+
16
+ # Filter entries by exact names and glob patterns
17
+ filtered: list[str] = []
18
+ for e in entries:
19
+ if e in exclude_names:
20
+ continue
21
+ if any(fnmatch.fnmatch(e, pat) for pat in exclude_patterns):
22
+ continue
23
+ filtered.append(e)
24
+
25
+ filtered.sort()
26
+
27
+ for index, entry in enumerate(filtered):
28
+ full_path = os.path.join(directory, entry)
29
+ connector = "└── " if index == len(filtered) - 1 else "├── "
30
+ print(f"{prefix}{connector}{entry}")
31
+
32
+ if os.path.isdir(full_path):
33
+ new_prefix = " " if index == len(filtered) - 1 else "│ "
34
+ print_tree(full_path, prefix + new_prefix, exclude_names, exclude_patterns)
35
+
36
+
37
+ def main() -> None:
38
+ parser = ArgumentParser(description="Draw a directory in a tree format with exclusion patterns.")
39
+ parser.add_argument("--path", type=str, default=".", help="Path to directory to tree.")
40
+ parser.add_argument("--exclude", type=str, nargs="*", default=[], help="List of directory/file names to exclude (exact match).")
41
+ parser.add_argument("--exclude-pattern", type=str, nargs="*", default=[], help="List of glob patterns to exclude (e.g. __pycache__, *.egg-info)")
42
+ args = parser.parse_args()
43
+
44
+ print(f"Directory tree for: {args.path}")
45
+ if args.exclude:
46
+ print(f"Excluded names: {', '.join(args.exclude)}")
47
+ if args.exclude_pattern:
48
+ print(f"Excluded patterns: {', '.join(args.exclude_pattern)}")
49
+
50
+ print_tree(args.path, exclude_names=args.exclude, exclude_patterns=args.exclude_pattern)