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

Dev/5.7.7 #281

Closed
wants to merge 3 commits 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.7
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=0bf47d2ff3fb0ed78ed013f4fcbc59e2
=====================================
-->

# runtimepy ([5.7.5](https://pypi.org/project/runtimepy/))
# runtimepy ([5.7.7](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: 7
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.7"
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=ed12f307211d1317e3ce6a7c4409b97f
# =====================================

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

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

# 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
Loading