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,131 @@
1
+ """dkt http (req) - a tiny, pretty HTTP client for poking at APIs."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import ssl
7
+ import time
8
+ import urllib.error
9
+ import urllib.request
10
+
11
+ from ..utils import DktError, c, human_size, info, kv, sym
12
+
13
+ META = ("Servers & Network", "http", "req", "quick HTTP requests with pretty JSON, timing, headers")
14
+
15
+ EPILOG = """\
16
+ examples:
17
+ dkt http https://api.github.com/zen
18
+ dkt http GET api.example.com/users -H "Authorization: Bearer abc" -i
19
+ dkt http POST localhost:3000/login -j '{"user":"me","pw":"x"}'
20
+ dkt http POST httpbin.org/post -d "a=1&b=2"
21
+ dkt http https://example.com/file.zip -o file.zip
22
+ """
23
+
24
+ METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
25
+
26
+
27
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
28
+ def redirect_request(self, *a, **k):
29
+ return None
30
+
31
+
32
+ def run(args) -> int:
33
+ items = list(args.target)
34
+ method = "GET"
35
+ if items and items[0].upper() in METHODS:
36
+ method = items.pop(0).upper()
37
+ if len(items) != 1:
38
+ raise DktError("Usage: dkt http [METHOD] URL (see 'dkt http --help')")
39
+ url = items[0]
40
+ if "://" not in url:
41
+ url = ("http://" if url.startswith(("localhost", "127.", "0.0.0.0", "192.168.")) else "https://") + url
42
+ headers = {"User-Agent": "dkt/1.0", "Accept": "*/*"}
43
+ for h in args.header or []:
44
+ if ":" not in h:
45
+ raise DktError("Header must look like 'Name: value' (got '%s')" % h)
46
+ k, v = h.split(":", 1)
47
+ headers[k.strip()] = v.strip()
48
+ body = None
49
+ if args.json_body is not None:
50
+ try:
51
+ json.loads(args.json_body)
52
+ except ValueError as e:
53
+ raise DktError("--json is not valid JSON: %s" % e)
54
+ body = args.json_body.encode("utf-8")
55
+ headers.setdefault("Content-Type", "application/json")
56
+ if method == "GET":
57
+ method = "POST"
58
+ elif args.data is not None:
59
+ body = args.data.encode("utf-8")
60
+ headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
61
+ if method == "GET":
62
+ method = "POST"
63
+ req = urllib.request.Request(url, data=body, headers=headers, method=method)
64
+ ctx = ssl.create_default_context()
65
+ if args.insecure:
66
+ ctx.check_hostname = False
67
+ ctx.verify_mode = ssl.CERT_NONE
68
+ handlers = [urllib.request.HTTPSHandler(context=ctx)]
69
+ if args.no_redirect:
70
+ handlers.append(_NoRedirect())
71
+ opener = urllib.request.build_opener(*handlers)
72
+ t0 = time.perf_counter()
73
+ try:
74
+ resp = opener.open(req, timeout=args.timeout)
75
+ except urllib.error.HTTPError as e:
76
+ resp = e
77
+ except urllib.error.URLError as e:
78
+ raise DktError("Request failed: %s" % e.reason)
79
+ except (OSError, ValueError) as e:
80
+ raise DktError("Request failed: %s" % e)
81
+ data = resp.read()
82
+ dt = time.perf_counter() - t0
83
+ status = resp.status if hasattr(resp, "status") else resp.code
84
+ col = "bgreen" if status < 300 else "byellow" if status < 400 else "bred"
85
+ print("%s %s %s %s %s" % (c("%d %s" % (status, resp.reason), "bold " + col), c(method, "bold"),
86
+ c("%.0f ms" % (dt * 1000), "gray"), c(human_size(len(data)), "gray"),
87
+ c(resp.geturl() if hasattr(resp, "geturl") else url, "gray")))
88
+ if args.include or args.head:
89
+ print()
90
+ kv([(k, v) for k, v in resp.headers.items()], key_color="bcyan")
91
+ if args.head or method == "HEAD":
92
+ return 0 if status < 400 else 1
93
+ if args.output:
94
+ with open(args.output, "wb") as f:
95
+ f.write(data)
96
+ info(c("%s saved %s to %s" % (sym("ok"), human_size(len(data)), args.output), "green"))
97
+ return 0 if status < 400 else 1
98
+ print()
99
+ text = data.decode(resp.headers.get_content_charset() or "utf-8", errors="replace")
100
+ ctype = resp.headers.get("Content-Type", "")
101
+ if "json" in ctype or text.lstrip().startswith(("{", "[")):
102
+ try:
103
+ from .jsontool import colorize
104
+ print(colorize(json.dumps(json.loads(text), indent=2, ensure_ascii=False)))
105
+ return 0 if status < 400 else 1
106
+ except ValueError:
107
+ pass
108
+ if len(text) > args.max_chars:
109
+ print(text[:args.max_chars])
110
+ print(c("\n... truncated (%d more chars, use -o FILE to save)" % (len(text) - args.max_chars), "gray"))
111
+ else:
112
+ print(text)
113
+ return 0 if status < 400 else 1
114
+
115
+
116
+ def register(sub, common) -> None:
117
+ p = sub.add_parser("http", aliases=["req"], parents=[common], help=META[3],
118
+ description="Make an HTTP request and show a readable response.",
119
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
120
+ p.add_argument("target", nargs="+", metavar="[METHOD] URL")
121
+ p.add_argument("-H", "--header", action="append", metavar="'K: V'")
122
+ p.add_argument("-j", "--json", dest="json_body", metavar="JSON", help="send a JSON body")
123
+ p.add_argument("-d", "--data", metavar="TEXT", help="send a form/text body")
124
+ p.add_argument("-i", "--include", action="store_true", help="show response headers")
125
+ p.add_argument("-I", "--head", action="store_true", help="headers only")
126
+ p.add_argument("-o", "--output", metavar="FILE", help="save the response body to a file")
127
+ p.add_argument("--timeout", type=float, default=20)
128
+ p.add_argument("--no-redirect", action="store_true")
129
+ p.add_argument("-k", "--insecure", action="store_true", help="skip TLS verification")
130
+ p.add_argument("--max-chars", type=int, default=20000)
131
+ p.set_defaults(func=run)
@@ -0,0 +1,213 @@
1
+ """dkt ignore (gi) and dkt license (lic) - generate .gitignore and LICENSE files."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from datetime import date
6
+ from pathlib import Path
7
+
8
+ from ..utils import DktError, c, confirm, get_setting, ok, resolve_folder, warn
9
+
10
+ META = ("Project & Code", "ignore", "gi", ".gitignore generator (python, node, java, unity...) + dkt license")
11
+
12
+ EPILOG = """\
13
+ examples:
14
+ dkt gi python node create/merge a .gitignore for Python + Node
15
+ dkt gi --list available templates
16
+ dkt gi unity vscode --print print instead of writing
17
+ dkt license mit write a LICENSE with your name + year
18
+ dkt license --list
19
+ """
20
+
21
+ IGN = {
22
+ "python": "__pycache__/\n*.py[cod]\n*.egg-info/\n.eggs/\nbuild/\ndist/\n.venv/\nvenv/\nenv/\n.pytest_cache/\n.mypy_cache/\n.ruff_cache/\n.coverage\nhtmlcov/\n.ipynb_checkpoints/\n.tox/",
23
+ "node": "node_modules/\nnpm-debug.log*\nyarn-error.log*\n.pnpm-store/\n.next/\n.nuxt/\n.turbo/\n.parcel-cache/\ndist/\nbuild/\ncoverage/\n.eslintcache\n*.tsbuildinfo",
24
+ "java": "*.class\n*.jar\n*.war\ntarget/\n.gradle/\nbuild/\nout/\nhs_err_pid*",
25
+ "go": "*.exe\n*.test\n*.out\nvendor/\nbin/\n/dist/",
26
+ "rust": "target/\n**/*.rs.bk\nCargo.lock.bak",
27
+ "dotnet": "bin/\nobj/\n*.user\n*.suo\n.vs/\npackages/\n*.nupkg\nTestResults/",
28
+ "c": "*.o\n*.obj\n*.a\n*.lib\n*.so\n*.dll\n*.dylib\n*.exe\n*.out\nbuild/\nCMakeCache.txt\nCMakeFiles/\ncmake-build-*/",
29
+ "unity": "[Ll]ibrary/\n[Tt]emp/\n[Oo]bj/\n[Bb]uild/\n[Bb]uilds/\n[Ll]ogs/\n[Uu]ser[Ss]ettings/\n*.csproj\n*.sln\n*.pidb\n.vs/\n/[Mm]emory[Cc]aptures/\nsysinfo.txt\n*.apk\n*.aab",
30
+ "godot": ".godot/\n.import/\nexport.cfg\nexport_presets.cfg\n*.translation\nbuild/",
31
+ "minecraft": "logs/\ncrash-reports/\nworld*/\n*.log\n*.jar\n!gradle/wrapper/gradle-wrapper.jar\nrun/\n.gradle/\nbuild/",
32
+ "php": "vendor/\n.phpunit.result.cache\nstorage/*.key\n.env",
33
+ "ruby": "*.gem\n.bundle/\nvendor/bundle/\nlog/\ntmp/\ncoverage/",
34
+ "flutter": ".dart_tool/\n.packages\nbuild/\n.flutter-plugins\n.flutter-plugins-dependencies\n*.iml",
35
+ "env": ".env\n.env.*\n!.env.example\n*.pem\n*.key\nsecrets.json",
36
+ "vscode": ".vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json",
37
+ "jetbrains": ".idea/\n*.iml\n*.ipr\n*.iws\nout/",
38
+ "macos": ".DS_Store\n.AppleDouble\n.LSOverride\n._*\n.Spotlight-V100\n.Trashes",
39
+ "windows": "Thumbs.db\nehthumbs.db\nDesktop.ini\n$RECYCLE.BIN/\n*.lnk",
40
+ "linux": "*~\n.directory\n.Trash-*\n.nfs*",
41
+ "logs": "*.log\nlogs/\n*.tmp\n*.bak\n*.swp",
42
+ }
43
+
44
+ LICENSES = {
45
+ "mit": ("MIT", """MIT License
46
+
47
+ Copyright (c) {year} {name}
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining a copy
50
+ of this software and associated documentation files (the "Software"), to deal
51
+ in the Software without restriction, including without limitation the rights
52
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
53
+ copies of the Software, and to permit persons to whom the Software is
54
+ furnished to do so, subject to the following conditions:
55
+
56
+ The above copyright notice and this permission notice shall be included in all
57
+ copies or substantial portions of the Software.
58
+
59
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
60
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
61
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
62
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
63
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
64
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
65
+ SOFTWARE.
66
+ """),
67
+ "isc": ("ISC", """ISC License
68
+
69
+ Copyright (c) {year} {name}
70
+
71
+ Permission to use, copy, modify, and/or distribute this software for any
72
+ purpose with or without fee is hereby granted, provided that the above
73
+ copyright notice and this permission notice appear in all copies.
74
+
75
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
76
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
77
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
78
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
79
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
80
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
81
+ PERFORMANCE OF THIS SOFTWARE.
82
+ """),
83
+ "bsd-2": ("BSD 2-Clause", """BSD 2-Clause License
84
+
85
+ Copyright (c) {year}, {name}
86
+
87
+ Redistribution and use in source and binary forms, with or without
88
+ modification, are permitted provided that the following conditions are met:
89
+
90
+ 1. Redistributions of source code must retain the above copyright notice, this
91
+ list of conditions and the following disclaimer.
92
+
93
+ 2. Redistributions in binary form must reproduce the above copyright notice,
94
+ this list of conditions and the following disclaimer in the documentation
95
+ and/or other materials provided with the distribution.
96
+
97
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
98
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
99
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
100
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
101
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
102
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
103
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
104
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
105
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
106
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
107
+ """),
108
+ "unlicense": ("The Unlicense (public domain)", """This is free and unencumbered software released into the public domain.
109
+
110
+ Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
111
+ software, either in source code form or as a compiled binary, for any purpose,
112
+ commercial or non-commercial, and by any means.
113
+
114
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
115
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
116
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
117
+
118
+ For more information, please refer to <https://unlicense.org>
119
+ """),
120
+ }
121
+ LICENSE_LINKS = {"apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0.txt",
122
+ "gpl-3.0": "https://www.gnu.org/licenses/gpl-3.0.txt",
123
+ "mpl-2.0": "https://www.mozilla.org/media/MPL/2.0/index.f75d2927d3c1.txt",
124
+ "agpl-3.0": "https://www.gnu.org/licenses/agpl-3.0.txt"}
125
+
126
+
127
+ def run_ignore(args) -> int:
128
+ if args.list or not args.templates:
129
+ print(" " + c("templates:", "bold") + " " + " ".join(sorted(IGN)))
130
+ if not args.templates:
131
+ print(c("\n usage: dkt gi python node vscode", "gray"))
132
+ return 0
133
+ unknown = [t for t in args.templates if t.lower() not in IGN]
134
+ if unknown:
135
+ raise DktError("Unknown template(s): %s. See 'dkt gi --list'." % ", ".join(unknown))
136
+ block = []
137
+ for t in args.templates:
138
+ block.append("# --- %s ---\n%s" % (t.lower(), IGN[t.lower()]))
139
+ text = "\n\n".join(block) + "\n"
140
+ if args.print:
141
+ print(text)
142
+ return 0
143
+ path = resolve_folder(args.folder) / ".gitignore"
144
+ if path.exists():
145
+ existing = path.read_text(encoding="utf-8", errors="replace")
146
+ have = set(x.strip() for x in existing.splitlines())
147
+ add = []
148
+ for t in args.templates:
149
+ lines = [ln for ln in IGN[t.lower()].splitlines() if ln.strip() not in have]
150
+ if lines:
151
+ add.append("# --- %s ---\n%s" % (t.lower(), "\n".join(lines)))
152
+ if not add:
153
+ ok(".gitignore already covers everything")
154
+ return 0
155
+ path.write_text(existing.rstrip("\n") + "\n\n" + "\n\n".join(add) + "\n", encoding="utf-8")
156
+ ok("added missing rules to existing .gitignore")
157
+ else:
158
+ path.write_text(text, encoding="utf-8")
159
+ ok("created %s" % path)
160
+ return 0
161
+
162
+
163
+ def run_license(args) -> int:
164
+ if args.list or not args.kind:
165
+ for k, (name, _) in LICENSES.items():
166
+ print(" %s %s" % (c(k.ljust(10), "bcyan"), name))
167
+ for k, url in LICENSE_LINKS.items():
168
+ print(" %s %s" % (c(k.ljust(10), "gray"), c("download full text: " + url, "gray")))
169
+ return 0
170
+ k = args.kind.lower()
171
+ if k in LICENSE_LINKS:
172
+ print(" The %s text is long - grab it from: %s" % (k.upper(), LICENSE_LINKS[k]))
173
+ return 0
174
+ if k not in LICENSES:
175
+ raise DktError("Unknown license '%s'. See 'dkt license --list'." % args.kind)
176
+ name = args.name or get_setting("author")
177
+ if not name:
178
+ from ..utils import ask, run_capture
179
+ try:
180
+ _, out, _ = run_capture(["git", "config", "user.name"])
181
+ name = out.strip()
182
+ except DktError:
183
+ name = ""
184
+ name = name or ask("Copyright holder name", "Your Name")
185
+ text = LICENSES[k][1].format(year=args.year or date.today().year, name=name or "Your Name")
186
+ path = resolve_folder(args.folder) / "LICENSE"
187
+ if path.exists() and not args.force and not confirm("LICENSE already exists. Overwrite?"):
188
+ print(" kept existing LICENSE")
189
+ return 0
190
+ path.write_text(text, encoding="utf-8")
191
+ ok("wrote %s license to %s" % (LICENSES[k][0], path))
192
+ return 0
193
+
194
+
195
+ def register(sub, common) -> None:
196
+ p = sub.add_parser("ignore", aliases=["gi"], parents=[common], help=META[3],
197
+ description="Generate or extend a .gitignore from templates.",
198
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
199
+ p.add_argument("templates", nargs="*")
200
+ p.add_argument("-f", "--folder", metavar="PATH")
201
+ p.add_argument("--list", action="store_true")
202
+ p.add_argument("--print", action="store_true", help="print instead of writing")
203
+ p.set_defaults(func=run_ignore)
204
+
205
+ lp = sub.add_parser("license", aliases=["lic"], parents=[common], help="write a LICENSE file (MIT, ISC, BSD, Unlicense)",
206
+ description="Create a LICENSE file.")
207
+ lp.add_argument("kind", nargs="?", help="mit | isc | bsd-2 | unlicense")
208
+ lp.add_argument("--name", help="copyright holder (default: git user.name or `dkt config set author ...`)")
209
+ lp.add_argument("--year", type=int)
210
+ lp.add_argument("-f", "--folder", metavar="PATH")
211
+ lp.add_argument("--force", action="store_true", help="overwrite without asking")
212
+ lp.add_argument("--list", action="store_true")
213
+ lp.set_defaults(func=run_license)
@@ -0,0 +1,166 @@
1
+ """dkt init (new) - scaffold a new project with sensible defaults."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import re
6
+ from datetime import date
7
+ from pathlib import Path
8
+
9
+ from ..utils import DktError, c, git, ok, run_capture, sym, tool_version, get_setting, ask
10
+ from .ignore import IGN, LICENSES
11
+
12
+ META = ("Project & Code", "init", "new", "scaffold a project: python, node, web, cli, with git + license")
13
+
14
+ EPILOG = """\
15
+ examples:
16
+ dkt new python mytool src-layout package with pyproject, tests, .gitignore
17
+ dkt new node my-api package.json + index.js
18
+ dkt new web landing index.html + style.css + script.js
19
+ dkt new python mytool --no-git --license isc
20
+ """
21
+
22
+ KINDS = ["python", "node", "web"]
23
+
24
+
25
+ def w(path: Path, text: str) -> None:
26
+ path.parent.mkdir(parents=True, exist_ok=True)
27
+ path.write_text(text, encoding="utf-8")
28
+ print(" %s %s" % (c(sym("ok"), "bgreen"), path))
29
+
30
+
31
+ def pkg_name(name: str) -> str:
32
+ return re.sub(r"[^a-z0-9_]", "_", name.lower()).strip("_") or "app"
33
+
34
+
35
+ def scaffold_python(root: Path, name: str, author: str) -> None:
36
+ pkg = pkg_name(name)
37
+ w(root / "pyproject.toml", '''[build-system]
38
+ requires = ["setuptools>=61"]
39
+ build-backend = "setuptools.build_meta"
40
+
41
+ [project]
42
+ name = "%s"
43
+ version = "0.1.0"
44
+ description = ""
45
+ readme = "README.md"
46
+ requires-python = ">=3.8"
47
+ authors = [{ name = "%s" }]
48
+ dependencies = []
49
+
50
+ [project.scripts]
51
+ %s = "%s.cli:main"
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["src"]
55
+ ''' % (name, author, name, pkg))
56
+ w(root / "src" / pkg / "__init__.py", '__version__ = "0.1.0"\n')
57
+ w(root / "src" / pkg / "cli.py", '''import argparse
58
+
59
+
60
+ def main(argv=None):
61
+ parser = argparse.ArgumentParser(prog="%s")
62
+ parser.add_argument("name", nargs="?", default="world")
63
+ args = parser.parse_args(argv)
64
+ print("Hello, %%s!" %% args.name)
65
+ return 0
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())
70
+ ''' % name)
71
+ w(root / "src" / pkg / "__main__.py", "from .cli import main\n\nraise SystemExit(main())\n")
72
+ w(root / "tests" / ("test_%s.py" % pkg), '''import unittest
73
+
74
+ from %s.cli import main
75
+
76
+
77
+ class TestCli(unittest.TestCase):
78
+ def test_runs(self):
79
+ self.assertEqual(main(["dev"]), 0)
80
+
81
+
82
+ if __name__ == "__main__":
83
+ unittest.main()
84
+ ''' % pkg)
85
+ w(root / "README.md", "# %s\n\n## Install\n```bash\npip install -e .\n```\n\n## Run\n```bash\n%s\n```\n" % (name, name))
86
+
87
+
88
+ def scaffold_node(root: Path, name: str, author: str) -> None:
89
+ w(root / "package.json", '''{
90
+ "name": "%s",
91
+ "version": "0.1.0",
92
+ "description": "",
93
+ "main": "index.js",
94
+ "scripts": { "start": "node index.js", "test": "node --test" },
95
+ "author": "%s",
96
+ "license": "MIT"
97
+ }
98
+ ''' % (name, author))
99
+ w(root / "index.js", "console.log('Hello from %s');\n" % name)
100
+ w(root / "README.md", "# %s\n\n```bash\nnpm start\n```\n" % name)
101
+
102
+
103
+ def scaffold_web(root: Path, name: str, author: str) -> None:
104
+ w(root / "index.html", '''<!doctype html>
105
+ <html lang="en">
106
+ <head>
107
+ <meta charset="utf-8">
108
+ <meta name="viewport" content="width=device-width, initial-scale=1">
109
+ <title>%s</title>
110
+ <link rel="stylesheet" href="style.css">
111
+ </head>
112
+ <body>
113
+ <main>
114
+ <h1>%s</h1>
115
+ <p>Edit <code>index.html</code> and run <code>dkt serve --open</code>.</p>
116
+ </main>
117
+ <script src="script.js"></script>
118
+ </body>
119
+ </html>
120
+ ''' % (name, name))
121
+ w(root / "style.css", ''':root { color-scheme: light dark; }
122
+ body { font-family: system-ui, sans-serif; margin: 0; display: grid; min-height: 100vh; place-items: center; }
123
+ main { max-width: 40rem; padding: 2rem; }
124
+ ''')
125
+ w(root / "script.js", "console.log('ready');\n")
126
+ w(root / "README.md", "# %s\n\n```bash\ndkt serve --open\n```\n" % name)
127
+
128
+
129
+ def run(args) -> int:
130
+ root = Path(args.name).expanduser().resolve()
131
+ if root.exists() and any(root.iterdir()):
132
+ raise DktError("%s already exists and is not empty." % root)
133
+ name = root.name
134
+ author = args.author or get_setting("author") or ""
135
+ if not author:
136
+ try:
137
+ author = run_capture(["git", "config", "user.name"])[1].strip()
138
+ except DktError:
139
+ author = ""
140
+ author = author or "Your Name"
141
+ print("\n creating %s project %s\n" % (c(args.kind, "bold bcyan"), c(name, "bold")))
142
+ {"python": scaffold_python, "node": scaffold_node, "web": scaffold_web}[args.kind](root, name, author)
143
+ ign = {"python": ["python"], "node": ["node"], "web": []}[args.kind] + ["env", "vscode", "macos", "windows"]
144
+ w(root / ".gitignore", "\n\n".join(IGN[i] for i in ign) + "\n")
145
+ if args.license != "none":
146
+ w(root / "LICENSE", LICENSES[args.license][1].format(year=date.today().year, name=author))
147
+ if not args.no_git:
148
+ code, _, err = run_capture(["git", "init", str(root)])
149
+ if code == 0:
150
+ ok("initialized git repository")
151
+ else:
152
+ print(c(" (git init skipped: %s)" % err.strip(), "gray"))
153
+ print("\n next: cd %s\n" % root.name)
154
+ return 0
155
+
156
+
157
+ def register(sub, common) -> None:
158
+ p = sub.add_parser("init", aliases=["new"], parents=[common], help=META[3],
159
+ description="Create a new project folder from a template.",
160
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
161
+ p.add_argument("kind", choices=KINDS)
162
+ p.add_argument("name", help="folder / project name")
163
+ p.add_argument("--license", choices=list(LICENSES) + ["none"], default="mit")
164
+ p.add_argument("--author")
165
+ p.add_argument("--no-git", action="store_true")
166
+ p.set_defaults(func=run)