Skip to content

Commit

Permalink
Merge pull request #282 from vkottler/dev/5.7.6
Browse files Browse the repository at this point in the history
Dev/5.7.6
  • Loading branch information
vkottler authored Oct 23, 2024
2 parents a9fa941 + 941ad68 commit 0311186
Show file tree
Hide file tree
Showing 18 changed files with 162 additions and 49 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>
18 changes: 15 additions & 3 deletions runtimepy/data/server_base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,23 @@ structs:
init:
- runtimepy.net.arbiter.housekeeping.init

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

# Serve these applications by default at these paths.
http_app_paths: ["/app.html"]

# Handles config["http_app_prefixes"].
config_builders:
- runtimepy.net.html.arbiter.web_app_paths

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"}
32 changes: 32 additions & 0 deletions runtimepy/net/html/arbiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
A module implementing connection-arbiter related utilities.
"""

# internal
from runtimepy.net.arbiter.config import ConfigObject


def web_app_paths(config: ConfigObject) -> None:
"""
Register boilerplate path handling for additional application-serving URIs.
"""

config.setdefault("config", {})
redirects = config["config"].setdefault("http_redirects", {})
app_paths = config["config"].setdefault("http_app_paths", [])

for prefix in config["config"].get("http_app_prefixes", []):
if not prefix.startswith("/"):
prefix = "/" + prefix

assert not prefix.endswith("/"), prefix

# Add re-directs.
index_path = f"{prefix}/index.html"
app_path = f"{prefix}/app.html"
redirects.setdefault(prefix, index_path)
redirects.setdefault(index_path, app_path)

# Add app path.
if app_path not in app_paths:
app_paths.append(app_path)
70 changes: 47 additions & 23 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 All @@ -250,7 +272,7 @@ async def get_handler(
return self.favicon_data

# Try serving a file and handling redirects.
for handler in [self.try_redirect, self.try_file]:
for handler in [self.try_file, self.try_redirect]:
result = await handler(
request.target.origin_form, response
)
Expand All @@ -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)
7 changes: 4 additions & 3 deletions runtimepy/net/server/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ 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
html_app = create_app(app, getattr(_import_module(module), method))
target: str
for target in app.config_param("http_app_paths", []):
RuntimepyServerConnection.apps[target] = html_app

# 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
2 changes: 2 additions & 0 deletions tests/data/valid/connection_arbiter/runtimepy_http.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ config:
foo: bar
xdg_fragment: "wave1,hide-tabs,hide-channels/wave1:sin,cos"

http_app_prefixes: [rando_app_prefix]

ports:
- {name: tftp_server, type: udp}

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
Loading

0 comments on commit 0311186

Please sign in to comment.