Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
  • Loading branch information
pre-commit-ci[bot] committed May 2, 2026
commit 845268d0af11c48071fd4d489488ad9a0bb00044
20 changes: 11 additions & 9 deletions 16-sync-http-clients/src/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ async def fetch(self, request):
path = "/" + path.split("?", 1)[0] if path else "/"

if path == "/":
return Response.json({
"example": "Synchronous HTTP clients in Python Workers",
"target": TARGET_URL,
"clients": ["requests", "urllib3", "httpx.Client"],
"endpoints": {
"GET /sync": "Fetch using requests, urllib3, and httpx.Client",
"GET /all": "Alias for /sync",
},
})
return Response.json(
{
"example": "Synchronous HTTP clients in Python Workers",
"target": TARGET_URL,
"clients": ["requests", "urllib3", "httpx.Client"],
"endpoints": {
"GET /sync": "Fetch using requests, urllib3, and httpx.Client",
"GET /all": "Alias for /sync",
},
}
)

if path in ("/sync", "/all"):
return Response.json({"target": TARGET_URL, "results": self.fetch_sync()})
Expand Down
65 changes: 49 additions & 16 deletions 17-r2/src/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ def is_missing(value):
"""True for JS null or undefined values crossing into Python."""
if value is None or value is jsnull:
return True
return str(type(value)) == "<class 'pyodide.ffi.JsProxy'>" and str(value) == "undefined"
return (
str(type(value)) == "<class 'pyodide.ffi.JsProxy'>"
and str(value) == "undefined"
)


def json_response(data, status=200):
Expand Down Expand Up @@ -197,11 +200,15 @@ def range_options_from_header(range_header):
if not range_header:
return None, None
if not range_header.startswith("bytes="):
return None, json_response({"error": "only bytes ranges are supported"}, status=400)
return None, json_response(
{"error": "only bytes ranges are supported"}, status=400
)

spec = range_header[len("bytes=") :].strip()
if "," in spec or "-" not in spec:
return None, json_response({"error": "only a single byte range is supported"}, status=400)
return None, json_response(
{"error": "only a single byte range is supported"}, status=400
)

start, end = spec.split("-", 1)
try:
Expand All @@ -217,7 +224,9 @@ def range_options_from_header(range_header):
return {"range": {"offset": offset}}, None
end_number = int(end)
if end_number < offset:
return None, json_response({"error": "range end must be >= start"}, status=400)
return None, json_response(
{"error": "range end must be >= start"}, status=400
)
return {"range": {"offset": offset, "length": end_number - offset + 1}}, None
except ValueError:
return None, json_response({"error": "invalid range"}, status=400)
Expand Down Expand Up @@ -266,9 +275,13 @@ def object_response(obj, is_range=False):
offset = int(obj_range.offset)
length = int(obj_range.length)
headers.set("content-length", str(length))
headers.set("content-range", f"bytes {offset}-{offset + length - 1}/{int(obj.size)}")
headers.set(
"content-range", f"bytes {offset}-{offset + length - 1}/{int(obj.size)}"
)

return JsResponse.new(obj.body, to_js_object({"status": status, "headers": headers}))
return JsResponse.new(
obj.body, to_js_object({"status": status, "headers": headers})
)


def head_response(obj):
Expand Down Expand Up @@ -378,7 +391,9 @@ async def handle_large_file(self, path, method, request):

if is_check:
if method != "GET":
return json_response({"error": f"method {method} not allowed"}, status=405)
return json_response(
{"error": f"method {method} not allowed"}, status=405
)
return await self.check_large_file(name, info)

if method not in ("GET", "HEAD"):
Expand Down Expand Up @@ -454,9 +469,13 @@ async def delete_objects(self, request):
data = body.to_py() if hasattr(body, "to_py") else body
keys = data.get("keys") if isinstance(data, dict) else None
if not isinstance(keys, list) or not keys:
return json_response({"error": "body must be JSON like {'keys': ['a', 'b']}"}, status=400)
return json_response(
{"error": "body must be JSON like {'keys': ['a', 'b']}"}, status=400
)
if len(keys) > 1000:
return json_response({"error": "R2 delete() accepts at most 1000 keys"}, status=400)
return json_response(
{"error": "R2 delete() accepts at most 1000 keys"}, status=400
)

await self.env.BUCKET.delete(to_js(keys))
return json_response({"deleted": keys})
Expand All @@ -476,7 +495,9 @@ async def handle_multipart(self, request, url, path, method):
upload = await self.env.BUCKET.createMultipartUpload(key)
else:
upload = await self.env.BUCKET.createMultipartUpload(key, options)
return json_response({"key": str(upload.key), "uploadId": str(upload.uploadId)}, status=201)
return json_response(
{"key": str(upload.key), "uploadId": str(upload.uploadId)}, status=201
)

if not parts:
return json_response({"error": "missing uploadId"}, status=400)
Expand All @@ -488,15 +509,21 @@ async def handle_multipart(self, request, url, path, method):
try:
part_number = int(parts[1])
except ValueError:
return json_response({"error": "partNumber must be an integer"}, status=400)
return json_response(
{"error": "partNumber must be an integer"}, status=400
)

options, error = object_options_from_request(request)
if error:
return error
if options is None:
uploaded_part = await upload.uploadPart(part_number, request.js_object.body)
uploaded_part = await upload.uploadPart(
part_number, request.js_object.body
)
else:
uploaded_part = await upload.uploadPart(part_number, request.js_object.body, options)
uploaded_part = await upload.uploadPart(
part_number, request.js_object.body, options
)
return json_response(
{
"key": key,
Expand Down Expand Up @@ -538,7 +565,9 @@ async def list_objects(self, url):
except ValueError:
return json_response({"error": "limit must be an integer"}, status=400)
if parsed_limit < 1 or parsed_limit > 1000:
return json_response({"error": "limit must be between 1 and 1000"}, status=400)
return json_response(
{"error": "limit must be between 1 and 1000"}, status=400
)
options["limit"] = parsed_limit
if delimiter:
options["delimiter"] = delimiter
Expand All @@ -558,14 +587,18 @@ async def list_objects(self, url):
for i in range(int(listing.objects.length))
]
delimited_prefixes = []
if hasattr(listing, "delimitedPrefixes") and not is_missing(listing.delimitedPrefixes):
if hasattr(listing, "delimitedPrefixes") and not is_missing(
listing.delimitedPrefixes
):
delimited_prefixes = list(listing.delimitedPrefixes.to_py())

return json_response(
{
"objects": objects,
"truncated": bool(listing.truncated),
"cursor": None if is_missing(getattr(listing, "cursor", None)) else str(listing.cursor),
"cursor": None
if is_missing(getattr(listing, "cursor", None))
else str(listing.cursor),
"delimitedPrefixes": delimited_prefixes,
}
)
6 changes: 4 additions & 2 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ def test_17_r2(dev_server):
assert root["example"] == "Cloudflare R2 from a Python Worker"
assert root["large_files"]["hvsc"]["key"] == "vsc/84/raw/HVSC_84-all-of-them.7z"
assert root["large_files"]["shakespeare"]["key"] == "pg100-h.zip"
assert root["large_files"]["shakespeare"]["source_url"] == "https://www.gutenberg.org/cache/epub/100/pg100-h.zip"
assert (
root["large_files"]["shakespeare"]["source_url"]
== "https://www.gutenberg.org/cache/epub/100/pg100-h.zip"
)

payload = b"Hello from R2 and Python Workers!"
response = requests.put(
Expand Down Expand Up @@ -216,7 +219,6 @@ def test_16_sync_http_clients(dev_server):
assert result["saw_expected_text"] is True



def test_08_cron(dev_server):
port = dev_server
response = requests.get(f"http://localhost:{port}")
Expand Down