You've already forked httpie-cli
							
							
				mirror of
				https://github.com/httpie/cli.git
				synced 2025-10-30 23:47:52 +02:00 
			
		
		
		
	| @@ -8,6 +8,7 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_. | ||||
|  | ||||
| `2.3.0-dev`_ (unreleased) | ||||
| ------------------------- | ||||
| * Added support for multipart upload streaming (`#684`_). | ||||
| * Added support for combining cookies specified on the CLI and in a session file (`#932`_). | ||||
| * Added out of the box SOCKS support with no extra installation (`#904`_). | ||||
| * Added ``--quiet, -q`` flag to enforce silent behaviour. | ||||
| @@ -451,6 +452,7 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_. | ||||
| .. _#128: https://github.com/jakubroztocil/httpie/issues/128 | ||||
| .. _#488: https://github.com/jakubroztocil/httpie/issues/488 | ||||
| .. _#668: https://github.com/jakubroztocil/httpie/issues/668 | ||||
| .. _#684: https://github.com/jakubroztocil/httpie/issues/684 | ||||
| .. _#718: https://github.com/jakubroztocil/httpie/issues/718 | ||||
| .. _#719: https://github.com/jakubroztocil/httpie/issues/719 | ||||
| .. _#840: https://github.com/jakubroztocil/httpie/issues/840 | ||||
|   | ||||
| @@ -694,6 +694,10 @@ override the inferred content type: | ||||
|  | ||||
|    $ http -f POST httpbin.org/post name='John Smith' cv@'~/files/data.bin;type=application/pdf' | ||||
|  | ||||
| Larger multipart uploads (i.e., ``--form`` requests with at least one ``file@path``) | ||||
| are always streamed to avoid memory issues. Additionally, the display of the | ||||
| request body on the terminal is suppressed. | ||||
|  | ||||
|  | ||||
| HTTP headers | ||||
| ============ | ||||
|   | ||||
| @@ -16,6 +16,7 @@ PACKAGES = [ | ||||
|     'httpie', | ||||
|     'Pygments', | ||||
|     'requests', | ||||
|     'requests-toolbelt', | ||||
|     'certifi', | ||||
|     'urllib3', | ||||
|     'idna', | ||||
|   | ||||
| @@ -99,15 +99,13 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]: | ||||
|     parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE) | ||||
|     filename = parts[0] | ||||
|     mime_type = parts[1] if len(parts) > 1 else None | ||||
|  | ||||
|     try: | ||||
|         with open(os.path.expanduser(filename), 'rb') as f: | ||||
|             contents = f.read() | ||||
|         f = open(os.path.expanduser(filename), 'rb') | ||||
|     except IOError as e: | ||||
|         raise ParseError('"%s": %s' % (arg.orig, e)) | ||||
|     return ( | ||||
|         os.path.basename(filename), | ||||
|         BytesIO(contents), | ||||
|         f, | ||||
|         mime_type or get_content_type(filename), | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -11,14 +11,15 @@ from urllib.parse import urlparse, urlunparse | ||||
| import requests | ||||
| # noinspection PyPackageRequirements | ||||
| import urllib3 | ||||
|  | ||||
| from httpie import __version__ | ||||
| from httpie.cli.dicts import RequestHeadersDict | ||||
| from httpie.plugins.registry import plugin_manager | ||||
| from httpie.sessions import get_httpie_session | ||||
| from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter | ||||
| from httpie.uploads import get_multipart_data | ||||
| from httpie.utils import get_expired_cookies, repr_dict | ||||
|  | ||||
|  | ||||
| urllib3.disable_warnings() | ||||
|  | ||||
| FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8' | ||||
| @@ -256,6 +257,7 @@ def make_request_kwargs( | ||||
|     Translate our `args` into `requests.Request` keyword arguments. | ||||
|  | ||||
|     """ | ||||
|     files = args.files | ||||
|     # Serialize JSON data, if needed. | ||||
|     data = args.data | ||||
|     auto_json = data and not args.form | ||||
| @@ -274,6 +276,10 @@ def make_request_kwargs( | ||||
|     headers.update(args.headers) | ||||
|     headers = finalize_headers(headers) | ||||
|  | ||||
|     if args.form and files: | ||||
|         data, headers['Content-Type'] = get_multipart_data(data, files) | ||||
|         files = None | ||||
|  | ||||
|     kwargs = { | ||||
|         'method': args.method.lower(), | ||||
|         'url': args.url, | ||||
| @@ -281,7 +287,7 @@ def make_request_kwargs( | ||||
|         'data': data, | ||||
|         'auth': args.auth, | ||||
|         'params': args.params, | ||||
|         'files': args.files, | ||||
|         'files': files, | ||||
|     } | ||||
|  | ||||
|     return kwargs | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from typing import Callable, Iterable, Union | ||||
| from httpie.context import Environment | ||||
| from httpie.models import HTTPMessage | ||||
| from httpie.output.processing import Conversion, Formatting | ||||
| from requests_toolbelt import MultipartEncoder | ||||
|  | ||||
|  | ||||
| BINARY_SUPPRESSED_NOTICE = ( | ||||
| @@ -14,11 +15,29 @@ BINARY_SUPPRESSED_NOTICE = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| class BinarySuppressedError(Exception): | ||||
| LARGE_UPLOAD_SUPPRESSED_NOTICE = ( | ||||
|     b'\n' | ||||
|     b'+--------------------------------------------------------+\n' | ||||
|     b'| NOTE: large form upload data not shown in the terminal |\n' | ||||
|     b'+--------------------------------------------------------+' | ||||
| ) | ||||
|  | ||||
|  | ||||
| class DataSuppressedError(Exception): | ||||
|     message = None | ||||
|  | ||||
|  | ||||
| class BinarySuppressedError(DataSuppressedError): | ||||
|     """An error indicating that the body is binary and won't be written, | ||||
|      e.g., for terminal output).""" | ||||
|     message = BINARY_SUPPRESSED_NOTICE | ||||
|  | ||||
|  | ||||
| class LargeUploadSuppressedError(DataSuppressedError): | ||||
|     """An error indicating that the body is binary and won't be written, | ||||
|      e.g., for terminal output).""" | ||||
|  | ||||
|     message = BINARY_SUPPRESSED_NOTICE | ||||
|     message = LARGE_UPLOAD_SUPPRESSED_NOTICE | ||||
|  | ||||
|  | ||||
| class BaseStream: | ||||
| @@ -63,7 +82,7 @@ class BaseStream: | ||||
|                     yield chunk | ||||
|                     if self.on_body_chunk_downloaded: | ||||
|                         self.on_body_chunk_downloaded(chunk) | ||||
|             except BinarySuppressedError as e: | ||||
|             except DataSuppressedError as e: | ||||
|                 if self.with_headers: | ||||
|                     yield b'\n' | ||||
|                 yield e.message | ||||
| @@ -184,6 +203,8 @@ class BufferedPrettyStream(PrettyStream): | ||||
|         body = bytearray() | ||||
|  | ||||
|         for chunk in self.msg.iter_body(self.CHUNK_SIZE): | ||||
|             if isinstance(chunk, MultipartEncoder): | ||||
|                 raise LargeUploadSuppressedError() | ||||
|             if not converter and b'\0' in chunk: | ||||
|                 converter = self.conversion.get_converter(self.mime) | ||||
|                 if not converter: | ||||
|   | ||||
							
								
								
									
										20
									
								
								httpie/uploads.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								httpie/uploads.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| from typing import Tuple, Union | ||||
|  | ||||
| from httpie.cli.dicts import RequestDataDict, RequestFilesDict | ||||
| from requests_toolbelt import MultipartEncoder | ||||
|  | ||||
|  | ||||
| # Multipart uploads smaller than this size gets buffered (otherwise streamed). | ||||
| # NOTE: Unbuffered upload requests cannot be displayed on the terminal. | ||||
| UPLOAD_BUFFER = 1024 * 100 | ||||
|  | ||||
|  | ||||
| def get_multipart_data( | ||||
|     data: RequestDataDict, | ||||
|     files: RequestFilesDict | ||||
| ) -> Tuple[Union[MultipartEncoder, bytes], str]: | ||||
|     fields = list(data.items()) + list(files.items()) | ||||
|     encoder = MultipartEncoder(fields=fields) | ||||
|     content_type = encoder.content_type | ||||
|     data = encoder.to_string() if encoder.len < UPLOAD_BUFFER else encoder | ||||
|     return data, content_type | ||||
| @@ -111,3 +111,5 @@ def get_expired_cookies( | ||||
|         for cookie in cookies | ||||
|         if cookie.get('expires', float('Inf')) <= now | ||||
|     ] | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| @@ -38,6 +38,7 @@ tests_require = [ | ||||
| install_requires = [ | ||||
|     'requests[socks]>=2.22.0', | ||||
|     'Pygments>=2.5.2', | ||||
|     'requests-toolbelt>=0.9.1', | ||||
| ] | ||||
| install_requires_win_only = [ | ||||
|     'colorama>=0.2.4', | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import os | ||||
| from unittest import mock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from httpie.cli.exceptions import ParseError | ||||
| from httpie.output.streams import LARGE_UPLOAD_SUPPRESSED_NOTICE | ||||
| from httpie.status import ExitStatus | ||||
| from utils import MockEnvironment, http, HTTP_OK | ||||
| from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT | ||||
| @@ -39,15 +41,32 @@ class TestMultipartFormDataFileUpload: | ||||
|         assert r.count('Content-Type: text/plain') == 2 | ||||
|  | ||||
|     def test_upload_custom_content_type(self, httpbin): | ||||
|         r = http('--form', '--verbose', 'POST', httpbin.url + '/post', | ||||
|                  f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon') | ||||
|         r = http( | ||||
|             '--form', | ||||
|             '--verbose', | ||||
|             httpbin.url + '/post', | ||||
|             f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon' | ||||
|         ) | ||||
|         assert HTTP_OK in r | ||||
|         # Content type is stripped from the filename | ||||
|         assert 'Content-Disposition: form-data; name="test-file";' \ | ||||
|                f' filename="{os.path.basename(FILE_PATH)}"' in r | ||||
|         assert FILE_CONTENT in r | ||||
|         assert r.count(FILE_CONTENT) == 2 | ||||
|         assert 'Content-Type: image/vnd.microsoft.icon' in r | ||||
|  | ||||
|     @mock.patch('httpie.uploads.UPLOAD_BUFFER', 0) | ||||
|     def test_large_upload_display_suppressed(self, httpbin): | ||||
|         r = http( | ||||
|             '--form', | ||||
|             '--verbose', | ||||
|             httpbin.url + '/post', | ||||
|             f'test-file@{FILE_PATH_ARG}', | ||||
|             'foo=bar', | ||||
|         ) | ||||
|         assert HTTP_OK in r | ||||
|         assert r.count(FILE_CONTENT) == 1 | ||||
|         assert LARGE_UPLOAD_SUPPRESSED_NOTICE.decode() in r | ||||
|  | ||||
|  | ||||
| class TestRequestBodyFromFilePath: | ||||
|     """ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user