Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:

- run: |
mk python-release owner=libre-embedded \
repo=runtimepy version=5.15.7
repo=runtimepy version=5.15.8
if: |
matrix.python-version == '3.12'
&& matrix.system == 'ubuntu-latest'
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
=====================================
generator=datazen
version=3.2.3
hash=fd0b9f87927033d8ee2eb5448d8aec76
hash=c354cb6439439285d244ad2bdcc32cc8
=====================================
-->

# runtimepy ([5.15.7](https://pypi.org/project/runtimepy/))
# runtimepy ([5.15.8](https://pypi.org/project/runtimepy/))

[![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/)
![Build Status](https://github.com/libre-embedded/runtimepy/workflows/Python%20Package/badge.svg)
Expand Down
2 changes: 1 addition & 1 deletion local/variables/package.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
major: 5
minor: 15
patch: 7
patch: 8
entry: runtimepy
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__"

[project]
name = "runtimepy"
version = "5.15.7"
version = "5.15.8"
description = "A framework for implementing Python services."
readme = "README.md"
requires-python = ">=3.12"
Expand Down
4 changes: 2 additions & 2 deletions runtimepy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# =====================================
# generator=datazen
# version=3.2.3
# hash=7591d3ee09b156575678bdf806e1b5c7
# hash=be3f79364e840f1950bc121205810f02
# =====================================

"""
Expand All @@ -10,7 +10,7 @@

DESCRIPTION = "A framework for implementing Python services."
PKG_NAME = "runtimepy"
VERSION = "5.15.7"
VERSION = "5.15.8"

# runtimepy-specific content.
METRICS_NAME = "metrics"
Expand Down
22 changes: 18 additions & 4 deletions runtimepy/net/http/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
"""

# built-in
from abc import ABC, abstractmethod
import http
from io import StringIO
import logging
from pathlib import Path
from typing import AsyncIterator, cast
from typing import AsyncIterator, Optional, cast

# third-party
import aiofiles
Expand Down Expand Up @@ -96,19 +97,32 @@ def static_resource(
self["Cache-Control"] = value


class AsyncResponse:
class AsyncResponse(ABC):
"""Interface for asynchronous responses."""

@abstractmethod
async def size(self) -> Optional[int]:
"""Get this response's size."""

@abstractmethod
async def process(self) -> AsyncIterator[bytes]:
"""Yield chunks to write asynchronously."""
yield bytes() # pragma: nocover


class AsyncFile(AsyncResponse):
"""
A class facilitating asynchronous server responses for e.g.
file-system files.
"""

def __init__(self, path: Path, chunk_size: int = 1024) -> None:
def __init__(self, path: Path, chunk_size: int = 4096) -> None:
"""Initialize this instance."""

self.path = path
self.chunk_size = chunk_size

async def size(self) -> int:
async def size(self) -> Optional[int]:
"""Get this response's size."""
return cast(int, await aiofiles.os.path.getsize(self.path))

Expand Down
18 changes: 15 additions & 3 deletions runtimepy/net/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import logging
import mimetypes
from pathlib import Path
from typing import Any, Optional, TextIO, Union
from typing import Any, Awaitable, Callable, Optional, TextIO, Union
from urllib.parse import urlencode

# third-party
Expand All @@ -22,7 +22,7 @@
from runtimepy.net.html import full_markdown_page
from runtimepy.net.http.header import RequestHeader
from runtimepy.net.http.request_target import PathMaybeQuery
from runtimepy.net.http.response import AsyncResponse, ResponseHeader
from runtimepy.net.http.response import AsyncFile, ResponseHeader
from runtimepy.net.server.html import HtmlApp, HtmlApps, get_html, html_handler
from runtimepy.net.server.json import encode_json, json_handler
from runtimepy.net.server.markdown import DIR_FILE, markdown_for_dir
Expand All @@ -41,11 +41,17 @@ def package_data_dir() -> Path:
return result.parent


CustomAsync = Callable[
[ResponseHeader, RequestHeader, Optional[bytearray]], Awaitable[HttpResult]
]


class RuntimepyServerConnection(HttpConnection):
"""A class implementing a server-connection interface for this package."""

# Can register application methods to URL paths.
apps: HtmlApps = {"/mux.html": mux_app}
custom: dict[str, CustomAsync] = {}
default_app: Optional[HtmlApp] = None

# Can load additional data into this dictionary for easy HTTP access.
Expand Down Expand Up @@ -245,7 +251,7 @@ async def try_file(
response.static_resource()

# Return the file data.
result = AsyncResponse(candidate)
result = AsyncFile(candidate)
break

# Handle a directory as a last resort.
Expand Down Expand Up @@ -335,6 +341,12 @@ async def get_handler(
response.static_resource()
return self.favicon_data

# Check for a custom handler.
if path in self.custom:
return await self.custom[path](
response, request, request_data
)

# Try serving a file and handling redirects.
for handler in [self.try_redirect, self.try_file]:
result = await handler(
Expand Down
10 changes: 5 additions & 5 deletions runtimepy/net/tcp/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,13 @@ async def _send(
) -> None:
"""Send a request or response to a request."""

# Set content length.
header["content-length"] = "0"

size = None
if isinstance(data, AsyncResponse):
header["content-length"] = str(await data.size())
size = await data.size()
elif data is not None:
header["content-length"] = str(len(data))
size = len(data)
if size is not None:
header["content-length"] = str(size)

self.send_binary(bytes(header))

Expand Down
8 changes: 5 additions & 3 deletions tests/net/http/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pytest import mark

# module under test
from runtimepy.net.http.response import AsyncResponse
from runtimepy.net.http.response import AsyncFile

# built-in
from tests.resources import resource
Expand All @@ -16,7 +16,9 @@
async def test_async_response_basic():
"""Test basic async responses."""

inst = AsyncResponse(resource("test_bigger.txt"))
assert await inst.size() > 0
inst = AsyncFile(resource("test_bigger.txt"))
size = await inst.size()
assert size is not None
assert size > 0
async for chunk in inst.process():
assert chunk
2 changes: 2 additions & 0 deletions tests/net/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ async def runtimepy_http_client_server(
client.request(RequestHeader(target="/pyproject.toml")),
# favicon.ico.
client.request(RequestHeader(target="/favicon.ico")),
# Custom handlers.
client.request(RequestHeader(target="/custom_handler")),
# JSON queries.
client.request_json(RequestHeader(target="/json")),
client.request_json(RequestHeader(target="/json//////")),
Expand Down
20 changes: 17 additions & 3 deletions tests/net/stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@
from runtimepy.message import JsonMessage
from runtimepy.net.arbiter.info import AppInfo
from runtimepy.net.http.header import RequestHeader
from runtimepy.net.http.response import ResponseHeader
from runtimepy.net.http.response import AsyncFile, ResponseHeader
from runtimepy.net.server import RuntimepyServerConnection
from runtimepy.net.server.websocket import RuntimepyWebsocketConnection
from runtimepy.net.stream import StringMessageConnection
from runtimepy.net.stream.json import JsonMessageConnection
from runtimepy.net.tcp.http import HttpConnection
from runtimepy.net.tcp.http import HttpConnection, HttpResult
from runtimepy.net.udp import UdpConnection
from tests.net.server import (
runtimepy_http_client_server,
runtimepy_websocket_client,
)

# internal
from tests.resources import SampleArbiterTask
from tests.resources import SampleArbiterTask, resource


async def sample_handler(
Expand Down Expand Up @@ -106,6 +106,20 @@ async def runtimepy_http_test(app: AppInfo) -> int:
server = conn
assert server is not None

async def custom_handler(
response: ResponseHeader,
request: RequestHeader,
data: Optional[bytearray],
) -> HttpResult:
"""Sample custom handler."""

del response
del request
del data
return AsyncFile(resource("test_bigger.txt"))

RuntimepyServerConnection.custom["/custom_handler"] = custom_handler

await runtimepy_http_client_server(app, client, server)

await runtimepy_websocket_client(
Expand Down
Loading