From 1117a91df62866ab1e1e59adccfdf9cdae801683 Mon Sep 17 00:00:00 2001 From: DENKweit Date: Mon, 27 Sep 2021 14:15:56 +0200 Subject: [PATCH 01/11] requests upload working --- gql/transport/requests.py | 75 ++++++++- tests/test_requests.py | 338 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 408 insertions(+), 5 deletions(-) diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 7f9ff26a..8fb527d7 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -1,8 +1,11 @@ +import io import json import logging -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Generator, Optional, Tuple, Type, Union +import aiohttp import requests +from requests_toolbelt.multipart.encoder import MultipartEncoder from graphql import DocumentNode, ExecutionResult, print_ast from requests.adapters import HTTPAdapter, Retry from requests.auth import AuthBase @@ -10,6 +13,7 @@ from gql.transport import Transport +from ..utils import extract_files from .exceptions import ( TransportAlreadyConnected, TransportClosed, @@ -27,6 +31,10 @@ class RequestsHTTPTransport(Transport): The transport uses the requests library to send HTTP POST requests. """ + file_classes: Tuple[Type[Any], ...] = ( + io.IOBase, + ) + def __init__( self, url: str, @@ -104,6 +112,7 @@ def execute( # type: ignore operation_name: Optional[str] = None, timeout: Optional[int] = None, extra_args: Dict[str, Any] = None, + upload_files: bool = False, ) -> ExecutionResult: """Execute GraphQL query. @@ -116,6 +125,7 @@ def execute( # type: ignore Only required in multi-operation documents (Default: None). :param timeout: Specifies a default timeout for requests (Default: None). :param extra_args: additional arguments to send to the requests post method + :param upload_files: Set to True if you want to put files in the variable values :return: The result of execution. `data` is the result of executing the query, `errors` is null if no errors occurred, and is a non-empty array if an error occurred. @@ -126,21 +136,76 @@ def execute( # type: ignore query_str = print_ast(document) payload: Dict[str, Any] = {"query": query_str} - if variable_values: - payload["variables"] = variable_values + if operation_name: payload["operationName"] = operation_name - data_key = "json" if self.use_json else "data" post_args = { "headers": self.headers, "auth": self.auth, "cookies": self.cookies, "timeout": timeout or self.default_timeout, "verify": self.verify, - data_key: payload, } + if upload_files: + # If the upload_files flag is set, then we need variable_values + assert variable_values is not None + + # If we upload files, we will extract the files present in the + # variable_values dict and replace them by null values + nulled_variable_values, files = extract_files( + variables=variable_values, file_classes=self.file_classes, + ) + + # Save the nulled variable values in the payload + payload["variables"] = nulled_variable_values + + # Add the payload to the operations field + operations_str = json.dumps(payload) + log.debug("operations %s", operations_str) + + # Generate the file map + # path is nested in a list because the spec allows multiple pointers + # to the same file. But we don't support that. + # Will generate something like {"0": ["variables.file"]} + file_map = {str(i): [path] for i, path in enumerate(files)} + + # Enumerate the file streams + # Will generate something like {'0': <_io.BufferedReader ...>} + file_streams = {str(i): files[path] for i, path in enumerate(files)} + + # Add the file map field + file_map_str = json.dumps(file_map) + log.debug("file_map %s", file_map_str) + + fields = {"operations": operations_str, + "map": file_map_str} + + # Add the extracted files as remaining fields + for k, v in file_streams.items(): + fields[k] = (getattr(v, "name", k), v) + + # Prepare requests http to send multipart-encoded data + data = MultipartEncoder(fields=fields) + + post_args["data"] = data + + if self.headers is None: + post_args["headers"] = {"Content-Type": data.content_type} + else: + post_args["headers"]["Content-Type"] = data.content_type + + else: + if variable_values: + payload["variables"] = variable_values + + if log.isEnabledFor(logging.INFO): + log.info(">>> %s", json.dumps(payload)) + + data_key = "json" if self.use_json else "data" + post_args[data_key] = payload + # Log the payload if log.isEnabledFor(logging.INFO): log.info(">>> %s", json.dumps(payload)) diff --git a/tests/test_requests.py b/tests/test_requests.py index e18875a2..499064c5 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,3 +1,5 @@ +from gql.transport import requests +from tests.conftest import TemporaryFile import pytest from gql import Client, gql @@ -332,3 +334,339 @@ def test_code(): assert execution_result.extensions["key1"] == "val1" await run_sync_test(event_loop, server, test_code) + + +file_upload_server_answer = '{"data":{"success":true}}' + +file_upload_mutation_1 = """ + mutation($file: Upload!) { + uploadFile(input:{other_var:$other_var, file:$file}) { + success + } + } +""" + +file_upload_mutation_1_operations = ( + '{"query": "mutation ($file: Upload!) {\\n uploadFile(input: {other_var: ' + '$other_var, file: $file}) {\\n success\\n }\\n}\\n", "variables": ' + '{"file": null, "other_var": 42}}' +) + +file_upload_mutation_1_map = '{"0": ["variables.file"]}' + +file_1_content = """ +This is a test file +This file will be sent in the GraphQL mutation +""" + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_file_upload(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def single_upload_handler(request): + from aiohttp import web + + reader = await request.multipart() + + field_0 = await reader.next() + assert field_0.name == "operations" + field_0_text = await field_0.text() + assert field_0_text == file_upload_mutation_1_operations + + field_1 = await reader.next() + assert field_1.name == "map" + field_1_text = await field_1.text() + assert field_1_text == file_upload_mutation_1_map + + field_2 = await reader.next() + assert field_2.name == "0" + field_2_text = await field_2.text() + assert field_2_text == file_1_content + + field_3 = await reader.next() + assert field_3 is None + + return web.Response(text=file_upload_server_answer, content_type="application/json") + + app = web.Application() + app.router.add_route("POST", "/", single_upload_handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + sample_transport = RequestsHTTPTransport(url=url) + + with TemporaryFile(file_1_content) as test_file: + with Client( + transport=sample_transport) as session: + query = gql(file_upload_mutation_1) + + file_path = test_file.filename + + with open(file_path, "rb") as f: + + params = {"file": f, "other_var": 42} + execution_result = session._execute( + query, variable_values=params, upload_files=True + ) + + assert execution_result.data["success"] + + await run_sync_test(event_loop, server, test_code) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_binary_file_upload(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + # This is a sample binary file content containing all possible byte values + binary_file_content = bytes(range(0, 256)) + + async def binary_upload_handler(request): + + from aiohttp import web + + reader = await request.multipart() + + field_0 = await reader.next() + assert field_0.name == "operations" + field_0_text = await field_0.text() + assert field_0_text == file_upload_mutation_1_operations + + field_1 = await reader.next() + assert field_1.name == "map" + field_1_text = await field_1.text() + assert field_1_text == file_upload_mutation_1_map + + field_2 = await reader.next() + assert field_2.name == "0" + field_2_binary = await field_2.read() + assert field_2_binary == binary_file_content + + field_3 = await reader.next() + assert field_3 is None + + return web.Response(text=file_upload_server_answer, content_type="application/json") + + app = web.Application() + app.router.add_route("POST", "/", binary_upload_handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + sample_transport = RequestsHTTPTransport(url=url) + + def test_code(): + with TemporaryFile(binary_file_content) as test_file: + with Client(transport=sample_transport,) as session: + + query = gql(file_upload_mutation_1) + + file_path = test_file.filename + + with open(file_path, "rb") as f: + + params = {"file": f, "other_var": 42} + + execution_result = session._execute( + query, variable_values=params, upload_files=True + ) + + assert execution_result.data["success"] + + await run_sync_test(event_loop, server, test_code) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_file_upload_two_files(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + file_upload_mutation_2 = """ + mutation($file1: Upload!, $file2: Upload!) { + uploadFile(input:{file1:$file, file2:$file}) { + success + } + } + """ + + file_upload_mutation_2_operations = ( + '{"query": "mutation ($file1: Upload!, $file2: Upload!) {\\n ' + 'uploadFile(input: {file1: $file, file2: $file}) {\\n success\\n }\\n}\\n", ' + '"variables": {"file1": null, "file2": null}}' + ) + + file_upload_mutation_2_map = '{"0": ["variables.file1"], "1": ["variables.file2"]}' + + file_2_content = """ + This is a second test file + This file will also be sent in the GraphQL mutation + """ + + async def handler(request): + + reader = await request.multipart() + + field_0 = await reader.next() + assert field_0.name == "operations" + field_0_text = await field_0.text() + assert field_0_text == file_upload_mutation_2_operations + + field_1 = await reader.next() + assert field_1.name == "map" + field_1_text = await field_1.text() + assert field_1_text == file_upload_mutation_2_map + + field_2 = await reader.next() + assert field_2.name == "0" + field_2_text = await field_2.text() + assert field_2_text == file_1_content + + field_3 = await reader.next() + assert field_3.name == "1" + field_3_text = await field_3.text() + assert field_3_text == file_2_content + + field_4 = await reader.next() + assert field_4 is None + + return web.Response( + text=file_upload_server_answer, content_type="application/json" + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + sample_transport = RequestsHTTPTransport(url=url) + + with TemporaryFile(file_1_content) as test_file_1: + with TemporaryFile(file_2_content) as test_file_2: + + with Client(transport=sample_transport,) as session: + + query = gql(file_upload_mutation_2) + + file_path_1 = test_file_1.filename + file_path_2 = test_file_2.filename + + f1 = open(file_path_1, "rb") + f2 = open(file_path_2, "rb") + + params = { + "file1": f1, + "file2": f2, + } + + execution_result = session._execute( + query, variable_values=params, upload_files=True + ) + + assert execution_result.data["success"] + + f1.close() + f2.close() + + await run_sync_test(event_loop, server, test_code) + + +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_file_upload_list_of_two_files(event_loop, aiohttp_server, run_sync_test): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + file_upload_mutation_3 = """ + mutation($files: [Upload!]!) { + uploadFiles(input:{files:$files}) { + success + } + } + """ + + file_upload_mutation_3_operations = ( + '{"query": "mutation ($files: [Upload!]!) {\\n uploadFiles(input: {files: $files})' + ' {\\n success\\n }\\n}\\n", "variables": {"files": [null, null]}}' + ) + + file_upload_mutation_3_map = '{"0": ["variables.files.0"], "1": ["variables.files.1"]}' + + file_2_content = """ + This is a second test file + This file will also be sent in the GraphQL mutation + """ + + async def handler(request): + + reader = await request.multipart() + + field_0 = await reader.next() + assert field_0.name == "operations" + field_0_text = await field_0.text() + assert field_0_text == file_upload_mutation_3_operations + + field_1 = await reader.next() + assert field_1.name == "map" + field_1_text = await field_1.text() + assert field_1_text == file_upload_mutation_3_map + + field_2 = await reader.next() + assert field_2.name == "0" + field_2_text = await field_2.text() + assert field_2_text == file_1_content + + field_3 = await reader.next() + assert field_3.name == "1" + field_3_text = await field_3.text() + assert field_3_text == file_2_content + + field_4 = await reader.next() + assert field_4 is None + + return web.Response( + text=file_upload_server_answer, content_type="application/json" + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + sample_transport = RequestsHTTPTransport(url=url) + + with TemporaryFile(file_1_content) as test_file_1: + with TemporaryFile(file_2_content) as test_file_2: + with Client(transport=sample_transport,) as session: + + query = gql(file_upload_mutation_3) + + file_path_1 = test_file_1.filename + file_path_2 = test_file_2.filename + + f1 = open(file_path_1, "rb") + f2 = open(file_path_2, "rb") + + params = {"files": [f1, f2]} + + execution_result = session._execute( + query, variable_values=params, upload_files=True + ) + + assert execution_result.data["success"] + + f1.close() + f2.close() + + await run_sync_test(event_loop, server, test_code) From 68ac727e6053b4221d92a9ff6618b50d720e6500 Mon Sep 17 00:00:00 2001 From: DENKweit Date: Mon, 27 Sep 2021 14:20:57 +0200 Subject: [PATCH 02/11] cleanup --- gql/transport/requests.py | 12 ++++-------- tests/test_requests.py | 26 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 8fb527d7..7c83cfdc 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -1,15 +1,14 @@ import io import json import logging -from typing import Any, Dict, Generator, Optional, Tuple, Type, Union +from typing import Any, Dict, Optional, Tuple, Type, Union -import aiohttp import requests -from requests_toolbelt.multipart.encoder import MultipartEncoder from graphql import DocumentNode, ExecutionResult, print_ast from requests.adapters import HTTPAdapter, Retry from requests.auth import AuthBase from requests.cookies import RequestsCookieJar +from requests_toolbelt.multipart.encoder import MultipartEncoder from gql.transport import Transport @@ -31,9 +30,7 @@ class RequestsHTTPTransport(Transport): The transport uses the requests library to send HTTP POST requests. """ - file_classes: Tuple[Type[Any], ...] = ( - io.IOBase, - ) + file_classes: Tuple[Type[Any], ...] = (io.IOBase,) def __init__( self, @@ -179,8 +176,7 @@ def execute( # type: ignore file_map_str = json.dumps(file_map) log.debug("file_map %s", file_map_str) - fields = {"operations": operations_str, - "map": file_map_str} + fields = {"operations": operations_str, "map": file_map_str} # Add the extracted files as remaining fields for k, v in file_streams.items(): diff --git a/tests/test_requests.py b/tests/test_requests.py index 499064c5..5d937960 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,5 +1,3 @@ -from gql.transport import requests -from tests.conftest import TemporaryFile import pytest from gql import Client, gql @@ -10,6 +8,7 @@ TransportQueryError, TransportServerError, ) +from tests.conftest import TemporaryFile # Marking all tests in this file with the requests marker pytestmark = pytest.mark.requests @@ -389,7 +388,9 @@ async def single_upload_handler(request): field_3 = await reader.next() assert field_3 is None - return web.Response(text=file_upload_server_answer, content_type="application/json") + return web.Response( + text=file_upload_server_answer, content_type="application/json" + ) app = web.Application() app.router.add_route("POST", "/", single_upload_handler) @@ -401,8 +402,7 @@ def test_code(): sample_transport = RequestsHTTPTransport(url=url) with TemporaryFile(file_1_content) as test_file: - with Client( - transport=sample_transport) as session: + with Client(transport=sample_transport) as session: query = gql(file_upload_mutation_1) file_path = test_file.filename @@ -452,7 +452,9 @@ async def binary_upload_handler(request): field_3 = await reader.next() assert field_3 is None - return web.Response(text=file_upload_server_answer, content_type="application/json") + return web.Response( + text=file_upload_server_answer, content_type="application/json" + ) app = web.Application() app.router.add_route("POST", "/", binary_upload_handler) @@ -485,7 +487,9 @@ def test_code(): @pytest.mark.aiohttp @pytest.mark.asyncio -async def test_requests_file_upload_two_files(event_loop, aiohttp_server, run_sync_test): +async def test_requests_file_upload_two_files( + event_loop, aiohttp_server, run_sync_test +): from aiohttp import web from gql.transport.requests import RequestsHTTPTransport @@ -582,7 +586,9 @@ def test_code(): @pytest.mark.aiohttp @pytest.mark.asyncio -async def test_requests_file_upload_list_of_two_files(event_loop, aiohttp_server, run_sync_test): +async def test_requests_file_upload_list_of_two_files( + event_loop, aiohttp_server, run_sync_test +): from aiohttp import web from gql.transport.requests import RequestsHTTPTransport @@ -599,7 +605,9 @@ async def test_requests_file_upload_list_of_two_files(event_loop, aiohttp_server ' {\\n success\\n }\\n}\\n", "variables": {"files": [null, null]}}' ) - file_upload_mutation_3_map = '{"0": ["variables.files.0"], "1": ["variables.files.1"]}' + file_upload_mutation_3_map = ( + '{"0": ["variables.files.0"], "1": ["variables.files.1"]}' + ) file_2_content = """ This is a second test file From e75e6e3f5fcb368a354fcdde82b832c9c7842c8a Mon Sep 17 00:00:00 2001 From: DENKweit Date: Mon, 27 Sep 2021 17:47:25 +0200 Subject: [PATCH 03/11] added requests_toolbelt requirement --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 248099ab..c936dc40 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ install_requests_requires = [ "requests>=2.23,<3", + "requests_toolbelt>=0.9.1", ] install_websockets_requires = [ From db98fb0330c671d338ca912f4d7d17ee7b8595bc Mon Sep 17 00:00:00 2001 From: DENKweit Date: Tue, 28 Sep 2021 10:17:07 +0200 Subject: [PATCH 04/11] fixed formatting --- tests/test_requests.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index 5d937960..a2d046ee 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -485,6 +485,13 @@ def test_code(): await run_sync_test(event_loop, server, test_code) +file_upload_mutation_2_operations = ( + '{"query": "mutation ($file1: Upload!, $file2: Upload!) {\\n ' + 'uploadFile(input: {file1: $file, file2: $file}) {\\n success\\n }\\n}\\n", ' + '"variables": {"file1": null, "file2": null}}' +) + + @pytest.mark.aiohttp @pytest.mark.asyncio async def test_requests_file_upload_two_files( @@ -501,12 +508,6 @@ async def test_requests_file_upload_two_files( } """ - file_upload_mutation_2_operations = ( - '{"query": "mutation ($file1: Upload!, $file2: Upload!) {\\n ' - 'uploadFile(input: {file1: $file, file2: $file}) {\\n success\\n }\\n}\\n", ' - '"variables": {"file1": null, "file2": null}}' - ) - file_upload_mutation_2_map = '{"0": ["variables.file1"], "1": ["variables.file2"]}' file_2_content = """ @@ -584,6 +585,12 @@ def test_code(): await run_sync_test(event_loop, server, test_code) +file_upload_mutation_3_operations = ( + '{"query": "mutation ($files: [Upload!]!) {\\n uploadFiles(input: {files: $files})' + ' {\\n success\\n }\\n}\\n", "variables": {"files": [null, null]}}' +) + + @pytest.mark.aiohttp @pytest.mark.asyncio async def test_requests_file_upload_list_of_two_files( @@ -600,11 +607,6 @@ async def test_requests_file_upload_list_of_two_files( } """ - file_upload_mutation_3_operations = ( - '{"query": "mutation ($files: [Upload!]!) {\\n uploadFiles(input: {files: $files})' - ' {\\n success\\n }\\n}\\n", "variables": {"files": [null, null]}}' - ) - file_upload_mutation_3_map = ( '{"0": ["variables.files.0"], "1": ["variables.files.1"]}' ) From 8dd0f419b3fa742e18a11a5d6a79224bc6425149 Mon Sep 17 00:00:00 2001 From: DENKweit Date: Tue, 28 Sep 2021 10:22:12 +0200 Subject: [PATCH 05/11] increased coverage --- tests/test_requests.py | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/test_requests.py b/tests/test_requests.py index a2d046ee..d0cc7eb7 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -419,6 +419,70 @@ def test_code(): await run_sync_test(event_loop, server, test_code) +@pytest.mark.aiohttp +@pytest.mark.asyncio +async def test_requests_file_upload_additional_headers( + event_loop, aiohttp_server, run_sync_test +): + from aiohttp import web + from gql.transport.requests import RequestsHTTPTransport + + async def single_upload_handler(request): + from aiohttp import web + + assert request.headers["X-Auth"] == "foobar" + + reader = await request.multipart() + + field_0 = await reader.next() + assert field_0.name == "operations" + field_0_text = await field_0.text() + assert field_0_text == file_upload_mutation_1_operations + + field_1 = await reader.next() + assert field_1.name == "map" + field_1_text = await field_1.text() + assert field_1_text == file_upload_mutation_1_map + + field_2 = await reader.next() + assert field_2.name == "0" + field_2_text = await field_2.text() + assert field_2_text == file_1_content + + field_3 = await reader.next() + assert field_3 is None + + return web.Response( + text=file_upload_server_answer, content_type="application/json" + ) + + app = web.Application() + app.router.add_route("POST", "/", single_upload_handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + def test_code(): + sample_transport = RequestsHTTPTransport(url=url, headers={"X-Auth": "foobar"}) + + with TemporaryFile(file_1_content) as test_file: + with Client(transport=sample_transport) as session: + query = gql(file_upload_mutation_1) + + file_path = test_file.filename + + with open(file_path, "rb") as f: + + params = {"file": f, "other_var": 42} + execution_result = session._execute( + query, variable_values=params, upload_files=True + ) + + assert execution_result.data["success"] + + await run_sync_test(event_loop, server, test_code) + + @pytest.mark.aiohttp @pytest.mark.asyncio async def test_requests_binary_file_upload(event_loop, aiohttp_server, run_sync_test): From d976f7d9d342ac210fdbfd020f6b99bde36719ad Mon Sep 17 00:00:00 2001 From: DENKweit Date: Tue, 28 Sep 2021 10:30:39 +0200 Subject: [PATCH 06/11] small refactor --- gql/transport/requests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 7c83cfdc..7eb949cc 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -187,10 +187,10 @@ def execute( # type: ignore post_args["data"] = data - if self.headers is None: - post_args["headers"] = {"Content-Type": data.content_type} - else: - post_args["headers"]["Content-Type"] = data.content_type + if post_args["headers"] is None: + post_args["headers"] = {} + + post_args["headers"]["Content-Type"] = data.content_type else: if variable_values: From dcf7af24be2bfe0361891dc132380bb210cb466c Mon Sep 17 00:00:00 2001 From: DENKweit Date: Tue, 28 Sep 2021 11:55:01 +0200 Subject: [PATCH 07/11] changed required version for requests_toolbelt --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c936dc40..75a91062 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ install_requests_requires = [ "requests>=2.23,<3", - "requests_toolbelt>=0.9.1", + "requests_toolbelt==0.9.1", ] install_websockets_requires = [ From d974fa0bd53141ae8946b770dd01b8d982b75c16 Mon Sep 17 00:00:00 2001 From: DENKweit Date: Tue, 28 Sep 2021 11:55:34 +0200 Subject: [PATCH 08/11] changed required version for requests_toolbelt --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 75a91062..ead75821 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ install_requests_requires = [ "requests>=2.23,<3", - "requests_toolbelt==0.9.1", + "requests_toolbelt>=0.9.1,<1", ] install_websockets_requires = [ From 77042ca8d36b504f00ddd0fa5afb1b939f0a8e48 Mon Sep 17 00:00:00 2001 From: DENKweit Date: Thu, 30 Sep 2021 12:14:40 +0200 Subject: [PATCH 09/11] fixed header persist bug --- gql/transport/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 7eb949cc..7564cfc6 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -138,7 +138,7 @@ def execute( # type: ignore payload["operationName"] = operation_name post_args = { - "headers": self.headers, + "headers": {**self.headers}, "auth": self.auth, "cookies": self.cookies, "timeout": timeout or self.default_timeout, From 089f5c8c9d05cc9619810d0ffe8057314733f1b5 Mon Sep 17 00:00:00 2001 From: DENKweit Date: Thu, 30 Sep 2021 12:22:29 +0200 Subject: [PATCH 10/11] fixes header bug --- gql/transport/requests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 7564cfc6..68b4144b 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -138,7 +138,7 @@ def execute( # type: ignore payload["operationName"] = operation_name post_args = { - "headers": {**self.headers}, + "headers": self.headers, "auth": self.auth, "cookies": self.cookies, "timeout": timeout or self.default_timeout, @@ -189,6 +189,8 @@ def execute( # type: ignore if post_args["headers"] is None: post_args["headers"] = {} + else: + post_args["headers"] = {**post_args["headers"]} post_args["headers"]["Content-Type"] = data.content_type From 36975431b303e067dc8dfe76e6cbd1047a5b1580 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Wed, 6 Oct 2021 14:23:24 +0200 Subject: [PATCH 11/11] Update File upload docs --- docs/usage/file_upload.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/usage/file_upload.rst b/docs/usage/file_upload.rst index 18718e75..cfc85df9 100644 --- a/docs/usage/file_upload.rst +++ b/docs/usage/file_upload.rst @@ -2,6 +2,7 @@ File uploads ============ GQL supports file uploads with the :ref:`aiohttp transport ` +and the :ref:`requests transport ` using the `GraphQL multipart request spec`_. .. _GraphQL multipart request spec: https://github.com/jaydenseric/graphql-multipart-request-spec @@ -18,6 +19,7 @@ In order to upload a single file, you need to: .. code-block:: python transport = AIOHTTPTransport(url='YOUR_URL') + # Or transport = RequestsHTTPTransport(url='YOUR_URL') client = Client(transport=transport) @@ -45,6 +47,7 @@ It is also possible to upload multiple files using a list. .. code-block:: python transport = AIOHTTPTransport(url='YOUR_URL') + # Or transport = RequestsHTTPTransport(url='YOUR_URL') client = Client(transport=transport) @@ -84,6 +87,9 @@ We provide methods to do that for two different uses cases: * Sending local files * Streaming downloaded files from an external URL to the GraphQL API +.. note:: + Streaming is only supported with the :ref:`aiohttp transport ` + Streaming local files ^^^^^^^^^^^^^^^^^^^^^