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)
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
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
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

0 comments on commit d28ab9d

Please sign in to comment.