curlify3 0.7__tar.gz → 0.8__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.
- {curlify3-0.7 → curlify3-0.8}/PKG-INFO +33 -5
- {curlify3-0.7 → curlify3-0.8}/README.md +31 -3
- curlify3-0.8/curlify3/__init__.py +9 -0
- curlify3-0.8/curlify3/_curl.py +140 -0
- {curlify3-0.7 → curlify3-0.8}/pyproject.toml +1 -1
- curlify3-0.7/curlify3/__init__.py +0 -7
- curlify3-0.7/curlify3/_curl.py +0 -91
- {curlify3-0.7 → curlify3-0.8}/.gitignore +0 -0
- {curlify3-0.7 → curlify3-0.8}/curlify3/_base.py +0 -0
- {curlify3-0.7 → curlify3-0.8}/curlify3/_req_aiohttp.py +0 -0
- {curlify3-0.7 → curlify3-0.8}/curlify3/_req_httpx.py +0 -0
- {curlify3-0.7 → curlify3-0.8}/curlify3/_req_httpx2.py +0 -0
- {curlify3-0.7 → curlify3-0.8}/curlify3/_req_requests.py +0 -0
- {curlify3-0.7 → curlify3-0.8}/curlify3/_req_starlette.py +0 -0
- {curlify3-0.7 → curlify3-0.8}/curlify3/_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: curlify3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8
|
|
4
4
|
Summary: yet another library to convert request object to curl command
|
|
5
5
|
Project-URL: Repository, https://github.com/shpaker/curlify3
|
|
6
6
|
Author-email: "A.Shpak" <shpaker@gmail.com>
|
|
@@ -24,6 +24,7 @@ Convert request objects from popular Python HTTP libraries into ready-to-run `cu
|
|
|
24
24
|
- Works with **client-side** requests (`requests`, `httpx`, `httpx2`) and **server-side** incoming requests (`aiohttp.web`, `starlette` / `fastapi`)
|
|
25
25
|
- Faithful rendering of headers, query parameters, cookies (`-b`), and bodies
|
|
26
26
|
- Body payloads: text, JSON, form-encoded, multipart, binary
|
|
27
|
+
- POSIX shell output by default, Windows PowerShell output with `shell="powershell"`
|
|
27
28
|
- Zero runtime dependencies
|
|
28
29
|
|
|
29
30
|
## Installation
|
|
@@ -116,6 +117,31 @@ print(to_curl(req))
|
|
|
116
117
|
|
|
117
118
|
`to_curl_async()` works with `httpx2.Request` too.
|
|
118
119
|
|
|
120
|
+
### Windows PowerShell
|
|
121
|
+
|
|
122
|
+
By default the command is formatted for POSIX shells. Pass `shell="powershell"` to get one that pastes into Windows PowerShell 5.1.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import requests
|
|
126
|
+
from curlify3 import to_curl
|
|
127
|
+
|
|
128
|
+
req = requests.Request(
|
|
129
|
+
"POST",
|
|
130
|
+
"https://httpbin.org/post",
|
|
131
|
+
json={"date": "2026-08-10"},
|
|
132
|
+
).prepare()
|
|
133
|
+
|
|
134
|
+
print(to_curl(req, shell="powershell"))
|
|
135
|
+
# curl.exe --% -X POST -H "content-type: application/json" -d "{\"date\": \"2026-08-10\"}" "https://httpbin.org/post"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`curl.exe` avoids the `Invoke-WebRequest` alias, and `--%` — PowerShell's stop-parsing token — hands the rest to `curl.exe` verbatim. The token is what makes arbitrary JSON survive: without it, 5.1's argument binder re-quotes values by counting every double quote, escaped or not, and mangles the body. Two consequences worth knowing:
|
|
139
|
+
|
|
140
|
+
- `%NAME%` environment-variable references in a payload are still expanded.
|
|
141
|
+
- The command is for PowerShell only — in `cmd`, git-bash, or WSL, `curl.exe` chokes on `--%`; use the default `shell="sh"` output there. On `pwsh` 7.2+, run `$PSNativeCommandArgumentPassing = 'Legacy'` in the session first.
|
|
142
|
+
|
|
143
|
+
The constants `curlify3.SH` and `curlify3.POWERSHELL` are exported for use instead of the raw strings.
|
|
144
|
+
|
|
119
145
|
### `aiohttp` — server-side
|
|
120
146
|
|
|
121
147
|
Render an incoming request inside a handler. The async variant is required because the body is read from the stream.
|
|
@@ -146,15 +172,17 @@ async def echo(request: Request):
|
|
|
146
172
|
|
|
147
173
|
## API
|
|
148
174
|
|
|
149
|
-
### `to_curl(request) -> str`
|
|
175
|
+
### `to_curl(request, shell="sh") -> str`
|
|
150
176
|
|
|
151
177
|
Render a request object as a `curl` command. Use for synchronous request types (`requests.PreparedRequest`, `httpx.Request`, `httpx2.Request`).
|
|
152
178
|
|
|
153
|
-
### `to_curl_async(request) -> str`
|
|
179
|
+
### `to_curl_async(request, shell="sh") -> str`
|
|
154
180
|
|
|
155
181
|
Async variant. Use for server-side request objects whose body must be `await`-ed (`aiohttp.web.Request`, `starlette.requests.Request`) or when you prefer the async pathway for `httpx` / `httpx2`.
|
|
156
182
|
|
|
157
|
-
|
|
183
|
+
`shell` selects the output dialect: `"sh"` (default, POSIX shells) or `"powershell"` (Windows PowerShell 5.1; for `pwsh` 7.2+ see the PowerShell section).
|
|
184
|
+
|
|
185
|
+
Both functions raise `ValueError` if the request type or the `shell` value is not recognized.
|
|
158
186
|
|
|
159
187
|
## Supported request objects
|
|
160
188
|
|
|
@@ -15,6 +15,7 @@ Convert request objects from popular Python HTTP libraries into ready-to-run `cu
|
|
|
15
15
|
- Works with **client-side** requests (`requests`, `httpx`, `httpx2`) and **server-side** incoming requests (`aiohttp.web`, `starlette` / `fastapi`)
|
|
16
16
|
- Faithful rendering of headers, query parameters, cookies (`-b`), and bodies
|
|
17
17
|
- Body payloads: text, JSON, form-encoded, multipart, binary
|
|
18
|
+
- POSIX shell output by default, Windows PowerShell output with `shell="powershell"`
|
|
18
19
|
- Zero runtime dependencies
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
@@ -107,6 +108,31 @@ print(to_curl(req))
|
|
|
107
108
|
|
|
108
109
|
`to_curl_async()` works with `httpx2.Request` too.
|
|
109
110
|
|
|
111
|
+
### Windows PowerShell
|
|
112
|
+
|
|
113
|
+
By default the command is formatted for POSIX shells. Pass `shell="powershell"` to get one that pastes into Windows PowerShell 5.1.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import requests
|
|
117
|
+
from curlify3 import to_curl
|
|
118
|
+
|
|
119
|
+
req = requests.Request(
|
|
120
|
+
"POST",
|
|
121
|
+
"https://httpbin.org/post",
|
|
122
|
+
json={"date": "2026-08-10"},
|
|
123
|
+
).prepare()
|
|
124
|
+
|
|
125
|
+
print(to_curl(req, shell="powershell"))
|
|
126
|
+
# curl.exe --% -X POST -H "content-type: application/json" -d "{\"date\": \"2026-08-10\"}" "https://httpbin.org/post"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`curl.exe` avoids the `Invoke-WebRequest` alias, and `--%` — PowerShell's stop-parsing token — hands the rest to `curl.exe` verbatim. The token is what makes arbitrary JSON survive: without it, 5.1's argument binder re-quotes values by counting every double quote, escaped or not, and mangles the body. Two consequences worth knowing:
|
|
130
|
+
|
|
131
|
+
- `%NAME%` environment-variable references in a payload are still expanded.
|
|
132
|
+
- The command is for PowerShell only — in `cmd`, git-bash, or WSL, `curl.exe` chokes on `--%`; use the default `shell="sh"` output there. On `pwsh` 7.2+, run `$PSNativeCommandArgumentPassing = 'Legacy'` in the session first.
|
|
133
|
+
|
|
134
|
+
The constants `curlify3.SH` and `curlify3.POWERSHELL` are exported for use instead of the raw strings.
|
|
135
|
+
|
|
110
136
|
### `aiohttp` — server-side
|
|
111
137
|
|
|
112
138
|
Render an incoming request inside a handler. The async variant is required because the body is read from the stream.
|
|
@@ -137,15 +163,17 @@ async def echo(request: Request):
|
|
|
137
163
|
|
|
138
164
|
## API
|
|
139
165
|
|
|
140
|
-
### `to_curl(request) -> str`
|
|
166
|
+
### `to_curl(request, shell="sh") -> str`
|
|
141
167
|
|
|
142
168
|
Render a request object as a `curl` command. Use for synchronous request types (`requests.PreparedRequest`, `httpx.Request`, `httpx2.Request`).
|
|
143
169
|
|
|
144
|
-
### `to_curl_async(request) -> str`
|
|
170
|
+
### `to_curl_async(request, shell="sh") -> str`
|
|
145
171
|
|
|
146
172
|
Async variant. Use for server-side request objects whose body must be `await`-ed (`aiohttp.web.Request`, `starlette.requests.Request`) or when you prefer the async pathway for `httpx` / `httpx2`.
|
|
147
173
|
|
|
148
|
-
|
|
174
|
+
`shell` selects the output dialect: `"sh"` (default, POSIX shells) or `"powershell"` (Windows PowerShell 5.1; for `pwsh` 7.2+ see the PowerShell section).
|
|
175
|
+
|
|
176
|
+
Both functions raise `ValueError` if the request type or the `shell` value is not recognized.
|
|
149
177
|
|
|
150
178
|
## Supported request objects
|
|
151
179
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from asyncio import iscoroutine
|
|
4
|
+
from typing import Callable, NamedTuple
|
|
5
|
+
|
|
6
|
+
from curlify3._utils import make_request_obj, make_request_obj_async
|
|
7
|
+
|
|
8
|
+
MULTIPART_FORM_DATA = re.compile(rb'form-data; name="(.[^"]+)"\r\n\r\n(.+)\r\n')
|
|
9
|
+
MULTIPART_FILE_DATA = re.compile(rb'form-data; name="(.[^"]+)"; filename="(.[^"]+)"')
|
|
10
|
+
|
|
11
|
+
PS_QUOTE = re.compile(r'(\\*)"')
|
|
12
|
+
PS_TRAILING_BACKSLASHES = re.compile(r"\\+$")
|
|
13
|
+
|
|
14
|
+
SH = "sh"
|
|
15
|
+
POWERSHELL = "powershell"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def quote_sh(value):
|
|
19
|
+
return f"'{value}'"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def quote_sh_url(url):
|
|
23
|
+
return url if not "&" in url else quote_sh(url)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def quote_sh_cookies(cookies):
|
|
27
|
+
return quote_sh(cookies) if " " in cookies else cookies
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def quote_powershell(value):
|
|
31
|
+
# the command is emitted behind the --% stop-parsing token, so the only parser left is the
|
|
32
|
+
# C runtime of curl.exe: wrap in "...", escape " as \" doubling any run of backslashes
|
|
33
|
+
# directly before it, and double a trailing run so it cannot swallow the closing quote
|
|
34
|
+
value = PS_QUOTE.sub(lambda matched: matched.group(1) * 2 + '\\"', str(value))
|
|
35
|
+
value = PS_TRAILING_BACKSLASHES.sub(lambda matched: matched.group() * 2, value)
|
|
36
|
+
return f'"{value}"'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ShellConfig(NamedTuple):
|
|
40
|
+
binary: str
|
|
41
|
+
args_prefix: str
|
|
42
|
+
quote: Callable
|
|
43
|
+
quote_url: Callable
|
|
44
|
+
quote_cookies: Callable
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
SHELLS = {
|
|
48
|
+
SH: ShellConfig("curl", "", quote_sh, quote_sh_url, quote_sh_cookies),
|
|
49
|
+
# --% is the stop-parsing token: Windows PowerShell 5.1 (the dialect's target) hands
|
|
50
|
+
# everything after it to curl.exe verbatim (only %VAR% references expand), leaving the
|
|
51
|
+
# C runtime as the single parser — 5.1's own argument binder re-quotes by counting every
|
|
52
|
+
# double quote, escaped or not, and corrupts JSON payloads no matter how they are written;
|
|
53
|
+
# pwsh 7.2+ binds even these arguments the new way and needs
|
|
54
|
+
# $PSNativeCommandArgumentPassing = 'Legacy' in the session first (documented in README);
|
|
55
|
+
# the url and cookies are always quoted to keep whitespace safe for the C runtime parser
|
|
56
|
+
POWERSHELL: ShellConfig("curl.exe", "--%", quote_powershell, quote_powershell, quote_powershell),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_curl_headers(headers, quote):
|
|
61
|
+
results = []
|
|
62
|
+
for header, value in headers.items():
|
|
63
|
+
results.append(f"-H {quote(f'{header}: {value}')}")
|
|
64
|
+
return " ".join(results)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def make_curl_cookies(cookies, quote_cookies):
|
|
68
|
+
if not cookies:
|
|
69
|
+
return None
|
|
70
|
+
return f"-b {quote_cookies(cookies)}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def make_multipart_curl_args(body, quote):
|
|
74
|
+
body_parts = []
|
|
75
|
+
body = body.encode() if isinstance(body, str) else body
|
|
76
|
+
for matched in MULTIPART_FORM_DATA.finditer(body):
|
|
77
|
+
groups = matched.groups()
|
|
78
|
+
body_parts.append(f"-F {quote(f'{groups[0].decode()}={groups[1].decode()}')}")
|
|
79
|
+
for matched in MULTIPART_FILE_DATA.finditer(body):
|
|
80
|
+
groups = matched.groups()
|
|
81
|
+
body_parts.append(f"-F {quote(f'{groups[0].decode()}=@{groups[1].decode()}')}")
|
|
82
|
+
return " ".join(body_parts)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def make_curl_body(body, headers, quote):
|
|
86
|
+
if "multipart" in headers.get("content-type", ""):
|
|
87
|
+
return make_multipart_curl_args(body, quote)
|
|
88
|
+
if not body:
|
|
89
|
+
return ""
|
|
90
|
+
return f"-d {quote(body)}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def make_curl_string(method, url, headers, body, cookies, http2=False, shell=SH):
|
|
94
|
+
if shell not in SHELLS:
|
|
95
|
+
raise ValueError(f"unknown shell: {shell!r}, expected one of {sorted(SHELLS)}")
|
|
96
|
+
shell_conf = SHELLS[shell]
|
|
97
|
+
if "content-length" in headers:
|
|
98
|
+
del headers["content-length"]
|
|
99
|
+
if body and isinstance(body, (str, bytes)) and not headers.get("content-type"):
|
|
100
|
+
headers["content-type"] = "plain/text"
|
|
101
|
+
cli_parts = [
|
|
102
|
+
shell_conf.binary,
|
|
103
|
+
shell_conf.args_prefix,
|
|
104
|
+
"--http2" if http2 else None,
|
|
105
|
+
f"-X {method}" if method != "GET" else None,
|
|
106
|
+
make_curl_cookies(cookies, shell_conf.quote_cookies),
|
|
107
|
+
make_curl_headers(headers, shell_conf.quote),
|
|
108
|
+
make_curl_body(body, headers, shell_conf.quote),
|
|
109
|
+
shell_conf.quote_url(url),
|
|
110
|
+
]
|
|
111
|
+
return " ".join([str(entity) for entity in cli_parts if entity])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def to_curl(request, shell=SH):
|
|
115
|
+
data = make_request_obj(request)
|
|
116
|
+
return make_curl_string(
|
|
117
|
+
method=data.method,
|
|
118
|
+
url=data.url,
|
|
119
|
+
headers=data.headers,
|
|
120
|
+
body=data.body(),
|
|
121
|
+
cookies=data.cookies,
|
|
122
|
+
http2=getattr(data, "http2", False),
|
|
123
|
+
shell=shell,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def to_curl_async(request, shell=SH):
|
|
128
|
+
data = make_request_obj_async(request)
|
|
129
|
+
body = data.body()
|
|
130
|
+
if iscoroutine(body):
|
|
131
|
+
body = await body
|
|
132
|
+
return make_curl_string(
|
|
133
|
+
method=data.method,
|
|
134
|
+
url=data.url,
|
|
135
|
+
headers=data.headers,
|
|
136
|
+
body=body,
|
|
137
|
+
cookies=data.cookies,
|
|
138
|
+
http2=getattr(data, "http2", False),
|
|
139
|
+
shell=shell,
|
|
140
|
+
)
|
curlify3-0.7/curlify3/_curl.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
from asyncio import iscoroutine
|
|
4
|
-
|
|
5
|
-
from curlify3._utils import make_request_obj, make_request_obj_async
|
|
6
|
-
|
|
7
|
-
MULTIPART_FORM_DATA = re.compile(rb'form-data; name="(.[^"]+)"\r\n\r\n(.+)\r\n')
|
|
8
|
-
MULTIPART_FILE_DATA = re.compile(rb'form-data; name="(.[^"]+)"; filename="(.[^"]+)"')
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def make_full_url(url) -> str:
|
|
12
|
-
return url if not "&" in url else f"'{url}'"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def make_curl_headers(headers):
|
|
16
|
-
results = []
|
|
17
|
-
for header, value in headers.items():
|
|
18
|
-
results.append(f"-H '{header}: {value}'")
|
|
19
|
-
return " ".join(results)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def make_curl_cookies(cookies):
|
|
23
|
-
if not cookies:
|
|
24
|
-
return None
|
|
25
|
-
if " " in cookies:
|
|
26
|
-
cookies = f"'{cookies}'"
|
|
27
|
-
return f"-b {cookies}"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def make_multipart_curl_args(body):
|
|
31
|
-
body_parts = []
|
|
32
|
-
body = body.encode() if isinstance(body, str) else body
|
|
33
|
-
for matched in MULTIPART_FORM_DATA.finditer(body):
|
|
34
|
-
groups = matched.groups()
|
|
35
|
-
body_parts.append(f"-F '{groups[0].decode()}={groups[1].decode()}'")
|
|
36
|
-
for matched in MULTIPART_FILE_DATA.finditer(body):
|
|
37
|
-
groups = matched.groups()
|
|
38
|
-
body_parts.append(f"-F '{groups[0].decode()}=@{groups[1].decode()}'")
|
|
39
|
-
return " ".join(body_parts)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def make_curl_body(body, headers):
|
|
43
|
-
if "multipart" in headers.get("content-type", ""):
|
|
44
|
-
return make_multipart_curl_args(body)
|
|
45
|
-
if not body:
|
|
46
|
-
return ""
|
|
47
|
-
return f"-d '{body}'"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def make_curl_string(method, url, headers, body, cookies, http2=False):
|
|
51
|
-
if "content-length" in headers:
|
|
52
|
-
del headers["content-length"]
|
|
53
|
-
if body and isinstance(body, (str, bytes)) and not headers.get("content-type"):
|
|
54
|
-
headers["content-type"] = "plain/text"
|
|
55
|
-
cli_parts = [
|
|
56
|
-
f"curl",
|
|
57
|
-
"--http2" if http2 else None,
|
|
58
|
-
f"-X {method}" if method != "GET" else None,
|
|
59
|
-
make_curl_cookies(cookies),
|
|
60
|
-
make_curl_headers(headers),
|
|
61
|
-
make_curl_body(body, headers),
|
|
62
|
-
make_full_url(url),
|
|
63
|
-
]
|
|
64
|
-
return " ".join([str(entity) for entity in cli_parts if entity])
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def to_curl(request):
|
|
68
|
-
data = make_request_obj(request)
|
|
69
|
-
return make_curl_string(
|
|
70
|
-
method=data.method,
|
|
71
|
-
url=data.url,
|
|
72
|
-
headers=data.headers,
|
|
73
|
-
body=data.body(),
|
|
74
|
-
cookies=data.cookies,
|
|
75
|
-
http2=getattr(data, "http2", False),
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
async def to_curl_async(request):
|
|
80
|
-
data = make_request_obj_async(request)
|
|
81
|
-
body = data.body()
|
|
82
|
-
if iscoroutine(body):
|
|
83
|
-
body = await body
|
|
84
|
-
return make_curl_string(
|
|
85
|
-
method=data.method,
|
|
86
|
-
url=data.url,
|
|
87
|
-
headers=data.headers,
|
|
88
|
-
body=body,
|
|
89
|
-
cookies=data.cookies,
|
|
90
|
-
http2=getattr(data, "http2", False),
|
|
91
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|