diff --git a/curlify.py b/curlify.py index 7a5fc5f..b3ec740 100644 --- a/curlify.py +++ b/curlify.py @@ -13,30 +13,60 @@ def to_curl(request, compressed=False, verify=True, pretty=False): verify : bool If `False` then a `--insecure` argument will be added to the result, disabling TLS certificate verification + pretty : bool + If `True`, then the resulting command will be pretty-printed to include + one option per line. + modern : bool + If `False`, then the resulting command will not use the --json option + added in curl 7.82. """ command = [] inferred_method = 'GET' - if request.body is not None: + inferred_length = None + data_type = None + body = request.body + + if body is not None: inferred_method = 'POST' + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + inferred_length = len(body) + + if modern and not body.startswith('@') and request.headers.get('content-type') == request.headers.get('accept') == 'application/json': + data_type = '--json' + elif body.startswith('@'): # -d @filename causes curl to read from file + data_type = '--data-raw' + else: + data_type = '-d' + if request.method != inferred_method: command.append('-X ' + quote(request.method)) for k, v in request.headers.items(): - if v: - command.append('-H ' + quote('{0}: {1}'.format(k, v))) + lk = k.lower() + if data_type == '--json' and lk in ('content-type', 'accept'): + pass # --json will set this automatically + elif lk == 'content-type' and v == 'application/x-www-form-urlencoded': + pass # -d/--data-raw will set this automatically + elif lk == 'content-length' and str(inferred_length) == v: + pass # this will be set automatically with the body + elif lk == 'connection' and v == 'keep-alive': + pass # this is curl's default behavior + elif v: + if lk == 'user-agent': + command.append('-A ' + quote(v)) + elif lk == 'cookie': + command.append('-b ' + quote(v)) + else: + command.append('-H ' + quote('{0}: {1}'.format(k, v))) else: # -H 'Accept:' disables sending the Accept header, use semicolon to send # empty header command.append('-H ' + quote('{0};'.format(k))) - if request.body: - body = request.body - if isinstance(body, bytes): - body = body.decode('utf-8') - data_type = '-d' - if body.startswith('@'): # -d @filename causes curl to read from file - data_type = '--data-raw' + if body is not None: command.append(data_type + ' ' + quote(body)) if compressed: diff --git a/curlify_test.py b/curlify_test.py index 757c51d..eb1039e 100644 --- a/curlify_test.py +++ b/curlify_test.py @@ -10,10 +10,9 @@ def test_empty_data(): ) assert curlify.to_curl(r.request) == ( "curl -X POST " - "-H 'user-agent: mytest' " + "-A mytest " "-H 'Accept-Encoding: gzip, deflate' " "-H 'Accept: */*' " - "-H 'Connection: keep-alive' " "-H 'Content-Length: 0' " "http://google.ru/" ) @@ -28,13 +27,10 @@ def test_ok(): ) assert curlify.to_curl(r.request) == ( "curl -X GET " - "-H 'user-agent: mytest' " + "-A mytest " "-H 'Accept-Encoding: gzip, deflate' " "-H 'Accept: */*' " - "-H 'Connection: keep-alive' " - "-H 'Cookie: foo=bar' " - "-H 'Content-Length: 3' " - "-H 'Content-Type: application/x-www-form-urlencoded' " + "-b foo=bar " "-d a=b http://google.ru/" ) @@ -47,7 +43,7 @@ def test_prepare_request(): assert curlify.to_curl(request.prepare()) == ( "curl " - "-H 'user-agent: UA' " + "-A UA " "http://google.ru/" ) @@ -58,7 +54,7 @@ def test_compressed(): headers={"user-agent": "UA"}, ) assert curlify.to_curl(request.prepare(), compressed=True) == ( - "curl -H 'user-agent: UA' --compressed http://google.ru/" + "curl -A UA --compressed http://google.ru/" ) @@ -68,7 +64,7 @@ def test_verify(): headers={"user-agent": "UA"}, ) assert curlify.to_curl(request.prepare(), verify=False) == ( - "curl -H 'user-agent: UA' --insecure http://google.ru/" + "curl -A UA --insecure http://google.ru/" ) @@ -80,12 +76,40 @@ def test_post_json(): curlified = curlify.to_curl(r.prepare()) assert curlified == ( - "curl -H 'Content-Length: 14' " + "curl " "-H 'Content-Type: application/json' " "-d '{\"foo\": \"bar\"}' https://httpbin.org/post" ) +def test_post_and_accept_json(): + data = {'foo': 'bar'} + url = 'https://httpbin.org/post' + headers = {'accept': 'application/json'} + + r = requests.Request('POST', url, json=data, headers=headers) + curlified = curlify.to_curl(r.prepare()) + + assert curlified == ( + "curl " + "--json '{\"foo\": \"bar\"}' https://httpbin.org/post" + ) + + +def test_post_and_accept_json_with_older_curl(): + data = {'foo': 'bar'} + url = 'https://httpbin.org/post' + headers = {'accept': 'application/json'} + + r = requests.Request('POST', url, json=data, headers=headers) + curlified = curlify.to_curl(r.prepare(), modern=False) + + assert curlified == ( + "curl -H 'accept: application/json' -H 'Content-Type: application/json' " + "-d '{\"foo\": \"bar\"}' https://httpbin.org/post" + ) + + def test_post_csv_file(): r = requests.Request( method='POST', @@ -99,8 +123,8 @@ def test_post_csv_file(): expected = ( 'curl' - ' -H \'User-agent: UA\'' - ' -H \'Content-Length: 519\'' + ' -A UA' + f' -H \'Content-Length: 519\'' f' -H \'Content-Type: multipart/form-data; boundary={boundary}\'' f' -d \'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="data.csv"\r\n\r\n' '"Id";"Title";"Content"\n' @@ -119,7 +143,7 @@ def test_data_with_at(): data='@example.com' ) assert curlify.to_curl(request.prepare()) == ( - "curl -X GET -H 'Content-Length: 12' --data-raw @example.com http://google.ru/" + "curl -X GET --data-raw @example.com http://google.ru/" ) @@ -136,11 +160,11 @@ def test_empty_header(): def test_pretty(): request = requests.Request( 'GET', "http://google.ru", - data={'foo': 'bar'} + data={'foo': 'bar'}, + cookies={'baz': 'quux'}, ) assert curlify.to_curl(request.prepare(), pretty=True) == '''curl -X GET \\ - -H 'Content-Length: 7' \\ - -H 'Content-Type: application/x-www-form-urlencoded' \\ + -b baz=quux \\ -d foo=bar \\ http://google.ru/'''