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

5.7.6 - Minor fixes #280

Closed
wants to merge 1 commit into from
Closed
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 @@ -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