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.
- devkittools/__init__.py +3 -0
- devkittools/__main__.py +6 -0
- devkittools/cli.py +179 -0
- devkittools/tools/__init__.py +1 -0
- devkittools/tools/bench.py +73 -0
- devkittools/tools/changeloggen.py +181 -0
- devkittools/tools/clean.py +110 -0
- devkittools/tools/colortool.py +126 -0
- devkittools/tools/commitwriter.py +199 -0
- devkittools/tools/contextpacker.py +226 -0
- devkittools/tools/cron.py +152 -0
- devkittools/tools/doctor.py +74 -0
- devkittools/tools/du.py +72 -0
- devkittools/tools/enc.py +131 -0
- devkittools/tools/envtool.py +141 -0
- devkittools/tools/findtool.py +119 -0
- devkittools/tools/gen.py +182 -0
- devkittools/tools/gx.py +184 -0
- devkittools/tools/hashtool.py +93 -0
- devkittools/tools/httptool.py +131 -0
- devkittools/tools/ignore.py +213 -0
- devkittools/tools/initproj.py +166 -0
- devkittools/tools/jsontool.py +200 -0
- devkittools/tools/net.py +114 -0
- devkittools/tools/portkiller.py +246 -0
- devkittools/tools/regextool.py +84 -0
- devkittools/tools/repostats.py +197 -0
- devkittools/tools/secrets_scan.py +108 -0
- devkittools/tools/serve.py +118 -0
- devkittools/tools/snip.py +94 -0
- devkittools/tools/system.py +157 -0
- devkittools/tools/timetool.py +130 -0
- devkittools/tools/todo.py +69 -0
- devkittools/tools/tree.py +86 -0
- devkittools/tools/watch.py +82 -0
- devkittools/utils.py +561 -0
- dktcli-1.0.0.dist-info/METADATA +148 -0
- dktcli-1.0.0.dist-info/RECORD +42 -0
- dktcli-1.0.0.dist-info/WHEEL +5 -0
- dktcli-1.0.0.dist-info/entry_points.txt +3 -0
- dktcli-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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)
|