From bda0183005d6c093ddd2f741e2cc7c883356eadc Mon Sep 17 00:00:00 2001 From: Vaughn Kottler Date: Wed, 24 Jan 2024 19:54:53 -0600 Subject: [PATCH 1/4] 3.4.0 - Initial scaffolding for 'task' command --- .github/workflows/python-package.yml | 2 +- README.md | 30 +++++++++++++++--- local/configs/package.yaml | 2 ++ local/includes/sub_commands.yaml | 2 +- local/variables/package.yaml | 4 +-- manifest.yaml | 1 + pyproject.toml | 2 +- runtimepy/__init__.py | 4 +-- runtimepy/commands/all.py | 8 ++++- runtimepy/commands/task.py | 46 ++++++++++++++++++++++++++++ runtimepy/data/factories.yaml | 3 ++ runtimepy/task/trig/__init__.py | 22 +++++++++++++ tests/commands/test_task.py | 16 ++++++++++ 13 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 runtimepy/commands/task.py create mode 100644 runtimepy/task/trig/__init__.py create mode 100644 tests/commands/test_task.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 37927858..7dd6c0ca 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -68,7 +68,7 @@ jobs: - run: | mk python-release owner=vkottler \ - repo=runtimepy version=3.3.1 + repo=runtimepy version=3.4.0 if: | matrix.python-version == '3.11' && matrix.system == 'ubuntu-latest' diff --git a/README.md b/README.md index 119a3c97..112a8e40 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ===================================== generator=datazen version=3.1.4 - hash=8abaee2821e021cb2521ae3ee0a9f79d + hash=c6a4e8d162ebe8d48c6bce5ffcd9a663 ===================================== --> -# runtimepy ([3.3.1](https://pypi.org/project/runtimepy/)) +# runtimepy ([3.4.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) @@ -48,7 +48,7 @@ This package is tested on the following platforms: $ ./venv3.11/bin/runtimepy -h usage: runtimepy [-h] [--version] [-v] [-q] [--curses] [--no-uvloop] [-C DIR] - {arbiter,server,tui,noop} ... + {arbiter,server,task,tui,noop} ... A framework for implementing Python services. @@ -62,10 +62,11 @@ options: -C DIR, --dir DIR execute from a specific directory commands: - {arbiter,server,tui,noop} + {arbiter,server,task,tui,noop} set of available commands arbiter run a connection-arbiter application from a config server run a server for a specific connection factory + task run a task from a specific task factory tui run a terminal interface for the channel environment noop command stub (does nothing) @@ -120,6 +121,27 @@ options: ``` +### `task` + +``` +$ ./venv3.11/bin/runtimepy task -h + +usage: runtimepy task [-h] [-i] [-w] factory [configs ...] + +positional arguments: + factory name of task factory to create task with + configs the configuration to load + +options: + -h, --help show this help message and exit + -i, --init_only, --init-only + exit after completing initialization + -w, --wait-for-stop, --wait_for_stop + ensure that a 'wait_for_stop' application method is + run last + +``` + ### `tui` ``` diff --git a/local/configs/package.yaml b/local/configs/package.yaml index b08342e1..cc8281cb 100644 --- a/local/configs/package.yaml +++ b/local/configs/package.yaml @@ -20,6 +20,8 @@ commands: description: "run a connection-arbiter application from a config" - name: server description: "run a server for a specific connection factory" + - name: task + description: "run a task from a specific task factory" - name: tui description: "run a terminal interface for the channel environment" diff --git a/local/includes/sub_commands.yaml b/local/includes/sub_commands.yaml index ee2826f0..3f06dbdb 100644 --- a/local/includes/sub_commands.yaml +++ b/local/includes/sub_commands.yaml @@ -3,7 +3,7 @@ default_dirs: false commands: -{% for command in ["arbiter", "server", "tui"] %} +{% for command in ["arbiter", "server", "task", "tui"] %} - name: help-{{command}} command: "./venv{{python_version}}/bin/{{entry}}" force: true diff --git a/local/variables/package.yaml b/local/variables/package.yaml index c0ca5190..754a4036 100644 --- a/local/variables/package.yaml +++ b/local/variables/package.yaml @@ -1,5 +1,5 @@ --- major: 3 -minor: 3 -patch: 1 +minor: 4 +patch: 0 entry: runtimepy diff --git a/manifest.yaml b/manifest.yaml index 4ae0c1a0..d78af0db 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -32,6 +32,7 @@ renders: - commands-help - commands-help-arbiter - commands-help-server + - commands-help-task - commands-help-tui - name: app.py diff --git a/pyproject.toml b/pyproject.toml index 4a0ca0d7..6f183377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__" [project] name = "runtimepy" -version = "3.3.1" +version = "3.4.0" description = "A framework for implementing Python services." readme = "README.md" requires-python = ">=3.11" diff --git a/runtimepy/__init__.py b/runtimepy/__init__.py index b3481daa..313d4a63 100644 --- a/runtimepy/__init__.py +++ b/runtimepy/__init__.py @@ -1,7 +1,7 @@ # ===================================== # generator=datazen # version=3.1.4 -# hash=3303e95749a72345c2ef69c1528de1a7 +# hash=3ac128eb7b7140a6e7e096f51a3b3e43 # ===================================== """ @@ -10,7 +10,7 @@ DESCRIPTION = "A framework for implementing Python services." PKG_NAME = "runtimepy" -VERSION = "3.3.1" +VERSION = "3.4.0" # runtimepy-specific content. METRICS_NAME = "metrics" diff --git a/runtimepy/commands/all.py b/runtimepy/commands/all.py index cd818e97..e696f83e 100644 --- a/runtimepy/commands/all.py +++ b/runtimepy/commands/all.py @@ -1,7 +1,7 @@ # ===================================== # generator=datazen # version=3.1.4 -# hash=2fec0392db71770da35d98fdbc8ce142 +# hash=89bfb0e2f56effabf3cb8f70d6349c13 # ===================================== """ @@ -18,6 +18,7 @@ # internal from runtimepy.commands.arbiter import add_arbiter_cmd from runtimepy.commands.server import add_server_cmd +from runtimepy.commands.task import add_task_cmd from runtimepy.commands.tui import add_tui_cmd @@ -35,6 +36,11 @@ def commands() -> _List[_Tuple[str, str, _CommandRegister]]: "run a server for a specific connection factory", add_server_cmd, ), + ( + "task", + "run a task from a specific task factory", + add_task_cmd, + ), ( "tui", "run a terminal interface for the channel environment", diff --git a/runtimepy/commands/task.py b/runtimepy/commands/task.py new file mode 100644 index 00000000..4090b467 --- /dev/null +++ b/runtimepy/commands/task.py @@ -0,0 +1,46 @@ +""" +An entry-point for the 'task' command. +""" + +# built-in +from argparse import ArgumentParser as _ArgumentParser +from argparse import Namespace as _Namespace +from typing import Any, Dict + +# third-party +from vcorelib.args import CommandFunction as _CommandFunction + +# internal +from runtimepy import PKG_NAME +from runtimepy.commands.arbiter import arbiter_cmd +from runtimepy.commands.common import arbiter_args, cmd_with_jit + + +def config_data(args: _Namespace) -> Dict[str, Any]: + """Get configuration data for the 'task' command.""" + + # handle period + + return { + "includes": [f"package://{PKG_NAME}/factories.yaml"], + "tasks": [{"name": args.factory, "factory": args.factory}], + } + + +def task_cmd(args: _Namespace) -> int: + """Execute the task command.""" + + return cmd_with_jit(arbiter_cmd, args, config_data(args)) + + +def add_task_cmd(parser: _ArgumentParser) -> _CommandFunction: + """Add task-command arguments to its parser.""" + + with arbiter_args(parser, nargs="*"): + parser.add_argument( + "factory", help="name of task factory to create task with" + ) + + # period cli arg + + return task_cmd diff --git a/runtimepy/data/factories.yaml b/runtimepy/data/factories.yaml index a2edda1a..57049f58 100644 --- a/runtimepy/data/factories.yaml +++ b/runtimepy/data/factories.yaml @@ -19,6 +19,9 @@ factories: # Useful protocols. - {name: runtimepy.net.factories.Http} + # Useful tasks. + - {name: runtimepy.task.trig.Sinusoid} + ports: # Reserve ports for JSON listeners. - {name: udp_json, type: udp} diff --git a/runtimepy/task/trig/__init__.py b/runtimepy/task/trig/__init__.py new file mode 100644 index 00000000..3ca507cd --- /dev/null +++ b/runtimepy/task/trig/__init__.py @@ -0,0 +1,22 @@ +""" + A module implementing basic trigonometric tasks. +""" + +# internal +from runtimepy.net.arbiter.task import ArbiterTask as _ArbiterTask +from runtimepy.net.arbiter.task import TaskFactory as _TaskFactory + + +class SinusoidTask(_ArbiterTask): + """A task for logging metrics.""" + + async def dispatch(self) -> bool: + """Dispatch an iteration of this task.""" + + return True + + +class Sinusoid(_TaskFactory[SinusoidTask]): + """A factory for the sinusoid task.""" + + kind = SinusoidTask diff --git a/tests/commands/test_task.py b/tests/commands/test_task.py new file mode 100644 index 00000000..fb12cd14 --- /dev/null +++ b/tests/commands/test_task.py @@ -0,0 +1,16 @@ +""" +Test the 'commands.task' module. +""" + +# module under test +from runtimepy.entry import main as runtimepy_main + +# internal +from tests.resources import base_args + + +def test_task_command_basic(): + """Test basic usages of the 'task' command.""" + + base = base_args("task") + assert runtimepy_main(base + ["sinusoid"]) == 0 From 4a6e86f5e1f26bded3e9a98967634d9dba8560c2 Mon Sep 17 00:00:00 2001 From: Vaughn Kottler Date: Sat, 27 Jan 2024 01:21:48 -0600 Subject: [PATCH 2/4] Add 'rate' argument --- .pylintrc | 1 + runtimepy/codec/system/__init__.py | 6 +++--- runtimepy/commands/task.py | 22 +++++++++++++++++----- runtimepy/net/arbiter/config.py | 8 +++++--- runtimepy/net/stream/json/handlers.py | 14 ++++++++------ 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.pylintrc b/.pylintrc index 842d3447..cc385a0e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -2,6 +2,7 @@ max-args=8 max-attributes=14 max-parents=12 +max-branches=13 [MESSAGES CONTROL] disable=too-few-public-methods,duplicate-code diff --git a/runtimepy/codec/system/__init__.py b/runtimepy/codec/system/__init__.py index 8802f0fd..31d7139f 100644 --- a/runtimepy/codec/system/__init__.py +++ b/runtimepy/codec/system/__init__.py @@ -102,9 +102,9 @@ def register(self, name: str, *namespace: str) -> Protocol: """Register a custom type.""" new_type = Protocol(self._enums) - self.custom[ - self._name(name, *namespace, check_available=True) - ] = new_type + self.custom[self._name(name, *namespace, check_available=True)] = ( + new_type + ) return new_type def get_protocol( diff --git a/runtimepy/commands/task.py b/runtimepy/commands/task.py index 4090b467..067e0d98 100644 --- a/runtimepy/commands/task.py +++ b/runtimepy/commands/task.py @@ -19,11 +19,15 @@ def config_data(args: _Namespace) -> Dict[str, Any]: """Get configuration data for the 'task' command.""" - # handle period - return { "includes": [f"package://{PKG_NAME}/factories.yaml"], - "tasks": [{"name": args.factory, "factory": args.factory}], + "tasks": [ + { + "name": args.factory, + "factory": args.factory, + "period_s": 1.0 / args.rate, + } + ], } @@ -37,10 +41,18 @@ def add_task_cmd(parser: _ArgumentParser) -> _CommandFunction: """Add task-command arguments to its parser.""" with arbiter_args(parser, nargs="*"): + parser.add_argument( + "-r", + "--rate", + default=10, + type=float, + help=( + "rate (in Hz) that the task should run " + "(default: %(default)s)" + ), + ) parser.add_argument( "factory", help="name of task factory to create task with" ) - # period cli arg - return task_cmd diff --git a/runtimepy/net/arbiter/config.py b/runtimepy/net/arbiter/config.py index 2c9ef985..0441d9f6 100644 --- a/runtimepy/net/arbiter/config.py +++ b/runtimepy/net/arbiter/config.py @@ -57,9 +57,11 @@ def init(self, data: _JsonObject) -> None: item["host"], port_overrides.get(item["name"], item["port"]), ), - kind=_socket.SOCK_STREAM - if item["type"] == "tcp" - else _socket.SOCK_DGRAM, + kind=( + _socket.SOCK_STREAM + if item["type"] == "tcp" + else _socket.SOCK_DGRAM + ), ).port self.app: _Optional[str] = data.get("app") # type: ignore diff --git a/runtimepy/net/stream/json/handlers.py b/runtimepy/net/stream/json/handlers.py index 04afd863..d7fb2ec9 100644 --- a/runtimepy/net/stream/json/handlers.py +++ b/runtimepy/net/stream/json/handlers.py @@ -49,9 +49,9 @@ async def handler(outbox: JsonMessage, request: ChannelCommand) -> None: # Respond with an error (environment not found). else: outbox["success"] = False - outbox[ - "reason" - ] = f"No environment '{env_name}', options: {envs.keys()}." + outbox["reason"] = ( + f"No environment '{env_name}', options: {envs.keys()}." + ) return handler @@ -108,9 +108,11 @@ async def find_file_request_handler( path, includes_key=request.data["includes_key"], expect_overwrite=request.data["expect_overwrite"], - strategy=MergeStrategy.UPDATE - if request.data["update"] - else MergeStrategy.RECURSIVE, + strategy=( + MergeStrategy.UPDATE + if request.data["update"] + else MergeStrategy.RECURSIVE + ), ) decoded["success"] = result.success decoded["data"] = result.data From 55cb201671f3c46ef6c95e9812684d14092ede2a Mon Sep 17 00:00:00 2001 From: Vaughn Kottler Date: Sat, 27 Jan 2024 01:53:02 -0600 Subject: [PATCH 3/4] More lenience for factory registration --- runtimepy/net/arbiter/config.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/runtimepy/net/arbiter/config.py b/runtimepy/net/arbiter/config.py index 0441d9f6..c9b4aa8c 100644 --- a/runtimepy/net/arbiter/config.py +++ b/runtimepy/net/arbiter/config.py @@ -170,16 +170,24 @@ async def process_config( ) -> None: """Register clients and servers from a configuration object.""" + names = set() + # Registier factories. for factory in config.factories: name = factory["name"] - assert self.register_module_factory( - name, - *factory.get("namespaces", []), - **dict_resolve_env_vars( - factory.get("kwargs", {}), env=config.ports # type: ignore - ), - ), f"Couldn't register factory '{factory}'!" + + # Double specifying a factory (because of include shenanigans) + # should be fine. + if name not in names: + assert self.register_module_factory( + name, + *factory.get("namespaces", []), + **dict_resolve_env_vars( + factory.get("kwargs", {}), + env=config.ports, # type: ignore + ), + ), f"Couldn't register factory '{factory}'!" + names.add(name) # Register clients. for client in config.clients: From 27b27368c1876fa88a29aefa01ccea90b9511653 Mon Sep 17 00:00:00 2001 From: Vaughn Kottler Date: Sat, 27 Jan 2024 02:19:44 -0600 Subject: [PATCH 4/4] Implement Sinusoid task --- runtimepy/task/trig/__init__.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/runtimepy/task/trig/__init__.py b/runtimepy/task/trig/__init__.py index 3ca507cd..a3bdb1b6 100644 --- a/runtimepy/task/trig/__init__.py +++ b/runtimepy/task/trig/__init__.py @@ -1,18 +1,40 @@ """ - A module implementing basic trigonometric tasks. +A module implementing basic trigonometric tasks. """ +# built-in +import math + # internal from runtimepy.net.arbiter.task import ArbiterTask as _ArbiterTask from runtimepy.net.arbiter.task import TaskFactory as _TaskFactory +from runtimepy.primitives import Float as _Float class SinusoidTask(_ArbiterTask): """A task for logging metrics.""" + auto_finalize = True + + def _init_state(self) -> None: + """Add channels to this instance's channel environment.""" + + self.sin = _Float() + self.cos = _Float() + self.steps = _Float(10.0) + + self.env.channel("sin", self.sin) + self.env.channel("cos", self.cos) + self.env.channel("steps", self.steps, commandable=True) + async def dispatch(self) -> bool: """Dispatch an iteration of this task.""" + step = (math.tau / self.steps.value) * self.metrics.dispatches.value + + self.sin.value = math.sin(step) + self.cos.value = math.cos(step) + return True