Skip to content

Commit

Permalink
Merge pull request #238 from vkottler/dev/5.2.0
Browse files Browse the repository at this point in the history
5.2.0 - More features
  • Loading branch information
vkottler authored Jul 8, 2024
2 parents 0d2ec61 + 691f877 commit e06d03a
Show file tree
Hide file tree
Showing 30 changed files with 802 additions and 90 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:

- run: mk docs
if: |
matrix.python-version == '3.11'
matrix.python-version == '3.12'
&& matrix.system == 'ubuntu-latest'
- run: mk python-test
Expand All @@ -70,16 +70,16 @@ jobs:
env:
TWINE_USERNAME: __token__
if: |
matrix.python-version == '3.11'
matrix.python-version == '3.12'
&& matrix.system == 'ubuntu-latest'
&& env.TWINE_PASSWORD != ''
&& github.ref_name == 'master'
- run: |
mk python-release owner=vkottler \
repo=runtimepy version=5.1.0
repo=runtimepy version=5.2.0
if: |
matrix.python-version == '3.11'
matrix.python-version == '3.12'
&& matrix.system == 'ubuntu-latest'
&& env.GITHUB_API_TOKEN != ''
&& github.ref_name == 'master'
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=bc8310897ee0818dfb82ec8021aefc60
hash=31357aac5f63a79eefae7021942451c4
=====================================
-->

# runtimepy ([5.1.0](https://pypi.org/project/runtimepy/))
# runtimepy ([5.2.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
17 changes: 17 additions & 0 deletions local/arbiter/post_request.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

set -e

# curl -X POST http://localhost:8000/pdu/toggle/outlet.1.on

set -x

curl -X POST http://localhost:8000
curl -X POST http://localhost:8000/a
curl -X POST http://localhost:8000/a/b/c

curl -X POST http://localhost:8000/app/help
curl -X POST http://localhost:8000/app/toggle
curl -X POST http://localhost:8000/app/toggle/asdf
curl -X POST http://localhost:8000/app/toggle/paused
curl -X POST http://localhost:8000/app/toggle/paused
2 changes: 1 addition & 1 deletion local/configs/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ entry: {{entry}}

requirements:
- vcorelib>=3.3.1
- svgen>=0.6.7
- svgen>=0.6.8
- websockets
- psutil
- "windows-curses; sys_platform == 'win32' and python_version < '3.12'"
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: 1
minor: 2
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 = "5.1.0"
version = "5.2.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=c57910000e21ff16644bf037b474eeb4
# hash=1aab90a330330d0191c7da801d4b4313
# =====================================

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

DESCRIPTION = "A framework for implementing Python services."
PKG_NAME = "runtimepy"
VERSION = "5.1.0"
VERSION = "5.2.0"

# runtimepy-specific content.
METRICS_NAME = "metrics"
Expand Down
1 change: 1 addition & 0 deletions runtimepy/channel/environment/command/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ChannelCommand(StrEnum):
SET = "set"
TOGGLE = "toggle"
GET = "get"
CUSTOM = "custom"


class CommandParser(ArgumentParser):
Expand Down
37 changes: 35 additions & 2 deletions runtimepy/channel/environment/command/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# built-in
from argparse import Namespace
from typing import Any, Callable, Optional, cast
from typing import Any, Awaitable, Callable, Optional, cast

# third-party
from vcorelib.logging import LoggerType
Expand All @@ -23,6 +23,11 @@

CommandHook = Callable[[Namespace, Optional[FieldOrChannel]], None]

# Custom commands require async processing (e.g. AsyncCommandProcessingMixin).
CustomCommand = Callable[
[Namespace, Optional[FieldOrChannel]], Awaitable[None]
]


class ChannelCommandProcessor(ChannelEnvironmentMixin):
"""A command processing interface for channel environments."""
Expand All @@ -36,12 +41,26 @@ def __init__(
self.logger = logger
self.hooks: list[CommandHook] = []

self.custom_commands: dict[str, CustomCommand] = {}

self.parser_data: dict[str, Any] = {}
self.parser = CommandParser(prog="")
self.parser.data = self.parser_data

self.parser.initialize()

def register_custom_commands(
self, *custom_commands: CustomCommand
) -> None:
"""Register custom commands."""

for cmd in custom_commands:
name = cmd.__name__
assert (
name not in self.custom_commands
), f"Duplicate custom command name '{name}'!"
self.custom_commands[name] = cmd

def get_suggestion(self, value: str) -> Optional[str]:
"""Get an input suggestion."""

Expand Down Expand Up @@ -115,7 +134,7 @@ def handle_command(self, args: Namespace) -> CommandResult:

# Handle remote commands by processing hooks and returning (hooks
# implement remote command behavior and capability).
if args.remote:
if args.remote or args.command == ChannelCommand.CUSTOM:
for hook in self.hooks:
hook(args, None)
return result
Expand Down Expand Up @@ -181,6 +200,20 @@ def command(self, value: str) -> CommandResult:

return result

async def handle_custom_command(
self, args: Namespace, channel: Optional[FieldOrChannel]
) -> None:
"""Handle a custom command."""

if args.channel in self.custom_commands:
await self.custom_commands[args.channel](args, channel)
else:
self.logger.warning(
"Unknown custom command '%s' (try one of: %s).",
args.channel,
", ".join(self.custom_commands),
)


EnvironmentMap = dict[str, ChannelCommandProcessor]

Expand Down
2 changes: 1 addition & 1 deletion runtimepy/channel/environment/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def sample_env(env: ChannelEnvironment = None) -> ChannelEnvironment:
commandable=True,
default=2,
)
env.bool_channel("bool")
env.bool_channel("bool", default=True, commandable=True)
env.int_channel("int", commandable=True)
env.int_channel(
"scaled_int", commandable=True, scaling=[1.0, 2.0]
Expand Down
2 changes: 2 additions & 0 deletions runtimepy/data/factories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ factories:
- {name: runtimepy.net.factories.RuntimepyWebsocketJson}
- {name: runtimepy.net.factories.RuntimepyWebsocketData}

# Device drivers.
- {name: runtimepy.net.factories.Np05b}
- {name: runtimepy.net.tcp.scpi.ScpiConn}

# Useful tasks.
Expand Down
9 changes: 8 additions & 1 deletion runtimepy/data/js/classes/TabInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class TabInterface {
}

/* Initialize enumeration command drop downs. */
for (let enums of this.queryAll("select")) {
for (let enums of this.queryAll("td>select")) {
enums.onchange = this.setHandler(enums);
}

Expand Down Expand Up @@ -160,6 +160,13 @@ class TabInterface {
}
};
}

/* Initialize custom-command sending interfaces. */
let selector = this.query("#custom-commands");
let send = this.query("#send-custom-commands");
if (selector && send) {
send.onclick = () => { this.worker.command(`custom ${selector.value}`); };
}
}

setHandler(elem) {
Expand Down
18 changes: 13 additions & 5 deletions runtimepy/mixins/async_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

# built-in
from abc import ABC, abstractmethod
from argparse import Namespace
import asyncio
from typing import Optional
Expand All @@ -14,27 +13,30 @@

# internal
from runtimepy.channel.environment.command import FieldOrChannel
from runtimepy.channel.environment.command.parser import ChannelCommand
from runtimepy.channel.environment.command.processor import (
ChannelCommandProcessor,
CustomCommand,
)

ChannelCommandParams = tuple[Namespace, Optional[FieldOrChannel]]


class AsyncCommandProcessingMixin(LoggerMixin, ABC):
class AsyncCommandProcessingMixin(LoggerMixin):
"""A class mixin for handling asynchronous commands."""

command: ChannelCommandProcessor
outgoing_commands: asyncio.Queue[ChannelCommandParams]

def _setup_async_commands(self) -> None:
def _setup_async_commands(self, *custom_commands: CustomCommand) -> None:
"""Setup asynchronous commands."""

if not hasattr(self, "outgoing_commands"):
self.outgoing_commands = asyncio.Queue()
self.command.hooks.append(self._handle_command)

@abstractmethod
self.command.register_custom_commands(*custom_commands)

async def handle_command(
self, args: Namespace, channel: Optional[FieldOrChannel]
) -> None:
Expand All @@ -51,5 +53,11 @@ async def process_command_queue(self) -> None:
"""Process any outgoing command requests."""

while not self.outgoing_commands.empty():
await self.handle_command(*self.outgoing_commands.get_nowait())
params = self.outgoing_commands.get_nowait()

if params[0].command == ChannelCommand.CUSTOM:
await self.command.handle_custom_command(*params)
else:
await self.handle_command(*params)

self.outgoing_commands.task_done()
1 change: 1 addition & 0 deletions runtimepy/mixins/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def _log_level_changed(self, _: int, new_value: int) -> None:
"""Handle a change in log level."""

self.logger.setLevel(new_value)
self.logger.log(new_value, "Log level updated to %d.", new_value)

def setup_level_channel(
self,
Expand Down
7 changes: 7 additions & 0 deletions runtimepy/net/factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
TcpConnection,
)
from runtimepy.net.tcp.http import HttpConnection
from runtimepy.net.tcp.telnet.np_05b import Np05bConnection
from runtimepy.net.udp import (
EchoUdpConnection,
NullUdpConnection,
Expand Down Expand Up @@ -163,6 +164,12 @@ class Http(TcpConnectionFactory[HttpConnection]):
kind = HttpConnection


class Np05b(TcpConnectionFactory[Np05bConnection]):
"""A networked-PDU connection factory."""

kind = Np05bConnection


class RuntimepyHttp(TcpConnectionFactory[RuntimepyServerConnection]):
"""HTTP connection factory for this package."""

Expand Down
Loading

0 comments on commit e06d03a

Please sign in to comment.