curlify3 0.6__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.8/PKG-INFO ADDED
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.5
2
+ Name: curlify3
3
+ Version: 0.8
4
+ Summary: yet another library to convert request object to curl command
5
+ Project-URL: Repository, https://github.com/shpaker/curlify3
6
+ Author-email: "A.Shpak" <shpaker@gmail.com>
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+
10
+ # curlify3
11
+
12
+ Convert request objects from popular Python HTTP libraries into ready-to-run `curl` commands.
13
+
14
+ [![PyPI](https://img.shields.io/pypi/v/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
15
+ [![Downloads](https://img.shields.io/pypi/dm/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
16
+ [![Python](https://img.shields.io/pypi/pyversions/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
17
+ [![Tests](https://github.com/shpaker/curlify3/actions/workflows/tests.yml/badge.svg)](https://github.com/shpaker/curlify3/actions/workflows/tests.yml)
18
+
19
+ `curlify3` takes a request object from any supported client or server framework and renders it as an equivalent `curl` command — useful for logging, debugging, sharing reproductions, and copy-pasting from your IDE into a terminal.
20
+
21
+ ## Features
22
+
23
+ - Single dispatch entrypoint — `to_curl()` (sync) and `to_curl_async()` (async)
24
+ - Works with **client-side** requests (`requests`, `httpx`, `httpx2`) and **server-side** incoming requests (`aiohttp.web`, `starlette` / `fastapi`)
25
+ - Faithful rendering of headers, query parameters, cookies (`-b`), and bodies
26
+ - Body payloads: text, JSON, form-encoded, multipart, binary
27
+ - POSIX shell output by default, Windows PowerShell output with `shell="powershell"`
28
+ - Zero runtime dependencies
29
+
30
+ ## Installation
31
+
32
+ ```sh
33
+ pip install curlify3
34
+ ```
35
+
36
+ Requires **Python 3.10+**.
37
+
38
+ ## Comparison with `curlify` and `curlify2`
39
+
40
+ | | [`curlify`](https://pypi.org/project/curlify/) | [`curlify2`](https://pypi.org/project/curlify2/) | **`curlify3`** |
41
+ | --- | :---: | :---: | :---: |
42
+ | `requests` | [x] | [x] | [x] |
43
+ | `httpx` | [ ] | [x] | [x] |
44
+ | `httpx2` (HTTP/2) | [ ] | [ ] | [x] |
45
+ | `aiohttp` (server-side) | [ ] | [ ] | [x] |
46
+ | `starlette` / `fastapi` (server-side) | [ ] | [ ] | [x] |
47
+ | Async API | [ ] | [ ] | [x] |
48
+ | Python | 3.7+ | 3.7–3.11 | 3.10+ |
49
+
50
+ `curlify` is the original and covers only `requests`. `curlify2` added `httpx` but is sync-only, client-side-only, and has not seen a release since 2023. `curlify3` extends the same idea with HTTP/2 (`httpx2`), an async entrypoint, and server-side adapters for `aiohttp` and `starlette` / `fastapi` so you can dump incoming requests as `curl` from inside a handler.
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ import requests
56
+ from curlify3 import to_curl
57
+
58
+ response = requests.get("https://httpbin.org/get")
59
+ print(to_curl(response.request))
60
+ # curl -H 'user-agent: python-requests/2.32.3' -H 'accept-encoding: gzip, deflate' \
61
+ # -H 'accept: */*' -H 'connection: keep-alive' https://httpbin.org/get
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### `requests`
67
+
68
+ ```python
69
+ import requests
70
+ from curlify3 import to_curl
71
+
72
+ req = requests.Request(
73
+ "POST",
74
+ "https://httpbin.org/post",
75
+ json={"hello": "world"},
76
+ ).prepare()
77
+
78
+ print(to_curl(req))
79
+ ```
80
+
81
+ ### `httpx` (sync)
82
+
83
+ ```python
84
+ import httpx
85
+ from curlify3 import to_curl
86
+
87
+ req = httpx.Request("POST", "https://httpbin.org/post", json={"hello": "world"})
88
+ print(to_curl(req))
89
+ ```
90
+
91
+ ### `httpx` (async)
92
+
93
+ ```python
94
+ import asyncio
95
+ import httpx
96
+ from curlify3 import to_curl_async
97
+
98
+ async def main():
99
+ req = httpx.Request("POST", "https://httpbin.org/post", json={"a": 1})
100
+ print(await to_curl_async(req))
101
+
102
+ asyncio.run(main())
103
+ ```
104
+
105
+ ### `httpx2` — HTTP/2
106
+
107
+ The generated command includes `--http2`.
108
+
109
+ ```python
110
+ import httpx2
111
+ from curlify3 import to_curl
112
+
113
+ req = httpx2.Request("GET", "https://httpbin.org/get")
114
+ print(to_curl(req))
115
+ # curl --http2 -H 'host: httpbin.org' https://httpbin.org/get
116
+ ```
117
+
118
+ `to_curl_async()` works with `httpx2.Request` too.
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
+
145
+ ### `aiohttp` — server-side
146
+
147
+ Render an incoming request inside a handler. The async variant is required because the body is read from the stream.
148
+
149
+ ```python
150
+ from aiohttp import web
151
+ from curlify3 import to_curl_async
152
+
153
+ async def handler(request: web.Request) -> web.Response:
154
+ curl = await to_curl_async(request)
155
+ print(curl)
156
+ return web.json_response({"ok": True})
157
+ ```
158
+
159
+ ### `starlette` / `fastapi` — server-side
160
+
161
+ ```python
162
+ from fastapi import FastAPI, Request
163
+ from curlify3 import to_curl_async
164
+
165
+ app = FastAPI()
166
+
167
+ @app.post("/echo")
168
+ async def echo(request: Request):
169
+ curl = await to_curl_async(request)
170
+ return {"curl": curl}
171
+ ```
172
+
173
+ ## API
174
+
175
+ ### `to_curl(request, shell="sh") -> str`
176
+
177
+ Render a request object as a `curl` command. Use for synchronous request types (`requests.PreparedRequest`, `httpx.Request`, `httpx2.Request`).
178
+
179
+ ### `to_curl_async(request, shell="sh") -> str`
180
+
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`.
182
+
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.
186
+
187
+ ## Supported request objects
188
+
189
+ | Library | Type | `to_curl` | `to_curl_async` | Notes |
190
+ | --- | --- | :---: | :---: | --- |
191
+ | `requests` | `PreparedRequest` | [x] | [ ] | Pass `Request(...).prepare()` |
192
+ | `httpx` | `httpx.Request` | [x] | [x] | |
193
+ | `httpx2` | `httpx2.Request` | [x] | [x] | Adds `--http2` |
194
+ | `aiohttp` | `aiohttp.web.Request` | [ ] | [x] | Server-side |
195
+ | `starlette` / `fastapi` | `starlette.requests.Request` | [ ] | [x] | Server-side |
196
+
197
+ ## Payload handling
198
+
199
+ | Payload | Rendered as |
200
+ | --- | --- |
201
+ | Plain text | `-d 'text'` |
202
+ | JSON | `-d '{"k":"v"}'` with `content-type: application/json` |
203
+ | Form-encoded | `-d 'k=v&k2=v2'` with `content-type: application/x-www-form-urlencoded` |
204
+ | Multipart / files | `-F 'field=@file' -F 'other=value'` |
205
+ | Binary | `-d` with raw bytes (falls back when not UTF-8) |
206
+ | Cookies | `-b 'k=v'` (lifted out of `Cookie` header) |
207
+ | Headers | `-H 'name: value'` (lowercased) |
208
+
209
+ `Content-Length` is dropped. If a body is present without `Content-Type`, `content-type: plain/text` is added so `curl` does not guess.
210
+
211
+ ## Development
212
+
213
+ The project uses [`uv`](https://docs.astral.sh/uv/) and [`just`](https://just.systems/).
214
+
215
+ ```sh
216
+ uv sync --group dev
217
+ just tests # uv run pytest -vv
218
+ just fmt # isort + black
219
+ ```
220
+
221
+ CI runs the test suite on Python 3.10–3.14.
222
+
223
+ ## Changelog
224
+
225
+ See [CHANGELOG.md](CHANGELOG.md).
curlify3-0.8/README.md ADDED
@@ -0,0 +1,216 @@
1
+ # curlify3
2
+
3
+ Convert request objects from popular Python HTTP libraries into ready-to-run `curl` commands.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
6
+ [![Downloads](https://img.shields.io/pypi/dm/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
7
+ [![Python](https://img.shields.io/pypi/pyversions/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
8
+ [![Tests](https://github.com/shpaker/curlify3/actions/workflows/tests.yml/badge.svg)](https://github.com/shpaker/curlify3/actions/workflows/tests.yml)
9
+
10
+ `curlify3` takes a request object from any supported client or server framework and renders it as an equivalent `curl` command — useful for logging, debugging, sharing reproductions, and copy-pasting from your IDE into a terminal.
11
+
12
+ ## Features
13
+
14
+ - Single dispatch entrypoint — `to_curl()` (sync) and `to_curl_async()` (async)
15
+ - Works with **client-side** requests (`requests`, `httpx`, `httpx2`) and **server-side** incoming requests (`aiohttp.web`, `starlette` / `fastapi`)
16
+ - Faithful rendering of headers, query parameters, cookies (`-b`), and bodies
17
+ - Body payloads: text, JSON, form-encoded, multipart, binary
18
+ - POSIX shell output by default, Windows PowerShell output with `shell="powershell"`
19
+ - Zero runtime dependencies
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ pip install curlify3
25
+ ```
26
+
27
+ Requires **Python 3.10+**.
28
+
29
+ ## Comparison with `curlify` and `curlify2`
30
+
31
+ | | [`curlify`](https://pypi.org/project/curlify/) | [`curlify2`](https://pypi.org/project/curlify2/) | **`curlify3`** |
32
+ | --- | :---: | :---: | :---: |
33
+ | `requests` | [x] | [x] | [x] |
34
+ | `httpx` | [ ] | [x] | [x] |
35
+ | `httpx2` (HTTP/2) | [ ] | [ ] | [x] |
36
+ | `aiohttp` (server-side) | [ ] | [ ] | [x] |
37
+ | `starlette` / `fastapi` (server-side) | [ ] | [ ] | [x] |
38
+ | Async API | [ ] | [ ] | [x] |
39
+ | Python | 3.7+ | 3.7–3.11 | 3.10+ |
40
+
41
+ `curlify` is the original and covers only `requests`. `curlify2` added `httpx` but is sync-only, client-side-only, and has not seen a release since 2023. `curlify3` extends the same idea with HTTP/2 (`httpx2`), an async entrypoint, and server-side adapters for `aiohttp` and `starlette` / `fastapi` so you can dump incoming requests as `curl` from inside a handler.
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ import requests
47
+ from curlify3 import to_curl
48
+
49
+ response = requests.get("https://httpbin.org/get")
50
+ print(to_curl(response.request))
51
+ # curl -H 'user-agent: python-requests/2.32.3' -H 'accept-encoding: gzip, deflate' \
52
+ # -H 'accept: */*' -H 'connection: keep-alive' https://httpbin.org/get
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### `requests`
58
+
59
+ ```python
60
+ import requests
61
+ from curlify3 import to_curl
62
+
63
+ req = requests.Request(
64
+ "POST",
65
+ "https://httpbin.org/post",
66
+ json={"hello": "world"},
67
+ ).prepare()
68
+
69
+ print(to_curl(req))
70
+ ```
71
+
72
+ ### `httpx` (sync)
73
+
74
+ ```python
75
+ import httpx
76
+ from curlify3 import to_curl
77
+
78
+ req = httpx.Request("POST", "https://httpbin.org/post", json={"hello": "world"})
79
+ print(to_curl(req))
80
+ ```
81
+
82
+ ### `httpx` (async)
83
+
84
+ ```python
85
+ import asyncio
86
+ import httpx
87
+ from curlify3 import to_curl_async
88
+
89
+ async def main():
90
+ req = httpx.Request("POST", "https://httpbin.org/post", json={"a": 1})
91
+ print(await to_curl_async(req))
92
+
93
+ asyncio.run(main())
94
+ ```
95
+
96
+ ### `httpx2` — HTTP/2
97
+
98
+ The generated command includes `--http2`.
99
+
100
+ ```python
101
+ import httpx2
102
+ from curlify3 import to_curl
103
+
104
+ req = httpx2.Request("GET", "https://httpbin.org/get")
105
+ print(to_curl(req))
106
+ # curl --http2 -H 'host: httpbin.org' https://httpbin.org/get
107
+ ```
108
+
109
+ `to_curl_async()` works with `httpx2.Request` too.
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
+
136
+ ### `aiohttp` — server-side
137
+
138
+ Render an incoming request inside a handler. The async variant is required because the body is read from the stream.
139
+
140
+ ```python
141
+ from aiohttp import web
142
+ from curlify3 import to_curl_async
143
+
144
+ async def handler(request: web.Request) -> web.Response:
145
+ curl = await to_curl_async(request)
146
+ print(curl)
147
+ return web.json_response({"ok": True})
148
+ ```
149
+
150
+ ### `starlette` / `fastapi` — server-side
151
+
152
+ ```python
153
+ from fastapi import FastAPI, Request
154
+ from curlify3 import to_curl_async
155
+
156
+ app = FastAPI()
157
+
158
+ @app.post("/echo")
159
+ async def echo(request: Request):
160
+ curl = await to_curl_async(request)
161
+ return {"curl": curl}
162
+ ```
163
+
164
+ ## API
165
+
166
+ ### `to_curl(request, shell="sh") -> str`
167
+
168
+ Render a request object as a `curl` command. Use for synchronous request types (`requests.PreparedRequest`, `httpx.Request`, `httpx2.Request`).
169
+
170
+ ### `to_curl_async(request, shell="sh") -> str`
171
+
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`.
173
+
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.
177
+
178
+ ## Supported request objects
179
+
180
+ | Library | Type | `to_curl` | `to_curl_async` | Notes |
181
+ | --- | --- | :---: | :---: | --- |
182
+ | `requests` | `PreparedRequest` | [x] | [ ] | Pass `Request(...).prepare()` |
183
+ | `httpx` | `httpx.Request` | [x] | [x] | |
184
+ | `httpx2` | `httpx2.Request` | [x] | [x] | Adds `--http2` |
185
+ | `aiohttp` | `aiohttp.web.Request` | [ ] | [x] | Server-side |
186
+ | `starlette` / `fastapi` | `starlette.requests.Request` | [ ] | [x] | Server-side |
187
+
188
+ ## Payload handling
189
+
190
+ | Payload | Rendered as |
191
+ | --- | --- |
192
+ | Plain text | `-d 'text'` |
193
+ | JSON | `-d '{"k":"v"}'` with `content-type: application/json` |
194
+ | Form-encoded | `-d 'k=v&k2=v2'` with `content-type: application/x-www-form-urlencoded` |
195
+ | Multipart / files | `-F 'field=@file' -F 'other=value'` |
196
+ | Binary | `-d` with raw bytes (falls back when not UTF-8) |
197
+ | Cookies | `-b 'k=v'` (lifted out of `Cookie` header) |
198
+ | Headers | `-H 'name: value'` (lowercased) |
199
+
200
+ `Content-Length` is dropped. If a body is present without `Content-Type`, `content-type: plain/text` is added so `curl` does not guess.
201
+
202
+ ## Development
203
+
204
+ The project uses [`uv`](https://docs.astral.sh/uv/) and [`just`](https://just.systems/).
205
+
206
+ ```sh
207
+ uv sync --group dev
208
+ just tests # uv run pytest -vv
209
+ just fmt # isort + black
210
+ ```
211
+
212
+ CI runs the test suite on Python 3.10–3.14.
213
+
214
+ ## Changelog
215
+
216
+ See [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,9 @@
1
+ from curlify3._curl import POWERSHELL, SH, to_curl, to_curl_async
2
+
3
+ __version__ = "0.8"
4
+ __all__ = [
5
+ "POWERSHELL",
6
+ "SH",
7
+ "to_curl",
8
+ "to_curl_async",
9
+ ]
@@ -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
+ )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "curlify3"
3
- version = "0.6"
3
+ version = "0.8"
4
4
  description = "yet another library to convert request object to curl command"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
curlify3-0.6/PKG-INFO DELETED
@@ -1,61 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: curlify3
3
- Version: 0.6
4
- Summary: yet another library to convert request object to curl command
5
- Project-URL: Repository, https://github.com/shpaker/curlify3
6
- Author-email: "A.Shpak" <shpaker@gmail.com>
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
-
10
- # yet another library to convert request object to curl command
11
-
12
- [![PyPI](https://img.shields.io/pypi/v/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
13
- [![PyPI](https://img.shields.io/pypi/dm/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
14
-
15
- ### Support request's objects from:
16
-
17
- - requests
18
- - httpx
19
- - [httpx2](https://github.com/pydantic/httpx2) (adds `--http2` to the curl)
20
- - aiohttp server
21
- - starlette/fastapi
22
-
23
- Requires Python 3.10+.
24
-
25
- ## Installation
26
-
27
- ```sh
28
- pip install curlify3
29
- ```
30
-
31
- ## Example
32
-
33
- ```py
34
- from curlify3 import to_curl
35
- import requests
36
-
37
- response = requests.get("http://google.ru")
38
- print(to_curl(response.request))
39
- # curl -H 'user-agent: python-requests/2.32.3' -H 'accept-encoding: gzip, deflate' -H 'accept: */*' -H 'connection: keep-alive' http://www.google.ru/
40
- ```
41
-
42
- ### httpx2 (HTTP/2)
43
-
44
- ```py
45
- import httpx2
46
- from curlify3 import to_curl
47
-
48
- req = httpx2.Request("GET", "https://httpbin.org/get")
49
- print(to_curl(req))
50
- # curl --http2 -H 'host: httpbin.org' https://httpbin.org/get
51
- ```
52
-
53
- ### async
54
-
55
- ```py
56
- import asyncio, httpx
57
- from curlify3 import to_curl_async
58
-
59
- req = httpx.Request("POST", "https://httpbin.org/post", json={"a": 1})
60
- print(asyncio.run(to_curl_async(req)))
61
- ```
curlify3-0.6/README.md DELETED
@@ -1,52 +0,0 @@
1
- # yet another library to convert request object to curl command
2
-
3
- [![PyPI](https://img.shields.io/pypi/v/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
4
- [![PyPI](https://img.shields.io/pypi/dm/curlify3.svg)](https://pypi.python.org/pypi/curlify3)
5
-
6
- ### Support request's objects from:
7
-
8
- - requests
9
- - httpx
10
- - [httpx2](https://github.com/pydantic/httpx2) (adds `--http2` to the curl)
11
- - aiohttp server
12
- - starlette/fastapi
13
-
14
- Requires Python 3.10+.
15
-
16
- ## Installation
17
-
18
- ```sh
19
- pip install curlify3
20
- ```
21
-
22
- ## Example
23
-
24
- ```py
25
- from curlify3 import to_curl
26
- import requests
27
-
28
- response = requests.get("http://google.ru")
29
- print(to_curl(response.request))
30
- # curl -H 'user-agent: python-requests/2.32.3' -H 'accept-encoding: gzip, deflate' -H 'accept: */*' -H 'connection: keep-alive' http://www.google.ru/
31
- ```
32
-
33
- ### httpx2 (HTTP/2)
34
-
35
- ```py
36
- import httpx2
37
- from curlify3 import to_curl
38
-
39
- req = httpx2.Request("GET", "https://httpbin.org/get")
40
- print(to_curl(req))
41
- # curl --http2 -H 'host: httpbin.org' https://httpbin.org/get
42
- ```
43
-
44
- ### async
45
-
46
- ```py
47
- import asyncio, httpx
48
- from curlify3 import to_curl_async
49
-
50
- req = httpx.Request("POST", "https://httpbin.org/post", json={"a": 1})
51
- print(asyncio.run(to_curl_async(req)))
52
- ```
@@ -1,7 +0,0 @@
1
- from curlify3._curl import to_curl, to_curl_async
2
-
3
- __version__ = "0.6"
4
- __all__ = [
5
- "to_curl",
6
- "to_curl_async",
7
- ]
@@ -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