asgi-cli 0.4.4__tar.gz → 0.4.6__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.
- {asgi_cli-0.4.4/src/asgi_cli.egg-info → asgi_cli-0.4.6}/PKG-INFO +18 -20
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/README.md +15 -17
- asgi_cli-0.4.6/pyproject.toml +57 -0
- asgi_cli-0.4.6/src/asgi_cli/__init__.py +1 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/builder.py +13 -18
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/executor.py +4 -4
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/loader.py +11 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/main.py +5 -4
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/parser.py +21 -8
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/typing.py +7 -5
- {asgi_cli-0.4.4 → asgi_cli-0.4.6/src/asgi_cli.egg-info}/PKG-INFO +18 -20
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli.egg-info/SOURCES.txt +1 -6
- asgi_cli-0.4.4/pyproject.toml +0 -55
- asgi_cli-0.4.4/src/asgi_cli/__init__.py +0 -1
- asgi_cli-0.4.4/src/asgi_cli/tests/__init__.py +0 -0
- asgi_cli-0.4.4/src/asgi_cli/tests/test_formatter.py +0 -28
- asgi_cli-0.4.4/src/asgi_cli/tests/test_loader.py +0 -8
- asgi_cli-0.4.4/src/asgi_cli/tests/test_main.py +0 -35
- asgi_cli-0.4.4/src/asgi_cli/tests/test_parser.py +0 -24
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/LICENSE +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/MANIFEST.in +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/setup.cfg +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/formatter.py +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/py.typed +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli/timing.py +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli.egg-info/dependency_links.txt +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli.egg-info/entry_points.txt +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli.egg-info/top_level.txt +0 -0
- {asgi_cli-0.4.4 → asgi_cli-0.4.6}/src/asgi_cli.egg-info/zip-safe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asgi-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
4
4
|
Summary: Call ASGI Python application from command line, just like CURL
|
|
5
5
|
Author-email: Andriy Kornatskyy <andriy.kornatskyy@live.com>
|
|
6
6
|
License: MIT
|
|
@@ -13,18 +13,18 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
21
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
22
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
23
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
24
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
25
25
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
-
Requires-Python: >=3.
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Dynamic: license-file
|
|
@@ -53,32 +53,30 @@ asgi-cli --help
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
|
-
usage:
|
|
57
|
-
[-
|
|
56
|
+
usage: asgi_cli [-h] [-V] [--app-dir APP_DIR] [-X METHOD] [-H HEADER]
|
|
57
|
+
[-d DATA | -F MULTIPART] [-I | -b | -p | -v]
|
|
58
|
+
[--root-path ROOT_PATH] [-n NUMBER]
|
|
58
59
|
app [url]
|
|
59
60
|
|
|
60
61
|
positional arguments:
|
|
61
62
|
app an application module
|
|
62
63
|
url a uniform resource locator or path (default /)
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
options:
|
|
65
66
|
-h, --help show this help message and exit
|
|
66
67
|
-V, --version show program's version number and exit
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
application/json'
|
|
73
|
-
-d DATA, --data DATA request body data, e.g. '{"msg":"hello"}', 'msg=hello'
|
|
74
|
-
-F MULTIPART, --form MULTIPART
|
|
75
|
-
specify HTTP multipart POST data, e.g. name=value or
|
|
76
|
-
name=@file
|
|
68
|
+
--app-dir APP_DIR look for APP in the specified directory, by adding this to the PYTHONPATH
|
|
69
|
+
-X, --request METHOD specify request method to use, e.g. POST (default GET)
|
|
70
|
+
-H, --header HEADER pass custom header line, e.g. -H='Accept: application/json'
|
|
71
|
+
-d, --data DATA request body data, e.g. '{"msg":"hello"}', 'msg=hello'
|
|
72
|
+
-F, --form MULTIPART specify HTTP multipart POST data, e.g. name=value or name=@file
|
|
77
73
|
-I, --head show status and headers only
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
|
|
74
|
+
--root-path ROOT_PATH
|
|
75
|
+
set the ASGI 'root_path'
|
|
76
|
+
-b, --benchmark issue a number of requests through repeated iterations (reports
|
|
77
|
+
throughtput and average call time)
|
|
78
|
+
-p, --profile prints out a report of top 10 functions ordered by internal time, saves to
|
|
79
|
+
'stats.cprof' file
|
|
82
80
|
-n NUMBER a number of requests to issue (default 100K)
|
|
83
81
|
-v, --verbose make the operation more talkative
|
|
84
82
|
```
|
|
@@ -22,32 +22,30 @@ asgi-cli --help
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
```text
|
|
25
|
-
usage:
|
|
26
|
-
[-
|
|
25
|
+
usage: asgi_cli [-h] [-V] [--app-dir APP_DIR] [-X METHOD] [-H HEADER]
|
|
26
|
+
[-d DATA | -F MULTIPART] [-I | -b | -p | -v]
|
|
27
|
+
[--root-path ROOT_PATH] [-n NUMBER]
|
|
27
28
|
app [url]
|
|
28
29
|
|
|
29
30
|
positional arguments:
|
|
30
31
|
app an application module
|
|
31
32
|
url a uniform resource locator or path (default /)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
options:
|
|
34
35
|
-h, --help show this help message and exit
|
|
35
36
|
-V, --version show program's version number and exit
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
application/json'
|
|
42
|
-
-d DATA, --data DATA request body data, e.g. '{"msg":"hello"}', 'msg=hello'
|
|
43
|
-
-F MULTIPART, --form MULTIPART
|
|
44
|
-
specify HTTP multipart POST data, e.g. name=value or
|
|
45
|
-
name=@file
|
|
37
|
+
--app-dir APP_DIR look for APP in the specified directory, by adding this to the PYTHONPATH
|
|
38
|
+
-X, --request METHOD specify request method to use, e.g. POST (default GET)
|
|
39
|
+
-H, --header HEADER pass custom header line, e.g. -H='Accept: application/json'
|
|
40
|
+
-d, --data DATA request body data, e.g. '{"msg":"hello"}', 'msg=hello'
|
|
41
|
+
-F, --form MULTIPART specify HTTP multipart POST data, e.g. name=value or name=@file
|
|
46
42
|
-I, --head show status and headers only
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
43
|
+
--root-path ROOT_PATH
|
|
44
|
+
set the ASGI 'root_path'
|
|
45
|
+
-b, --benchmark issue a number of requests through repeated iterations (reports
|
|
46
|
+
throughtput and average call time)
|
|
47
|
+
-p, --profile prints out a report of top 10 functions ordered by internal time, saves to
|
|
48
|
+
'stats.cprof' file
|
|
51
49
|
-n NUMBER a number of requests to issue (default 100K)
|
|
52
50
|
-v, --verbose make the operation more talkative
|
|
53
51
|
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "asgi-cli"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Call ASGI Python application from command line, just like CURL"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Andriy Kornatskyy", email = "andriy.kornatskyy@live.com" }]
|
|
13
|
+
keywords = []
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Environment :: Web Environment",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Natural Language :: English",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Python :: 3.15",
|
|
28
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
29
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
30
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
31
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
34
|
+
]
|
|
35
|
+
dependencies = []
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/akornatskyy/asgi-cli"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
asgi-cli = "asgi_cli.main:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools]
|
|
44
|
+
packages = ["asgi_cli"]
|
|
45
|
+
package-dir = { "" = "src" }
|
|
46
|
+
zip-safe = true
|
|
47
|
+
include-package-data = true
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
asgi_cli = ["py.typed"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.dynamic]
|
|
53
|
+
version = { attr = "asgi_cli.__version__" }
|
|
54
|
+
|
|
55
|
+
[tool.coverage.run]
|
|
56
|
+
source = ["src/asgi_cli"]
|
|
57
|
+
omit = ["*/test_*.py", "*/tests/*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.6"
|
|
@@ -13,7 +13,6 @@ default_scope: Scope = {
|
|
|
13
13
|
"asgi": {"version": "3.0", "spec_version": "2.1"},
|
|
14
14
|
"http_version": "1.1",
|
|
15
15
|
"client": ("127.0.0.1", 49327),
|
|
16
|
-
"root_path": "",
|
|
17
16
|
"headers": [
|
|
18
17
|
(b"accept", b"*/*"),
|
|
19
18
|
(b"user-agent", b"asgi-cli/" + __version__.encode("latin-1")),
|
|
@@ -23,11 +22,10 @@ default_scope: Scope = {
|
|
|
23
22
|
|
|
24
23
|
def build_scope(options: Options) -> Scope:
|
|
25
24
|
scope: Scope = copy.deepcopy(default_scope)
|
|
26
|
-
if options.headers_only:
|
|
27
|
-
|
|
28
|
-
options.command = "HEAD"
|
|
25
|
+
if options.headers_only and not options.method:
|
|
26
|
+
options.method = "HEAD"
|
|
29
27
|
|
|
30
|
-
scope["method"] = (options.
|
|
28
|
+
scope["method"] = (options.method or "GET").upper()
|
|
31
29
|
|
|
32
30
|
for line in options.header:
|
|
33
31
|
name, value = line.encode("latin-1").split(b": ", 1)
|
|
@@ -44,6 +42,7 @@ def build_scope(options: Options) -> Scope:
|
|
|
44
42
|
port = default_port[scheme]
|
|
45
43
|
scope["server"] = (host, port)
|
|
46
44
|
scope["headers"].append((b"host", netloc.encode("latin-1")))
|
|
45
|
+
scope["root_path"] = options.root_path
|
|
47
46
|
scope["raw_path"] = urljoin(path, query).encode()
|
|
48
47
|
scope["path"] = unquote(path)
|
|
49
48
|
scope["query_string"] = query.encode()
|
|
@@ -68,27 +67,23 @@ async def gen_chunks(options: Options) -> typing.AsyncIterator[Message]:
|
|
|
68
67
|
"body": b"".join(
|
|
69
68
|
(
|
|
70
69
|
boundary,
|
|
71
|
-
b
|
|
70
|
+
b'\r\nContent-Disposition: form-data; name="',
|
|
72
71
|
name.encode("utf-8"),
|
|
73
72
|
b'"; filename="',
|
|
74
73
|
fn.encode("utf-8"),
|
|
75
|
-
b'"\r\
|
|
74
|
+
b'"\r\nContent-Type: ',
|
|
76
75
|
mime_type.encode("latin-1"),
|
|
77
76
|
b"\r\n\r\n",
|
|
78
77
|
)
|
|
79
78
|
),
|
|
80
79
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"more_body": True,
|
|
89
|
-
"body": data,
|
|
90
|
-
}
|
|
91
|
-
f.close()
|
|
80
|
+
with open(fn, "rb") as f: # noqa: ASYNC230
|
|
81
|
+
while data := f.read(1024):
|
|
82
|
+
yield {
|
|
83
|
+
"type": "http.request",
|
|
84
|
+
"more_body": True,
|
|
85
|
+
"body": data,
|
|
86
|
+
}
|
|
92
87
|
yield {
|
|
93
88
|
"type": "http.request",
|
|
94
89
|
"more_body": True,
|
|
@@ -8,18 +8,18 @@ from asgi_cli.typing import ASGICallable, Headers, Message, Scope
|
|
|
8
8
|
pp = pprint.PrettyPrinter()
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class Response
|
|
11
|
+
class Response:
|
|
12
12
|
def __init__(self) -> None:
|
|
13
13
|
self.status_code: int = 0
|
|
14
14
|
self.headers: Headers = []
|
|
15
|
-
self.chunks:
|
|
15
|
+
self.chunks: list[bytes] = []
|
|
16
16
|
|
|
17
17
|
@property
|
|
18
18
|
def body(self) -> bytes:
|
|
19
19
|
return b"".join(self.chunks)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class Executor
|
|
22
|
+
class Executor:
|
|
23
23
|
def __init__(
|
|
24
24
|
self,
|
|
25
25
|
app: ASGICallable,
|
|
@@ -55,7 +55,7 @@ class Executor(object):
|
|
|
55
55
|
res.headers = message["headers"]
|
|
56
56
|
elif message["type"] == "http.response.body":
|
|
57
57
|
# TODO: more_body
|
|
58
|
-
chunk:
|
|
58
|
+
chunk: bytes | None = message.get("body")
|
|
59
59
|
if chunk is not None:
|
|
60
60
|
res.chunks.append(chunk)
|
|
61
61
|
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import importlib
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
2
4
|
import typing
|
|
3
5
|
|
|
4
6
|
from asgi_cli.typing import ASGICallable
|
|
5
7
|
|
|
6
8
|
|
|
9
|
+
def should_add_to_syspath(app_dir: str) -> bool:
|
|
10
|
+
if not os.path.exists(app_dir):
|
|
11
|
+
return False
|
|
12
|
+
for path in sys.path:
|
|
13
|
+
if os.path.exists(path) and os.path.samefile(app_dir, path):
|
|
14
|
+
return False
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
|
|
7
18
|
def from_string(import_str: str) -> ASGICallable:
|
|
8
19
|
module_str, _, attrs_str = import_str.partition(":")
|
|
9
20
|
if not module_str or not attrs_str:
|
|
@@ -2,16 +2,17 @@ import asyncio
|
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
import traceback
|
|
5
|
-
import typing
|
|
6
5
|
|
|
7
6
|
from asgi_cli import builder, formatter, loader, parser
|
|
8
7
|
from asgi_cli.executor import Executor
|
|
9
8
|
from asgi_cli.typing import Options
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
async def go(args:
|
|
11
|
+
async def go(args: list[str]) -> int:
|
|
13
12
|
try:
|
|
14
13
|
options: Options = parser.parse_options(args)
|
|
14
|
+
if loader.should_add_to_syspath(options.app_dir):
|
|
15
|
+
sys.path.append(options.app_dir)
|
|
15
16
|
app = loader.from_string(options.app)
|
|
16
17
|
scope = builder.build_scope(options)
|
|
17
18
|
executor = Executor(app, scope, builder.gen_chunks(options))
|
|
@@ -32,13 +33,13 @@ async def go(args: typing.List[str]) -> int:
|
|
|
32
33
|
res = await executor.capture_call()
|
|
33
34
|
formatter.print_body(res)
|
|
34
35
|
return 0
|
|
35
|
-
except Exception as err:
|
|
36
|
+
except Exception as err: # noqa: BLE001
|
|
36
37
|
errno: int = getattr(err, "errno", 1)
|
|
37
38
|
traceback.print_exc(file=sys.stderr)
|
|
38
39
|
return errno
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
def main(args:
|
|
42
|
+
def main(args: list[str] | None = None) -> int:
|
|
42
43
|
if args is None: # pragma: nocover
|
|
43
44
|
args = sys.argv[1:]
|
|
44
45
|
sys.path.extend([os.path.abspath(".")])
|
|
@@ -21,8 +21,8 @@ def parse_number(s: str) -> int:
|
|
|
21
21
|
return n
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def parse_options(args:
|
|
25
|
-
parser = argparse.ArgumentParser()
|
|
24
|
+
def parse_options(args: list[str]) -> Options:
|
|
25
|
+
parser = argparse.ArgumentParser(prog="asgi-cli")
|
|
26
26
|
parser.add_argument("app", help="an application module")
|
|
27
27
|
parser.add_argument(
|
|
28
28
|
"url",
|
|
@@ -33,11 +33,18 @@ def parse_options(args: typing.List[str]) -> Options:
|
|
|
33
33
|
parser.add_argument(
|
|
34
34
|
"-V", "--version", action="version", version="%(prog)s " + __version__
|
|
35
35
|
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--app-dir",
|
|
38
|
+
help="look for APP in the specified directory, by "
|
|
39
|
+
"adding this to the PYTHONPATH",
|
|
40
|
+
default="",
|
|
41
|
+
dest="app_dir",
|
|
42
|
+
)
|
|
36
43
|
parser.add_argument(
|
|
37
44
|
"-X",
|
|
38
45
|
"--request",
|
|
39
|
-
help="specify request
|
|
40
|
-
dest="
|
|
46
|
+
help="specify request method to use, e.g. POST (default GET)",
|
|
47
|
+
dest="method",
|
|
41
48
|
)
|
|
42
49
|
parser.add_argument(
|
|
43
50
|
"-H",
|
|
@@ -73,6 +80,12 @@ def parse_options(args: typing.List[str]) -> Options:
|
|
|
73
80
|
default=False,
|
|
74
81
|
dest="headers_only",
|
|
75
82
|
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--root-path",
|
|
85
|
+
help="set the ASGI 'root_path'",
|
|
86
|
+
default="",
|
|
87
|
+
dest="root_path",
|
|
88
|
+
)
|
|
76
89
|
group.add_argument(
|
|
77
90
|
"-b",
|
|
78
91
|
"--benchmark",
|
|
@@ -114,15 +127,15 @@ def parse_options(args: typing.List[str]) -> Options:
|
|
|
114
127
|
)
|
|
115
128
|
return
|
|
116
129
|
if options.data:
|
|
117
|
-
if options.
|
|
118
|
-
options.
|
|
130
|
+
if options.method is None:
|
|
131
|
+
options.method = "POST"
|
|
119
132
|
data = "&".join(options.data)
|
|
120
133
|
if not has_content_type(options.header):
|
|
121
134
|
content_type = guess_content_type(data)
|
|
122
135
|
options.header.append(f"content-type: {content_type}")
|
|
123
136
|
options.data = data.encode("utf-8")
|
|
124
137
|
elif options.multipart:
|
|
125
|
-
options.
|
|
138
|
+
options.method = "POST"
|
|
126
139
|
boundary = "----" + "".join(
|
|
127
140
|
random.choice(string.ascii_letters) for _ in range(12)
|
|
128
141
|
)
|
|
@@ -143,7 +156,7 @@ def guess_content_type(data: str) -> str:
|
|
|
143
156
|
return "application/x-www-form-urlencoded"
|
|
144
157
|
|
|
145
158
|
|
|
146
|
-
def has_content_type(headers:
|
|
159
|
+
def has_content_type(headers: list[str]) -> bool:
|
|
147
160
|
for line in headers:
|
|
148
161
|
name, _ = line.split(": ", 1)
|
|
149
162
|
if name.lower() == "content-type":
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
-
Scope =
|
|
3
|
+
Scope = dict[str, typing.Any]
|
|
4
4
|
Message = typing.MutableMapping[str, typing.Any]
|
|
5
5
|
Receive = typing.Callable[[], typing.Awaitable[Message]]
|
|
6
6
|
Send = typing.Callable[[Message], typing.Awaitable[None]]
|
|
7
7
|
ASGICallable = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
|
|
8
|
-
Headers =
|
|
8
|
+
Headers = list[tuple[bytes, bytes]]
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Options:
|
|
12
|
-
|
|
12
|
+
method: str
|
|
13
|
+
app_dir: str
|
|
13
14
|
headers_only: bool
|
|
14
|
-
header:
|
|
15
|
+
header: list[str]
|
|
15
16
|
data: bytes
|
|
16
|
-
multipart:
|
|
17
|
+
multipart: list[str]
|
|
17
18
|
boundary: bytes
|
|
19
|
+
root_path: str
|
|
18
20
|
benchmark: bool
|
|
19
21
|
profile: bool
|
|
20
22
|
number: int
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asgi-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
4
4
|
Summary: Call ASGI Python application from command line, just like CURL
|
|
5
5
|
Author-email: Andriy Kornatskyy <andriy.kornatskyy@live.com>
|
|
6
6
|
License: MIT
|
|
@@ -13,18 +13,18 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
21
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
22
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
23
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
24
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
25
25
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
-
Requires-Python: >=3.
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Dynamic: license-file
|
|
@@ -53,32 +53,30 @@ asgi-cli --help
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
|
-
usage:
|
|
57
|
-
[-
|
|
56
|
+
usage: asgi_cli [-h] [-V] [--app-dir APP_DIR] [-X METHOD] [-H HEADER]
|
|
57
|
+
[-d DATA | -F MULTIPART] [-I | -b | -p | -v]
|
|
58
|
+
[--root-path ROOT_PATH] [-n NUMBER]
|
|
58
59
|
app [url]
|
|
59
60
|
|
|
60
61
|
positional arguments:
|
|
61
62
|
app an application module
|
|
62
63
|
url a uniform resource locator or path (default /)
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
options:
|
|
65
66
|
-h, --help show this help message and exit
|
|
66
67
|
-V, --version show program's version number and exit
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
application/json'
|
|
73
|
-
-d DATA, --data DATA request body data, e.g. '{"msg":"hello"}', 'msg=hello'
|
|
74
|
-
-F MULTIPART, --form MULTIPART
|
|
75
|
-
specify HTTP multipart POST data, e.g. name=value or
|
|
76
|
-
name=@file
|
|
68
|
+
--app-dir APP_DIR look for APP in the specified directory, by adding this to the PYTHONPATH
|
|
69
|
+
-X, --request METHOD specify request method to use, e.g. POST (default GET)
|
|
70
|
+
-H, --header HEADER pass custom header line, e.g. -H='Accept: application/json'
|
|
71
|
+
-d, --data DATA request body data, e.g. '{"msg":"hello"}', 'msg=hello'
|
|
72
|
+
-F, --form MULTIPART specify HTTP multipart POST data, e.g. name=value or name=@file
|
|
77
73
|
-I, --head show status and headers only
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
|
|
74
|
+
--root-path ROOT_PATH
|
|
75
|
+
set the ASGI 'root_path'
|
|
76
|
+
-b, --benchmark issue a number of requests through repeated iterations (reports
|
|
77
|
+
throughtput and average call time)
|
|
78
|
+
-p, --profile prints out a report of top 10 functions ordered by internal time, saves to
|
|
79
|
+
'stats.cprof' file
|
|
82
80
|
-n NUMBER a number of requests to issue (default 100K)
|
|
83
81
|
-v, --verbose make the operation more talkative
|
|
84
82
|
```
|
|
@@ -17,9 +17,4 @@ src/asgi_cli.egg-info/SOURCES.txt
|
|
|
17
17
|
src/asgi_cli.egg-info/dependency_links.txt
|
|
18
18
|
src/asgi_cli.egg-info/entry_points.txt
|
|
19
19
|
src/asgi_cli.egg-info/top_level.txt
|
|
20
|
-
src/asgi_cli.egg-info/zip-safe
|
|
21
|
-
src/asgi_cli/tests/__init__.py
|
|
22
|
-
src/asgi_cli/tests/test_formatter.py
|
|
23
|
-
src/asgi_cli/tests/test_loader.py
|
|
24
|
-
src/asgi_cli/tests/test_main.py
|
|
25
|
-
src/asgi_cli/tests/test_parser.py
|
|
20
|
+
src/asgi_cli.egg-info/zip-safe
|
asgi_cli-0.4.4/pyproject.toml
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "asgi-cli"
|
|
7
|
-
dynamic = ["version"]
|
|
8
|
-
description = "Call ASGI Python application from command line, just like CURL"
|
|
9
|
-
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
|
-
requires-python = ">=3.10"
|
|
11
|
-
license = {text = "MIT"}
|
|
12
|
-
authors = [
|
|
13
|
-
{name = "Andriy Kornatskyy", email = "andriy.kornatskyy@live.com"}
|
|
14
|
-
]
|
|
15
|
-
keywords = []
|
|
16
|
-
classifiers = [
|
|
17
|
-
"Development Status :: 5 - Production/Stable",
|
|
18
|
-
"Environment :: Console",
|
|
19
|
-
"Environment :: Web Environment",
|
|
20
|
-
"Intended Audience :: Developers",
|
|
21
|
-
"License :: OSI Approved :: MIT License",
|
|
22
|
-
"Natural Language :: English",
|
|
23
|
-
"Operating System :: OS Independent",
|
|
24
|
-
"Programming Language :: Python",
|
|
25
|
-
"Programming Language :: Python :: 3.10",
|
|
26
|
-
"Programming Language :: Python :: 3.11",
|
|
27
|
-
"Programming Language :: Python :: 3.12",
|
|
28
|
-
"Programming Language :: Python :: 3.13",
|
|
29
|
-
"Programming Language :: Python :: 3.14",
|
|
30
|
-
"Programming Language :: Python :: Implementation :: CPython",
|
|
31
|
-
"Programming Language :: Python :: Implementation :: PyPy",
|
|
32
|
-
"Topic :: Internet :: WWW/HTTP",
|
|
33
|
-
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
34
|
-
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
35
|
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
36
|
-
]
|
|
37
|
-
dependencies = []
|
|
38
|
-
|
|
39
|
-
[project.urls]
|
|
40
|
-
Homepage = "https://github.com/akornatskyy/asgi-cli"
|
|
41
|
-
|
|
42
|
-
[project.scripts]
|
|
43
|
-
asgi-cli = "asgi_cli.main:main"
|
|
44
|
-
|
|
45
|
-
[tool.setuptools]
|
|
46
|
-
zip-safe = true
|
|
47
|
-
|
|
48
|
-
[tool.setuptools.dynamic]
|
|
49
|
-
version = {attr = "asgi_cli.__version__"}
|
|
50
|
-
|
|
51
|
-
[tool.setuptools.packages.find]
|
|
52
|
-
where = ["src"]
|
|
53
|
-
|
|
54
|
-
[tool.setuptools.package-data]
|
|
55
|
-
asgi_cli = ["py.typed"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.4"
|
|
File without changes
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
|
|
3
|
-
from asgi_cli.formatter import format_number
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class FormatterTestCase(unittest.TestCase):
|
|
7
|
-
def test_format_number(self) -> None:
|
|
8
|
-
for number, expected in [
|
|
9
|
-
(12345678901, "12,345.68M"),
|
|
10
|
-
(1234567890, "1,234.57M"),
|
|
11
|
-
(123456789.1, "123.46M"),
|
|
12
|
-
(12345678.9, "12.35M"),
|
|
13
|
-
(1234567.89, "1.23M"),
|
|
14
|
-
(123456.789, "123.46K"),
|
|
15
|
-
(12345.6789, "12.35K"),
|
|
16
|
-
(1234.56789, "1.23K"),
|
|
17
|
-
(123.456789, "123.46"),
|
|
18
|
-
(12.3456789, "12.35"),
|
|
19
|
-
(1.23456789, "1.23"),
|
|
20
|
-
(0.12345678, "123.46m"),
|
|
21
|
-
(0.01234567, "12.35m"),
|
|
22
|
-
(0.00123456, "1.23m"),
|
|
23
|
-
(0.00012345, "123.45u"),
|
|
24
|
-
(0.00001234, "12.34u"),
|
|
25
|
-
(0.00000123, "1.23u"),
|
|
26
|
-
(-1234.56789, "-1,234.57"),
|
|
27
|
-
]:
|
|
28
|
-
self.assertEqual(format_number(number), expected)
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
|
|
3
|
-
from asgi_cli.main import main
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class MainTestCase(unittest.TestCase):
|
|
7
|
-
def test_main(self) -> None:
|
|
8
|
-
main(["unknown:app"])
|
|
9
|
-
main(["example:app"])
|
|
10
|
-
main(["-v", "example:app"])
|
|
11
|
-
main(["-d", "", "example:app"])
|
|
12
|
-
main(["-d", "{}", "example:app"])
|
|
13
|
-
main(["example:app", "/receiver"])
|
|
14
|
-
main(["-F", "x=123", "example:app", "/receiver"])
|
|
15
|
-
main(["-F", "fn=@README.md", "example:app", "/receiver"])
|
|
16
|
-
main(["-X", "POST", "-v", "example:app"])
|
|
17
|
-
main(["-X", "POST", "-d", "msg=hello", "example:app"])
|
|
18
|
-
main(["-I", "example:app"])
|
|
19
|
-
main(["-b", "-n", "1K", "example:app"])
|
|
20
|
-
main(["-p", "-n", "1K", "example:app"])
|
|
21
|
-
main(["-H", "Accept: application/json", "example:app"])
|
|
22
|
-
main(["-v", "example:app", "http://example.com/uk/welcome"])
|
|
23
|
-
main(
|
|
24
|
-
[
|
|
25
|
-
"-v",
|
|
26
|
-
"-X",
|
|
27
|
-
"PATCH",
|
|
28
|
-
"-H",
|
|
29
|
-
"Content-Type: application/json",
|
|
30
|
-
"-d",
|
|
31
|
-
"[]",
|
|
32
|
-
"example:app",
|
|
33
|
-
"welcome",
|
|
34
|
-
]
|
|
35
|
-
)
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
|
|
3
|
-
from asgi_cli.parser import parse_number
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ParserTestCase(unittest.TestCase):
|
|
7
|
-
def test_parse_number(self) -> None:
|
|
8
|
-
for s, expected in [
|
|
9
|
-
("12345", 12345),
|
|
10
|
-
("12345678901", 12345678901),
|
|
11
|
-
("1K", 1e3),
|
|
12
|
-
("25K", 25e3),
|
|
13
|
-
("1M", 1e6),
|
|
14
|
-
("10M", 1e7),
|
|
15
|
-
]:
|
|
16
|
-
self.assertEqual(parse_number(s), expected)
|
|
17
|
-
|
|
18
|
-
for s in [
|
|
19
|
-
"1KK",
|
|
20
|
-
"1G",
|
|
21
|
-
"K",
|
|
22
|
-
"M",
|
|
23
|
-
]:
|
|
24
|
-
self.assertRaises(ValueError, lambda s: parse_number(s), s)
|
|
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
|