curlipie 0.0.0__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.
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: curlipie
3
+ Version: 0.0.0
4
+ Summary: Library to convert cURL command line to HTTPie
5
+ Keywords: api,cli,conversion,curl,http,httpie
6
+ Author-Email: =?utf-8?b?Tmd1eeG7hW4gSOG7k25nIFF1w6Ju?= <ng.hong.quan@gmail.com>
7
+ Maintainer-Email: =?utf-8?b?Tmd1eeG7hW4gSOG7k25nIFF1w6Ju?= <ng.hong.quan@gmail.com>
8
+ License-Expression: MPL-2.0
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Topic :: Internet :: WWW/HTTP
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Project-URL: repository, https://github.com/hongquan/CurliPie.git
15
+ Requires-Python: <4.0,>=3.14
16
+ Requires-Dist: click>=8.5.0
17
+ Requires-Dist: first>=2.0.2
18
+ Requires-Dist: http-constants>=0.5.0
19
+ Requires-Dist: kiss-headers>=2.5.0
20
+ Requires-Dist: logbook>=1.10.1
21
+ Requires-Dist: multidict>=6.9.1
22
+ Requires-Dist: orjson>=3.12.0
23
+ Requires-Dist: pydantic>=2.13.5
24
+ Requires-Dist: pydantic-settings>=2.15.0
25
+ Requires-Dist: typed-argument-parser>=1.12.0
26
+ Requires-Dist: yarl>=1.25.1
27
+ Description-Content-Type: text/x-rst
28
+
29
+ ========
30
+ CurliPie
31
+ ========
32
+
33
+ .. image:: https://madewithlove.vercel.app/vn?heart=true&colorA=%23ffcd00&colorB=%23da251d
34
+ .. image:: https://badgen.net/pypi/v/curlipie
35
+ :target: https://pypi.org/project/curlipie
36
+
37
+ Python library to convert `cURL`_ command to `HTTPie`_.
38
+
39
+ It will convert
40
+
41
+ .. code-block:: sh
42
+
43
+ curl -d name=admin -d shoesize=12 -d color=green&food=wet http://quan.hoabinh.vn
44
+
45
+ to
46
+
47
+ .. code-block:: sh
48
+
49
+ http -f http://quan.hoabinh.vn name=admin shoesize=12 color=green food=wet
50
+
51
+
52
+ Motivation
53
+ ----------
54
+
55
+ This library was born when I joined a project with a team of non-Linux, non-Python developers. Because the project didn't have proper documentation, the other team often shared API usage example to me in form of cURL command, generated from their daily-used Postman. Those cURL commands are usually ugly, like this:
56
+
57
+
58
+ .. code-block:: sh
59
+
60
+ curl --location --request POST 'http://app-staging.dev/api' \
61
+ --header 'Content-Type: application/json' \
62
+ --data-raw '{
63
+ "userId": "abc-xyz",
64
+ "planAmount": 50000,
65
+ "isPromotion": false,
66
+ "createdAt": "2019-12-13 10:00:00"
67
+ }'
68
+
69
+ I am more comfortable with HTTPie (shorter syntax, has highlighting and is a Python application), so I often converted it to HTTPie:
70
+
71
+ .. code-block:: sh
72
+
73
+ http -F app-staging.dev/api userId=abc-xyz planAmount:=50000 isPromotion:=false createdAt='2019-12-13 10:00:00'
74
+
75
+ Though Postman can generate HTTPie, it does result in even uglier command:
76
+
77
+ .. code-block:: sh
78
+
79
+ printf '{
80
+ "userId": "abc-xyz",
81
+ "planAmount": 50000,
82
+ "isPromotion": false,
83
+ "createdAt": "2019-12-13 10:00:00"
84
+ }'| http --follow --timeout 3600 POST app-staging.dev/api \
85
+ Content-Type:'application/json'
86
+
87
+ Initially, I had to do conversion manually and quickly got tired from it. I tried to find a conversion tool but failed. There is an online tool `curl2httpie.online`_, but it failed with above example. So I decide to write my own tool.
88
+
89
+ I don't bother to help fix the online tool above, because it is written in Go. The rich ecosystem of Python, with these built-in libraries, enable me to finish the job fast:
90
+
91
+ - |shlex|_: Help parse the command line in form of shell language, handle the string escaping, quoting for me.
92
+ - |argparse|_: Help parse cURL options and arguments. Note that, cURL arguments syntax follow GNU style, which is common in Linux (and Python) world but not popular in Go world (see `this tutorial <go_tutorial_>`_), so it feels more natural with Python.
93
+
94
+
95
+ Usage
96
+ -----
97
+
98
+ Command-line
99
+ ~~~~~~~~~~~~
100
+
101
+ When installed, CurliPie registers a ``curlipie`` command:
102
+
103
+ .. code-block:: sh
104
+
105
+ # Pass the cURL command as an argument
106
+ curlipie "curl -X POST http://api.example.com -d 'name=admin'"
107
+
108
+ # Or pipe from stdin (handy with clipboard tools)
109
+ echo "curl http://example.com --user admin:secret" | curlipie
110
+
111
+ # Run with no arguments to enter interactive mode — paste your cURL command
112
+ # (including multi-line backslash-continued commands from Postman or Swagger),
113
+ # then press Enter twice or Ctrl-D to convert
114
+ curlipie
115
+
116
+ # Use long-form HTTPie options instead of short flags
117
+ curlipie --long "curl -L -X DELETE http://api.example.com/users/1"
118
+
119
+ # Show help
120
+ curlipie --help
121
+
122
+ Python library
123
+ ~~~~~~~~~~~~~~
124
+
125
+ .. code-block:: python
126
+
127
+ >>> from curlipie import curl_to_httpie
128
+
129
+ >>> curl = """curl -XPUT elastic.dev/movies/_doc/1 -d '{"director": "Burton, Tim", "year": 1996, "title": "Mars Attacks!"}' -H 'Content-Type: application/json'"""
130
+
131
+ >>> curl_to_httpie(curl)
132
+ ConversionResult(httpie="http PUT elastic.dev/movies/_doc/1 director='Burton, Tim' year:=1996 title='Mars Attacks!'", errors=[])
133
+
134
+ >>> result = curl_to_httpie(curl)
135
+
136
+ >>> result.httpie
137
+ "http PUT elastic.dev/movies/_doc/1 director='Burton, Tim' year:=1996 title='Mars Attacks!'"
138
+
139
+
140
+ Online tool
141
+ -----------
142
+
143
+ CurliPie is not very usable if it stays in library form, so I made an online tool for you to use it quickly:
144
+
145
+ https://curlipie.open-api.vn
146
+
147
+ The site also provide HTTP API for you to develop a client for it.
148
+
149
+
150
+ Development
151
+ -----------
152
+
153
+ This repo contains three components:
154
+
155
+ - Python library ``curlipie``. This is the one `published`_ to PyPI.
156
+
157
+ - An API server built with `FastAPI`_, playing role of backend for `curlipie.open-api.vn`_.
158
+
159
+ - A minimal frontend app built with `AlpineJS`_ and `EncreCSS`_ (CSS class names following `TailwindCSS`_ convention).
160
+
161
+ - Python dependencies are managed with `uv`_.
162
+
163
+ To try running on localhost:
164
+
165
+ - Run backend with:
166
+
167
+ .. code-block:: sh
168
+
169
+ uvicorn api.main:app
170
+
171
+ - The front-end are just static files, served by backend also, so you can access it via http://localhost:8000/. The CSS is generated depending on which CSS classes are used.
172
+
173
+ .. code-block:: sh
174
+
175
+ ./tools/generate-css.sh
176
+
177
+
178
+ Unit test:
179
+
180
+ .. code-block:: sh
181
+
182
+ pytest
183
+
184
+
185
+ Credit
186
+ ------
187
+
188
+ Brought to you by `Nguyễn Hồng Quân <author_>`_.
189
+
190
+
191
+ .. _cURL: https://curl.haxx.se
192
+ .. _HTTPie: https://httpie.org
193
+ .. _curl2httpie.online: https://curl2httpie.online/
194
+ .. |shlex| replace:: ``shlex``
195
+ .. _shlex: https://docs.python.org/3/library/shlex.html
196
+ .. |argparse| replace:: ``argparse``
197
+ .. _argparse: https://docs.python.org/3/library/argparse.html
198
+ .. _go_tutorial: https://gobyexample.com/command-line-flags
199
+ .. _published: https://pypi.org/project/curlipie/
200
+ .. _fastapi: https://github.com/tiangolo/fastapi
201
+ .. _curlipie.open-api.vn: https://curlipie.open-api.vn/
202
+ .. _vuejs: https://vuejs.org/
203
+ .. _alpinejs: https://github.com/alpinejs/alpine
204
+ .. _encrecss: https://encrecss.uk.to
205
+ .. _tailwindcss: https://tailwindcss.com
206
+ .. _uv: https://docs.astral.sh/uv/
207
+ .. _author: https://quan.hoabinh.vn
@@ -0,0 +1,179 @@
1
+ ========
2
+ CurliPie
3
+ ========
4
+
5
+ .. image:: https://madewithlove.vercel.app/vn?heart=true&colorA=%23ffcd00&colorB=%23da251d
6
+ .. image:: https://badgen.net/pypi/v/curlipie
7
+ :target: https://pypi.org/project/curlipie
8
+
9
+ Python library to convert `cURL`_ command to `HTTPie`_.
10
+
11
+ It will convert
12
+
13
+ .. code-block:: sh
14
+
15
+ curl -d name=admin -d shoesize=12 -d color=green&food=wet http://quan.hoabinh.vn
16
+
17
+ to
18
+
19
+ .. code-block:: sh
20
+
21
+ http -f http://quan.hoabinh.vn name=admin shoesize=12 color=green food=wet
22
+
23
+
24
+ Motivation
25
+ ----------
26
+
27
+ This library was born when I joined a project with a team of non-Linux, non-Python developers. Because the project didn't have proper documentation, the other team often shared API usage example to me in form of cURL command, generated from their daily-used Postman. Those cURL commands are usually ugly, like this:
28
+
29
+
30
+ .. code-block:: sh
31
+
32
+ curl --location --request POST 'http://app-staging.dev/api' \
33
+ --header 'Content-Type: application/json' \
34
+ --data-raw '{
35
+ "userId": "abc-xyz",
36
+ "planAmount": 50000,
37
+ "isPromotion": false,
38
+ "createdAt": "2019-12-13 10:00:00"
39
+ }'
40
+
41
+ I am more comfortable with HTTPie (shorter syntax, has highlighting and is a Python application), so I often converted it to HTTPie:
42
+
43
+ .. code-block:: sh
44
+
45
+ http -F app-staging.dev/api userId=abc-xyz planAmount:=50000 isPromotion:=false createdAt='2019-12-13 10:00:00'
46
+
47
+ Though Postman can generate HTTPie, it does result in even uglier command:
48
+
49
+ .. code-block:: sh
50
+
51
+ printf '{
52
+ "userId": "abc-xyz",
53
+ "planAmount": 50000,
54
+ "isPromotion": false,
55
+ "createdAt": "2019-12-13 10:00:00"
56
+ }'| http --follow --timeout 3600 POST app-staging.dev/api \
57
+ Content-Type:'application/json'
58
+
59
+ Initially, I had to do conversion manually and quickly got tired from it. I tried to find a conversion tool but failed. There is an online tool `curl2httpie.online`_, but it failed with above example. So I decide to write my own tool.
60
+
61
+ I don't bother to help fix the online tool above, because it is written in Go. The rich ecosystem of Python, with these built-in libraries, enable me to finish the job fast:
62
+
63
+ - |shlex|_: Help parse the command line in form of shell language, handle the string escaping, quoting for me.
64
+ - |argparse|_: Help parse cURL options and arguments. Note that, cURL arguments syntax follow GNU style, which is common in Linux (and Python) world but not popular in Go world (see `this tutorial <go_tutorial_>`_), so it feels more natural with Python.
65
+
66
+
67
+ Usage
68
+ -----
69
+
70
+ Command-line
71
+ ~~~~~~~~~~~~
72
+
73
+ When installed, CurliPie registers a ``curlipie`` command:
74
+
75
+ .. code-block:: sh
76
+
77
+ # Pass the cURL command as an argument
78
+ curlipie "curl -X POST http://api.example.com -d 'name=admin'"
79
+
80
+ # Or pipe from stdin (handy with clipboard tools)
81
+ echo "curl http://example.com --user admin:secret" | curlipie
82
+
83
+ # Run with no arguments to enter interactive mode — paste your cURL command
84
+ # (including multi-line backslash-continued commands from Postman or Swagger),
85
+ # then press Enter twice or Ctrl-D to convert
86
+ curlipie
87
+
88
+ # Use long-form HTTPie options instead of short flags
89
+ curlipie --long "curl -L -X DELETE http://api.example.com/users/1"
90
+
91
+ # Show help
92
+ curlipie --help
93
+
94
+ Python library
95
+ ~~~~~~~~~~~~~~
96
+
97
+ .. code-block:: python
98
+
99
+ >>> from curlipie import curl_to_httpie
100
+
101
+ >>> curl = """curl -XPUT elastic.dev/movies/_doc/1 -d '{"director": "Burton, Tim", "year": 1996, "title": "Mars Attacks!"}' -H 'Content-Type: application/json'"""
102
+
103
+ >>> curl_to_httpie(curl)
104
+ ConversionResult(httpie="http PUT elastic.dev/movies/_doc/1 director='Burton, Tim' year:=1996 title='Mars Attacks!'", errors=[])
105
+
106
+ >>> result = curl_to_httpie(curl)
107
+
108
+ >>> result.httpie
109
+ "http PUT elastic.dev/movies/_doc/1 director='Burton, Tim' year:=1996 title='Mars Attacks!'"
110
+
111
+
112
+ Online tool
113
+ -----------
114
+
115
+ CurliPie is not very usable if it stays in library form, so I made an online tool for you to use it quickly:
116
+
117
+ https://curlipie.open-api.vn
118
+
119
+ The site also provide HTTP API for you to develop a client for it.
120
+
121
+
122
+ Development
123
+ -----------
124
+
125
+ This repo contains three components:
126
+
127
+ - Python library ``curlipie``. This is the one `published`_ to PyPI.
128
+
129
+ - An API server built with `FastAPI`_, playing role of backend for `curlipie.open-api.vn`_.
130
+
131
+ - A minimal frontend app built with `AlpineJS`_ and `EncreCSS`_ (CSS class names following `TailwindCSS`_ convention).
132
+
133
+ - Python dependencies are managed with `uv`_.
134
+
135
+ To try running on localhost:
136
+
137
+ - Run backend with:
138
+
139
+ .. code-block:: sh
140
+
141
+ uvicorn api.main:app
142
+
143
+ - The front-end are just static files, served by backend also, so you can access it via http://localhost:8000/. The CSS is generated depending on which CSS classes are used.
144
+
145
+ .. code-block:: sh
146
+
147
+ ./tools/generate-css.sh
148
+
149
+
150
+ Unit test:
151
+
152
+ .. code-block:: sh
153
+
154
+ pytest
155
+
156
+
157
+ Credit
158
+ ------
159
+
160
+ Brought to you by `Nguyễn Hồng Quân <author_>`_.
161
+
162
+
163
+ .. _cURL: https://curl.haxx.se
164
+ .. _HTTPie: https://httpie.org
165
+ .. _curl2httpie.online: https://curl2httpie.online/
166
+ .. |shlex| replace:: ``shlex``
167
+ .. _shlex: https://docs.python.org/3/library/shlex.html
168
+ .. |argparse| replace:: ``argparse``
169
+ .. _argparse: https://docs.python.org/3/library/argparse.html
170
+ .. _go_tutorial: https://gobyexample.com/command-line-flags
171
+ .. _published: https://pypi.org/project/curlipie/
172
+ .. _fastapi: https://github.com/tiangolo/fastapi
173
+ .. _curlipie.open-api.vn: https://curlipie.open-api.vn/
174
+ .. _vuejs: https://vuejs.org/
175
+ .. _alpinejs: https://github.com/alpinejs/alpine
176
+ .. _encrecss: https://encrecss.uk.to
177
+ .. _tailwindcss: https://tailwindcss.com
178
+ .. _uv: https://docs.astral.sh/uv/
179
+ .. _author: https://quan.hoabinh.vn
@@ -0,0 +1,4 @@
1
+ from .pie import curl_to_httpie, ConversionResult # NOQA
2
+
3
+
4
+ __version__ = '0.10.0'
@@ -0,0 +1,60 @@
1
+ import sys
2
+
3
+ import click
4
+
5
+ from .pie import curl_to_httpie
6
+
7
+
8
+ PROMPT = 'Paste your cURL command (press Enter twice or Ctrl-D when done):'
9
+
10
+
11
+ def _read_interactive() -> str:
12
+ """Prompt the user to paste a cURL command, reading until a blank line or EOF."""
13
+ click.echo(PROMPT, err=True)
14
+ lines: list[str] = []
15
+ try:
16
+ while True:
17
+ line = input()
18
+ if line == '' and lines:
19
+ break
20
+ lines.append(line)
21
+ except EOFError:
22
+ click.echo(err=True)
23
+ except KeyboardInterrupt:
24
+ click.echo(err=True)
25
+ return '\n'.join(lines)
26
+
27
+
28
+ @click.command('curlipie')
29
+ @click.argument('curl_command', required=False, default=None)
30
+ @click.option(
31
+ '-l',
32
+ '--long',
33
+ 'long_option',
34
+ is_flag=True,
35
+ help='Use long-form HTTPie options (e.g. --follow instead of -F).',
36
+ )
37
+ @click.version_option()
38
+ def main(curl_command: str | None, long_option: bool) -> None:
39
+ """Convert a cURL command to an HTTPie command."""
40
+ if curl_command is not None:
41
+ curl_cmd = curl_command.strip()
42
+ elif not sys.stdin.isatty():
43
+ curl_cmd = sys.stdin.read().strip()
44
+ else:
45
+ curl_cmd = _read_interactive().strip()
46
+
47
+ if not curl_cmd:
48
+ return
49
+
50
+ result = curl_to_httpie(curl_cmd, long_option=long_option)
51
+
52
+ if result.errors:
53
+ for err in result.errors:
54
+ click.echo(f'Warning: {err}', err=True)
55
+
56
+ click.echo(result.httpie)
57
+
58
+
59
+ if __name__ == '__main__':
60
+ main()
@@ -0,0 +1,208 @@
1
+ import collections.abc
2
+ from dataclasses import dataclass, field
3
+ from collections import OrderedDict, deque
4
+ from typing import cast
5
+ from urllib.parse import parse_qsl
6
+
7
+ import yarl
8
+ import orjson
9
+ from tap import Tap
10
+ from logbook import Logger
11
+ from kiss_headers import parse_it, get_polymorphic, ContentType, Accept, BasicAuthorization
12
+ from kiss_headers import Headers, Header
13
+ from http_constants.headers import HttpHeaders as HH
14
+
15
+
16
+ logger = Logger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class DataArgParseResult:
21
+ data: deque[tuple[str, str]] = field(default_factory=deque)
22
+ errors: deque[str] = field(default_factory=deque)
23
+
24
+
25
+ # Ref: https://helpmanual.io/help/curl/
26
+ class CURLArgumentParser(Tap):
27
+ url: str
28
+ verbose: bool = False
29
+ include: bool = False
30
+ location: bool = False
31
+ append: bool = False
32
+ silent: bool = False
33
+ fail: bool = False
34
+ show_error: bool = False
35
+ globoff: bool = False
36
+ insecure: bool = False
37
+ http1_0: bool = False
38
+ tlsv1: bool = False
39
+ sslv2: bool = False
40
+ sslv3: bool = False
41
+ netrc: bool = False
42
+ proxytunnel: bool = False
43
+ use_ascii: bool = False
44
+ no_buffer: bool = False
45
+ remote_name: bool = False
46
+ remote_time: bool = False
47
+ remote_header_name: bool = False
48
+ max_redirs: int = 0
49
+ max_time: float = 0
50
+ request: str | None = None
51
+ proxy: str | None = None
52
+ user: str | None = None
53
+ cert: str | None = None
54
+ cacert: str | None = None
55
+ header: list[str] = []
56
+ form: list[str] = []
57
+ data: list[str] = []
58
+ data_raw: list[str] = []
59
+ data_binary: list[str] = []
60
+ user_agent: str | None = None
61
+ head: bool = False
62
+ get: bool = False
63
+ output: str | None = None
64
+ http2: bool = False
65
+ # Intermediate converted data
66
+ _url: str = ''
67
+ _auth: BasicAuthorization | None = None
68
+ _params: deque[tuple[str, str]] = field(default_factory=deque)
69
+ _data: deque[tuple[str, str]] = field(default_factory=deque)
70
+ _headers: Headers
71
+ _request_json: bool = False
72
+ _accept_json: bool = False
73
+ _errors: list[str] = []
74
+
75
+ def _get_class_variables(self, exclude_tap_ignores: bool = True) -> OrderedDict[str, str]:
76
+ """Overide to exclude our private variables"""
77
+ all_variables = super()._get_class_variables(exclude_tap_ignores)
78
+ return OrderedDict((k, v) for k, v in all_variables.items() if not k.startswith('_'))
79
+
80
+ def configure(self) -> None:
81
+ self.add_argument('url')
82
+ self.add_argument('-v', '--verbose')
83
+ self.add_argument('-i', '--include')
84
+ self.add_argument('-L', '--location')
85
+ self.add_argument('-a', '--append')
86
+ self.add_argument('-s', '--silent')
87
+ self.add_argument('-f', '--fail')
88
+ self.add_argument('-S', '--show-error')
89
+ self.add_argument('-g', '--globoff')
90
+ self.add_argument('-k', '--insecure')
91
+ self.add_argument('-0', '--http1.0', dest='http1_0')
92
+ self.add_argument('-1', '--tlsv1')
93
+ self.add_argument('-2', '--sslv2')
94
+ self.add_argument('-3', '--sslv3')
95
+ self.add_argument('-n', '--netrc')
96
+ self.add_argument('-p', '--proxytunnel')
97
+ self.add_argument('-B', '--use-ascii')
98
+ self.add_argument('-N', '--no-buffer')
99
+ self.add_argument('-O', '--remote-name')
100
+ self.add_argument('-R', '--remote-time')
101
+ self.add_argument('-J', '--remote-header-name')
102
+ self.add_argument('-X', '--request')
103
+ self.add_argument('-m', '--max-time')
104
+ self.add_argument('-x', '--proxy', nargs='?')
105
+ self.add_argument('-u', '--user')
106
+ self.add_argument('-E', '--cert')
107
+ self.add_argument('--cacert')
108
+ self.add_argument('-H', '--header', nargs='?', action='append')
109
+ self.add_argument('-d', '--data', nargs='?', action='append')
110
+ self.add_argument('--data-raw', nargs='?', action='append', default=[])
111
+ self.add_argument('--data-binary', nargs='?', action='append')
112
+ self.add_argument('-F', '--form', nargs='?', action='append')
113
+ self.add_argument('-A', '--user-agent')
114
+ self.add_argument('-I', '--head')
115
+ self.add_argument('-G', '--get')
116
+ self.add_argument('-o', '--output')
117
+ self._headers = Headers()
118
+
119
+ def process_args(self) -> None:
120
+ u = yarl.URL(self.url)
121
+ # Clean fragment, if exist
122
+ url = str(u.with_fragment(None).with_query(None))
123
+ # Strip leading "http://" to be short
124
+ self._url = url[7:] if u.scheme == 'http' else url
125
+ self._params = deque(u.query.items())
126
+ self._data = deque()
127
+ for dstring in self.data:
128
+ result = parse_post_data(dstring)
129
+ self._data.extend(result.data)
130
+ self._errors.extend(result.errors)
131
+ for dstring in self.data_raw:
132
+ result = parse_post_data(dstring, ignore_at=True)
133
+ self._data.extend(result.data)
134
+ self._errors.extend(result.errors)
135
+ for dstring in self.form:
136
+ result = parse_post_data(dstring)
137
+ self._data.extend(result.data)
138
+ self._errors.extend(result.errors)
139
+ for h in self.header:
140
+ headers = parse_it(h)
141
+ if not headers:
142
+ continue
143
+ if HH.CONTENT_TYPE in headers:
144
+ hx = cast(ContentType | None, get_polymorphic(headers, ContentType))
145
+ if hx and hx.get_mime() == HH.CONTENT_TYPE_VALUES.json:
146
+ self._request_json = True
147
+ continue
148
+ elif HH.ACCEPT in headers:
149
+ hx = cast(Accept | None, get_polymorphic(headers, Accept))
150
+ if hx and hx.has(HH.CONTENT_TYPE_VALUES.json):
151
+ self._accept_json = True
152
+ continue
153
+ elif HH.AUTHORIZATION in headers:
154
+ auth_header = cast(Header, headers.authorization)
155
+ if auth_header.content.startswith('Basic '):
156
+ hx = cast(BasicAuthorization | None, get_polymorphic(headers, BasicAuthorization))
157
+ self._auth = hx
158
+ continue
159
+ # kiss-header doesn't prevent duplicate, so we have to check ourselve
160
+ # Please note the behavior of kiss-headers: The "Accept-Encoding: gzip, deflate"
161
+ # will be parsed to two Header objects, to get all the "value" side, we have to
162
+ # convert the parse result to dict.
163
+ first_header = cast(Header, headers[0])
164
+ name = first_header.pretty_name
165
+ if self._headers.has(name):
166
+ del self._headers[name]
167
+ value = headers.to_dict()[name]
168
+ self._headers += Header(name, value)
169
+
170
+ def error(self, message: str) -> None: # type: ignore[override]
171
+ # Override to prevent parser from terminating our program
172
+ pass
173
+
174
+
175
+ def parse_post_data(string: str, ignore_at: bool = False) -> DataArgParseResult:
176
+ # https://ec.haxx.se/http/http-post
177
+ if not string:
178
+ return DataArgParseResult()
179
+ data = deque(parse_qsl(string))
180
+ if data:
181
+ return DataArgParseResult(data=data)
182
+ # Standard parse_qsl failed to parse it
183
+ if not ignore_at and '@' in string and not string.startswith('@'):
184
+ # cURL spec says that the filename should already be url-encoded.
185
+ key, filename = string.split('@')[:2]
186
+ return DataArgParseResult(data=deque([(key, filename)]))
187
+ # HTTPie doesn't support sending raw content as request body
188
+ # (though it allows to specify raw content as the value for a field),
189
+ # so we can ignore cURL "content", "=content", "@filename" syntaxes.
190
+ errors: deque[str] = deque()
191
+ if string.startswith('@'):
192
+ errors.append('@filename syntax (without field name) is not supported')
193
+ return DataArgParseResult(data, errors)
194
+ if string.startswith('='):
195
+ errors.append('=content syntax (without field name) is not supported')
196
+ return DataArgParseResult(data, errors)
197
+ # Maybe JSON?
198
+ try:
199
+ jsdata = orjson.loads(string.encode())
200
+ except orjson.JSONDecodeError:
201
+ # Not JSON
202
+ errors.append('Cannot guess post data format')
203
+ return DataArgParseResult(data, errors)
204
+ if isinstance(jsdata, collections.abc.Mapping):
205
+ data = deque(jsdata.items())
206
+ return DataArgParseResult(data, errors)
207
+ errors.append('JSON content does not represent an object')
208
+ return DataArgParseResult(errors=errors)
@@ -0,0 +1,154 @@
1
+ import re
2
+ import shlex
3
+ import logging
4
+ from shlex import quote
5
+ from collections import deque
6
+
7
+ import orjson
8
+ from first import first
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+ from pydantic.config import JsonValue
11
+ from http_constants.headers import HttpHeaders as HH
12
+ from .curly import CURLArgumentParser
13
+
14
+
15
+ REGEX_SINGLE_OPT = re.compile(r'^-\w+$')
16
+ REGEX_SHELL_LINEBREAK = re.compile(r'\\\s+')
17
+ logger = logging.getLogger(__name__)
18
+ EXAMPLE: JsonValue = {'httpie': 'http -fa admin:xxx quan.hoabinh.vn/api/users name=meow', 'errors': []}
19
+
20
+
21
+ class ConversionResult(BaseModel):
22
+ model_config = ConfigDict(json_schema_extra={'example': EXAMPLE})
23
+ httpie: str
24
+ errors: deque[str] = Field(default_factory=deque)
25
+
26
+
27
+ def join_previous_arg(cmds: deque[str], name: str) -> None:
28
+ prev_arg = cmds[-1]
29
+ if REGEX_SINGLE_OPT.match(prev_arg):
30
+ cmds[-1] += name
31
+ else:
32
+ cmds.append(f'-{name}')
33
+
34
+
35
+ def clean_curl(cmd: str) -> str:
36
+ """Remove slash-escaped newlines and normal newlines from curl command."""
37
+ stripped = REGEX_SHELL_LINEBREAK.sub(' ', cmd)
38
+ return ' '.join(stripped.splitlines())
39
+
40
+
41
+ def curl_to_httpie(cmd: str, long_option: bool = False) -> ConversionResult:
42
+ # The cmd can be multiline string, with escape symbols, shlex doesn't support it, so
43
+ # we should convert it to one-line first.
44
+ oneline = clean_curl(cmd)
45
+ try:
46
+ cargs = shlex.split(oneline)
47
+ except ValueError as e:
48
+ logger.error('Failed to parse as shell command. Error: %s', e)
49
+ return ConversionResult(httpie='', errors=deque([str(e)]))
50
+ if not cargs:
51
+ return ConversionResult(httpie='')
52
+ if cargs[0] == 'curl':
53
+ cargs = cargs[1:]
54
+ if not cargs:
55
+ return ConversionResult(httpie='http')
56
+ args = CURLArgumentParser().parse_args(cargs)
57
+ cmds = deque(['http'])
58
+ if args.verbose:
59
+ cmds.append('--verbose' if long_option else '-v')
60
+ if args.location:
61
+ if long_option:
62
+ cmds.append('--follow')
63
+ else:
64
+ join_previous_arg(cmds, 'F')
65
+ if args.remote_name:
66
+ if long_option:
67
+ cmds.append('--download')
68
+ else:
69
+ join_previous_arg(cmds, 'd')
70
+ if args._data and not args._request_json:
71
+ if long_option:
72
+ cmds.append('--form')
73
+ else:
74
+ join_previous_arg(cmds, 'f')
75
+ elif not args._data and (args._request_json or args._accept_json):
76
+ if long_option:
77
+ cmds.append('--json')
78
+ else:
79
+ join_previous_arg(cmds, 'j')
80
+ if args.proxy:
81
+ cmds.extend(('--proxy', args.proxy))
82
+ user = args.user if args.user else (':'.join(args._auth.get_username_password()) if args._auth else None)
83
+ if user:
84
+ if long_option:
85
+ cmds.extend(('--auth', quote(user)))
86
+ else:
87
+ join_previous_arg(cmds, 'a')
88
+ cmds.append(quote(user))
89
+
90
+ if args.include:
91
+ cmds.append('--all')
92
+ if args.insecure:
93
+ cmds.extend(('--verify', 'no'))
94
+ elif args.cacert:
95
+ cmds.extend(('--verify', args.cacert))
96
+ if args.cert:
97
+ cmds.extend(('--cert', quote(args.cert)))
98
+ if args.max_redirs:
99
+ cmds.extend(('--max-redirects', str(args.max_redirs)))
100
+ if args.max_time:
101
+ cmds.extend(('--timeout', str(args.max_time)))
102
+ if args.head:
103
+ cmds.append('HEAD')
104
+ elif args.request and not (args._data and args.request == 'POST'):
105
+ cmds.append(args.request)
106
+ # URL
107
+ cmds.append(args._url)
108
+ # Headers
109
+ for k, v in args._headers.to_dict().items():
110
+ cmds.append(f'{quote(k)}:{quote(v)}')
111
+ if args.user_agent:
112
+ cmds.append(f'{HH.USER_AGENT}:{quote(args.user_agent)}')
113
+ # Params
114
+ for k, v in args._params:
115
+ if k.startswith('-'):
116
+ cmds.append('--')
117
+ k = k.replace('=', r'\=')
118
+ cmds.append(f'{quote(k)}=={quote(v)}')
119
+ # Data
120
+ for p, v in args._data:
121
+ p = str(p)
122
+ if p.startswith('-'):
123
+ cmds.append('--')
124
+ p = p.replace('=', r'\=')
125
+ qp = quote(p)
126
+ # Syntax for uploading file
127
+ if isinstance(v, str) and v.startswith('@') and not args._request_json:
128
+ # Strip beginning @
129
+ filepath = v[1:]
130
+ cmds.append(f'{qp}@{quote(filepath)}')
131
+ continue
132
+ # Not uploading file
133
+ # Python shlex's quote will turn bool value to empty string, that is not we want
134
+ if isinstance(v, bool):
135
+ js_bool = str(v).lower()
136
+ cmds.append(f'{qp}:={js_bool}' if not args.get else f'{qp}=={str(v)}')
137
+ continue
138
+ try:
139
+ qv = quote(v)
140
+ cmds.append(f'{qp}={qv}' if not args.get else f'{qp}=={qv}')
141
+ except TypeError: # v is not string, normally after parsed from JSON
142
+ if isinstance(v, (list, dict)):
143
+ v = quote(orjson.dumps(v).decode())
144
+ cmds.append(f'{qp}:={v}' if not args.get else f'{qp}=={quote(str(v))}')
145
+ if args.data_binary:
146
+ fn = first(v for v in args.data_binary if v and v.startswith('@'))
147
+ if fn:
148
+ # Strip @
149
+ fn = fn[1:]
150
+ cmds.append(f'@{quote(fn)}')
151
+ if args.output:
152
+ param = '-o' if not long_option else '--output'
153
+ cmds.extend((param, quote(args.output)))
154
+ return ConversionResult(httpie=' '.join(cmds), errors=deque(frozenset(args._errors)))
File without changes
@@ -0,0 +1,144 @@
1
+ [project]
2
+ name = "curlipie"
3
+ description = "Library to convert cURL command line to HTTPie"
4
+ authors = [
5
+ { name = "Nguyễn Hồng Quân", email = "ng.hong.quan@gmail.com" },
6
+ ]
7
+ maintainers = [
8
+ { name = "Nguyễn Hồng Quân", email = "ng.hong.quan@gmail.com" },
9
+ ]
10
+ dynamic = []
11
+ readme = "README.rst"
12
+ license = "MPL-2.0"
13
+ requires-python = "<4.0,>=3.14"
14
+ dependencies = [
15
+ "click>=8.5.0",
16
+ "first>=2.0.2",
17
+ "http-constants>=0.5.0",
18
+ "kiss-headers>=2.5.0",
19
+ "logbook>=1.10.1",
20
+ "multidict>=6.9.1",
21
+ "orjson>=3.12.0",
22
+ "pydantic>=2.13.5",
23
+ "pydantic-settings>=2.15.0",
24
+ "typed-argument-parser>=1.12.0",
25
+ "yarl>=1.25.1",
26
+ ]
27
+ classifiers = [
28
+ "Environment :: Web Environment",
29
+ "Intended Audience :: Developers",
30
+ "Intended Audience :: System Administrators",
31
+ "Topic :: Internet :: WWW/HTTP",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ ]
34
+ keywords = [
35
+ "api",
36
+ "cli",
37
+ "conversion",
38
+ "curl",
39
+ "http",
40
+ "httpie",
41
+ ]
42
+ version = "0.0.0"
43
+
44
+ [project.scripts]
45
+ curlipie = "curlipie.cli:main"
46
+
47
+ [project.urls]
48
+ repository = "https://github.com/hongquan/CurliPie.git"
49
+
50
+ [dependency-groups]
51
+ dev = [
52
+ "aiofiles>=25.1.0",
53
+ "devtools>=0.12.2",
54
+ "fastapi>=0.141.1",
55
+ "jinja2>=3.1.6",
56
+ "uvicorn>=0.53.0",
57
+ ]
58
+ lint = [
59
+ "ruff>=0.16.8",
60
+ ]
61
+ test = [
62
+ "mypy>=2.3.1",
63
+ "pytest>=9.1.1",
64
+ "pytest-mock>=3.15.1",
65
+ "pytest-mypy>=1.0.1",
66
+ "types-first>=2.0.5.20260408",
67
+ ]
68
+
69
+ [build-system]
70
+ requires = [
71
+ "pdm-backend",
72
+ ]
73
+ build-backend = "pdm.backend"
74
+
75
+ [tool.mypy]
76
+ python_version = "3.12"
77
+ allow_redefinition = true
78
+ plugins = [
79
+ "pydantic.mypy",
80
+ ]
81
+ follow_imports = "silent"
82
+ warn_redundant_casts = true
83
+ warn_unused_ignores = true
84
+ disallow_any_generics = true
85
+
86
+ [[tool.mypy.overrides]]
87
+ module = [
88
+ "devtools.*",
89
+ "logbook.*",
90
+ "http_constants.headers.*",
91
+ ]
92
+ ignore_missing_imports = true
93
+
94
+ [tool.pydantic-mypy]
95
+ init_typed = true
96
+ init_forbid_extra = true
97
+ warn_required_dynamic_aliases = true
98
+
99
+ [tool.pytest]
100
+ addopts = [
101
+ "--mypy",
102
+ ]
103
+ testpaths = [
104
+ "tests",
105
+ "curlipie",
106
+ ]
107
+
108
+ [tool.ruff]
109
+ line-length = 120
110
+ exclude = [
111
+ ".bzr",
112
+ ".direnv",
113
+ ".eggs",
114
+ ".git",
115
+ ".hg",
116
+ ".mypy_cache",
117
+ ".nox",
118
+ ".pants.d",
119
+ ".ruff_cache",
120
+ ".svn",
121
+ ".tox",
122
+ ".venv",
123
+ "__pypackages__",
124
+ "_build",
125
+ "buck-out",
126
+ "build",
127
+ "dist",
128
+ "node_modules",
129
+ "venv",
130
+ ]
131
+ target-version = "py314"
132
+
133
+ [tool.ruff.lint]
134
+ select = [
135
+ "E",
136
+ "F",
137
+ "UP",
138
+ "ANN",
139
+ ]
140
+ ignore = []
141
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
142
+
143
+ [tool.ruff.format]
144
+ quote-style = "single"
File without changes
@@ -0,0 +1,67 @@
1
+ from click.testing import CliRunner
2
+ from pytest_mock import MockerFixture
3
+
4
+ from curlipie.cli import main
5
+
6
+
7
+ runner = CliRunner()
8
+
9
+
10
+ def test_simple_get() -> None:
11
+ result = runner.invoke(main, ['curl http://example.com'])
12
+ assert result.exit_code == 0
13
+ assert result.output.strip() == 'http example.com'
14
+
15
+
16
+ def test_post_form() -> None:
17
+ result = runner.invoke(main, ["curl -X POST http://example.com -d 'name=admin&size=12'"])
18
+ assert result.exit_code == 0
19
+ assert result.output.strip() == 'http -f example.com name=admin size=12'
20
+
21
+
22
+ def test_long_options() -> None:
23
+ result = runner.invoke(main, ['--long', 'curl -L http://example.com'])
24
+ assert result.exit_code == 0
25
+ assert '--follow' in result.output
26
+ assert '-F' not in result.output
27
+
28
+
29
+ def test_stdin_input() -> None:
30
+ result = runner.invoke(main, input='curl http://example.com')
31
+ assert result.exit_code == 0
32
+ assert result.output.strip() == 'http example.com'
33
+
34
+
35
+ def test_empty_stdin_exits_ok() -> None:
36
+ # Piping empty input exits 0 silently.
37
+ result = runner.invoke(main, input='')
38
+ assert result.exit_code == 0
39
+
40
+
41
+ def test_warnings_go_to_stderr() -> None:
42
+ # A parse error (unterminated quote) should print a warning to stderr
43
+ # and still exit cleanly (exit 0), printing whatever httpie output exists.
44
+ result = runner.invoke(main, ["curl 'unterminated"])
45
+ assert result.exit_code == 0
46
+ assert 'Warning' in result.output
47
+
48
+
49
+ def test_pipe_multiline_curl() -> None:
50
+ curl = (
51
+ 'curl -X POST \\\nhttp://example.com/api \\\n-H \'Content-Type: application/json\' \\\n-d \'{"name": "bob"}\''
52
+ )
53
+ result = runner.invoke(main, input=curl)
54
+ assert result.exit_code == 0
55
+ assert 'example.com/api' in result.output
56
+ assert 'name=bob' in result.output
57
+
58
+
59
+ def test_interactive_input(mocker: MockerFixture) -> None:
60
+ # When stdin looks like a TTY, the CLI prompts and reads interactive input.
61
+ stdin = mocker.patch('curlipie.cli.sys.stdin')
62
+ stdin.isatty.return_value = True
63
+ stdin.read.return_value = ''
64
+
65
+ result = runner.invoke(main, input='curl http://example.com\n')
66
+ assert result.exit_code == 0
67
+ assert result.output.strip() == 'http example.com'
@@ -0,0 +1,171 @@
1
+ import pytest
2
+
3
+ from devtools import debug
4
+ from curlipie.pie import curl_to_httpie
5
+
6
+
7
+ test_data = (
8
+ ("curl -d 'name=admin&shoesize=12' http://quan.hoabinh.vn", 'http -f quan.hoabinh.vn name=admin shoesize=12'),
9
+ (
10
+ 'curl -d name=admin -d shoesize=12 https://quan.hoabinh.vn',
11
+ 'http -f https://quan.hoabinh.vn name=admin shoesize=12',
12
+ ),
13
+ (
14
+ 'curl -d name=admin -d shoesize=12 -d color=green&food=wet quan.hoabinh.vn',
15
+ 'http -f quan.hoabinh.vn name=admin shoesize=12 color=green food=wet',
16
+ ),
17
+ ('curl -I http://quan.hoabinh.vn', 'http HEAD quan.hoabinh.vn'),
18
+ ('curl http://quan.hoabinh.vn --user username:password', 'http -a username:password quan.hoabinh.vn'),
19
+ (
20
+ "curl --header 'Content-Type: application/json' --header 'Host: quan.hoabinh.vn' http://103.92.28.225",
21
+ 'http -j 103.92.28.225 Host:quan.hoabinh.vn',
22
+ ),
23
+ ('curl --request DELETE http://quan.hoabinh.vn/users/1', 'http DELETE quan.hoabinh.vn/users/1'),
24
+ (
25
+ "curl -X POST http://quan.hoabinh.vn -d 'username=yourusername&password=yourpassword'",
26
+ 'http -f quan.hoabinh.vn username=yourusername password=yourpassword',
27
+ ),
28
+ (
29
+ 'curl -X POST http://quan.hoabinh.vn/api/users --user admin:xxx -d name=meow',
30
+ 'http -fa admin:xxx quan.hoabinh.vn/api/users name=meow',
31
+ ),
32
+ (
33
+ 'curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823"',
34
+ 'http -F https://keyserver.ubuntu.com/pks/lookup op==get search==0x2EE0EA64E40A89B84B2DF73499E82A75642AC823',
35
+ ),
36
+ (
37
+ 'curl -u "$USER:$PASS" "https://analysiscenter.veracode.com/api/5.0/uploadlargefile.do?'
38
+ 'app_id=$APP_ID&filename=file.bca" --cacert ~/Desktop/cert.pem',
39
+ "http -a '$USER:$PASS' --verify ~/Desktop/cert.pem "
40
+ "https://analysiscenter.veracode.com/api/5.0/uploadlargefile.do app_id=='$APP_ID' filename==file.bca",
41
+ ),
42
+ (
43
+ 'curl --data-binary "@build/veracode.bca" -H "Content-Type: binary/octet-stream" '
44
+ 'https://analysiscenter.veracode.com/api/5.0/uploadlargefile.do',
45
+ 'http https://analysiscenter.veracode.com/api/5.0/uploadlargefile.do '
46
+ 'Content-Type:binary/octet-stream @build/veracode.bca',
47
+ ),
48
+ ('curl -F file=@~/path/image.png http://quan.hoabinh.vn', "http -f quan.hoabinh.vn file@'~/path/image.png'"),
49
+ )
50
+
51
+
52
+ @pytest.mark.parametrize('curl, expected', test_data)
53
+ def test_converting(curl: str, expected: str) -> None:
54
+ httpie = curl_to_httpie(curl).httpie
55
+ assert httpie == expected
56
+
57
+
58
+ def test_json_form() -> None:
59
+ curl = (
60
+ """curl -XPUT elastic.dev/movies/_doc/1 -d '{"director": "Burton, Tim", """
61
+ """ "year": 1996, "title": "Mars Attacks!"}' -H 'Content-Type: application/json'"""
62
+ )
63
+ output = curl_to_httpie(curl).httpie
64
+ assert output == (
65
+ """http PUT elastic.dev/movies/_doc/1 director='Burton, Tim' """
66
+ """year:=1996 title='Mars Attacks!'"""
67
+ )
68
+
69
+
70
+ def test_json_value_not_primitive() -> None:
71
+ curl = (
72
+ """curl -XPUT elastic.dev/movies/_doc/1 -d '{"genre": ["Comedy", "Sci-Fi"],"""
73
+ """ "actor": ["Jack Nicholson","Pierce Brosnan","Sarah Jessica Parker"]}' """
74
+ """-H 'Content-Type: application/json'"""
75
+ )
76
+ output = curl_to_httpie(curl).httpie
77
+ debug(output)
78
+ assert output == (
79
+ """http PUT elastic.dev/movies/_doc/1 genre:='["Comedy","Sci-Fi"]' """
80
+ """actor:='["Jack Nicholson","Pierce Brosnan","Sarah Jessica Parker"]'"""
81
+ )
82
+
83
+
84
+ def test_curl_postman_generated() -> None:
85
+ curl = (
86
+ """curl --location --request POST 'http://stupid.site/sync-info' \\"""
87
+ """--header 'Content-Type: application/json' \\"""
88
+ "--data-raw '{"
89
+ ' "userId": "4-abc-xyz",'
90
+ ' "planAmount": 50000,'
91
+ ' "isPromotion": false,'
92
+ ' "createdAt": "2019-12-13 10:00:00"'
93
+ "}'"
94
+ )
95
+ httpie = curl_to_httpie(curl).httpie
96
+ assert httpie == (
97
+ """http -F stupid.site/sync-info userId=4-abc-xyz planAmount:=50000 """
98
+ """isPromotion:=false createdAt='2019-12-13 10:00:00'"""
99
+ )
100
+
101
+
102
+ def test_multi_line() -> None:
103
+ curl = (
104
+ """curl -X POST \\\nhttp://172.16.0.19/api/access-cards/2392919198/call-elevator \\\n-H 'Accept: */*' """
105
+ """\\\n-H 'Accept-Encoding: gzip, deflate' \\\n-H 'Authorization: """
106
+ """Basic dXNlcjp4eHg=' \\\n-H 'Cache-Control: no-cache' """
107
+ """\\\n-H 'Connection: keep-alive' \\\n-H 'Content-Length: 407' \\\n"""
108
+ """-H 'Content-Type: multipart/form-data; boundary=--------------------------539724411903816199149731' """
109
+ """\\\n-H 'Host: 172.16.0.19' \\\n-H 'Postman-Token: 24e4f6f7' \\\n"""
110
+ """-H 'User-Agent: PostmanRuntime/7.19.0' """
111
+ """\\\n-H 'cache-control: no-cache' \\\n-H 'content-type: multipart/form-data; """
112
+ """boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \\\n"""
113
+ """-F boarding_floor=1 \\\n-F destination_floor=9 \\\n-F elevator_bank_number=3"""
114
+ )
115
+ httpie = curl_to_httpie(curl).httpie
116
+ debug(httpie)
117
+ assert httpie == (
118
+ """http -fa user:xxx 172.16.0.19/api/access-cards/2392919198/call-elevator """
119
+ """Accept:'*/*' Accept-Encoding:'gzip, deflate' """
120
+ """Connection:keep-alive Content-Length:407 """
121
+ """Host:172.16.0.19 Postman-Token:24e4f6f7 User-Agent:PostmanRuntime/7.19.0 """
122
+ """Cache-Control:no-cache Content-Type:'multipart/form-data; """
123
+ """boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' """
124
+ """boarding_floor=1 destination_floor=9 elevator_bank_number=3"""
125
+ )
126
+
127
+
128
+ def test_escaped_linebreak() -> None:
129
+ curl = r"""curl -H 'Content-Type: application/json' \
130
+ -X POST http://127.0.0.1:5984/demo \
131
+ -d '{"company": "Example, Inc."}'"""
132
+ httpie = curl_to_httpie(curl).httpie
133
+ assert httpie == "http 127.0.0.1:5984/demo company='Example, Inc.'"
134
+
135
+
136
+ def test_json_with_quote_escape() -> None:
137
+ curl = (
138
+ 'curl --url http://localhost:3000/posts -H "Content-Type: application/json;charset=UTF-8" '
139
+ r'-d "{\"title\":\"murat\",\"author\":\"öner\"}"'
140
+ )
141
+ httpie = curl_to_httpie(curl).httpie
142
+ debug(httpie)
143
+ assert httpie == "http localhost:3000/posts title=murat author='öner'"
144
+
145
+
146
+ def test_user_query_json_accept_and_content_type() -> None:
147
+ curl = (
148
+ "curl -X 'POST' \\\n"
149
+ " 'https://orch.foobar.com/rest/environments/4/deprecate' \\\n"
150
+ " -H 'accept: application/json' \\\n"
151
+ " -H 'Content-Type: application/json' \\\n"
152
+ " -d '{\n"
153
+ ' "replacementEnvironmentId": "1493"\n'
154
+ "}'"
155
+ )
156
+ httpie = curl_to_httpie(curl).httpie
157
+ assert httpie == 'http https://orch.foobar.com/rest/environments/4/deprecate replacementEnvironmentId=1493'
158
+
159
+
160
+ def test_accept_json_without_data() -> None:
161
+ curl = "curl -H 'Accept: application/json' https://api.example.com/items"
162
+ httpie = curl_to_httpie(curl).httpie
163
+ assert httpie == 'http -j https://api.example.com/items'
164
+ httpie_long = curl_to_httpie(curl, long_option=True).httpie
165
+ assert httpie_long == 'http --json https://api.example.com/items'
166
+
167
+
168
+ def test_accept_json_with_flags_compound() -> None:
169
+ curl = "curl -L -u user:pass -H 'Accept: application/json' https://api.example.com/items"
170
+ httpie = curl_to_httpie(curl).httpie
171
+ assert httpie == 'http -Fja user:pass https://api.example.com/items'
@@ -0,0 +1,103 @@
1
+ import shlex
2
+
3
+ from devtools import debug
4
+ from kiss_headers import Headers, Header
5
+ from curlipie.curly import CURLArgumentParser
6
+
7
+
8
+ def parsed_args(cmd_args_string: str) -> CURLArgumentParser:
9
+ return CURLArgumentParser().parse_args(shlex.split(cmd_args_string))
10
+
11
+
12
+ def test_curl_form_data_single_urlencoded() -> None:
13
+ cmline = "-d 'name=admin&shoesize=12' http://quan.hoabinh.vn"
14
+ args = parsed_args(cmline)
15
+ assert args.data == ['name=admin&shoesize=12']
16
+ assert args.url == 'http://quan.hoabinh.vn'
17
+
18
+
19
+ def test_curl_form_data_multi() -> None:
20
+ cmline = '-d name=admin -d shoesize=12 http://quan.hoabinh.vn'
21
+ args = parsed_args(cmline)
22
+ assert args.data == ['name=admin', 'shoesize=12']
23
+
24
+
25
+ def test_curl_form_data_multi_mixed() -> None:
26
+ cmline = '-d name=admin -d shoesize=12 -d color=green&food=wet http://quan.hoabinh.vn'
27
+ args = parsed_args(cmline)
28
+ assert args.data == ['name=admin', 'shoesize=12', 'color=green&food=wet']
29
+ debug(args._data)
30
+ assert tuple(args._data) == (('name', 'admin'), ('shoesize', '12'), ('color', 'green'), ('food', 'wet'))
31
+
32
+
33
+ def test_head() -> None:
34
+ cmline = '-I http://quan.hoabinh.vn'
35
+ args = parsed_args(cmline)
36
+ assert args.head
37
+
38
+
39
+ def test_basic_auth() -> None:
40
+ cmline = 'http://quan.hoabinh.vn --user username:password'
41
+ args = parsed_args(cmline)
42
+ assert args.user == 'username:password'
43
+
44
+
45
+ def test_headers() -> None:
46
+ cmline = "--header 'Content-Type: application/json' --header 'Host: quan.hoabinh.vn' http://103.92.28.225"
47
+ args = parsed_args(cmline)
48
+ debug(args.header)
49
+ assert args.header == ['Content-Type: application/json', 'Host: quan.hoabinh.vn']
50
+ debug(args._headers)
51
+ assert args._headers == Headers(Header('Host', 'quan.hoabinh.vn'))
52
+ assert args._request_json
53
+
54
+
55
+ def test_accept_json_header() -> None:
56
+ cmline = "--header 'Accept: application/json' --header 'Host: quan.hoabinh.vn' http://103.92.28.225"
57
+ args = parsed_args(cmline)
58
+ assert args.header == ['Accept: application/json', 'Host: quan.hoabinh.vn']
59
+ assert args._headers == Headers(Header('Host', 'quan.hoabinh.vn'))
60
+ assert args._accept_json
61
+
62
+
63
+ def test_method() -> None:
64
+ cmline = '--request DELETE http://quan.hoabinh.vn'
65
+ args = parsed_args(cmline)
66
+ assert args.request == 'DELETE'
67
+
68
+
69
+ def test_post_data() -> None:
70
+ cmline = "-X POST http://quan.hoabinh.vn -d 'username=yourusername&password=yourpassword'"
71
+ args = parsed_args(cmline)
72
+ assert args.request == 'POST'
73
+ assert args.data == ['username=yourusername&password=yourpassword']
74
+
75
+
76
+ def test_query_params() -> None:
77
+ cmline = '-sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823"'
78
+ args = parsed_args(cmline)
79
+ assert tuple(args._params) == (('op', 'get'), ('search', '0x2EE0EA64E40A89B84B2DF73499E82A75642AC823'))
80
+
81
+
82
+ def test_upload() -> None:
83
+ cmline = 'curl -F file=@~/path/image.png http://quan.hoabinh.vn'
84
+ args = parsed_args(cmline)
85
+ assert tuple(args._data) == (('file', '@~/path/image.png'),)
86
+
87
+
88
+ def test_data_raw() -> None:
89
+ cmline = """
90
+ --location --request POST 'https://quan.hoabinh.vn/searching?apikey=xxx'
91
+ --header 'Content-Type: application/json'
92
+ --data-raw '{
93
+ "title": "yyy",
94
+ "categories": [
95
+ "zzz"
96
+ ],
97
+ "domain": "quan.hoabinh.vn"
98
+ }'
99
+ """
100
+ args = parsed_args(cmline)
101
+ assert args.header == ['Content-Type: application/json']
102
+ debug(args._data)
103
+ assert tuple(args._data) == (('title', 'yyy'), ('categories', ['zzz']), ('domain', 'quan.hoabinh.vn'))