curlify3 0.8__tar.gz → 0.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: curlify3
3
- Version: 0.8
3
+ Version: 0.9
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>
@@ -25,6 +25,7 @@ Convert request objects from popular Python HTTP libraries into ready-to-run `cu
25
25
  - Faithful rendering of headers, query parameters, cookies (`-b`), and bodies
26
26
  - Body payloads: text, JSON, form-encoded, multipart, binary
27
27
  - POSIX shell output by default, Windows PowerShell output with `shell="powershell"`
28
+ - One-line output by default, multi-line with `pretty=True` and long option names with `long_options=True`
28
29
  - Zero runtime dependencies
29
30
 
30
31
  ## Installation
@@ -117,6 +118,31 @@ print(to_curl(req))
117
118
 
118
119
  `to_curl_async()` works with `httpx2.Request` too.
119
120
 
121
+ ### Readable output
122
+
123
+ `pretty=True` puts every option on its own line, and `long_options=True` spells the options out (`--header` instead of `-H`). They are independent, so either can be used alone.
124
+
125
+ ```python
126
+ import requests
127
+ from curlify3 import to_curl
128
+
129
+ req = requests.Request(
130
+ "POST",
131
+ "https://httpbin.org/post",
132
+ json={"date": "2026-08-10"},
133
+ ).prepare()
134
+
135
+ print(to_curl(req, pretty=True, long_options=True))
136
+ # curl https://httpbin.org/post \
137
+ # --request POST \
138
+ # --header 'content-type: application/json' \
139
+ # --data '{"date": "2026-08-10"}'
140
+ ```
141
+
142
+ The url moves to the first line, where `curl` reads it just as well as in the trailing position — the same layout Chrome DevTools' "Copy as cURL" produces. A request with no options stays on one line.
143
+
144
+ `pretty=True` is rejected with a `ValueError` for `shell="powershell"`: the `--%` token that dialect relies on is effective only until the next newline, and a backtick cannot extend it, so a multi-line command would be passed to `curl.exe` in pieces.
145
+
120
146
  ### Windows PowerShell
121
147
 
122
148
  By default the command is formatted for POSIX shells. Pass `shell="powershell"` to get one that pastes into Windows PowerShell 5.1.
@@ -172,17 +198,17 @@ async def echo(request: Request):
172
198
 
173
199
  ## API
174
200
 
175
- ### `to_curl(request, shell="sh") -> str`
201
+ ### `to_curl(request, shell="sh", pretty=False, long_options=False) -> str`
176
202
 
177
203
  Render a request object as a `curl` command. Use for synchronous request types (`requests.PreparedRequest`, `httpx.Request`, `httpx2.Request`).
178
204
 
179
- ### `to_curl_async(request, shell="sh") -> str`
205
+ ### `to_curl_async(request, shell="sh", pretty=False, long_options=False) -> str`
180
206
 
181
207
  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
208
 
183
- `shell` selects the output dialect: `"sh"` (default, POSIX shells) or `"powershell"` (Windows PowerShell 5.1; for `pwsh` 7.2+ see the PowerShell section).
209
+ `shell` selects the output dialect: `"sh"` (default, POSIX shells) or `"powershell"` (Windows PowerShell 5.1; for `pwsh` 7.2+ see the PowerShell section). `pretty` breaks the command across lines, `long_options` spells the options out; both default to `False`, which keeps the output on a single line with short options.
184
210
 
185
- Both functions raise `ValueError` if the request type or the `shell` value is not recognized.
211
+ Both functions raise `ValueError` if the request type or the `shell` value is not recognized, or if `pretty=True` is combined with `shell="powershell"`.
186
212
 
187
213
  ## Supported request objects
188
214
 
@@ -16,6 +16,7 @@ Convert request objects from popular Python HTTP libraries into ready-to-run `cu
16
16
  - Faithful rendering of headers, query parameters, cookies (`-b`), and bodies
17
17
  - Body payloads: text, JSON, form-encoded, multipart, binary
18
18
  - POSIX shell output by default, Windows PowerShell output with `shell="powershell"`
19
+ - One-line output by default, multi-line with `pretty=True` and long option names with `long_options=True`
19
20
  - Zero runtime dependencies
20
21
 
21
22
  ## Installation
@@ -108,6 +109,31 @@ print(to_curl(req))
108
109
 
109
110
  `to_curl_async()` works with `httpx2.Request` too.
110
111
 
112
+ ### Readable output
113
+
114
+ `pretty=True` puts every option on its own line, and `long_options=True` spells the options out (`--header` instead of `-H`). They are independent, so either can be used alone.
115
+
116
+ ```python
117
+ import requests
118
+ from curlify3 import to_curl
119
+
120
+ req = requests.Request(
121
+ "POST",
122
+ "https://httpbin.org/post",
123
+ json={"date": "2026-08-10"},
124
+ ).prepare()
125
+
126
+ print(to_curl(req, pretty=True, long_options=True))
127
+ # curl https://httpbin.org/post \
128
+ # --request POST \
129
+ # --header 'content-type: application/json' \
130
+ # --data '{"date": "2026-08-10"}'
131
+ ```
132
+
133
+ The url moves to the first line, where `curl` reads it just as well as in the trailing position — the same layout Chrome DevTools' "Copy as cURL" produces. A request with no options stays on one line.
134
+
135
+ `pretty=True` is rejected with a `ValueError` for `shell="powershell"`: the `--%` token that dialect relies on is effective only until the next newline, and a backtick cannot extend it, so a multi-line command would be passed to `curl.exe` in pieces.
136
+
111
137
  ### Windows PowerShell
112
138
 
113
139
  By default the command is formatted for POSIX shells. Pass `shell="powershell"` to get one that pastes into Windows PowerShell 5.1.
@@ -163,17 +189,17 @@ async def echo(request: Request):
163
189
 
164
190
  ## API
165
191
 
166
- ### `to_curl(request, shell="sh") -> str`
192
+ ### `to_curl(request, shell="sh", pretty=False, long_options=False) -> str`
167
193
 
168
194
  Render a request object as a `curl` command. Use for synchronous request types (`requests.PreparedRequest`, `httpx.Request`, `httpx2.Request`).
169
195
 
170
- ### `to_curl_async(request, shell="sh") -> str`
196
+ ### `to_curl_async(request, shell="sh", pretty=False, long_options=False) -> str`
171
197
 
172
198
  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
199
 
174
- `shell` selects the output dialect: `"sh"` (default, POSIX shells) or `"powershell"` (Windows PowerShell 5.1; for `pwsh` 7.2+ see the PowerShell section).
200
+ `shell` selects the output dialect: `"sh"` (default, POSIX shells) or `"powershell"` (Windows PowerShell 5.1; for `pwsh` 7.2+ see the PowerShell section). `pretty` breaks the command across lines, `long_options` spells the options out; both default to `False`, which keeps the output on a single line with short options.
175
201
 
176
- Both functions raise `ValueError` if the request type or the `shell` value is not recognized.
202
+ Both functions raise `ValueError` if the request type or the `shell` value is not recognized, or if `pretty=True` is combined with `shell="powershell"`.
177
203
 
178
204
  ## Supported request objects
179
205
 
@@ -1,6 +1,6 @@
1
1
  from curlify3._curl import POWERSHELL, SH, to_curl, to_curl_async
2
2
 
3
- __version__ = "0.8"
3
+ __version__ = "0.9"
4
4
  __all__ = [
5
5
  "POWERSHELL",
6
6
  "SH",
@@ -1,7 +1,7 @@
1
1
  import re
2
2
 
3
3
  from asyncio import iscoroutine
4
- from typing import Callable, NamedTuple
4
+ from typing import Callable, NamedTuple, Optional
5
5
 
6
6
  from curlify3._utils import make_request_obj, make_request_obj_async
7
7
 
@@ -14,6 +14,21 @@ PS_TRAILING_BACKSLASHES = re.compile(r"\\+$")
14
14
  SH = "sh"
15
15
  POWERSHELL = "powershell"
16
16
 
17
+ SHORT_OPTIONS = {
18
+ "request": "-X",
19
+ "header": "-H",
20
+ "cookie": "-b",
21
+ "data": "-d",
22
+ "form": "-F",
23
+ }
24
+ LONG_OPTIONS = {
25
+ "request": "--request",
26
+ "header": "--header",
27
+ "cookie": "--cookie",
28
+ "data": "--data",
29
+ "form": "--form",
30
+ }
31
+
17
32
 
18
33
  def quote_sh(value):
19
34
  return f"'{value}'"
@@ -42,10 +57,12 @@ class ShellConfig(NamedTuple):
42
57
  quote: Callable
43
58
  quote_url: Callable
44
59
  quote_cookies: Callable
60
+ # what separates arguments in pretty mode, None when the shell cannot span lines
61
+ pretty_separator: Optional[str]
45
62
 
46
63
 
47
64
  SHELLS = {
48
- SH: ShellConfig("curl", "", quote_sh, quote_sh_url, quote_sh_cookies),
65
+ SH: ShellConfig("curl", "", quote_sh, quote_sh_url, quote_sh_cookies, " \\\n "),
49
66
  # --% is the stop-parsing token: Windows PowerShell 5.1 (the dialect's target) hands
50
67
  # everything after it to curl.exe verbatim (only %VAR% references expand), leaving the
51
68
  # C runtime as the single parser — 5.1's own argument binder re-quotes by counting every
@@ -53,65 +70,72 @@ SHELLS = {
53
70
  # pwsh 7.2+ binds even these arguments the new way and needs
54
71
  # $PSNativeCommandArgumentPassing = 'Legacy' in the session first (documented in README);
55
72
  # 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),
73
+ # pretty is impossible here: --% is effective only until the next newline and the line
74
+ # continuation character (`) cannot extend it (about_Parsing), so a multi-line command
75
+ # would pass the backtick to curl.exe and run the next line on its own
76
+ POWERSHELL: ShellConfig("curl.exe", "--%", quote_powershell, quote_powershell, quote_powershell, None),
57
77
  }
58
78
 
59
79
 
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)
80
+ def make_curl_headers(headers, quote, options):
81
+ option = options["header"]
82
+ return [f"{option} {quote(f'{header}: {value}')}" for header, value in headers.items()]
65
83
 
66
84
 
67
- def make_curl_cookies(cookies, quote_cookies):
85
+ def make_curl_cookies(cookies, quote_cookies, options):
68
86
  if not cookies:
69
- return None
70
- return f"-b {quote_cookies(cookies)}"
87
+ return []
88
+ return [f"{options['cookie']} {quote_cookies(cookies)}"]
71
89
 
72
90
 
73
- def make_multipart_curl_args(body, quote):
91
+ def make_multipart_curl_args(body, quote, options):
92
+ option = options["form"]
74
93
  body_parts = []
75
94
  body = body.encode() if isinstance(body, str) else body
76
95
  for matched in MULTIPART_FORM_DATA.finditer(body):
77
96
  groups = matched.groups()
78
- body_parts.append(f"-F {quote(f'{groups[0].decode()}={groups[1].decode()}')}")
97
+ body_parts.append(f"{option} {quote(f'{groups[0].decode()}={groups[1].decode()}')}")
79
98
  for matched in MULTIPART_FILE_DATA.finditer(body):
80
99
  groups = matched.groups()
81
- body_parts.append(f"-F {quote(f'{groups[0].decode()}=@{groups[1].decode()}')}")
82
- return " ".join(body_parts)
100
+ body_parts.append(f"{option} {quote(f'{groups[0].decode()}=@{groups[1].decode()}')}")
101
+ return body_parts
83
102
 
84
103
 
85
- def make_curl_body(body, headers, quote):
104
+ def make_curl_body(body, headers, quote, options):
86
105
  if "multipart" in headers.get("content-type", ""):
87
- return make_multipart_curl_args(body, quote)
106
+ return make_multipart_curl_args(body, quote, options)
88
107
  if not body:
89
- return ""
90
- return f"-d {quote(body)}"
108
+ return []
109
+ return [f"{options['data']} {quote(body)}"]
91
110
 
92
111
 
93
- def make_curl_string(method, url, headers, body, cookies, http2=False, shell=SH):
112
+ def make_curl_string(method, url, headers, body, cookies, http2=False, shell=SH, pretty=False, long_options=False):
94
113
  if shell not in SHELLS:
95
114
  raise ValueError(f"unknown shell: {shell!r}, expected one of {sorted(SHELLS)}")
96
115
  shell_conf = SHELLS[shell]
116
+ if pretty and shell_conf.pretty_separator is None:
117
+ raise ValueError(f"pretty output is not supported for shell: {shell!r}")
118
+ options = LONG_OPTIONS if long_options else SHORT_OPTIONS
97
119
  if "content-length" in headers:
98
120
  del headers["content-length"]
99
121
  if body and isinstance(body, (str, bytes)) and not headers.get("content-type"):
100
122
  headers["content-type"] = "plain/text"
101
- cli_parts = [
102
- shell_conf.binary,
103
- shell_conf.args_prefix,
123
+ args = [
104
124
  "--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),
125
+ f"{options['request']} {method}" if method != "GET" else None,
126
+ *make_curl_cookies(cookies, shell_conf.quote_cookies, options),
127
+ *make_curl_headers(headers, shell_conf.quote, options),
128
+ *make_curl_body(body, headers, shell_conf.quote, options),
110
129
  ]
111
- return " ".join([str(entity) for entity in cli_parts if entity])
130
+ args = [str(entity) for entity in args if entity]
131
+ command = " ".join([part for part in (shell_conf.binary, shell_conf.args_prefix) if part])
132
+ url = shell_conf.quote_url(url)
133
+ if pretty:
134
+ return shell_conf.pretty_separator.join([f"{command} {url}", *args])
135
+ return " ".join([command, *args, url])
112
136
 
113
137
 
114
- def to_curl(request, shell=SH):
138
+ def to_curl(request, shell=SH, pretty=False, long_options=False):
115
139
  data = make_request_obj(request)
116
140
  return make_curl_string(
117
141
  method=data.method,
@@ -121,10 +145,12 @@ def to_curl(request, shell=SH):
121
145
  cookies=data.cookies,
122
146
  http2=getattr(data, "http2", False),
123
147
  shell=shell,
148
+ pretty=pretty,
149
+ long_options=long_options,
124
150
  )
125
151
 
126
152
 
127
- async def to_curl_async(request, shell=SH):
153
+ async def to_curl_async(request, shell=SH, pretty=False, long_options=False):
128
154
  data = make_request_obj_async(request)
129
155
  body = data.body()
130
156
  if iscoroutine(body):
@@ -137,4 +163,6 @@ async def to_curl_async(request, shell=SH):
137
163
  cookies=data.cookies,
138
164
  http2=getattr(data, "http2", False),
139
165
  shell=shell,
166
+ pretty=pretty,
167
+ long_options=long_options,
140
168
  )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "curlify3"
3
- version = "0.8"
3
+ version = "0.9"
4
4
  description = "yet another library to convert request object to curl command"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes