Skip to content
Merged
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 @@ -78,7 +78,7 @@ jobs:
- run: |
mk python-release owner=libre-embedded \
repo=runtimepy version=5.15.2
repo=runtimepy version=5.15.3
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.2.3
hash=f1c75c9b812e8ff29fdda46f58009aef
hash=ed2f5b3731ba189c4a3ec2f2252b622e
=====================================
-->

# runtimepy ([5.15.2](https://pypi.org/project/runtimepy/))
# runtimepy ([5.15.3](https://pypi.org/project/runtimepy/))

[![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/)
![Build Status](https://github.com/libre-embedded/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: 15
patch: 2
patch: 3
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.15.2"
version = "5.15.3"
description = "A framework for implementing Python services."
readme = "README.md"
requires-python = ">=3.12"
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.2.3
# hash=839ec3c3fa52de16f775789c0d38bf3e
# hash=f5155e85b2694b012648fdbe9c12b1cd
# =====================================

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

DESCRIPTION = "A framework for implementing Python services."
PKG_NAME = "runtimepy"
VERSION = "5.15.2"
VERSION = "5.15.3"

# runtimepy-specific content.
METRICS_NAME = "metrics"
Expand Down
35 changes: 35 additions & 0 deletions runtimepy/channel/environment/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

# third-party
from vcorelib import DEFAULT_ENCODING
from vcorelib.dict import GenericStrDict
from vcorelib.io import ARBITER, JsonObject
from vcorelib.io.bus import BUS
from vcorelib.logging import DEFAULT_TIME_FORMAT, LoggerMixin
from vcorelib.math import default_time_ns, nano_str
from vcorelib.names import name_search
Expand All @@ -25,6 +27,7 @@
CommandHook,
EnvironmentMap,
)
from runtimepy.channel.environment.command.result import CommandResult
from runtimepy.channel.registry import ParsedEvent
from runtimepy.mapping import DEFAULT_PATTERN

Expand Down Expand Up @@ -243,6 +246,38 @@ def register(self, name: str, env: ChannelCommandProcessor) -> None:
ENVIRONMENTS = GLOBAL


def global_command(env: str, value: str) -> Optional[CommandResult]:
"""Handle a global command."""

result = None
if env in GLOBAL:
result = GLOBAL[env].command(value)
else:
GLOBAL.logger.error(
"Couldn't run command env='%s' value='%s'.", env, value
)
return result


def global_commands(*cmds: tuple[str, str]) -> None:
"""Handle a global command."""
for env, value in cmds:
global_command(env, value)


async def global_command_bus(payload: GenericStrDict) -> None:
"""Handle a bus message."""

if "env" in payload and "value" in payload:
global_command(payload["env"], payload["value"])
elif "cmds" in payload:
global_commands(*payload["cmds"])


BUS.register_ro("command", global_command_bus)
BUS.register_ro("cmd", global_command_bus)


def clear_env() -> None:
"""Reset the global environment mapping."""
GLOBAL.clear()
Expand Down
4 changes: 4 additions & 0 deletions runtimepy/data/dummy_load.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ processes:
app: runtimepy.sample.program.run

program: runtimepy.sample.program.SampleProgram

commands:
- [udp_json_client, "set log_level warning"]
- [proc1.peer, "set log_level warning"]
10 changes: 10 additions & 0 deletions runtimepy/data/schemas/ConnectionArbiterConfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ properties:
items:
$ref: package://runtimepy/schemas/StructConfig.yaml

commands:
type: array
items:
type: array
minItems: 2
maxItems: 2
items:
- type: string
- type: string

tasks:
type: array
items:
Expand Down
8 changes: 8 additions & 0 deletions runtimepy/net/arbiter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from runtimepy.channel.environment.command import (
clear_env,
env_json_data,
global_commands,
register_env,
)
from runtimepy.net.arbiter.housekeeping import housekeeping
Expand Down Expand Up @@ -145,6 +146,8 @@ def __init__(
# A copy of named port mappings (loaded via external config).
self._ports: dict[str, int] = {}

self._commands: list[tuple[str, str]] = []

self._init()

def _init(self) -> None:
Expand Down Expand Up @@ -318,6 +321,11 @@ async def _main(

# Run initialization methods.
result = await self._run_apps_list(self._inits, info)

# Run commands.
await _asyncio.sleep(0)
global_commands(*self._commands)

if result == 0:
# Get application methods.
apps = self._apps
Expand Down
3 changes: 3 additions & 0 deletions runtimepy/net/arbiter/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,5 +245,8 @@ async def process_config(
assert "root" not in config.config, config.config
config.config["root"] = root

# Register commands.
self._commands = config.commands


ConfigConnectionArbiter.add_search_package(PKG_NAME)
5 changes: 5 additions & 0 deletions runtimepy/net/arbiter/config/codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ def init(self, data: _JsonObject) -> None:

self.directory = _Path(str(data.get("directory", ".")))

self.commands: list[tuple[str, str]] = data.get( # type: ignore
"commands",
[],
)

def asdict(self) -> _JsonObject:
"""Obtain a dictionary representing this instance."""
return self.data
17 changes: 11 additions & 6 deletions runtimepy/net/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

# internal
from runtimepy import DEFAULT_EXT, PKG_NAME
from runtimepy.channel.environment.command import GLOBAL
from runtimepy.channel.environment.command import GLOBAL, global_command
from runtimepy.net.html import full_markdown_page
from runtimepy.net.http.header import RequestHeader
from runtimepy.net.http.request_target import PathMaybeQuery
Expand Down Expand Up @@ -270,17 +270,22 @@ def handle_command(
response_data["success"] = False
response_data["message"] = "No command executed."

if not args or args[0] not in GLOBAL:
def cmd_usage() -> None:
"""Set usage information for the response."""
response_data["usage"] = (
"/<environment (arg0)>/<arg1>[/.../<argN>]"
)
response_data["environments"] = list(GLOBAL)

# Run command.
if args:
result = global_command(args[0], " ".join(args[1:]))
if result is None:
cmd_usage()
else:
response_data["success"] = result.success
response_data["message"] = str(result)
else:
result = GLOBAL[args[0]].command(" ".join(args[1:]))
response_data["success"] = result.success
response_data["message"] = str(result)
cmd_usage()

# Send response.
encode_json(stream, response, response_data)
Expand Down
12 changes: 12 additions & 0 deletions tests/channel/environment/test_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
import logging
from typing import Iterator

# third-party
from pytest import mark
from vcorelib.io.bus import BUS

# module under test
from runtimepy.channel.environment import ChannelEnvironment
from runtimepy.channel.environment.command import GlobalEnvironment
Expand Down Expand Up @@ -65,6 +69,14 @@ def global_test_env() -> Iterator[GlobalEnvironment]:
yield envs


@mark.asyncio
async def test_environment_bus_commands_basic():
"""Test basic commands sent via bus."""

await BUS.send_ro("cmd", {"cmds": [("asdf", "asdf"), ("asdf", "asdf")]})
await BUS.send_ro("cmd", {"env": "asdf", "value": "asdf"})


def test_global_environment_basic():
"""Test simple interactions with a global environment."""

Expand Down
5 changes: 5 additions & 0 deletions tests/data/valid/connection_arbiter/runtimepy_http.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ structs:
- name: noise
factory: gaussian_source

commands:
- [fake, "asdf"]
- [clock, ""]
- [clock, "toggle step"]

config:
experimental: true
foo: bar
Expand Down