multitool 0.9.0__tar.gz → 0.10.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.
- {multitool-0.9.0 → multitool-0.10.0}/PKG-INFO +1 -1
- {multitool-0.9.0 → multitool-0.10.0}/multitool/__init__.py +1 -1
- multitool-0.10.0/multitool/bootstrap/commands.py +654 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool.egg-info/PKG-INFO +1 -1
- {multitool-0.9.0 → multitool-0.10.0}/pyproject.toml +1 -1
- multitool-0.9.0/multitool/bootstrap/commands.py +0 -57
- {multitool-0.9.0 → multitool-0.10.0}/LICENSE +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/README.rst +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/__main__.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/cls.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/console.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/exceptions.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/plugins/__init__.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/plugins/commands.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/run/commands.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/silent.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/utils.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/utils_init.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool/verbose.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool.egg-info/SOURCES.txt +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool.egg-info/dependency_links.txt +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool.egg-info/entry_points.txt +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool.egg-info/requires.txt +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/multitool.egg-info/top_level.txt +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/setup.cfg +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/tests/__init__.py +0 -0
- {multitool-0.9.0 → multitool-0.10.0}/tests/test_utils.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
APP = "multitool"
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.10.0"
|
|
3
3
|
description = "Create and run plugin-based command-line tools."
|
|
4
4
|
long_description = ("Create and run plugin-based command-line tools by dynamically loading Click commands from local or Git-managed plugin repositories.")
|
|
5
5
|
|
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from multitool import console
|
|
6
|
+
from multitool.silent import common_silent_options
|
|
7
|
+
from multitool.utils import mkdir
|
|
8
|
+
from multitool.verbose import common_verbose_options
|
|
9
|
+
|
|
10
|
+
PROJECT_STRUCTURE = {
|
|
11
|
+
"pyproject.toml": None,
|
|
12
|
+
"{app}": {
|
|
13
|
+
"__init__.py": None,
|
|
14
|
+
"__main__.py": None,
|
|
15
|
+
"console.py": None,
|
|
16
|
+
"exceptions.py": None,
|
|
17
|
+
"request.py": None,
|
|
18
|
+
"utils.py": None,
|
|
19
|
+
"submodule": {
|
|
20
|
+
"__init__.py": None,
|
|
21
|
+
"commands.py": None,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
FILE_TEMPLATES = {
|
|
27
|
+
"pyproject.toml": """\
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["setuptools>=68"]
|
|
30
|
+
build-backend = "setuptools.build_meta"
|
|
31
|
+
|
|
32
|
+
[project]
|
|
33
|
+
name = "{app}"
|
|
34
|
+
version = "0.1.0"
|
|
35
|
+
description = "A Python package."
|
|
36
|
+
readme = "README.md"
|
|
37
|
+
requires-python = ">=3.10"
|
|
38
|
+
dependencies = [
|
|
39
|
+
"click>=8.1",
|
|
40
|
+
"requests>=2.31",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
{app} = "{app}.__main__:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["."]
|
|
48
|
+
""",
|
|
49
|
+
"__init__.py": """\
|
|
50
|
+
\"\"\"{app} package.\"\"\"
|
|
51
|
+
|
|
52
|
+
__version__ = "0.1.0"
|
|
53
|
+
""",
|
|
54
|
+
"__main__.py": """\
|
|
55
|
+
\"\"\"Command-line entry point for {app}.\"\"\"
|
|
56
|
+
|
|
57
|
+
from .submodule.commands import cli
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main() -> None:
|
|
61
|
+
\"\"\"Run the command-line interface.\"\"\"
|
|
62
|
+
cli()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|
|
67
|
+
""",
|
|
68
|
+
"console.py": """\
|
|
69
|
+
\"\"\"Console output helpers.\"\"\"
|
|
70
|
+
|
|
71
|
+
import click
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def echo(message: str = "") -> None:
|
|
75
|
+
\"\"\"Print a normal message.\"\"\"
|
|
76
|
+
click.echo(message)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def info(message: str) -> None:
|
|
80
|
+
\"\"\"Print an informational message.\"\"\"
|
|
81
|
+
click.echo(f"INFO: {message}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def success(message: str) -> None:
|
|
85
|
+
\"\"\"Print a success message.\"\"\"
|
|
86
|
+
click.echo(f"OK: {message}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def warning(message: str) -> None:
|
|
90
|
+
\"\"\"Print a warning message.\"\"\"
|
|
91
|
+
click.echo(f"WARNING: {message}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def error(message: str) -> None:
|
|
95
|
+
\"\"\"Print an error message to stderr.\"\"\"
|
|
96
|
+
click.echo(f"ERROR: {message}", err=True)
|
|
97
|
+
""",
|
|
98
|
+
"exceptions.py": """\
|
|
99
|
+
\"\"\"Application-specific exceptions.\"\"\"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AppError(Exception):
|
|
103
|
+
\"\"\"Base exception for application errors.\"\"\"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ConfigurationError(AppError):
|
|
107
|
+
\"\"\"Raised when configuration is invalid.\"\"\"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ValidationError(AppError):
|
|
111
|
+
\"\"\"Raised when input validation fails.\"\"\"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class RequestError(AppError):
|
|
115
|
+
\"\"\"Raised when an HTTP request fails.\"\"\"
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
message: str,
|
|
120
|
+
status_code: int | None = None,
|
|
121
|
+
response=None,
|
|
122
|
+
) -> None:
|
|
123
|
+
super().__init__(message)
|
|
124
|
+
self.status_code = status_code
|
|
125
|
+
self.response = response
|
|
126
|
+
""",
|
|
127
|
+
"request.py": """\
|
|
128
|
+
\"\"\"Generic helpers for making REST API requests.\"\"\"
|
|
129
|
+
|
|
130
|
+
from typing import Any
|
|
131
|
+
|
|
132
|
+
import requests
|
|
133
|
+
|
|
134
|
+
from .exceptions import RequestError
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
DEFAULT_TIMEOUT = 30
|
|
138
|
+
DEFAULT_VERIFY_SSL = True
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def request(
|
|
142
|
+
method: str,
|
|
143
|
+
url: str,
|
|
144
|
+
*,
|
|
145
|
+
params: dict[str, Any] | None = None,
|
|
146
|
+
data: Any = None,
|
|
147
|
+
json: Any = None,
|
|
148
|
+
headers: dict[str, str] | None = None,
|
|
149
|
+
timeout: int | float = DEFAULT_TIMEOUT,
|
|
150
|
+
verify_ssl: bool = DEFAULT_VERIFY_SSL,
|
|
151
|
+
**kwargs: Any,
|
|
152
|
+
) -> requests.Response:
|
|
153
|
+
\"\"\"Make an HTTP request.\"\"\"
|
|
154
|
+
try:
|
|
155
|
+
response = requests.request(
|
|
156
|
+
method=method,
|
|
157
|
+
url=url,
|
|
158
|
+
params=params,
|
|
159
|
+
data=data,
|
|
160
|
+
json=json,
|
|
161
|
+
headers=headers,
|
|
162
|
+
timeout=timeout,
|
|
163
|
+
verify=verify_ssl,
|
|
164
|
+
**kwargs,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
response.raise_for_status()
|
|
168
|
+
|
|
169
|
+
return response
|
|
170
|
+
|
|
171
|
+
except requests.RequestException as exc:
|
|
172
|
+
status_code = None
|
|
173
|
+
|
|
174
|
+
if exc.response is not None:
|
|
175
|
+
status_code = exc.response.status_code
|
|
176
|
+
|
|
177
|
+
raise RequestError(
|
|
178
|
+
str(exc),
|
|
179
|
+
status_code=status_code,
|
|
180
|
+
response=exc.response,
|
|
181
|
+
) from exc
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get(
|
|
185
|
+
url: str,
|
|
186
|
+
*,
|
|
187
|
+
params: dict[str, Any] | None = None,
|
|
188
|
+
headers: dict[str, str] | None = None,
|
|
189
|
+
timeout: int | float = DEFAULT_TIMEOUT,
|
|
190
|
+
verify_ssl: bool = DEFAULT_VERIFY_SSL,
|
|
191
|
+
**kwargs: Any,
|
|
192
|
+
) -> requests.Response:
|
|
193
|
+
\"\"\"Make a GET request.\"\"\"
|
|
194
|
+
return request(
|
|
195
|
+
"GET",
|
|
196
|
+
url,
|
|
197
|
+
params=params,
|
|
198
|
+
headers=headers,
|
|
199
|
+
timeout=timeout,
|
|
200
|
+
verify_ssl=verify_ssl,
|
|
201
|
+
**kwargs,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def post(
|
|
206
|
+
url: str,
|
|
207
|
+
*,
|
|
208
|
+
data: Any = None,
|
|
209
|
+
json: Any = None,
|
|
210
|
+
headers: dict[str, str] | None = None,
|
|
211
|
+
timeout: int | float = DEFAULT_TIMEOUT,
|
|
212
|
+
verify_ssl: bool = DEFAULT_VERIFY_SSL,
|
|
213
|
+
**kwargs: Any,
|
|
214
|
+
) -> requests.Response:
|
|
215
|
+
\"\"\"Make a POST request.\"\"\"
|
|
216
|
+
return request(
|
|
217
|
+
"POST",
|
|
218
|
+
url,
|
|
219
|
+
data=data,
|
|
220
|
+
json=json,
|
|
221
|
+
headers=headers,
|
|
222
|
+
timeout=timeout,
|
|
223
|
+
verify_ssl=verify_ssl,
|
|
224
|
+
**kwargs,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def put(
|
|
229
|
+
url: str,
|
|
230
|
+
*,
|
|
231
|
+
data: Any = None,
|
|
232
|
+
json: Any = None,
|
|
233
|
+
headers: dict[str, str] | None = None,
|
|
234
|
+
timeout: int | float = DEFAULT_TIMEOUT,
|
|
235
|
+
verify_ssl: bool = DEFAULT_VERIFY_SSL,
|
|
236
|
+
**kwargs: Any,
|
|
237
|
+
) -> requests.Response:
|
|
238
|
+
\"\"\"Make a PUT request.\"\"\"
|
|
239
|
+
return request(
|
|
240
|
+
"PUT",
|
|
241
|
+
url,
|
|
242
|
+
data=data,
|
|
243
|
+
json=json,
|
|
244
|
+
headers=headers,
|
|
245
|
+
timeout=timeout,
|
|
246
|
+
verify_ssl=verify_ssl,
|
|
247
|
+
**kwargs,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def patch(
|
|
252
|
+
url: str,
|
|
253
|
+
*,
|
|
254
|
+
data: Any = None,
|
|
255
|
+
json: Any = None,
|
|
256
|
+
headers: dict[str, str] | None = None,
|
|
257
|
+
timeout: int | float = DEFAULT_TIMEOUT,
|
|
258
|
+
verify_ssl: bool = DEFAULT_VERIFY_SSL,
|
|
259
|
+
**kwargs: Any,
|
|
260
|
+
) -> requests.Response:
|
|
261
|
+
\"\"\"Make a PATCH request.\"\"\"
|
|
262
|
+
return request(
|
|
263
|
+
"PATCH",
|
|
264
|
+
url,
|
|
265
|
+
data=data,
|
|
266
|
+
json=json,
|
|
267
|
+
headers=headers,
|
|
268
|
+
timeout=timeout,
|
|
269
|
+
verify_ssl=verify_ssl,
|
|
270
|
+
**kwargs,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def delete(
|
|
275
|
+
url: str,
|
|
276
|
+
*,
|
|
277
|
+
headers: dict[str, str] | None = None,
|
|
278
|
+
timeout: int | float = DEFAULT_TIMEOUT,
|
|
279
|
+
verify_ssl: bool = DEFAULT_VERIFY_SSL,
|
|
280
|
+
**kwargs: Any,
|
|
281
|
+
) -> requests.Response:
|
|
282
|
+
\"\"\"Make a DELETE request.\"\"\"
|
|
283
|
+
return request(
|
|
284
|
+
"DELETE",
|
|
285
|
+
url,
|
|
286
|
+
headers=headers,
|
|
287
|
+
timeout=timeout,
|
|
288
|
+
verify_ssl=verify_ssl,
|
|
289
|
+
**kwargs,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_json(
|
|
294
|
+
url: str,
|
|
295
|
+
*,
|
|
296
|
+
params: dict[str, Any] | None = None,
|
|
297
|
+
headers: dict[str, str] | None = None,
|
|
298
|
+
timeout: int | float = DEFAULT_TIMEOUT,
|
|
299
|
+
verify_ssl: bool = DEFAULT_VERIFY_SSL,
|
|
300
|
+
**kwargs: Any,
|
|
301
|
+
) -> Any:
|
|
302
|
+
\"\"\"Make a GET request and return decoded JSON.\"\"\"
|
|
303
|
+
response = get(
|
|
304
|
+
url,
|
|
305
|
+
params=params,
|
|
306
|
+
headers=headers,
|
|
307
|
+
timeout=timeout,
|
|
308
|
+
verify_ssl=verify_ssl,
|
|
309
|
+
**kwargs,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
return response.json()
|
|
314
|
+
except ValueError as exc:
|
|
315
|
+
raise RequestError(
|
|
316
|
+
"Response did not contain valid JSON.",
|
|
317
|
+
status_code=response.status_code,
|
|
318
|
+
response=response,
|
|
319
|
+
) from exc
|
|
320
|
+
""",
|
|
321
|
+
"utils.py": """\
|
|
322
|
+
\"\"\"General-purpose utility functions.\"\"\"
|
|
323
|
+
|
|
324
|
+
from pathlib import Path
|
|
325
|
+
from typing import Iterable
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def ensure_directory(path: Path) -> Path:
|
|
329
|
+
\"\"\"Create a directory if it does not already exist.\"\"\"
|
|
330
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
return path
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def read_text(path: Path, encoding: str = "utf-8") -> str:
|
|
335
|
+
\"\"\"Read text from a file.\"\"\"
|
|
336
|
+
return path.read_text(encoding=encoding)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def write_text(
|
|
340
|
+
path: Path,
|
|
341
|
+
content: str,
|
|
342
|
+
encoding: str = "utf-8",
|
|
343
|
+
) -> Path:
|
|
344
|
+
\"\"\"Write text to a file, creating parent directories when needed.\"\"\"
|
|
345
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
path.write_text(content, encoding=encoding)
|
|
347
|
+
return path
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def chunks(items: Iterable, size: int):
|
|
351
|
+
\"\"\"Yield an iterable in chunks of the requested size.\"\"\"
|
|
352
|
+
if size <= 0:
|
|
353
|
+
raise ValueError("size must be greater than zero")
|
|
354
|
+
|
|
355
|
+
chunk = []
|
|
356
|
+
|
|
357
|
+
for item in items:
|
|
358
|
+
chunk.append(item)
|
|
359
|
+
|
|
360
|
+
if len(chunk) == size:
|
|
361
|
+
yield chunk
|
|
362
|
+
chunk = []
|
|
363
|
+
|
|
364
|
+
if chunk:
|
|
365
|
+
yield chunk
|
|
366
|
+
""",
|
|
367
|
+
"submodule/__init__.py": """\
|
|
368
|
+
\"\"\"Submodule package for {app}.\"\"\"
|
|
369
|
+
""",
|
|
370
|
+
"commands.py": """\
|
|
371
|
+
\"\"\"CLI commands for {app}.\"\"\"
|
|
372
|
+
|
|
373
|
+
import click
|
|
374
|
+
|
|
375
|
+
from .. import __version__
|
|
376
|
+
from ..exceptions import RequestError
|
|
377
|
+
from ..request import get_json, post
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@click.group()
|
|
381
|
+
@click.version_option(__version__, prog_name="{app}")
|
|
382
|
+
def cli() -> None:
|
|
383
|
+
\"\"\"Command-line interface for {app}.\"\"\"
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@cli.command()
|
|
387
|
+
def hello() -> None:
|
|
388
|
+
\"\"\"Run a simple example command.\"\"\"
|
|
389
|
+
click.echo("Hello from {app}!")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@cli.command()
|
|
393
|
+
@click.argument("name")
|
|
394
|
+
def greet(name: str) -> None:
|
|
395
|
+
\"\"\"Greet NAME.\"\"\"
|
|
396
|
+
click.echo(f"Hello, {name}!")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@cli.command()
|
|
400
|
+
@click.option(
|
|
401
|
+
"--insecure",
|
|
402
|
+
is_flag=True,
|
|
403
|
+
help="Disable SSL certificate verification.",
|
|
404
|
+
)
|
|
405
|
+
def api_get(insecure: bool) -> None:
|
|
406
|
+
\"\"\"Example GET request against a REST API.\"\"\"
|
|
407
|
+
try:
|
|
408
|
+
data = get_json(
|
|
409
|
+
"https://jsonplaceholder.typicode.com/todos/1",
|
|
410
|
+
verify_ssl=not insecure,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
click.echo(data)
|
|
414
|
+
|
|
415
|
+
except RequestError as exc:
|
|
416
|
+
raise click.ClickException(str(exc)) from exc
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@cli.command()
|
|
420
|
+
@click.option(
|
|
421
|
+
"--insecure",
|
|
422
|
+
is_flag=True,
|
|
423
|
+
help="Disable SSL certificate verification.",
|
|
424
|
+
)
|
|
425
|
+
def api_post(insecure: bool) -> None:
|
|
426
|
+
\"\"\"Example POST request against a REST API.\"\"\"
|
|
427
|
+
|
|
428
|
+
payload = {
|
|
429
|
+
"title": "Example",
|
|
430
|
+
"body": "Created by {app}.",
|
|
431
|
+
"userId": 1,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
response = post(
|
|
436
|
+
"https://jsonplaceholder.typicode.com/posts",
|
|
437
|
+
json=payload,
|
|
438
|
+
verify_ssl=not insecure,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
click.echo(response.json())
|
|
442
|
+
|
|
443
|
+
except RequestError as exc:
|
|
444
|
+
raise click.ClickException(str(exc)) from exc
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@cli.command()
|
|
448
|
+
def rest_examples() -> None:
|
|
449
|
+
\"\"\"Show REST API usage examples.\"\"\"
|
|
450
|
+
|
|
451
|
+
click.echo(
|
|
452
|
+
\"\"\"
|
|
453
|
+
REST API examples
|
|
454
|
+
=================
|
|
455
|
+
|
|
456
|
+
GET
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
from {app}.request import get
|
|
460
|
+
|
|
461
|
+
response = get(
|
|
462
|
+
"https://jsonplaceholder.typicode.com/todos/1"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
print(response.json())
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
GET JSON
|
|
469
|
+
--------
|
|
470
|
+
|
|
471
|
+
from {app}.request import get_json
|
|
472
|
+
|
|
473
|
+
todo = get_json(
|
|
474
|
+
"https://jsonplaceholder.typicode.com/todos/1"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
print(todo)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
GET with query parameters
|
|
481
|
+
-------------------------
|
|
482
|
+
|
|
483
|
+
from {app}.request import get_json
|
|
484
|
+
|
|
485
|
+
users = get_json(
|
|
486
|
+
"https://jsonplaceholder.typicode.com/users",
|
|
487
|
+
params={{"id": 1}},
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
print(users)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
POST
|
|
494
|
+
----
|
|
495
|
+
|
|
496
|
+
from {app}.request import post
|
|
497
|
+
|
|
498
|
+
response = post(
|
|
499
|
+
"https://jsonplaceholder.typicode.com/posts",
|
|
500
|
+
json={{
|
|
501
|
+
"title": "Example",
|
|
502
|
+
"body": "Hello from {app}",
|
|
503
|
+
"userId": 1,
|
|
504
|
+
}},
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
print(response.json())
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
PUT
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
from {app}.request import put
|
|
514
|
+
|
|
515
|
+
response = put(
|
|
516
|
+
"https://jsonplaceholder.typicode.com/posts/1",
|
|
517
|
+
json={{
|
|
518
|
+
"id": 1,
|
|
519
|
+
"title": "Updated title",
|
|
520
|
+
"body": "Updated body",
|
|
521
|
+
"userId": 1,
|
|
522
|
+
}},
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
print(response.json())
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
PATCH
|
|
529
|
+
------
|
|
530
|
+
|
|
531
|
+
from {app}.request import patch
|
|
532
|
+
|
|
533
|
+
response = patch(
|
|
534
|
+
"https://jsonplaceholder.typicode.com/posts/1",
|
|
535
|
+
json={{
|
|
536
|
+
"title": "Updated title",
|
|
537
|
+
}},
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
print(response.json())
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
DELETE
|
|
544
|
+
------
|
|
545
|
+
|
|
546
|
+
from {app}.request import delete
|
|
547
|
+
|
|
548
|
+
response = delete(
|
|
549
|
+
"https://jsonplaceholder.typicode.com/posts/1"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
print(response.status_code)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
SSL verification
|
|
556
|
+
----------------
|
|
557
|
+
|
|
558
|
+
SSL verification is enabled by default:
|
|
559
|
+
|
|
560
|
+
response = get(
|
|
561
|
+
"https://api.example.com",
|
|
562
|
+
verify_ssl=True,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
To disable SSL verification:
|
|
566
|
+
|
|
567
|
+
response = get(
|
|
568
|
+
"https://localhost:8443",
|
|
569
|
+
verify_ssl=False,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
CLI commands
|
|
574
|
+
------------
|
|
575
|
+
|
|
576
|
+
{app} --help
|
|
577
|
+
{app} hello
|
|
578
|
+
{app} greet Matthew
|
|
579
|
+
{app} api-get
|
|
580
|
+
{app} api-get --insecure
|
|
581
|
+
{app} api-post
|
|
582
|
+
{app} api-post --insecure
|
|
583
|
+
{app} rest-examples
|
|
584
|
+
\"\"\"
|
|
585
|
+
)
|
|
586
|
+
""",
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def create_structure(
|
|
591
|
+
root: Path,
|
|
592
|
+
structure: dict,
|
|
593
|
+
app: str,
|
|
594
|
+
) -> None:
|
|
595
|
+
"""Create the project structure and populate files from templates."""
|
|
596
|
+
|
|
597
|
+
for name, children in structure.items():
|
|
598
|
+
path = root / name.replace("{app}", app)
|
|
599
|
+
|
|
600
|
+
if children is None:
|
|
601
|
+
content = FILE_TEMPLATES.get(name)
|
|
602
|
+
|
|
603
|
+
if content is None:
|
|
604
|
+
path.touch()
|
|
605
|
+
else:
|
|
606
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
607
|
+
|
|
608
|
+
# Only replace our application-name placeholder.
|
|
609
|
+
# Do not use str.format(), because generated Python
|
|
610
|
+
# contains its own braces.
|
|
611
|
+
content = content.replace("{app}", app)
|
|
612
|
+
|
|
613
|
+
path.write_text(
|
|
614
|
+
content,
|
|
615
|
+
encoding="utf-8",
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
else:
|
|
619
|
+
mkdir(path)
|
|
620
|
+
|
|
621
|
+
create_structure(
|
|
622
|
+
path,
|
|
623
|
+
children,
|
|
624
|
+
app,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@click.command(help="Create the directory structure for a new Python package project.")
|
|
629
|
+
@click.argument("name")
|
|
630
|
+
@click.argument(
|
|
631
|
+
"directory",
|
|
632
|
+
type=click.Path(path_type=Path),
|
|
633
|
+
default=".",
|
|
634
|
+
required=False,
|
|
635
|
+
)
|
|
636
|
+
@common_silent_options
|
|
637
|
+
@common_verbose_options
|
|
638
|
+
def bootstrap(silent, verbose, name, directory):
|
|
639
|
+
root = directory / name
|
|
640
|
+
|
|
641
|
+
mkdir(root)
|
|
642
|
+
|
|
643
|
+
create_structure(
|
|
644
|
+
root,
|
|
645
|
+
PROJECT_STRUCTURE,
|
|
646
|
+
name,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
console.echo(f"""Created project: {root}
|
|
650
|
+
|
|
651
|
+
Next steps:
|
|
652
|
+
cd {name}
|
|
653
|
+
python -m {name}
|
|
654
|
+
python3 -m {name}""")
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "multitool"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "Create and run plugin-based command-line tools."
|
|
9
9
|
readme = { file = "README.rst", content-type = "text/x-rst" }
|
|
10
10
|
requires-python = ">=3.7"
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
import click
|
|
4
|
-
|
|
5
|
-
from multitool import console
|
|
6
|
-
from multitool.silent import common_silent_options
|
|
7
|
-
from multitool.utils import mkdir, touch
|
|
8
|
-
from multitool.verbose import common_verbose_options
|
|
9
|
-
|
|
10
|
-
PROJECT_STRUCTURE = {
|
|
11
|
-
"pyproject.toml": None,
|
|
12
|
-
"{app}": {
|
|
13
|
-
"__init__.py": None,
|
|
14
|
-
"__main__.py": None,
|
|
15
|
-
# "cls.py": None,
|
|
16
|
-
"console.py": None,
|
|
17
|
-
"exceptions.py": None,
|
|
18
|
-
"silent.py": None,
|
|
19
|
-
"utils.py": None,
|
|
20
|
-
"utils_init.py": None,
|
|
21
|
-
"verbose.py": None,
|
|
22
|
-
"submodule": {
|
|
23
|
-
"__init__.py": None,
|
|
24
|
-
"commands.py": None,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def create_structure(root: Path, structure: dict, app: str):
|
|
31
|
-
for name, children in structure.items():
|
|
32
|
-
path = root / name.format(app=app)
|
|
33
|
-
|
|
34
|
-
if children is None:
|
|
35
|
-
touch(path)
|
|
36
|
-
else:
|
|
37
|
-
mkdir(path)
|
|
38
|
-
create_structure(path, children, app)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@click.command(help="Create the directory structure for a new Python package project.")
|
|
42
|
-
@click.argument("name")
|
|
43
|
-
@click.argument(
|
|
44
|
-
"directory",
|
|
45
|
-
type=click.Path(path_type=Path),
|
|
46
|
-
default=".",
|
|
47
|
-
required=False,
|
|
48
|
-
)
|
|
49
|
-
@common_silent_options
|
|
50
|
-
@common_verbose_options
|
|
51
|
-
def bootstrap(silent, verbose, name, directory):
|
|
52
|
-
root = directory / name
|
|
53
|
-
mkdir(root)
|
|
54
|
-
|
|
55
|
-
create_structure(root, PROJECT_STRUCTURE, name)
|
|
56
|
-
|
|
57
|
-
console.echo(f"Created project: {root}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|