Skip to content

Commit

Permalink
Merge pull request #168 from vkottler/dev/3.6.0
Browse files Browse the repository at this point in the history
3.6.0 - Useful JSON data for server
  • Loading branch information
vkottler authored Feb 19, 2024
2 parents 84b7954 + 9042d60 commit 3592cf9
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
- run: |
mk python-release owner=vkottler \
repo=runtimepy version=3.5.1
repo=runtimepy version=3.6.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=0d3102d72d813144bd511aadcc56bc20
hash=c012dc3b3a8635d7854d3e29da81c2e7
=====================================
-->

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

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

DESCRIPTION = "A framework for implementing Python services."
PKG_NAME = "runtimepy"
VERSION = "3.5.1"
VERSION = "3.6.0"

# runtimepy-specific content.
METRICS_NAME = "metrics"
5 changes: 5 additions & 0 deletions runtimepy/channel/environment/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ def clear_env() -> None:
ENVIRONMENTS.clear()


def env_json_data() -> dict[str, Any]:
"""Get JSON data for each environment."""
return {key: cmd.env.export_json for key, cmd in ENVIRONMENTS.items()}


def register_env(name: str, env: ChannelCommandProcessor) -> None:
"""Register a channel environment globally."""

Expand Down
21 changes: 20 additions & 1 deletion runtimepy/net/arbiter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
from vcorelib.namespace import NamespaceMixin as _NamespaceMixin

# internal
from runtimepy.channel.environment.command import clear_env, register_env
from runtimepy.channel.environment.command import (
clear_env,
env_json_data,
register_env,
)
from runtimepy.net.arbiter.housekeeping import metrics_poller
from runtimepy.net.arbiter.info import AppInfo, ConnectionMap
from runtimepy.net.arbiter.result import AppResult, ResultState
Expand All @@ -32,6 +36,7 @@
)
from runtimepy.net.connection import Connection as _Connection
from runtimepy.net.manager import ConnectionManager as _ConnectionManager
from runtimepy.net.server import RuntimepyServerConnection
from runtimepy.tui.mixin import CursesWindow, TuiMixin

NetworkApplication = _Callable[[AppInfo], _Awaitable[int]]
Expand Down Expand Up @@ -154,6 +159,17 @@ def register_connection(

return result

def _setup_server_json(self, info: AppInfo) -> None:
"""Add runtime data to the server's JSON data structure."""

cls = RuntimepyServerConnection

# Connect configuration data.
cls.json_data["config"] = info.original_config()

# Connect environment data.
cls.json_data["environments"] = env_json_data()

async def _entry(
self,
app: NetworkApplicationlike = None,
Expand Down Expand Up @@ -226,6 +242,9 @@ async def _entry(
task.env.finalize(strict=False)
self.logger.debug("Periodic tasks initialized.")

# Wire runtime data to server JSON.
self._setup_server_json(info)

# Start tasks.
self.logger.debug("Starting periodic tasks...")
await stack.enter_async_context(
Expand Down
6 changes: 5 additions & 1 deletion runtimepy/net/arbiter/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def init(self, data: _JsonObject) -> None:
# Process ports.
self.ports: _Dict[str, int] = {}
for item in _cast(_List[_Dict[str, _Any]], data.get("ports", [])):
self.ports[item["name"]] = get_free_socket_name(
port = get_free_socket_name(
local=normalize_host(
item["host"],
port_overrides.get(item["name"], item["port"]),
Expand All @@ -64,6 +64,10 @@ def init(self, data: _JsonObject) -> None:
),
).port

# Update the original structure.
self.ports[item["name"]] = port
item["port"] = port

self.app: _Optional[str] = data.get("app") # type: ignore
self.config: _Optional[_JsonObject] = _cast(
_JsonObject, data.get("config")
Expand Down
17 changes: 17 additions & 0 deletions runtimepy/net/arbiter/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,20 @@ def result(self, logger: _LoggerType = None) -> bool:
logger.error("An exception was not caught at runtime!")

return results(self.results, logger=logger)

def original_config(self) -> dict[str, Any]:
"""
Re-assemble a dictionary closer to the original configuration data
(than the .config attribute).
"""

result: dict[str, Any] = {"config": {}}
for key, val in self.config.items():
if key != "root":
result["config"][key] = val

for key, val in self.config.get("root", {}).items(): # type: ignore
if key != "config":
result[key] = val

return result
21 changes: 20 additions & 1 deletion runtimepy/net/server/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

# built-in
from json import JSONEncoder
from typing import Any, Optional, TextIO

# third-party
Expand All @@ -14,6 +15,18 @@
from runtimepy.net.http.response import ResponseHeader


class Encoder(JSONEncoder):
"""A custom JSON encoder."""

def default(self, o):
"""A simple override for default encoding behavior."""

if callable(o):
o = o()

return o


def json_handler(
stream: TextIO,
request: RequestHeader,
Expand All @@ -40,6 +53,9 @@ def json_handler(

curr_path.append(part)

if callable(data):
data = data()

# Handle error.
if not isinstance(data, dict):
error["path"]["part"] = part
Expand All @@ -58,9 +74,12 @@ def json_handler(

data = data[part] # type: ignore

if callable(data):
data = data()

# Use a convention for indexing data to non-dictionary leaf nodes.
if not isinstance(data, dict):
data = {"__raw__": data}

ARBITER.encode_stream(response_type, stream, data)
ARBITER.encode_stream(response_type, stream, data, cls=Encoder)
stream.write("\n")
29 changes: 28 additions & 1 deletion runtimepy/net/tcp/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import asyncio
from copy import copy
import http
from typing import Awaitable, Callable, Optional, Tuple, Union
from json import loads
from typing import Any, Awaitable, Callable, Optional, Tuple, Union

# third-party
from vcorelib import DEFAULT_ENCODING

# internal
from runtimepy import PKG_NAME, VERSION
Expand All @@ -32,6 +36,20 @@
HttpRequestHandlers = dict[http.HTTPMethod, HttpRequestHandler]


def to_json(response: HttpResponse) -> Any:
"""Get JSON data from an HTTP response."""

# Make sure the response is JSON.
header = response[0]
assert header["content-type"].startswith("application/json"), header[
"content-type"
]

return loads(
response[1].decode(encoding=DEFAULT_ENCODING), # type: ignore
)


class HttpConnection(_TcpConnection):
"""A class implementing a basic HTTP interface."""

Expand Down Expand Up @@ -109,6 +127,15 @@ async def request(

return result

async def request_json(
self, request: RequestHeader, data: Optional[bytes] = None
) -> Any:
"""
Perform a request and convert the response to a data structure by
decoding it as JSON.
"""
return to_json(await self.request(request, data))

def _send(
self,
header: Union[ResponseHeader, RequestHeader],
Expand Down
5 changes: 5 additions & 0 deletions server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

set -e

"./venv$PYTHON_VERSION/bin/runtimepy" server -w runtimepy_http
19 changes: 12 additions & 7 deletions tests/net/stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ async def runtimepy_http_test(app: AppInfo) -> int:
server = conn
assert server is not None

env = "connection_metrics_poller"
env_path = f"/json/environments/{env}"

# Make requests in parallel.
await asyncio.gather(
*(
Expand All @@ -101,13 +104,15 @@ async def runtimepy_http_test(app: AppInfo) -> int:
# favicon.ico.
client.request(RequestHeader(target="/favicon.ico")),
# JSON queries.
client.request(RequestHeader(target="/json")),
client.request(RequestHeader(target="/json//////")),
client.request(RequestHeader(target="/json/a")),
client.request(RequestHeader(target="/json/test")),
client.request(RequestHeader(target="/json/test/a")),
client.request(RequestHeader(target="/json/test/a/b")),
client.request(RequestHeader(target="/json/test/d")),
client.request_json(RequestHeader(target="/json")),
client.request_json(RequestHeader(target="/json//////")),
client.request_json(RequestHeader(target="/json/a")),
client.request_json(RequestHeader(target="/json/test")),
client.request_json(RequestHeader(target="/json/test/a")),
client.request_json(RequestHeader(target="/json/test/a/b")),
client.request_json(RequestHeader(target="/json/test/d")),
client.request_json(RequestHeader(target=env_path)),
client.request_json(RequestHeader(target=f"{env_path}/values")),
)
)

Expand Down

0 comments on commit 3592cf9

Please sign in to comment.