Skip to content

Commit

Permalink
5.0.0 - Some initial controls interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
vkottler committed Jun 21, 2024
1 parent 0c8f1dc commit 93ad999
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 42 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=4.6.1
repo=runtimepy version=5.0.0
if: |
matrix.python-version == '3.11'
&& 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=4418deec8fe739bca86f22f58d2f3e1f
hash=895daf8e1a0633698eac373d7017dbde
=====================================
-->

# runtimepy ([4.6.1](https://pypi.org/project/runtimepy/))
# runtimepy ([5.0.0](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
6 changes: 3 additions & 3 deletions local/variables/package.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
major: 4
minor: 6
patch: 1
major: 5
minor: 0
patch: 0
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 = "4.6.1"
version = "5.0.0"
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=1f6ecacb506da0891ce4397cd8209529
# hash=8c2e796a8c4689d3e6713f7e3647ed13
# =====================================

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

DESCRIPTION = "A framework for implementing Python services."
PKG_NAME = "runtimepy"
VERSION = "4.6.1"
VERSION = "5.0.0"

# runtimepy-specific content.
METRICS_NAME = "metrics"
Expand Down
13 changes: 13 additions & 0 deletions runtimepy/control/step.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from runtimepy.net.arbiter import AppInfo
from runtimepy.net.arbiter.info import RuntimeStruct
from runtimepy.net.arbiter.task import ArbiterTask, TaskFactory
from runtimepy.net.manager import ConnectionManager
from runtimepy.net.server.websocket import RuntimepyWebsocketConnection
from runtimepy.primitives import Bool, Double, Uint32


Expand All @@ -31,6 +33,14 @@ def should_poll(kind: type[RuntimeStruct]) -> bool:
return kind.__name__ not in {"ToggleStepper", "UiState"}


def refresh_all_plots(manager: ConnectionManager) -> None:
"""Signal to clients to refresh all data."""

for conn in manager.by_type(RuntimepyWebsocketConnection):
for interface in conn.send_interfaces.values():
interface({"actions": ["clear_points"]})


class ToggleStepper(RuntimeStruct):
"""A simple struct that ties a clock toggle to various runtime entities."""

Expand Down Expand Up @@ -126,6 +136,9 @@ def do_simulate_time(_: bool, curr: bool) -> None:
else:
restore_time_source()

# Signal to the UI to clear plot data.
refresh_all_plots(self.app.conn_manager)

self._poll_time()

self.simulate_time.register_callback(do_simulate_time)
Expand Down
14 changes: 14 additions & 0 deletions runtimepy/data/js/classes/TabInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,20 @@ class TabInterface {
}
}

/* Handle actions. */
if ("actions" in data) {
for (let action of data["actions"]) {
switch (action) {
case "clear_points":
this.clearPlotPoints();
break;
default:
console.log(`Action '${action}' not hangled!`);
break;
}
}
}

/* Stage any channel-table re-paints. */
if (this.channels) {
for (let key in data) {
Expand Down
43 changes: 26 additions & 17 deletions runtimepy/net/server/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,14 @@ def default(self, o):
return o


def json_handler(
stream: TextIO,
request: RequestHeader,
response: ResponseHeader,
request_data: Optional[bytes],
data: JsonObject,
) -> None:
"""Create an HTTP response from some JSON object data."""

del request_data

response_type = "json"
response["Content-Type"] = (
f"application/{response_type}; charset={DEFAULT_ENCODING}"
)
def traverse_dict(data: dict[str, Any], *paths: str) -> Any:
"""Attempt to traverse a dictionary by path names."""

error: dict[str, Any] = {"path": {}}

# Traverse path.
curr_path = []
for part in request.target.path.split("/")[2:]:
for part in paths:
if not part:
continue

Expand All @@ -72,11 +59,33 @@ def json_handler(
data = error
break

data = data[part] # type: ignore
data = data[part]

if callable(data):
data = data()

return data


def json_handler(
stream: TextIO,
request: RequestHeader,
response: ResponseHeader,
request_data: Optional[bytes],
data: JsonObject,
) -> None:
"""Create an HTTP response from some JSON object data."""

del request_data

response_type = "json"
response["Content-Type"] = (
f"application/{response_type}; charset={DEFAULT_ENCODING}"
)

# Traverse path.
data = traverse_dict(data, *request.target.path.split("/")[2:])

# Use a convention for indexing data to non-dictionary leaf nodes.
if not isinstance(data, dict):
data = {"__raw__": data}
Expand Down
26 changes: 10 additions & 16 deletions runtimepy/net/server/websocket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
# built-in
from collections import defaultdict

# third-party
from vcorelib.math import RateLimiter, metrics_time_ns, to_nanos

# internal
from runtimepy.message import JsonMessage
from runtimepy.net.arbiter.tcp.json import WebsocketJsonMessageConnection
Expand All @@ -22,10 +25,7 @@ class RuntimepyWebsocketConnection(WebsocketJsonMessageConnection):
ui_time: float
tabs: dict[str, TabState]

# The first UI client has exclusive access to some functions, like
# polling metrics.
first_client: bool
first_message: bool
poll_governor: RateLimiter

def tab_sender(self, name: str) -> TabMessageSender:
"""Get a tab message-sending interface."""
Expand All @@ -44,7 +44,7 @@ def _poll_ui_state(self, ui: UiState, time: float) -> None:
"""Update UI-specific state."""

# Only one connection needs to perform this task.
if self.first_client or ui.env.value("num_connections") == 1:
if self.poll_governor():
# Update time.
ui.env["time_ms"] = time
ui.env["frame_period_ms"] = time - self.ui_time
Expand All @@ -63,15 +63,6 @@ def _register_handlers(self) -> None:
async def ui_handler(outbox: JsonMessage, inbox: JsonMessage) -> None:
"""A simple loopback handler."""

# Add to num_connections.
if self.first_message:
ui = UiState.singleton()
if ui and ui.env.finalized:
self.first_client = (
ui.env.add_int("num_connections", 1) == 1
)
self.first_message = False

# Handle frame messages.
if "time" in inbox:
# Poll UI state.
Expand Down Expand Up @@ -109,8 +100,11 @@ def init(self) -> None:
self.send_interfaces = {}
self.ui_time = 0.0
self.tabs = defaultdict(TabState.create)
self.first_client = False
self.first_message = True

# Limit UI metrics update rate to 250 Hz.
self.poll_governor = RateLimiter(
to_nanos(1.0 / 250.0), source=metrics_time_ns
)

def disable_extra(self) -> None:
"""Additional tasks to perform when disabling."""
Expand Down
15 changes: 15 additions & 0 deletions tests/net/server/test_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Test the 'net.server.json' module.
"""

# module under test
from runtimepy.net.server.json import traverse_dict


def test_traverse_dict_basic():
"""Test basic dict-traversing scenarios."""

src = {"a": lambda: {"a": 1, "b": 2, "c": 3}}

assert traverse_dict(src, "", "a") == {"a": 1, "b": 2, "c": 3}
assert traverse_dict(src, "a", "b") == 2
6 changes: 6 additions & 0 deletions tests/net/stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# module under test
from runtimepy import PKG_NAME
from runtimepy.control.step import ToggleStepper
from runtimepy.message import JsonMessage
from runtimepy.net.arbiter.info import AppInfo
from runtimepy.net.http.header import RequestHeader
Expand Down Expand Up @@ -110,6 +111,11 @@ async def runtimepy_http_test(app: AppInfo) -> int:
app.single(pattern="client", kind=RuntimepyWebsocketConnection)
)

# Find stepper struct, toggle 'simualte_time' twice.
for inst in app.search_structs(ToggleStepper):
inst.simulate_time.toggle()
inst.simulate_time.toggle()

return 0


Expand Down

0 comments on commit 93ad999

Please sign in to comment.