Skip to content

Commit 955c576

Browse files
authored
CLI - Add auth, headers & timeout flags (#13)
* split on `:` or `=` for `auth` and `headers` * update version and Readme
1 parent 8b4b44b commit 955c576

File tree

7 files changed

+210
-23
lines changed

7 files changed

+210
-23
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ Check CLI usage
2929

3030
```
3131
❯ csv2http --help
32-
usage: csv2http [-h] [-c CONCURRENCY] [--method {POST,PATCH,PUT}] [-d] [-n] file url
32+
usage: csv2http [-h] [-c CONCURRENCY] [--method {POST,PATCH,PUT}] [-a AUTH] [-H [HEADER ...]] [-d] [-n] [-t TIMEOUT] file url
3333
34-
HTTP request for every row of a CSV file - v0.0.2a
34+
HTTP request for every row of a CSV file - v0.0.3a
3535
3636
positional arguments:
3737
file payload csv file
@@ -43,8 +43,13 @@ options:
4343
Maximum number of concurrent requests (default: 25)
4444
--method {POST,PATCH,PUT}
4545
HTTP method/verb (default: POST)
46+
-a AUTH, --auth AUTH Basic Authentication enter <USERNAME>:<PASSWORD>. If password is blank you will be prompted for input
47+
-H [HEADER ...], --header [HEADER ...]
48+
Header `key:value` pairs
4649
-d, --form-data Send payload as form encoded data instead of JSON (default: false)
4750
-n, --no-save Do not save results to log file (default: false)
51+
-t TIMEOUT, --timeout TIMEOUT
52+
Connection timeout of the request in seconds (default: 5)
4853
```
4954

5055
### Mockbin Example
@@ -75,6 +80,28 @@ Use the returned bin id from before.
7580
Check the bin log from.
7681
https://mockbin.org/bin/9e95289e-d048-4515-9a61-07f2c74810f5/log
7782

83+
### Set Auth and Headers
84+
85+
Header key, value pairs can be set with the `-H` or `-header` flag.
86+
87+
Key value pairs should be separated with either a `:` or `=`.
88+
89+
```
90+
csv2http my_file.csv httpbin.org/post -H user-agent:csv2http-cli x-custom-header=foobar
91+
```
92+
93+
To provide basic auth pass a username and password with `-a` or `--auth`.
94+
95+
If the password is omitted you will be prompted to provide it.
96+
97+
```
98+
--auth my_username:my_password
99+
```
100+
101+
```
102+
--auth my_username
103+
```
104+
78105
## Roadmap
79106

80107
- [x] As Library - Alpha

csv2http/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""_version"""
2-
__version__ = "0.0.2a1"
2+
__version__ = "0.0.3a"

csv2http/cli.py

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44
"""
55
import argparse
66
import pathlib
7-
from typing import Literal, NamedTuple, Union
7+
import re
8+
from typing import Literal, NamedTuple, Optional, Union
89

9-
from httpx import URL
10+
from httpx import URL, Headers
1011

1112
from csv2http._version import __version__
1213

1314
SUPPORTED_METHODS = ["POST", "PATCH", "PUT"]
14-
1515
CONCURRENCY_DEFAULT = 25
16+
TIMEOUT_DEFAULT = 5
17+
18+
_SPLIT_REGEX = r"[:=]"
19+
20+
21+
def _get_input(prompt: str):
22+
return input(prompt)
1623

1724

1825
def _normalize_url(value: Union[str, URL]) -> URL:
@@ -23,20 +30,22 @@ def _normalize_url(value: Union[str, URL]) -> URL:
2330
return url
2431

2532

26-
class Args(NamedTuple):
27-
"""Expected user Args."""
33+
def _resolve_auth(value: str) -> Union[tuple[str, str], tuple[None, None]]:
34+
"""
35+
Parse username & password. Prompt for password if not provided.
36+
"""
37+
username, *extras = re.split(_SPLIT_REGEX, value, maxsplit=1)
38+
password = " ".join(extras) if extras else _get_input("password:")
39+
return username, password
2840

29-
file: pathlib.Path
30-
url: Union[URL, str]
31-
concurrency: int
32-
method: Literal["POST", "PATCH", "PUT"]
33-
form_data: bool = False
34-
save_log: bool = True
35-
# verbose: bool = True
3641

42+
def _parse_header(value: str) -> tuple[str, str]:
43+
"""Splits string on `:` or `=` and returns a tuple of key, value."""
44+
key, value = re.split(_SPLIT_REGEX, value, maxsplit=1)
45+
return key, value
3746

38-
def get_args() -> Args:
39-
"""Get user args from the command line."""
47+
48+
def _bootstrap_parser() -> argparse.ArgumentParser:
4049
parser = argparse.ArgumentParser(
4150
description=f"HTTP request for every row of a CSV file - v{__version__}"
4251
)
@@ -59,6 +68,16 @@ def get_args() -> Args:
5968
default="POST",
6069
choices=SUPPORTED_METHODS,
6170
)
71+
parser.add_argument(
72+
"-a",
73+
"--auth",
74+
help="Basic Authentication enter <USERNAME>:<PASSWORD>."
75+
" If password is blank you will be prompted for input",
76+
type=_resolve_auth,
77+
)
78+
parser.add_argument(
79+
"-H", "--header", help="Header `key:value` pairs", nargs="*", type=_parse_header
80+
)
6281
parser.add_argument(
6382
"-d",
6483
"--form-data",
@@ -71,18 +90,53 @@ def get_args() -> Args:
7190
help="Do not save results to log file (default: false)",
7291
action="store_true",
7392
)
93+
parser.add_argument(
94+
"-t",
95+
"--timeout",
96+
help=f"Connection timeout of each request in seconds (default: {TIMEOUT_DEFAULT})",
97+
default=TIMEOUT_DEFAULT,
98+
type=int,
99+
)
74100
# parser.add_argument(
75-
# "-v", "--verbose", help="verbose stdout logging", default=False, type=bool
101+
# "-v",
102+
# "--verbose",
103+
# help="verbose stdout logging",
104+
# action="store_true",
76105
# )
106+
return parser
107+
108+
109+
class Args(NamedTuple):
110+
"""Expected user Args."""
77111

78-
args = parser.parse_args()
112+
file: pathlib.Path
113+
url: Union[URL, str]
114+
concurrency: int
115+
method: Literal["POST", "PATCH", "PUT"]
116+
auth: Optional[tuple[str, str]] = None
117+
headers: Optional[Headers] = None
118+
form_data: bool = False
119+
save_log: bool = True
120+
timeout: int = TIMEOUT_DEFAULT
121+
# verbose: bool = False
122+
123+
124+
_PARSER = _bootstrap_parser()
125+
126+
127+
def get_args() -> Args:
128+
"""Get user args from the command line."""
129+
args = _PARSER.parse_args()
79130
return Args(
80131
args.file,
81132
args.url,
82133
args.concurrency,
83134
args.method,
135+
args.auth,
136+
Headers(args.header),
84137
args.form_data,
85-
not args.no_save
138+
not args.no_save,
139+
args.timeout,
86140
# args.verbose,
87141
)
88142

csv2http/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ async def execute(args: cli.Args) -> int:
9292
content = "data" if args.form_data else "json"
9393
log_file = _add_timestamp_and_suffix(file_input, "log")
9494

95-
async with httpx.AsyncClient() as client_session:
95+
async with httpx.AsyncClient(
96+
auth=args.auth, headers=args.headers, timeout=args.timeout
97+
) as client_session:
9698

9799
print(f" {args.method} {args.url}")
98100

tests/test_cli.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import base64
2+
import pathlib
3+
14
import pytest
25
from httpx import URL
36

@@ -16,5 +19,98 @@ def test_normalize_ulr(url, expected):
1619
assert URL(expected) == cli._normalize_url(url)
1720

1821

22+
@pytest.mark.parametrize(
23+
"auth_input,expected",
24+
[
25+
("foo:bar", ("foo", "bar")),
26+
("foo=bar", ("foo", "bar")),
27+
("fizz", ("fizz", "fake_input")),
28+
("nopassword:", ("nopassword", "")),
29+
("nopassword=", ("nopassword", "")),
30+
# also not valid
31+
("", ("", "fake_input")),
32+
],
33+
)
34+
def test_resolve_auth(monkeypatch, auth_input, expected):
35+
monkeypatch.setattr(cli, "_get_input", lambda x: "fake_input", raising=True)
36+
37+
assert expected == cli._resolve_auth(auth_input)
38+
39+
40+
@pytest.mark.parametrize(
41+
"headers_input,expected",
42+
[
43+
("foo:bar", ("foo", "bar")),
44+
("foo=bar", ("foo", "bar")),
45+
(
46+
"Authorization=Bearer MY.JWT.TOKEN",
47+
("Authorization", "Bearer MY.JWT.TOKEN"),
48+
),
49+
(
50+
# ensure base64 padding is not removed
51+
f"foo={base64.standard_b64encode(b'bar64').decode('utf-8')}",
52+
("foo", base64.standard_b64encode(b"bar64").decode("utf-8")),
53+
),
54+
],
55+
)
56+
def test_pase_header(headers_input, expected):
57+
assert expected == cli._parse_header(headers_input)
58+
59+
60+
@pytest.mark.parametrize(
61+
"args,expected",
62+
[
63+
# minimal example
64+
(
65+
["myfile.csv", "example.com"],
66+
{
67+
"auth": None,
68+
"concurrency": cli.CONCURRENCY_DEFAULT,
69+
"file": pathlib.Path("myfile.csv"),
70+
"form_data": False,
71+
"header": None,
72+
"method": "POST",
73+
"no_save": False,
74+
"url": URL("https://example.com"),
75+
"timeout": cli.TIMEOUT_DEFAULT,
76+
},
77+
),
78+
(
79+
[
80+
"myfile.csv",
81+
"example.com",
82+
"-H",
83+
"x-foo:bar",
84+
"fizz=buzz",
85+
"-c",
86+
"23",
87+
"--auth",
88+
"bigetti:password",
89+
"--timeout",
90+
"3",
91+
],
92+
{
93+
"auth": ("bigetti", "password"),
94+
"concurrency": 23,
95+
"file": pathlib.Path("myfile.csv"),
96+
"form_data": False,
97+
"header": [
98+
("x-foo", "bar"),
99+
("fizz", "buzz"),
100+
],
101+
"method": "POST",
102+
"no_save": False,
103+
"url": URL("https://example.com"),
104+
"timeout": 3,
105+
},
106+
),
107+
],
108+
)
109+
def test_get_args(args, expected):
110+
parser = cli._bootstrap_parser()
111+
112+
assert expected == vars(parser.parse_args(args))
113+
114+
19115
if __name__ == "__main__":
20116
pytest.main(["-vv"])

tests/test_csv2http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33

44
def test_version():
5-
assert __version__ == "0.0.2a1"
5+
assert __version__ == "0.0.3a"

tests/test_utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import ast
2+
13
import httpx
24
import pytest
35
from respx import MockResponse
@@ -19,9 +21,15 @@
1921
)
2022
def test_summarize_responses(responses):
2123
summary = utils.summarize_responses(responses)
22-
print(summary)
2324
assert summary
2425

26+
_, status_codes = summary.split(" - ")
27+
status_codes_dict = ast.literal_eval(status_codes)
28+
print(status_codes_dict)
29+
30+
for r in responses:
31+
assert getattr(r, "status_code", r.__class__.__name__) in status_codes_dict
32+
2533

2634
if __name__ == "__main__":
2735
pytest.main(["-vv"])

0 commit comments

Comments
 (0)