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.
- curlipie-0.0.0/PKG-INFO +207 -0
- curlipie-0.0.0/README.rst +179 -0
- curlipie-0.0.0/curlipie/__init__.py +4 -0
- curlipie-0.0.0/curlipie/cli.py +60 -0
- curlipie-0.0.0/curlipie/curly.py +208 -0
- curlipie-0.0.0/curlipie/pie.py +154 -0
- curlipie-0.0.0/curlipie/py.typed +0 -0
- curlipie-0.0.0/pyproject.toml +144 -0
- curlipie-0.0.0/tests/__init__.py +0 -0
- curlipie-0.0.0/tests/test_cli.py +67 -0
- curlipie-0.0.0/tests/test_curlipie.py +171 -0
- curlipie-0.0.0/tests/test_parse_curl.py +103 -0
curlipie-0.0.0/PKG-INFO
ADDED
|
@@ -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,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'))
|