Skip to content

Commit c97cc4b

Browse files
committed
Merge branch 'refs/heads/main' into ruff-updates
# Conflicts: # adafruit_requests.py # examples/cpython/requests_cpython_simpletest.py # examples/esp32spi/requests_esp32spi_simpletest.py # examples/fona/requests_fona_simpletest.py # examples/wifi/expanded/requests_wifi_api_fitbit.py # examples/wifi/expanded/requests_wifi_api_openskynetwork_private.py # examples/wifi/expanded/requests_wifi_api_openskynetwork_private_area.py # examples/wifi/expanded/requests_wifi_api_openskynetwork_public.py # examples/wifi/expanded/requests_wifi_api_twitter.py # examples/wifi/expanded/requests_wifi_api_youtube.py # examples/wifi/requests_wifi_simpletest.py # examples/wiznet5k/requests_wiznet5k_simpletest.py
2 parents 69406bb + 6c56de7 commit c97cc4b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1702
-904
lines changed

adafruit_requests.py

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@
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
50-
from typing import Any, Dict, Optional, Type
53+
from typing import IO, Any, Dict, Optional, Type
5154

5255
from circuitpython_typing.socket import (
5356
SocketpoolModuleType,
@@ -83,6 +86,21 @@ class Response:
8386
"""The response from a request, contains all the headers/content"""
8487

8588
encoding = None
89+
socket: SocketType
90+
"""The underlying socket object (CircuitPython extension, not in standard requests)
91+
92+
Under the following circumstances, calling code may directly access the underlying
93+
socket object:
94+
95+
* The request was made with ``stream=True``
96+
* The request headers included ``{'connection': 'close'}``
97+
* No methods or properties on the Response object that access the response content
98+
may be used
99+
100+
Methods and properties that access response headers may be accessed.
101+
102+
It is still necessary to ``close`` the response object for correct management of
103+
sockets, including doing so implicitly via ``with requests.get(...) as response``."""
86104

87105
def __init__(self, sock: SocketType, session: "Session") -> None:
88106
self.socket = sock
@@ -245,7 +263,8 @@ def _parse_headers(self) -> None:
245263
header = self._readto(b"\r\n")
246264
if not header:
247265
break
248-
title, content = bytes(header).split(b": ", 1)
266+
title, content = bytes(header).split(b":", 1)
267+
content = content.strip()
249268
if title and content:
250269
# enforce that all headers are lowercase
251270
title = str(title, "utf-8").lower()
@@ -318,7 +337,7 @@ def json(self) -> Any:
318337
obj = json_module.load(self._raw)
319338
if not self._cached:
320339
self._cached = obj
321-
self.close()
340+
322341
return obj
323342

324343
def iter_content(self, chunk_size: int = 1, decode_unicode: bool = False) -> bytes:
@@ -354,18 +373,81 @@ def __init__(
354373
self._session_id = session_id
355374
self._last_response = None
356375

376+
def _build_boundary_data(self, files: dict): # pylint: disable=too-many-locals
377+
boundary_string = self._build_boundary_string()
378+
content_length = 0
379+
boundary_objects = []
380+
381+
for field_name, field_values in files.items():
382+
file_name = field_values[0]
383+
file_handle = field_values[1]
384+
385+
boundary_objects.append(
386+
f'--{boundary_string}\r\nContent-Disposition: form-data; name="{field_name}"'
387+
)
388+
if file_name is not None:
389+
boundary_objects.append(f'; filename="{file_name}"')
390+
boundary_objects.append("\r\n")
391+
if len(field_values) >= 3:
392+
file_content_type = field_values[2]
393+
boundary_objects.append(f"Content-Type: {file_content_type}\r\n")
394+
if len(field_values) >= 4:
395+
file_headers = field_values[3]
396+
for file_header_key, file_header_value in file_headers.items():
397+
boundary_objects.append(
398+
f"{file_header_key}: {file_header_value}\r\n"
399+
)
400+
boundary_objects.append("\r\n")
401+
402+
if hasattr(file_handle, "read"):
403+
content_length += self._get_file_length(file_handle)
404+
405+
boundary_objects.append(file_handle)
406+
boundary_objects.append("\r\n")
407+
408+
boundary_objects.append(f"--{boundary_string}--\r\n")
409+
410+
for boundary_object in boundary_objects:
411+
if isinstance(boundary_object, str):
412+
content_length += len(boundary_object)
413+
414+
return boundary_string, content_length, boundary_objects
415+
416+
@staticmethod
417+
def _build_boundary_string():
418+
return os.urandom(16).hex()
419+
357420
@staticmethod
358421
def _check_headers(headers: Dict[str, str]):
359422
if not isinstance(headers, dict):
360-
raise AttributeError("headers must be in dict format")
423+
raise TypeError("Headers must be in dict format")
361424

362425
for key, value in headers.items():
363426
if isinstance(value, (str, bytes)) or value is None:
364427
continue
365-
raise AttributeError(
428+
raise TypeError(
366429
f"Header part ({value}) from {key} must be of type str or bytes, not {type(value)}"
367430
)
368431

432+
@staticmethod
433+
def _get_file_length(file_handle: IO):
434+
is_binary = False
435+
try:
436+
file_handle.seek(0)
437+
# read at least 4 bytes incase we are reading a b64 stream
438+
content = file_handle.read(4)
439+
is_binary = isinstance(content, bytes)
440+
except UnicodeError:
441+
is_binary = False
442+
443+
if not is_binary:
444+
raise ValueError("Files must be opened in binary mode")
445+
446+
file_handle.seek(0, SEEK_END)
447+
content_length = file_handle.tell()
448+
file_handle.seek(0)
449+
return content_length
450+
369451
@staticmethod
370452
def _send(socket: SocketType, data: bytes):
371453
total_sent = 0
@@ -391,6 +473,22 @@ def _send(socket: SocketType, data: bytes):
391473
def _send_as_bytes(self, socket: SocketType, data: str):
392474
return self._send(socket, bytes(data, "utf-8"))
393475

476+
def _send_boundary_objects(self, socket: SocketType, boundary_objects: Any):
477+
for boundary_object in boundary_objects:
478+
if isinstance(boundary_object, str):
479+
self._send_as_bytes(socket, boundary_object)
480+
else:
481+
self._send_file(socket, boundary_object)
482+
483+
def _send_file(self, socket: SocketType, file_handle: IO):
484+
chunk_size = 36
485+
b = bytearray(chunk_size)
486+
while True:
487+
size = file_handle.readinto(b)
488+
if size == 0:
489+
break
490+
self._send(socket, b[:size])
491+
394492
def _send_header(self, socket, header, value):
395493
if value is None:
396494
return
@@ -411,6 +509,7 @@ def _send_request( # noqa: PLR0913 Too many arguments in function definition
411509
headers: Dict[str, str],
412510
data: Any,
413511
json: Any,
512+
files: Optional[Dict[str, tuple]],
414513
):
415514
# Check headers
416515
self._check_headers(headers)
@@ -421,11 +520,13 @@ def _send_request( # noqa: PLR0913 Too many arguments in function definition
421520
# If json is sent, set content type header and convert to string
422521
if json is not None:
423522
assert data is None
523+
assert files is None
424524
content_type_header = "application/json"
425525
data = json_module.dumps(json)
426526

427527
# If data is sent and it's a dict, set content type header and convert to string
428528
if data and isinstance(data, dict):
529+
assert files is None
429530
content_type_header = "application/x-www-form-urlencoded"
430531
_post_data = ""
431532
for k in data:
@@ -437,6 +538,23 @@ def _send_request( # noqa: PLR0913 Too many arguments in function definition
437538
if data and isinstance(data, str):
438539
data = bytes(data, "utf-8")
439540

541+
# If files are send, build data to send and calculate length
542+
content_length = 0
543+
data_is_file = False
544+
boundary_objects = None
545+
if files and isinstance(files, dict):
546+
boundary_string, content_length, boundary_objects = (
547+
self._build_boundary_data(files)
548+
)
549+
content_type_header = f"multipart/form-data; boundary={boundary_string}"
550+
elif data and hasattr(data, "read"):
551+
data_is_file = True
552+
content_length = self._get_file_length(data)
553+
else:
554+
if data is None:
555+
data = b""
556+
content_length = len(data)
557+
440558
self._send_as_bytes(socket, method)
441559
self._send(socket, b" /")
442560
self._send_as_bytes(socket, path)
@@ -452,16 +570,20 @@ def _send_request( # noqa: PLR0913 Too many arguments in function definition
452570
self._send_header(socket, "User-Agent", "Adafruit CircuitPython")
453571
if content_type_header and not "content-type" in supplied_headers:
454572
self._send_header(socket, "Content-Type", content_type_header)
455-
if data and not "content-length" in supplied_headers:
456-
self._send_header(socket, "Content-Length", str(len(data)))
573+
if (data or files) and not "content-length" in supplied_headers:
574+
self._send_header(socket, "Content-Length", str(content_length))
457575
# Iterate over keys to avoid tuple alloc
458576
for header in headers:
459577
self._send_header(socket, header, headers[header])
460578
self._send(socket, b"\r\n")
461579

462580
# Send data
463-
if data:
581+
if data_is_file:
582+
self._send_file(socket, data)
583+
elif data:
464584
self._send(socket, bytes(data))
585+
elif boundary_objects:
586+
self._send_boundary_objects(socket, boundary_objects)
465587

466588
def request( # noqa: PLR0912,PLR0913,PLR0915 Too many branches,Too many arguments in function definition,Too many statements
467589
self,
@@ -473,6 +595,7 @@ def request( # noqa: PLR0912,PLR0913,PLR0915 Too many branches,Too many argumen
473595
stream: bool = False,
474596
timeout: float = 60,
475597
allow_redirects: bool = True,
598+
files: Optional[Dict[str, tuple]] = None,
476599
) -> Response:
477600
"""Perform an HTTP request to the given url which we will parse to determine
478601
whether to use SSL ('https://') or not. We can also send some provided 'data'
@@ -521,7 +644,9 @@ def request( # noqa: PLR0912,PLR0913,PLR0915 Too many branches,Too many argumen
521644
)
522645
ok = True
523646
try:
524-
self._send_request(socket, host, method, path, headers, data, json)
647+
self._send_request(
648+
socket, host, method, path, headers, data, json, files
649+
)
525650
except OSError as exc:
526651
last_exc = exc
527652
ok = False

examples/cpython/requests_cpython_advanced.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,13 @@
1515
headers = {"user-agent": "blinka/1.0.0"}
1616

1717
print("Fetching JSON data from %s..." % JSON_GET_URL)
18-
response = requests.get(JSON_GET_URL, headers=headers)
19-
print("-" * 60)
20-
21-
json_data = response.json()
22-
headers = json_data["headers"]
23-
print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"]))
24-
print("-" * 60)
25-
26-
# Read Response's HTTP status code
27-
print("Response HTTP Status Code: ", response.status_code)
28-
print("-" * 60)
29-
30-
# Close, delete and collect the response data
31-
response.close()
18+
with requests.get(JSON_GET_URL, headers=headers) as response:
19+
print("-" * 60)
20+
json_data = response.json()
21+
headers = json_data["headers"]
22+
print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"]))
23+
print("-" * 60)
24+
25+
# Read Response's HTTP status code
26+
print("Response HTTP Status Code: ", response.status_code)
27+
print("-" * 60)

examples/cpython/requests_cpython_simpletest.py

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,31 @@
1414
JSON_POST_URL = "https://httpbin.org/post"
1515

1616
print("Fetching text from %s" % TEXT_URL)
17-
response = requests.get(TEXT_URL)
18-
print("-" * 40)
19-
20-
print("Text Response: ", response.text)
21-
print("-" * 40)
22-
response.close()
17+
with requests.get(TEXT_URL) as response:
18+
print("-" * 40)
19+
print("Text Response: ", response.text)
20+
print("-" * 40)
2321

2422
print("Fetching JSON data from %s" % JSON_GET_URL)
25-
response = requests.get(JSON_GET_URL)
26-
print("-" * 40)
27-
28-
print("JSON Response: ", response.json())
29-
print("-" * 40)
30-
response.close()
23+
with requests.get(JSON_GET_URL) as response:
24+
print("-" * 40)
25+
print("JSON Response: ", response.json())
26+
print("-" * 40)
3127

3228
data = "31F"
3329
print(f"POSTing data to {JSON_POST_URL}: {data}")
34-
response = requests.post(JSON_POST_URL, data=data)
35-
print("-" * 40)
36-
37-
json_resp = response.json()
38-
# Parse out the 'data' key from json_resp dict.
39-
print("Data received from server:", json_resp["data"])
40-
print("-" * 40)
41-
response.close()
30+
with requests.post(JSON_POST_URL, data=data) as response:
31+
print("-" * 40)
32+
json_resp = response.json()
33+
# Parse out the 'data' key from json_resp dict.
34+
print("Data received from server:", json_resp["data"])
35+
print("-" * 40)
4236

4337
json_data = {"Date": "July 25, 2019"}
4438
print(f"POSTing data to {JSON_POST_URL}: {json_data}")
45-
response = requests.post(JSON_POST_URL, json=json_data)
46-
print("-" * 40)
47-
48-
json_resp = response.json()
49-
# Parse out the 'json' key from json_resp dict.
50-
print("JSON Data received from server:", json_resp["json"])
51-
print("-" * 40)
52-
response.close()
39+
with requests.post(JSON_POST_URL, json=json_data) as response:
40+
print("-" * 40)
41+
json_resp = response.json()
42+
# Parse out the 'json' key from json_resp dict.
43+
print("JSON Data received from server:", json_resp["json"])
44+
print("-" * 40)

examples/esp32spi/requests_esp32spi_advanced.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,14 @@
5353
headers = {"user-agent": "blinka/1.0.0"}
5454

5555
print("Fetching JSON data from %s..." % JSON_GET_URL)
56-
response = requests.get(JSON_GET_URL, headers=headers)
57-
print("-" * 60)
56+
with requests.get(JSON_GET_URL, headers=headers) as response:
57+
print("-" * 60)
5858

59-
json_data = response.json()
60-
headers = json_data["headers"]
61-
print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"]))
62-
print("-" * 60)
59+
json_data = response.json()
60+
headers = json_data["headers"]
61+
print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"]))
62+
print("-" * 60)
6363

64-
# Read Response's HTTP status code
65-
print("Response HTTP Status Code: ", response.status_code)
66-
print("-" * 60)
67-
68-
# Close, delete and collect the response data
69-
response.close()
64+
# Read Response's HTTP status code
65+
print("Response HTTP Status Code: ", response.status_code)
66+
print("-" * 60)

0 commit comments

Comments
 (0)