Skip to content

Commit 930fca5

Browse files
authored
Feature allow to set the content-type of file uploads (#386)
1 parent 2981ce3 commit 930fca5

File tree

7 files changed

+245
-8
lines changed

7 files changed

+245
-8
lines changed

docs/usage/file_upload.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ In order to upload a single file, you need to:
4242
query, variable_values=params, upload_files=True
4343
)
4444
45+
Setting the content-type
46+
^^^^^^^^^^^^^^^^^^^^^^^^
47+
48+
If you need to set a specific Content-Type attribute to a file,
49+
you can set the :code:`content_type` attribute of the file like this:
50+
51+
.. code-block:: python
52+
53+
with open("YOUR_FILE_PATH", "rb") as f:
54+
55+
# Setting the content-type to a pdf file for example
56+
f.content_type = "application/pdf"
57+
58+
params = {"file": f}
59+
60+
result = client.execute(
61+
query, variable_values=params, upload_files=True
62+
)
63+
4564
File list
4665
---------
4766

gql/transport/aiohttp.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,11 @@ async def execute(
274274
data.add_field("map", file_map_str, content_type="application/json")
275275

276276
# Add the extracted files as remaining fields
277-
for k, v in file_streams.items():
278-
data.add_field(k, v, filename=getattr(v, "name", k))
277+
for k, f in file_streams.items():
278+
name = getattr(f, "name", k)
279+
content_type = getattr(f, "content_type", None)
280+
281+
data.add_field(k, f, filename=name, content_type=content_type)
279282

280283
post_args: Dict[str, Any] = {"data": data}
281284

gql/transport/httpx.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ def _prepare_file_uploads(self, variable_values, payload) -> Dict[str, Any]:
103103
# Prepare to send multipart-encoded data
104104
data: Dict[str, Any] = {}
105105
file_map: Dict[str, List[str]] = {}
106-
file_streams: Dict[str, Tuple[str, Any]] = {}
106+
file_streams: Dict[str, Tuple[str, ...]] = {}
107107

108-
for i, (path, val) in enumerate(files.items()):
108+
for i, (path, f) in enumerate(files.items()):
109109
key = str(i)
110110

111111
# Generate the file map
@@ -117,8 +117,13 @@ def _prepare_file_uploads(self, variable_values, payload) -> Dict[str, Any]:
117117
# Generate the file streams
118118
# Will generate something like
119119
# {"0": ("variables.file", <_io.BufferedReader ...>)}
120-
filename = cast(str, getattr(val, "name", key))
121-
file_streams[key] = (filename, val)
120+
name = cast(str, getattr(f, "name", key))
121+
content_type = getattr(f, "content_type", None)
122+
123+
if content_type is None:
124+
file_streams[key] = (name, f)
125+
else:
126+
file_streams[key] = (name, f, content_type)
122127

123128
# Add the payload to the operations field
124129
operations_str = self.json_serialize(payload)

gql/transport/requests.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,14 @@ def execute( # type: ignore
183183
fields = {"operations": operations_str, "map": file_map_str}
184184

185185
# Add the extracted files as remaining fields
186-
for k, v in file_streams.items():
187-
fields[k] = (getattr(v, "name", k), v)
186+
for k, f in file_streams.items():
187+
name = getattr(f, "name", k)
188+
content_type = getattr(f, "content_type", None)
189+
190+
if content_type is None:
191+
fields[k] = (name, f)
192+
else:
193+
fields[k] = (name, f, content_type)
188194

189195
# Prepare requests http to send multipart-encoded data
190196
data = MultipartEncoder(fields=fields)

tests/test_aiohttp.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,74 @@ async def test_aiohttp_file_upload(event_loop, aiohttp_server):
670670
assert success
671671

672672

673+
async def single_upload_handler_with_content_type(request):
674+
675+
from aiohttp import web
676+
677+
reader = await request.multipart()
678+
679+
field_0 = await reader.next()
680+
assert field_0.name == "operations"
681+
field_0_text = await field_0.text()
682+
assert field_0_text == file_upload_mutation_1_operations
683+
684+
field_1 = await reader.next()
685+
assert field_1.name == "map"
686+
field_1_text = await field_1.text()
687+
assert field_1_text == file_upload_mutation_1_map
688+
689+
field_2 = await reader.next()
690+
assert field_2.name == "0"
691+
field_2_text = await field_2.text()
692+
assert field_2_text == file_1_content
693+
694+
# Verifying the content_type
695+
assert field_2.headers["Content-Type"] == "application/pdf"
696+
697+
field_3 = await reader.next()
698+
assert field_3 is None
699+
700+
return web.Response(text=file_upload_server_answer, content_type="application/json")
701+
702+
703+
@pytest.mark.asyncio
704+
async def test_aiohttp_file_upload_with_content_type(event_loop, aiohttp_server):
705+
from aiohttp import web
706+
from gql.transport.aiohttp import AIOHTTPTransport
707+
708+
app = web.Application()
709+
app.router.add_route("POST", "/", single_upload_handler_with_content_type)
710+
server = await aiohttp_server(app)
711+
712+
url = server.make_url("/")
713+
714+
transport = AIOHTTPTransport(url=url, timeout=10)
715+
716+
with TemporaryFile(file_1_content) as test_file:
717+
718+
async with Client(transport=transport) as session:
719+
720+
query = gql(file_upload_mutation_1)
721+
722+
file_path = test_file.filename
723+
724+
with open(file_path, "rb") as f:
725+
726+
# Setting the content_type
727+
f.content_type = "application/pdf"
728+
729+
params = {"file": f, "other_var": 42}
730+
731+
# Execute query asynchronously
732+
result = await session.execute(
733+
query, variable_values=params, upload_files=True
734+
)
735+
736+
success = result["success"]
737+
738+
assert success
739+
740+
673741
@pytest.mark.asyncio
674742
async def test_aiohttp_file_upload_without_session(
675743
event_loop, aiohttp_server, run_sync_test

tests/test_httpx.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,74 @@ def test_code():
477477
await run_sync_test(event_loop, server, test_code)
478478

479479

480+
@pytest.mark.aiohttp
481+
@pytest.mark.asyncio
482+
async def test_httpx_file_upload_with_content_type(
483+
event_loop, aiohttp_server, run_sync_test
484+
):
485+
from aiohttp import web
486+
from gql.transport.httpx import HTTPXTransport
487+
488+
async def single_upload_handler(request):
489+
from aiohttp import web
490+
491+
reader = await request.multipart()
492+
493+
field_0 = await reader.next()
494+
assert field_0.name == "operations"
495+
field_0_text = await field_0.text()
496+
assert field_0_text == file_upload_mutation_1_operations
497+
498+
field_1 = await reader.next()
499+
assert field_1.name == "map"
500+
field_1_text = await field_1.text()
501+
assert field_1_text == file_upload_mutation_1_map
502+
503+
field_2 = await reader.next()
504+
assert field_2.name == "0"
505+
field_2_text = await field_2.text()
506+
assert field_2_text == file_1_content
507+
508+
# Verifying the content_type
509+
assert field_2.headers["Content-Type"] == "application/pdf"
510+
511+
field_3 = await reader.next()
512+
assert field_3 is None
513+
514+
return web.Response(
515+
text=file_upload_server_answer, content_type="application/json"
516+
)
517+
518+
app = web.Application()
519+
app.router.add_route("POST", "/", single_upload_handler)
520+
server = await aiohttp_server(app)
521+
522+
url = str(server.make_url("/"))
523+
524+
def test_code():
525+
transport = HTTPXTransport(url=url)
526+
527+
with TemporaryFile(file_1_content) as test_file:
528+
with Client(transport=transport) as session:
529+
query = gql(file_upload_mutation_1)
530+
531+
file_path = test_file.filename
532+
533+
with open(file_path, "rb") as f:
534+
535+
# Setting the content_type
536+
f.content_type = "application/pdf"
537+
538+
params = {"file": f, "other_var": 42}
539+
execution_result = session._execute(
540+
query, variable_values=params, upload_files=True
541+
)
542+
543+
assert execution_result.data["success"]
544+
545+
await run_sync_test(event_loop, server, test_code)
546+
547+
480548
@pytest.mark.aiohttp
481549
@pytest.mark.asyncio
482550
async def test_httpx_file_upload_additional_headers(

tests/test_requests.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,74 @@ def test_code():
479479
await run_sync_test(event_loop, server, test_code)
480480

481481

482+
@pytest.mark.aiohttp
483+
@pytest.mark.asyncio
484+
async def test_requests_file_upload_with_content_type(
485+
event_loop, aiohttp_server, run_sync_test
486+
):
487+
from aiohttp import web
488+
from gql.transport.requests import RequestsHTTPTransport
489+
490+
async def single_upload_handler(request):
491+
from aiohttp import web
492+
493+
reader = await request.multipart()
494+
495+
field_0 = await reader.next()
496+
assert field_0.name == "operations"
497+
field_0_text = await field_0.text()
498+
assert field_0_text == file_upload_mutation_1_operations
499+
500+
field_1 = await reader.next()
501+
assert field_1.name == "map"
502+
field_1_text = await field_1.text()
503+
assert field_1_text == file_upload_mutation_1_map
504+
505+
field_2 = await reader.next()
506+
assert field_2.name == "0"
507+
field_2_text = await field_2.text()
508+
assert field_2_text == file_1_content
509+
510+
# Verifying the content_type
511+
assert field_2.headers["Content-Type"] == "application/pdf"
512+
513+
field_3 = await reader.next()
514+
assert field_3 is None
515+
516+
return web.Response(
517+
text=file_upload_server_answer, content_type="application/json"
518+
)
519+
520+
app = web.Application()
521+
app.router.add_route("POST", "/", single_upload_handler)
522+
server = await aiohttp_server(app)
523+
524+
url = server.make_url("/")
525+
526+
def test_code():
527+
transport = RequestsHTTPTransport(url=url)
528+
529+
with TemporaryFile(file_1_content) as test_file:
530+
with Client(transport=transport) as session:
531+
query = gql(file_upload_mutation_1)
532+
533+
file_path = test_file.filename
534+
535+
with open(file_path, "rb") as f:
536+
537+
# Setting the content_type
538+
f.content_type = "application/pdf"
539+
540+
params = {"file": f, "other_var": 42}
541+
execution_result = session._execute(
542+
query, variable_values=params, upload_files=True
543+
)
544+
545+
assert execution_result.data["success"]
546+
547+
await run_sync_test(event_loop, server, test_code)
548+
549+
482550
@pytest.mark.aiohttp
483551
@pytest.mark.asyncio
484552
async def test_requests_file_upload_additional_headers(

0 commit comments

Comments
 (0)