Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Files arg #189

Merged
merged 16 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
dhalbert marked this conversation as resolved.
Show resolved Hide resolved

if not is_binary:
raise AttributeError("Files must be opened in binary mode")
dhalbert marked this conversation as resolved.
Show resolved Hide resolved

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")
dhalbert marked this conversation as resolved.
Show resolved Hide resolved

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
Loading