Skip to content

Commit d28ab9d

Browse files
authored
Merge pull request #189 from FoamyGuy/files_arg
Files arg
2 parents d500e0c + 55f159f commit d28ab9d

13 files changed

+359
-10
lines changed

adafruit_requests.py

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,13 @@
4141

4242
import errno
4343
import json as json_module
44+
import os
4445
import sys
4546

4647
from adafruit_connection_manager import get_connection_manager
4748

49+
SEEK_END = 2
50+
4851
if not sys.implementation.name == "circuitpython":
4952
from types import TracebackType
5053
from typing import Any, Dict, Optional, Type
@@ -357,10 +360,66 @@ def __init__(
357360
self._session_id = session_id
358361
self._last_response = None
359362

363+
def _build_boundary_data(self, files: dict): # pylint: disable=too-many-locals
364+
boundary_string = self._build_boundary_string()
365+
content_length = 0
366+
boundary_objects = []
367+
368+
for field_name, field_values in files.items():
369+
file_name = field_values[0]
370+
file_handle = field_values[1]
371+
372+
boundary_objects.append(
373+
f'--{boundary_string}\r\nContent-Disposition: form-data; name="{field_name}"'
374+
)
375+
if file_name is not None:
376+
boundary_objects.append(f'; filename="{file_name}"')
377+
boundary_objects.append("\r\n")
378+
if len(field_values) >= 3:
379+
file_content_type = field_values[2]
380+
boundary_objects.append(f"Content-Type: {file_content_type}\r\n")
381+
if len(field_values) >= 4:
382+
file_headers = field_values[3]
383+
for file_header_key, file_header_value in file_headers.items():
384+
boundary_objects.append(
385+
f"{file_header_key}: {file_header_value}\r\n"
386+
)
387+
boundary_objects.append("\r\n")
388+
389+
if hasattr(file_handle, "read"):
390+
is_binary = False
391+
try:
392+
content = file_handle.read(1)
393+
is_binary = isinstance(content, bytes)
394+
except UnicodeError:
395+
is_binary = False
396+
397+
if not is_binary:
398+
raise AttributeError("Files must be opened in binary mode")
399+
400+
file_handle.seek(0, SEEK_END)
401+
content_length += file_handle.tell()
402+
file_handle.seek(0)
403+
404+
boundary_objects.append(file_handle)
405+
boundary_objects.append("\r\n")
406+
407+
boundary_objects.append(f"--{boundary_string}--\r\n")
408+
409+
for boundary_object in boundary_objects:
410+
if isinstance(boundary_object, str):
411+
content_length += len(boundary_object)
412+
413+
return boundary_string, content_length, boundary_objects
414+
415+
@staticmethod
416+
def _build_boundary_string():
417+
return os.urandom(16).hex()
418+
360419
@staticmethod
361420
def _check_headers(headers: Dict[str, str]):
362421
if not isinstance(headers, dict):
363-
raise AttributeError("headers must be in dict format")
422+
raise AttributeError("Headers must be in dict format")
364423

365424
for key, value in headers.items():
366425
if isinstance(value, (str, bytes)) or value is None:
@@ -394,6 +453,19 @@ def _send(socket: SocketType, data: bytes):
394453
def _send_as_bytes(self, socket: SocketType, data: str):
395454
return self._send(socket, bytes(data, "utf-8"))
396455

456+
def _send_boundary_objects(self, socket: SocketType, boundary_objects: Any):
457+
for boundary_object in boundary_objects:
458+
if isinstance(boundary_object, str):
459+
self._send_as_bytes(socket, boundary_object)
460+
else:
461+
chunk_size = 32
462+
b = bytearray(chunk_size)
463+
while True:
464+
size = boundary_object.readinto(b)
465+
if size == 0:
466+
break
467+
self._send(socket, b[:size])
468+
397469
def _send_header(self, socket, header, value):
398470
if value is None:
399471
return
@@ -405,8 +477,7 @@ def _send_header(self, socket, header, value):
405477
self._send_as_bytes(socket, value)
406478
self._send(socket, b"\r\n")
407479

408-
# pylint: disable=too-many-arguments
409-
def _send_request(
480+
def _send_request( # pylint: disable=too-many-arguments
410481
self,
411482
socket: SocketType,
412483
host: str,
@@ -415,7 +486,8 @@ def _send_request(
415486
headers: Dict[str, str],
416487
data: Any,
417488
json: Any,
418-
):
489+
files: Optional[Dict[str, tuple]],
490+
): # pylint: disable=too-many-branches,too-many-locals,too-many-statements
419491
# Check headers
420492
self._check_headers(headers)
421493

@@ -425,11 +497,13 @@ def _send_request(
425497
# If json is sent, set content type header and convert to string
426498
if json is not None:
427499
assert data is None
500+
assert files is None
428501
content_type_header = "application/json"
429502
data = json_module.dumps(json)
430503

431504
# If data is sent and it's a dict, set content type header and convert to string
432505
if data and isinstance(data, dict):
506+
assert files is None
433507
content_type_header = "application/x-www-form-urlencoded"
434508
_post_data = ""
435509
for k in data:
@@ -441,6 +515,19 @@ def _send_request(
441515
if data and isinstance(data, str):
442516
data = bytes(data, "utf-8")
443517

518+
# If files are send, build data to send and calculate length
519+
content_length = 0
520+
boundary_objects = None
521+
if files and isinstance(files, dict):
522+
boundary_string, content_length, boundary_objects = (
523+
self._build_boundary_data(files)
524+
)
525+
content_type_header = f"multipart/form-data; boundary={boundary_string}"
526+
else:
527+
if data is None:
528+
data = b""
529+
content_length = len(data)
530+
444531
self._send_as_bytes(socket, method)
445532
self._send(socket, b" /")
446533
self._send_as_bytes(socket, path)
@@ -456,8 +543,8 @@ def _send_request(
456543
self._send_header(socket, "User-Agent", "Adafruit CircuitPython")
457544
if content_type_header and not "content-type" in supplied_headers:
458545
self._send_header(socket, "Content-Type", content_type_header)
459-
if data and not "content-length" in supplied_headers:
460-
self._send_header(socket, "Content-Length", str(len(data)))
546+
if (data or files) and not "content-length" in supplied_headers:
547+
self._send_header(socket, "Content-Length", str(content_length))
461548
# Iterate over keys to avoid tuple alloc
462549
for header in headers:
463550
self._send_header(socket, header, headers[header])
@@ -466,6 +553,8 @@ def _send_request(
466553
# Send data
467554
if data:
468555
self._send(socket, bytes(data))
556+
elif boundary_objects:
557+
self._send_boundary_objects(socket, boundary_objects)
469558

470559
# pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
471560
def request(
@@ -478,6 +567,7 @@ def request(
478567
stream: bool = False,
479568
timeout: float = 60,
480569
allow_redirects: bool = True,
570+
files: Optional[Dict[str, tuple]] = None,
481571
) -> Response:
482572
"""Perform an HTTP request to the given url which we will parse to determine
483573
whether to use SSL ('https://') or not. We can also send some provided 'data'
@@ -526,7 +616,9 @@ def request(
526616
)
527617
ok = True
528618
try:
529-
self._send_request(socket, host, method, path, headers, data, json)
619+
self._send_request(
620+
socket, host, method, path, headers, data, json, files
621+
)
530622
except OSError as exc:
531623
last_exc = exc
532624
ok = False
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
import adafruit_connection_manager
5+
import wifi
6+
7+
import adafruit_requests
8+
9+
URL = "https://httpbin.org/post"
10+
11+
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
12+
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
13+
requests = adafruit_requests.Session(pool, ssl_context)
14+
15+
with open("requests_wifi_file_upload_image.png", "rb") as file_handle:
16+
files = {
17+
"file": (
18+
"requests_wifi_file_upload_image.png",
19+
file_handle,
20+
"image/png",
21+
{"CustomHeader": "BlinkaRocks"},
22+
),
23+
"othervalue": (None, "HelloWorld"),
24+
}
25+
26+
with requests.post(URL, files=files) as response:
27+
print(response.content)
Loading
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-FileCopyrightText: 2024 Tim Cocks
2+
# SPDX-License-Identifier: CC-BY-4.0

optional_requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries
22
#
33
# SPDX-License-Identifier: Unlicense
4+
5+
requests

tests/files/green_red.png

125 Bytes
Loading

tests/files/green_red.png.license

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-FileCopyrightText: 2024 Justin Myers
2+
# SPDX-License-Identifier: Unlicense

tests/files/red_green.png

123 Bytes
Loading

tests/files/red_green.png.license

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-FileCopyrightText: 2024 Justin Myers
2+
# SPDX-License-Identifier: Unlicense

0 commit comments

Comments
 (0)