dktcli 1.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.
Files changed (42) hide show
  1. devkittools/__init__.py +3 -0
  2. devkittools/__main__.py +6 -0
  3. devkittools/cli.py +179 -0
  4. devkittools/tools/__init__.py +1 -0
  5. devkittools/tools/bench.py +73 -0
  6. devkittools/tools/changeloggen.py +181 -0
  7. devkittools/tools/clean.py +110 -0
  8. devkittools/tools/colortool.py +126 -0
  9. devkittools/tools/commitwriter.py +199 -0
  10. devkittools/tools/contextpacker.py +226 -0
  11. devkittools/tools/cron.py +152 -0
  12. devkittools/tools/doctor.py +74 -0
  13. devkittools/tools/du.py +72 -0
  14. devkittools/tools/enc.py +131 -0
  15. devkittools/tools/envtool.py +141 -0
  16. devkittools/tools/findtool.py +119 -0
  17. devkittools/tools/gen.py +182 -0
  18. devkittools/tools/gx.py +184 -0
  19. devkittools/tools/hashtool.py +93 -0
  20. devkittools/tools/httptool.py +131 -0
  21. devkittools/tools/ignore.py +213 -0
  22. devkittools/tools/initproj.py +166 -0
  23. devkittools/tools/jsontool.py +200 -0
  24. devkittools/tools/net.py +114 -0
  25. devkittools/tools/portkiller.py +246 -0
  26. devkittools/tools/regextool.py +84 -0
  27. devkittools/tools/repostats.py +197 -0
  28. devkittools/tools/secrets_scan.py +108 -0
  29. devkittools/tools/serve.py +118 -0
  30. devkittools/tools/snip.py +94 -0
  31. devkittools/tools/system.py +157 -0
  32. devkittools/tools/timetool.py +130 -0
  33. devkittools/tools/todo.py +69 -0
  34. devkittools/tools/tree.py +86 -0
  35. devkittools/tools/watch.py +82 -0
  36. devkittools/utils.py +561 -0
  37. dktcli-1.0.0.dist-info/METADATA +148 -0
  38. dktcli-1.0.0.dist-info/RECORD +42 -0
  39. dktcli-1.0.0.dist-info/WHEEL +5 -0
  40. dktcli-1.0.0.dist-info/entry_points.txt +3 -0
  41. dktcli-1.0.0.dist-info/licenses/LICENSE +21 -0
  42. dktcli-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,130 @@
1
+ """dkt time (tm) - convert timestamps, show time in many formats, compute differences."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import re
6
+ import time
7
+ from datetime import datetime, timedelta, timezone
8
+
9
+ from ..utils import DktError, c, kv, output_result
10
+
11
+ META = ("Generate & Convert", "time", "tm", "epoch <-> date converter, 'time ago', date math")
12
+
13
+ EPILOG = """\
14
+ examples:
15
+ dkt tm now in every common format
16
+ dkt tm 1700000000 epoch -> date (seconds, ms or us auto-detected)
17
+ dkt tm 2024-03-01T12:00:00Z date -> epoch
18
+ dkt tm "2024-03-01 09:30" --tz +05:30
19
+ dkt tm now --add 3d4h date math (units: s m h d w)
20
+ dkt tm 2024-01-01 --diff 2024-03-15 difference between two moments
21
+ """
22
+
23
+
24
+ def parse_offset(s: str) -> timezone:
25
+ if s.upper() in ("Z", "UTC"):
26
+ return timezone.utc
27
+ m = re.fullmatch(r"([+-])(\d{1,2})(?::?(\d{2}))?", s)
28
+ if not m:
29
+ raise DktError("Bad timezone '%s'. Use UTC or an offset like +05:30 / -08." % s)
30
+ delta = timedelta(hours=int(m.group(2)), minutes=int(m.group(3) or 0))
31
+ return timezone(delta if m.group(1) == "+" else -delta)
32
+
33
+
34
+ def parse_moment(s: str, assume_tz: timezone) -> datetime:
35
+ s = s.strip()
36
+ if s.lower() in ("", "now"):
37
+ return datetime.now(timezone.utc)
38
+ if re.fullmatch(r"-?\d+(\.\d+)?", s):
39
+ v = float(s)
40
+ a = abs(v)
41
+ if a >= 1e17:
42
+ v /= 1e9
43
+ elif a >= 1e14:
44
+ v /= 1e6
45
+ elif a >= 1e11:
46
+ v /= 1e3
47
+ return datetime.fromtimestamp(v, tz=timezone.utc)
48
+ t = s.replace("Z", "+00:00").replace("z", "+00:00")
49
+ for candidate in (t, t.replace(" ", "T", 1)):
50
+ try:
51
+ dt = datetime.fromisoformat(candidate)
52
+ return dt if dt.tzinfo else dt.replace(tzinfo=assume_tz)
53
+ except ValueError:
54
+ pass
55
+ for fmt in ("%Y/%m/%d %H:%M:%S", "%Y/%m/%d", "%d/%m/%Y", "%m/%d/%Y %H:%M", "%b %d %Y", "%d %b %Y", "%B %d, %Y"):
56
+ try:
57
+ return datetime.strptime(s, fmt).replace(tzinfo=assume_tz)
58
+ except ValueError:
59
+ pass
60
+ raise DktError("Couldn't understand '%s'. Try epoch seconds or ISO like 2024-03-01T12:00:00Z." % s)
61
+
62
+
63
+ def parse_delta(s: str) -> timedelta:
64
+ total = 0
65
+ found = False
66
+ for num, unit in re.findall(r"(-?\d+)\s*([smhdw])", s.lower()):
67
+ found = True
68
+ total += int(num) * {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}[unit]
69
+ if not found:
70
+ raise DktError("Bad duration '%s'. Example: 3d4h30m" % s)
71
+ return timedelta(seconds=total)
72
+
73
+
74
+ def human_delta(td: timedelta) -> str:
75
+ secs = int(abs(td.total_seconds()))
76
+ parts = []
77
+ for name, n in (("y", 31536000), ("d", 86400), ("h", 3600), ("m", 60), ("s", 1)):
78
+ if secs >= n:
79
+ parts.append("%d%s" % (secs // n, name))
80
+ secs %= n
81
+ return " ".join(parts[:3]) or "0s"
82
+
83
+
84
+ def relative(dt: datetime) -> str:
85
+ d = dt - datetime.now(timezone.utc)
86
+ return ("in " + human_delta(d)) if d.total_seconds() > 0 else (human_delta(d) + " ago")
87
+
88
+
89
+ def run(args) -> int:
90
+ tz = parse_offset(args.tz) if args.tz else datetime.now().astimezone().tzinfo
91
+ dt = parse_moment(args.moment or "now", tz)
92
+ if args.add:
93
+ dt += parse_delta(args.add)
94
+ if args.diff:
95
+ other = parse_moment(args.diff, tz)
96
+ d = other - dt
97
+ kv([("from", dt.isoformat()), ("to", other.isoformat()),
98
+ ("difference", c(human_delta(d), "bold") + (" later" if d.total_seconds() >= 0 else " earlier")),
99
+ ("seconds", "%d" % d.total_seconds()), ("days", "%.2f" % (d.total_seconds() / 86400))])
100
+ return 0
101
+ local = dt.astimezone(tz) if args.tz else dt.astimezone()
102
+ epoch = dt.timestamp()
103
+ rows = [("UTC", dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"))]
104
+ if not (args.tz and args.tz.upper() in ("UTC", "Z")):
105
+ rows.append((args.tz.upper() if args.tz else "local", local.strftime("%Y-%m-%d %H:%M:%S %z")))
106
+ rows += [
107
+ ("ISO 8601", dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")),
108
+ ("epoch (s)", "%d" % epoch),
109
+ ("epoch (ms)", "%d" % (epoch * 1000)),
110
+ ("RFC 2822", local.strftime("%a, %d %b %Y %H:%M:%S %z")),
111
+ ("day", "%s, week %s, day %s of year" % (local.strftime("%A"), local.strftime("%V"), local.strftime("%j"))),
112
+ ("relative", relative(dt)),
113
+ ]
114
+ if args.raw:
115
+ print("%d" % epoch)
116
+ return 0
117
+ kv([(k, c(v, "bold") if k in ("epoch (s)", "ISO 8601") else v) for k, v in rows])
118
+ return 0
119
+
120
+
121
+ def register(sub, common) -> None:
122
+ p = sub.add_parser("time", aliases=["tm"], parents=[common], help=META[3],
123
+ description="Convert and inspect timestamps.", epilog=EPILOG,
124
+ formatter_class=argparse.RawDescriptionHelpFormatter)
125
+ p.add_argument("moment", nargs="?", help="epoch, ISO date, or 'now' (default)")
126
+ p.add_argument("--tz", metavar="OFFSET", help="timezone offset for input/output, e.g. +05:30 or UTC")
127
+ p.add_argument("--add", metavar="DUR", help="add a duration, e.g. 3d4h or -90m")
128
+ p.add_argument("--diff", metavar="OTHER", help="show the difference to another moment")
129
+ p.add_argument("--raw", action="store_true", help="print only epoch seconds")
130
+ p.set_defaults(func=run)
@@ -0,0 +1,69 @@
1
+ """dkt todo (td) - collect TODO / FIXME / HACK comments across a project."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import re
7
+ from collections import Counter
8
+
9
+ from ..utils import c, is_binary_chunk, list_project_files, resolve_folder, sym, warn, human_num
10
+
11
+ META = ("Project & Code", "todo", "td", "list TODO/FIXME/HACK comments with file:line")
12
+
13
+ EPILOG = """\
14
+ examples:
15
+ dkt td scan the current project
16
+ dkt td --tags FIXME BUG only these tags
17
+ dkt td --json machine-readable
18
+ """
19
+
20
+ DEFAULT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "OPTIMIZE", "NOTE"]
21
+ TAG_COLOR = {"TODO": "bcyan", "FIXME": "bred", "BUG": "bred", "HACK": "byellow", "XXX": "byellow",
22
+ "OPTIMIZE": "bmagenta", "NOTE": "bgreen"}
23
+
24
+
25
+ def run(args) -> int:
26
+ folder = resolve_folder(args.folder)
27
+ tags = [t.upper() for t in (args.tags or DEFAULT_TAGS)]
28
+ rx = re.compile(r"(?:#|//|/\*|<!--|--|;|\*|%)\s*(?:@)?\b(" + "|".join(map(re.escape, tags)) + r")\b[\s:(\-]*(.*)", re.I)
29
+ found = []
30
+ for rel in list_project_files(folder):
31
+ p = folder / rel
32
+ try:
33
+ if p.stat().st_size > 1_000_000:
34
+ continue
35
+ raw = p.read_bytes()
36
+ except OSError:
37
+ continue
38
+ if is_binary_chunk(raw[:4096]):
39
+ continue
40
+ for i, line in enumerate(raw.decode("utf-8", errors="replace").splitlines(), 1):
41
+ m = rx.search(line)
42
+ if m:
43
+ found.append({"file": str(rel).replace("\\", "/"), "line": i,
44
+ "tag": m.group(1).upper(), "text": m.group(2).strip(" */-->")[:120]})
45
+ if args.json:
46
+ print(json.dumps(found, indent=2))
47
+ return 0
48
+ if not found:
49
+ print(" %s nothing to do - no %s found" % (c(sym("ok"), "bgreen"), "/".join(tags[:3])))
50
+ return 0
51
+ cur = None
52
+ for f in found:
53
+ if f["file"] != cur:
54
+ cur = f["file"]
55
+ print("\n " + c(cur, "bold underline"))
56
+ print(" %s %s %s" % (c(str(f["line"]).rjust(5), "gray"), c(f["tag"].ljust(8), TAG_COLOR.get(f["tag"], "white")), f["text"]))
57
+ counts = Counter(f["tag"] for f in found)
58
+ print("\n " + " ".join("%s %s" % (c(human_num(n), "bold"), t) for t, n in counts.most_common()))
59
+ return 0
60
+
61
+
62
+ def register(sub, common) -> None:
63
+ p = sub.add_parser("todo", aliases=["td"], parents=[common], help=META[3],
64
+ description="Find TODO-style comments in your project (respects .gitignore).",
65
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
66
+ p.add_argument("-f", "--folder", metavar="PATH")
67
+ p.add_argument("--tags", nargs="+", metavar="TAG")
68
+ p.add_argument("--json", action="store_true")
69
+ p.set_defaults(func=run)
@@ -0,0 +1,86 @@
1
+ """dkt tree (tr) - a pretty directory tree that respects .gitignore."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from collections import defaultdict
6
+ from pathlib import Path
7
+
8
+ from ..utils import c, human_size, list_project_files, resolve_folder, sym
9
+
10
+ META = ("Project & Code", "tree", "tr", "pretty directory tree with sizes, respects .gitignore")
11
+
12
+ EPILOG = """\
13
+ examples:
14
+ dkt tr tree of the current project
15
+ dkt tr -d 2 only two levels deep
16
+ dkt tr --sizes show file sizes
17
+ dkt tr --dirs directories only
18
+ dkt tr src --copy copy the tree to the clipboard (great for docs / AI prompts)
19
+ """
20
+
21
+ ICONS = {".py": "bgreen", ".js": "byellow", ".ts": "bblue", ".json": "yellow", ".md": "bcyan",
22
+ ".html": "bred", ".css": "bmagenta", ".bat": "gray", ".toml": "yellow", ".yml": "yellow", ".yaml": "yellow"}
23
+
24
+
25
+ def run(args) -> int:
26
+ root = resolve_folder(args.folder)
27
+ files = list_project_files(root)
28
+ tree = defaultdict(lambda: {"dirs": set(), "files": []})
29
+ for f in files:
30
+ parts = f.parts
31
+ for i in range(len(parts) - 1):
32
+ tree[Path(*parts[:i]) if i else Path(".")]["dirs"].add(parts[i])
33
+ tree[f.parent if len(parts) > 1 else Path(".")]["files"].append(f.name)
34
+ plain = []
35
+ out = []
36
+ counts = [0, 0]
37
+ T, L, P = sym("t") + sym("h") * 2 + " ", sym("l") + sym("h") * 2 + " ", sym("pipe") + " "
38
+
39
+ def walk(rel: Path, prefix: str, depth: int):
40
+ node = tree.get(rel, {"dirs": set(), "files": []})
41
+ entries = [(d, True) for d in sorted(node["dirs"], key=str.lower)]
42
+ if not args.dirs:
43
+ entries += [(f, False) for f in sorted(node["files"], key=str.lower)]
44
+ for i, (name, is_dir) in enumerate(entries):
45
+ last = i == len(entries) - 1
46
+ branch = L if last else T
47
+ if is_dir:
48
+ counts[0] += 1
49
+ out.append(prefix + c(branch, "gray") + c(name + "/", "bold bblue"))
50
+ plain.append(prefix + branch + name + "/")
51
+ if args.depth is None or depth < args.depth:
52
+ walk(rel / name if str(rel) != "." else Path(name), prefix + (" " if last else P), depth + 1)
53
+ else:
54
+ counts[1] += 1
55
+ size = ""
56
+ if args.sizes:
57
+ try:
58
+ size = " " + human_size((root / (rel / name)).stat().st_size)
59
+ except OSError:
60
+ size = ""
61
+ col = ICONS.get(Path(name).suffix.lower(), "white")
62
+ out.append(prefix + c(branch, "gray") + c(name, col) + c(size, "gray"))
63
+ plain.append(prefix + branch + name + size)
64
+
65
+ out.append(c(root.name + "/", "bold bmagenta"))
66
+ plain.append(root.name + "/")
67
+ walk(Path("."), "", 1)
68
+ print("\n".join(out))
69
+ print(c("\n %d directories, %d files" % tuple(counts), "gray"))
70
+ if args.copy:
71
+ from ..utils import copy_to_clipboard, info
72
+ if copy_to_clipboard("\n".join(plain)):
73
+ info(c(sym("ok") + " copied to clipboard", "green"))
74
+ return 0
75
+
76
+
77
+ def register(sub, common) -> None:
78
+ p = sub.add_parser("tree", aliases=["tr"], parents=[common], help=META[3],
79
+ description="Show a directory tree (git-ignored files and junk folders are skipped).",
80
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
81
+ p.add_argument("folder", nargs="?")
82
+ p.add_argument("-d", "--depth", type=int, metavar="N")
83
+ p.add_argument("--sizes", action="store_true")
84
+ p.add_argument("--dirs", action="store_true", help="directories only")
85
+ p.add_argument("--copy", action="store_true")
86
+ p.set_defaults(func=run)
@@ -0,0 +1,82 @@
1
+ """dkt watch (w) - re-run a command whenever files change."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import os
6
+ import subprocess
7
+ import time
8
+
9
+ from ..utils import DktError, c, list_project_files, resolve_folder, sym
10
+
11
+ META = ("Project & Code", "watch", "w", "re-run any command when files change")
12
+
13
+ EPILOG = """\
14
+ examples:
15
+ dkt w -c "pytest -q" run tests on every save
16
+ dkt w -c "npm run build" --ext .js .ts
17
+ dkt w -c "python main.py" --clear --debounce 0.5
18
+ dkt w -c "make" -f src --interval 0.3
19
+ """
20
+
21
+
22
+ def snapshot(root, exts):
23
+ snap = {}
24
+ for rel in list_project_files(root):
25
+ if exts and not str(rel).lower().endswith(exts):
26
+ continue
27
+ try:
28
+ st = (root / rel).stat()
29
+ snap[str(rel)] = (st.st_mtime_ns, st.st_size)
30
+ except OSError:
31
+ pass
32
+ return snap
33
+
34
+
35
+ def run(args) -> int:
36
+ root = resolve_folder(args.folder)
37
+ exts = tuple(e if e.startswith(".") else "." + e for e in args.ext) if args.ext else None
38
+ if not args.command:
39
+ raise DktError('Tell me what to run, e.g. dkt w -c "pytest -q"')
40
+ prev = snapshot(root, exts)
41
+ runs = 0
42
+
43
+ def execute(reason: str):
44
+ nonlocal runs
45
+ runs += 1
46
+ if args.clear:
47
+ os.system("cls" if os.name == "nt" else "clear")
48
+ print("%s %s %s" % (c(sym("arrow"), "bcyan"), c(time.strftime("%H:%M:%S"), "gray"), c(args.command, "bold")))
49
+ if reason:
50
+ print(c(" changed: " + reason, "gray"))
51
+ t0 = time.time()
52
+ rc = subprocess.call(args.command, shell=True, cwd=str(root))
53
+ dt = time.time() - t0
54
+ col, mark = ("bgreen", sym("ok")) if rc == 0 else ("bred", sym("fail"))
55
+ print("%s %s\n" % (c(mark + " exit %d" % rc, col), c("(%.2fs) waiting for changes... Ctrl+C to stop" % dt, "gray")))
56
+
57
+ execute("")
58
+ try:
59
+ while True:
60
+ time.sleep(args.interval)
61
+ cur = snapshot(root, exts)
62
+ if cur != prev:
63
+ changed = [k for k in set(cur) | set(prev) if cur.get(k) != prev.get(k)]
64
+ time.sleep(args.debounce)
65
+ prev = snapshot(root, exts)
66
+ execute(", ".join(sorted(changed)[:3]) + (" ..." if len(changed) > 3 else ""))
67
+ except KeyboardInterrupt:
68
+ print("\n stopped after %d run%s." % (runs, "" if runs == 1 else "s"))
69
+ return 0
70
+
71
+
72
+ def register(sub, common) -> None:
73
+ p = sub.add_parser("watch", aliases=["w"], parents=[common], help=META[3],
74
+ description="Watch a folder and re-run a command on changes (no dependencies).",
75
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
76
+ p.add_argument("-c", "--command", metavar="CMD", help="command to run")
77
+ p.add_argument("-f", "--folder", metavar="PATH", help="folder to watch (default: current)")
78
+ p.add_argument("--ext", nargs="+", metavar="EXT", help="only react to these extensions")
79
+ p.add_argument("--interval", type=float, default=0.7, help="poll seconds (default 0.7)")
80
+ p.add_argument("--debounce", type=float, default=0.3, help="settle time after a change")
81
+ p.add_argument("--clear", action="store_true", help="clear the screen before each run")
82
+ p.set_defaults(func=run)