Skip to content

Commit

Permalink
5.7.6 - Minor fixes
Browse files Browse the repository at this point in the history
* Better 404 and dual stack handling
* fixes for windows
  • Loading branch information
vkottler committed Oct 23, 2024
1 parent a9fa941 commit b53c9d2
Show file tree
Hide file tree
Showing 16 changed files with 117 additions and 48 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
- run: |
mk python-release owner=vkottler \
repo=runtimepy version=5.7.5
repo=runtimepy version=5.7.6
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.1.4
hash=c982a40a3245e84b1ae3b70a17cc6867
hash=41ff8742602f69f2831092a4b666cddc
=====================================
-->

# runtimepy ([5.7.5](https://pypi.org/project/runtimepy/))
# runtimepy ([5.7.6](https://pypi.org/project/runtimepy/))

[![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/)
![Build Status](https://github.com/vkottler/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: 7
patch: 5
patch: 6
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.7.5"
version = "5.7.6"
description = "A framework for implementing Python services."
readme = "README.md"
requires-python = ">=3.11"
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.1.4
# hash=3797b5155c13d8d7422bdbfcb1248cca
# hash=cab7cff2cb194a61178fa386b80eae85
# =====================================

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

DESCRIPTION = "A framework for implementing Python services."
PKG_NAME = "runtimepy"
VERSION = "5.7.5"
VERSION = "5.7.6"

# runtimepy-specific content.
METRICS_NAME = "metrics"
Expand Down
3 changes: 3 additions & 0 deletions runtimepy/data/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>runtimepy/5.7.6</title><style>@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Regular.woff2)}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Bold.woff2);font-weight:700}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-BoldItalic.woff2);font-weight:700;font-style:italic}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Italic.woff2);font-style:italic}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Regular.woff2)}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Bold.woff2);font-weight:700}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-BoldItalic.woff2);font-weight:700;font-style:italic}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Italic.woff2);font-style:italic}</style><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css></link>
<link href=https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css rel=stylesheet crossorigin=anonymous></link><style>html{height:100%}body{height:100%;margin:0;overflow:hidden}body>:first-child{height:100%}#runtimepy{height:100%}#runtimepy-tabs{width:min-content}#runtimepy-splash{position:fixed;top:0;left:0;width:100vw;height:100vh;opacity:1}.click-plot{cursor:pointer}.stale{color:var(--bs-warning-text-emphasis)!important}.slider{min-width:8em}.toggle-value{}.window-button{border-right:var(--bs-border-width)var(--bs-border-style)var(--bs-link-hover-color)!important}canvas:focus{outline:none}.flex-column-scroll-bodge{height:100%;flex-wrap:nowrap;overflow-y:scroll;flex-shrink:0}.scroll{overflow:scroll}.tab-content-bodge{width:100%;height:100%}.button-bodge{text-align:left}.collapsing{transition:none!important}.tab-pane.fade{transition:none!important}.modal.fade{transition:none!important}.modal-dialog{width:80%;max-width:80%}.table{margin-bottom:0;width:auto}.table-container{overflow-x:scroll;min-height:fit-content}.channel-column{overflow-y:scroll;overflow-x:hidden;flex-grow:0;flex-shrink:0}.channel-value{min-width:10em}.table>tbody>tr>td{vertical-align:middle}.collapse:not(.show){display:none!important}select.form-select{width:min-content}select.form-select:hover{cursor:pointer}textarea.text-logs{min-height:10em!important;padding-top:0!important;padding-bottom:0!important;border-top:0}.vertical-divider{flex-basis:.75em;flex-grow:0;flex-shrink:0}.vertical-divider:hover{cursor:col-resize;background-color:var(--bs-highlight-bg)!important}button:hover{background-color:var(--bs-tertiary-bg)}.channel-value-input{width:6em}:root,[data-bs-theme=dark],[data-bs-theme=light]{--bs-font-sans-serif:CascadiaCode, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace:CascadiaMono, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}</style><div id=runtimepy class="align-items-start bg-body d-flex" data-bs-theme=dark><div class="bg-dark-subtle h-100 d-flex flex-column"><button type=button id=theme-button class="btn btn-secondary rounded-0 font-monospace button-bodge text-nowrap has-tooltip" data-bs-title=" Toggle light/dark." data-bs-placement=right>
<i class="bi bi-lightbulb"></i></button></div><div class="overflow-y-auto d-flex flex-grow-1 h-100 flex-column justify-content-between text-body"><div></div><div class="d-flex flex-row justify-content-between"><div></div><div class="text-body p-3 pb-0"><h1><a href=https://libre-embedded.com><img alt=logo src=https://libre-embedded.com/static/png/chip-circle-bootstrap/128x128.png></a> Resource Not Found (404) <a href=https://libre-embedded.com><img alt=logo src=https://libre-embedded.com/static/png/chip-circle-bootstrap/128x128.png></a></h1><p>(<a href=https://libre-embedded.com>home</a>)</div><div></div></div><div></div></div></div><script>let lightMode=!1;function lightDarkClick(){lightMode=!lightMode,document.getElementById("runtimepy").setAttribute("data-bs-theme",lightMode?"light":"dark"),window.location.hash=lightMode?"#light-mode":""}let lightDarkButton=document.getElementById("theme-button");if(lightDarkButton&&lightDarkButton.addEventListener("click",lightDarkClick),window.location.hash){let e=window.location.hash.slice(1).split(",");e.includes("light-mode")&&lightDarkButton.click()}</script><script src=https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js crossorigin=anonymous></script>
11 changes: 8 additions & 3 deletions runtimepy/data/server_base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ structs:
init:
- runtimepy.net.arbiter.housekeeping.init

config:
# Default redirects.
http_redirects:
"/": "/app.html"
"/index.html": "/app.html"

servers:
- factory: runtimepy_http
kwargs: {port: "$runtimepy_http_server"}

- factory: runtimepy_websocket_json
kwargs: {host: "0.0.0.0", port: "$runtimepy_websocket_json_server"}
kwargs: {port: "$runtimepy_websocket_json_server"}
- factory: runtimepy_websocket_data
kwargs: {host: "0.0.0.0", port: "$runtimepy_websocket_data_server"}
kwargs: {port: "$runtimepy_websocket_data_server"}
68 changes: 46 additions & 22 deletions runtimepy/net/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ def init(self) -> None:
with favicon.open("rb") as favicon_fd:
type(self).favicon_data = favicon_fd.read()

def redirect_to(
self,
path: str,
response: ResponseHeader,
status: http.HTTPStatus = http.HTTPStatus.TEMPORARY_REDIRECT,
) -> bytes:
"""Handle responding with redirection status."""

response["Location"] = path
response.status = status
return bytes()

async def try_redirect(
self, path: PathMaybeQuery, response: ResponseHeader
) -> Optional[bytes]:
Expand All @@ -118,33 +130,39 @@ async def try_redirect(

curr = Path(path[0])
if curr in self.class_redirect_paths:
response["Location"] = str(self.class_redirect_paths[curr])
response.status = http.HTTPStatus.TEMPORARY_REDIRECT

# No data payload, but signal to caller that a response is ready.
result = bytes()
result = self.redirect_to(
str(self.class_redirect_paths[curr]), response
)

return result

async def render_markdown(
self, path: Path, response: ResponseHeader, **kwargs
def render_markdown(
self, content: str, response: ResponseHeader, **kwargs
) -> bytes:
"""Render a markdown file as HTML and return the result."""
"""Return rendered markdown content."""

document = get_html()

async with aiofiles.open(path, mode="r") as path_fd:
with IndentedFileWriter.string() as writer:
writer.write_markdown(await path_fd.read(), **kwargs)
full_markdown_page(
document,
writer.stream.getvalue(), # type: ignore
)
with IndentedFileWriter.string() as writer:
writer.write_markdown(content, **kwargs)
full_markdown_page(
document,
writer.stream.getvalue(), # type: ignore
)

response["Content-Type"] = f"text/html; charset={DEFAULT_ENCODING}"

return document.encode_str().encode()

async def render_markdown_file(
self, path: Path, response: ResponseHeader, **kwargs
) -> bytes:
"""Render a markdown file as HTML and return the result."""

async with aiofiles.open(path, mode="r") as path_fd:
return self.render_markdown(
await path_fd.read(), response, **kwargs
)

async def try_file(
self, path: PathMaybeQuery, response: ResponseHeader
) -> Optional[bytes]:
Expand All @@ -157,9 +175,12 @@ async def try_file(
candidate = search.joinpath(path[0][1:])

# Handle markdown sources.
md_candidate = candidate.with_suffix(".md")
if md_candidate.is_file():
return await self.render_markdown(md_candidate, response)
if candidate.name:
md_candidate = candidate.with_suffix(".md")
if md_candidate.is_file():
return await self.render_markdown_file(
md_candidate, response
)

if candidate.is_file():
mime, encoding = mimetypes.guess_type(candidate, strict=False)
Expand Down Expand Up @@ -239,6 +260,7 @@ async def get_handler(
request.log(self.logger, False, level=logging.INFO)

result = None
populated = False

with StringIO() as stream:
if request.target.origin_form:
Expand Down Expand Up @@ -266,10 +288,11 @@ async def get_handler(
request_data,
self.json_data,
)
populated = True

# Serve the application.
else:
await html_handler(
populated = await html_handler(
type(self).apps,
stream,
request,
Expand All @@ -278,6 +301,7 @@ async def get_handler(
default_app=type(self).default_app,
)

result = stream.getvalue().encode()
if populated:
result = stream.getvalue().encode()

return result
return result or self.redirect_to("/404.html", response)
6 changes: 3 additions & 3 deletions runtimepy/net/server/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ async def setup(app: AppInfo) -> int:
)

# Default application (environment tabs).
web_app = create_app(app, getattr(_import_module(module), method))
RuntimepyServerConnection.apps["/app.html"] = web_app
RuntimepyServerConnection.default_app = web_app
RuntimepyServerConnection.apps["/app.html"] = create_app(
app, getattr(_import_module(module), method)
)

# Register redirects.
redirects: dict[str, str] = app.config_param("http_redirects", {})
Expand Down
4 changes: 3 additions & 1 deletion runtimepy/net/server/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async def html_handler(
response: ResponseHeader,
request_data: Optional[bytes],
default_app: HtmlApp = None,
) -> None:
) -> bool:
"""Render an HTML document in response to an HTTP request."""

# Set response headers.
Expand All @@ -43,3 +43,5 @@ async def html_handler(
app = apps.get(request.target.path, default_app)
if app is not None:
(await app(get_html(), request, response, request_data)).render(stream)

return app is not None
15 changes: 11 additions & 4 deletions runtimepy/net/tcp/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ async def create_pair(
peer: type[V] = None,
serve_kwargs: dict[str, _Any] = None,
connect_kwargs: dict[str, _Any] = None,
host: str = "127.0.0.1",
) -> _AsyncIterator[tuple[V, T]]:
"""Create a connection pair."""

Expand All @@ -241,16 +242,22 @@ def callback(conn: V) -> None:
serve_kwargs = {}

server = await stack.enter_async_context(
peer.serve(callback, port=0, backlog=1, **serve_kwargs)
peer.serve(
callback,
host=host,
port=0,
backlog=1,
**serve_kwargs,
)
)

host = server.sockets[0].getsockname()

if connect_kwargs is None:
connect_kwargs = {}

client = await cls.create_connection(
host="localhost", port=host[1], **connect_kwargs
host=host,
port=server.sockets[0].getsockname()[1],
**connect_kwargs,
)
await cond.acquire()

Expand Down
4 changes: 0 additions & 4 deletions tests/data/valid/connection_arbiter/test_ssl.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@ servers:

- factory: runtimepy_websocket_json
kwargs:
host: "0.0.0.0"
port: "$runtimepy_secure_websocket_json_server"

certfile: tests/data/valid/certs/test.cert
keyfile: tests/data/valid/certs/test.key

- factory: runtimepy_websocket_data
kwargs:
host: "0.0.0.0"
port: "$runtimepy_secure_websocket_data_server"

certfile: tests/data/valid/certs/test.cert
keyfile: tests/data/valid/certs/test.key

Expand Down
29 changes: 28 additions & 1 deletion tests/net/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
from typing import Any

# module under test
from runtimepy.net.arbiter.info import AppInfo
from runtimepy.net.http.header import RequestHeader
from runtimepy.net.server import RuntimepyServerConnection
from runtimepy.net.server.websocket import RuntimepyWebsocketConnection
from runtimepy.net.tcp.http import HttpConnection

# internal
from tests.resources import resource
Expand Down Expand Up @@ -52,8 +54,30 @@ async def runtimepy_websocket_client(
send_ui(client, f"wave{idx}", {"kind": "tab.hidden"})


async def runtimepy_http_query_peer(app: AppInfo) -> None:
"""Test querying a peer program's web application."""

port: int = 0

# Try to find peer's HTTP server.
if "proc1" in app.peers:
for port in app.peers["proc1"].peer_config["ports"]: # type: ignore
if port["name"] == "runtimepy_http_server": # type: ignore
port = port["port"] # type: ignore
break

if port:
conn = await HttpConnection.create_connection(
host="localhost", port=port
)
async with conn.process_then_disable(stop_sig=app.stop):
await conn.request(RequestHeader(target="/app.html"))


async def runtimepy_http_client_server(
client: RuntimepyServerConnection, server: RuntimepyServerConnection
app: AppInfo,
client: RuntimepyServerConnection,
server: RuntimepyServerConnection,
) -> None:
"""Test HTTP client and server interactions."""

Expand All @@ -66,8 +90,11 @@ async def runtimepy_http_client_server(
# Make requests in parallel.
await asyncio.gather(
*(
runtimepy_http_query_peer(app),
# Application.
client.request(RequestHeader(target="/")),
client.request(RequestHeader(target="/app.html")),
client.request(RequestHeader(target="/app.html")),
client.request(RequestHeader(target="/index.html")),
client.request(RequestHeader(target="/test_json.html")),
client.request(RequestHeader(target="/landing_page.html")),
Expand Down
3 changes: 2 additions & 1 deletion tests/net/stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ async def http_test_loopback(
)

assert await client.request(RequestHeader(target="/index.html?param=true"))
assert await client.request(RequestHeader(target="/app.html"))

result = await client.request(RequestHeader(target="google.com"))
assert result[0]["Content-Type"]
Expand Down Expand Up @@ -105,7 +106,7 @@ async def runtimepy_http_test(app: AppInfo) -> int:
server = conn
assert server is not None

await runtimepy_http_client_server(client, server)
await runtimepy_http_client_server(app, client, server)

await runtimepy_websocket_client(
app.single(pattern="client", kind=RuntimepyWebsocketConnection)
Expand Down
1 change: 1 addition & 0 deletions tests/net/tcp/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ async def connect() -> None:
sig,
callback=app,
serving_callback=serve_cb,
host="0.0.0.0",
port=0,
manager=manager,
)
Expand Down
7 changes: 5 additions & 2 deletions tests/net/tcp/test_np_05b.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,12 @@ async def test_conns(client: Np05bConnection, _: MockNp05b) -> None:
# End the test.
stop_sig.set()

async with MockNp05b.serve(callback=conn_cb, port=0) as server:
async with MockNp05b.serve(
callback=conn_cb, host="127.0.0.1", port=0
) as server:
host = sockname(server.sockets[0])
conn = await Np05bConnection.create_connection(
host="localhost", port=sockname(server.sockets[0]).port
host=host.name, port=host.port
)
conn_srv = await queue.get()

Expand Down

0 comments on commit b53c9d2

Please sign in to comment.