Skip to content

Commit

Permalink
Merge pull request #189 from FoamyGuy/files_arg
Browse files Browse the repository at this point in the history
Files arg
  • Loading branch information
FoamyGuy committed May 17, 2024
2 parents d500e0c + 55f159f commit d28ab9d
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 10 deletions.
106 changes: 99 additions & 7 deletions adafruit_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@

import errno
import json as json_module
import os
import sys

from adafruit_connection_manager import get_connection_manager

SEEK_END = 2

if not sys.implementation.name == "circuitpython":
from types import TracebackType
from typing import Any, Dict, Optional, Type
Expand Down Expand Up @@ -357,10 +360,66 @@ def __init__(
self._session_id = session_id
self._last_response = None

def _build_boundary_data(self, files: dict): # pylint: disable=too-many-locals
boundary_string = self._build_boundary_string()
content_length = 0
boundary_objects = []

for field_name, field_values in files.items():
file_name = field_values[0]
file_handle = field_values[1]

boundary_objects.append(
f'--{boundary_string}\r\nContent-Disposition: form-data; name="{field_name}"'
)
if file_name is not None:
boundary_objects.append(f'; filename="{file_name}"')
boundary_objects.append("\r\n")
if len(field_values) >= 3:
file_content_type = field_values[2]
boundary_objects.append(f"Content-Type: {file_content_type}\r\n")
if len(field_values) >= 4:
file_headers = field_values[3]
for file_header_key, file_header_value in file_headers.items():
boundary_objects.append(
f"{file_header_key}: {file_header_value}\r\n"
)
boundary_objects.append("\r\n")

if hasattr(file_handle, "read"):
is_binary = False
try:
content = file_handle.read(1)
is_binary = isinstance(content, bytes)
except UnicodeError:
is_binary = False

if not is_binary:
raise AttributeError("Files must be opened in binary mode")

file_handle.seek(0, SEEK_END)
content_length += file_handle.tell()
file_handle.seek(0)

boundary_objects.append(file_handle)
boundary_objects.append("\r\n")

boundary_objects.append(f"--{boundary_string}--\r\n")

for boundary_object in boundary_objects:
if isinstance(boundary_object, str):
content_length += len(boundary_object)

return boundary_string, content_length, boundary_objects

@staticmethod
def _build_boundary_string():
return os.urandom(16).hex()

@staticmethod
def _check_headers(headers: Dict[str, str]):
if not isinstance(headers, dict):
raise AttributeError("headers must be in dict format")
raise AttributeError("Headers must be in dict format")

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

def _send_boundary_objects(self, socket: SocketType, boundary_objects: Any):
for boundary_object in boundary_objects:
if isinstance(boundary_object, str):
self._send_as_bytes(socket, boundary_object)
else:
chunk_size = 32
b = bytearray(chunk_size)
while True:
size = boundary_object.readinto(b)
if size == 0:
break
self._send(socket, b[:size])

def _send_header(self, socket, header, value):
if value is None:
return
Expand All @@ -405,8 +477,7 @@ def _send_header(self, socket, header, value):
self._send_as_bytes(socket, value)
self._send(socket, b"\r\n")

# pylint: disable=too-many-arguments
def _send_request(
def _send_request( # pylint: disable=too-many-arguments
self,
socket: SocketType,
host: str,
Expand All @@ -415,7 +486,8 @@ def _send_request(
headers: Dict[str, str],
data: Any,
json: Any,
):
files: Optional[Dict[str, tuple]],
): # pylint: disable=too-many-branches,too-many-locals,too-many-statements
# Check headers
self._check_headers(headers)

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

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

# If files are send, build data to send and calculate length
content_length = 0
boundary_objects = None
if files and isinstance(files, dict):
boundary_string, content_length, boundary_objects = (
self._build_boundary_data(files)
)
content_type_header = f"multipart/form-data; boundary={boundary_string}"
else:
if data is None:
data = b""
content_length = len(data)

self._send_as_bytes(socket, method)
self._send(socket, b" /")
self._send_as_bytes(socket, path)
Expand All @@ -456,8 +543,8 @@ def _send_request(
self._send_header(socket, "User-Agent", "Adafruit CircuitPython")
if content_type_header and not "content-type" in supplied_headers:
self._send_header(socket, "Content-Type", content_type_header)
if data and not "content-length" in supplied_headers:
self._send_header(socket, "Content-Length", str(len(data)))
if (data or files) and not "content-length" in supplied_headers:
self._send_header(socket, "Content-Length", str(content_length))
# Iterate over keys to avoid tuple alloc
for header in headers:
self._send_header(socket, header, headers[header])
Expand All @@ -466,6 +553,8 @@ def _send_request(
# Send data
if data:
self._send(socket, bytes(data))
elif boundary_objects:
self._send_boundary_objects(socket, boundary_objects)

# pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
def request(
Expand All @@ -478,6 +567,7 @@ def request(
stream: bool = False,
timeout: float = 60,
allow_redirects: bool = True,
files: Optional[Dict[str, tuple]] = None,
) -> Response:
"""Perform an HTTP request to the given url which we will parse to determine
whether to use SSL ('https://') or not. We can also send some provided 'data'
Expand Down Expand Up @@ -526,7 +616,9 @@ def request(
)
ok = True
try:
self._send_request(socket, host, method, path, headers, data, json)
self._send_request(
socket, host, method, path, headers, data, json, files
)
except OSError as exc:
last_exc = exc
ok = False
Expand Down
27 changes: 27 additions & 0 deletions examples/wifi/expanded/requests_wifi_file_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
# SPDX-License-Identifier: MIT

import adafruit_connection_manager
import wifi

import adafruit_requests

URL = "https://httpbin.org/post"

pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)

with open("requests_wifi_file_upload_image.png", "rb") as file_handle:
files = {
"file": (
"requests_wifi_file_upload_image.png",
file_handle,
"image/png",
{"CustomHeader": "BlinkaRocks"},
),
"othervalue": (None, "HelloWorld"),
}

with requests.post(URL, files=files) as response:
print(response.content)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks
# SPDX-License-Identifier: CC-BY-4.0
2 changes: 2 additions & 0 deletions optional_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense

requests
Binary file added tests/files/green_red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions tests/files/green_red.png.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: 2024 Justin Myers
# SPDX-License-Identifier: Unlicense
Binary file added tests/files/red_green.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions tests/files/red_green.png.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: 2024 Justin Myers
# SPDX-License-Identifier: Unlicense
Loading

0 comments on commit d28ab9d

Please sign in to comment.