Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.4.1 - Add 'find_file' JSON handler #98

Merged
merged 2 commits into from
Sep 5, 2023
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 @@ -70,7 +70,7 @@ jobs:

- run: |
mk python-release owner=vkottler \
repo=runtimepy version=2.4.0
repo=runtimepy version=2.4.1
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.2
hash=60f136576cc98984a993462a1db79767
hash=da48a80970b792c3494552683c21ce3c
=====================================
-->

# runtimepy ([2.4.0](https://pypi.org/project/runtimepy/))
# runtimepy ([2.4.1](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
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: 2
minor: 4
patch: 0
patch: 1
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 = "2.4.0"
version = "2.4.1"
description = "A framework for implementing Python services."
readme = "README.md"
requires-python = ">=3.8"
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.2
# hash=226d4c833cf393d232793d82d64217a2
# hash=33b3dfc332dc64b4caf209e69bd2e795
# =====================================

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

DESCRIPTION = "A framework for implementing Python services."
PKG_NAME = "runtimepy"
VERSION = "2.4.0"
VERSION = "2.4.1"
63 changes: 63 additions & 0 deletions runtimepy/data/schemas/FindFile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
includes:
- has_request_flag.yaml

type: object
required: [path]
additionalProperties: false

properties:
path:
type: [string, "null"]

parts:
type: array
items:
type: string

search_paths:
type: array
items:
type: string

include_cwd:
type: boolean
default: false

relative_to:
type: string

package:
type: string

package_subdir:
type: string
default: data

# Parameters for requesting to decode the file.
decode:
type: boolean
default: false
includes_key:
type: string
default: includes
expect_overwrite:
type: boolean
default: false
update:
type: boolean
default: false

# Decoded data (if any).
decoded:
type: object
required: [success, data]
additionalProperties: false

properties:
success:
type: boolean
data:
type: object
time_ns:
type: integer
5 changes: 5 additions & 0 deletions runtimepy/data/schemas/has_request_flag.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
properties:
is_request:
type: boolean
default: true
10 changes: 10 additions & 0 deletions runtimepy/net/stream/json/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
A module implementing a JSON message connection interface.
"""

# internal
from runtimepy.net.stream.json.base import JsonMessageConnection
from runtimepy.net.stream.json.handlers import event_wait
from runtimepy.net.stream.json.types import JsonMessage

__all__ = ["JsonMessageConnection", "event_wait", "JsonMessage"]
73 changes: 21 additions & 52 deletions runtimepy/net/stream/json.py → runtimepy/net/stream/json/base.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,38 @@
"""
A module implementing a JSON message connection interface.
A module implementing a base JSON messaging connection interface.
"""

# built-in
import asyncio
from copy import copy
from json import JSONDecodeError, dumps, loads
import logging
from typing import (
Any,
Awaitable,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
TypeVar,
Union,
)
from typing import Any, Dict, List, Optional, Tuple, Type, Union

# third-party
from vcorelib.dict.codec import JsonCodec

# internal
from runtimepy import PKG_NAME, VERSION
from runtimepy.net.stream.json.handlers import (
FindFile,
event_wait,
find_file_request_handler,
loopback_handler,
)
from runtimepy.net.stream.json.types import (
DEFAULT_LOOPBACK,
DEFAULT_TIMEOUT,
RESERVED_KEYS,
JsonMessage,
MessageHandler,
MessageHandlers,
T,
TypedHandler,
)
from runtimepy.net.stream.string import StringMessageConnection
from runtimepy.net.udp import UdpConnection

JsonMessage = Dict[str, Any]

#
# def message_handler(response: JsonMessage, data: JsonMessage) -> None:
# """A sample message handler."""
#
MessageHandler = Callable[[JsonMessage, JsonMessage], Awaitable[None]]
MessageHandlers = Dict[str, MessageHandler]
RESERVED_KEYS = {"keys_ignored", "__id__", "__log_messages__"}

#
# def message_handler(response: JsonMessage, data: JsonCodec) -> None:
# """A sample message handler."""
#
T = TypeVar("T", bound=JsonCodec)
TypedHandler = Callable[[JsonMessage, T], Awaitable[None]]

DEFAULT_LOOPBACK = {"a": 1, "b": 2, "c": 3}
DEFAULT_TIMEOUT = 3


async def loopback_handler(outbox: JsonMessage, inbox: JsonMessage) -> None:
"""A simple loopback handler."""

outbox.update(inbox)


async def event_wait(event: asyncio.Event, timeout: float) -> bool:
"""Wait for an event to be set within a timeout."""

result = True

try:
await asyncio.wait_for(event.wait(), timeout)
except asyncio.TimeoutError:
result = False

return result


class JsonMessageConnection(StringMessageConnection):
"""A connection interface for JSON messaging."""
Expand All @@ -77,6 +43,9 @@ class JsonMessageConnection(StringMessageConnection):
def _register_handlers(self) -> None:
"""Register connection-specific command handlers."""

# Extra handlers.
self.typed_handler("find_file", FindFile, find_file_request_handler)

def init(self) -> None:
"""Initialize this instance."""

Expand Down
79 changes: 79 additions & 0 deletions runtimepy/net/stream/json/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
A module defining some useful JSON message handlers.
"""

# built-in
import asyncio

# third-party
from vcorelib.dict import MergeStrategy
from vcorelib.dict.codec import BasicDictCodec
from vcorelib.io import ARBITER
from vcorelib.paths import find_file

# internal
from runtimepy.net.stream.json.types import JsonMessage
from runtimepy.schemas import RuntimepyDictCodec


async def loopback_handler(outbox: JsonMessage, inbox: JsonMessage) -> None:
"""A simple loopback handler."""

outbox.update(inbox)


async def event_wait(event: asyncio.Event, timeout: float) -> bool:
"""Wait for an event to be set within a timeout."""

result = True

try:
await asyncio.wait_for(event.wait(), timeout)
except asyncio.TimeoutError:
result = False

return result


class FindFile(RuntimepyDictCodec, BasicDictCodec):
"""A schema-validated find-file request."""

data: JsonMessage


async def find_file_request_handler(
outbox: JsonMessage, request: FindFile
) -> None:
"""Attempt to find a file path based on the request."""

if request.data["is_request"]:
path = find_file(
request.data["path"],
*request.data.get("parts", []),
search_paths=request.data.get("search_paths"),
include_cwd=request.data["include_cwd"],
relative_to=request.data.get("relative_to"),
package=request.data.get("package"),
package_subdir=request.data["package_subdir"],
)
outbox["path"] = str(path) if path is not None else None
outbox["is_request"] = False

# Attempt to decode the file if requested.
if request.data["decode"]:
decoded = {"success": False, "data": {}, "time_ns": -1}

if path is not None:
result = ARBITER.decode(
path,
includes_key=request.data["includes_key"],
expect_overwrite=request.data["expect_overwrite"],
strategy=MergeStrategy.UPDATE
if request.data["update"]
else MergeStrategy.RECURSIVE,
)
decoded["success"] = result.success
decoded["data"] = result.data
decoded["time_ns"] = result.time_ns

outbox["decoded"] = decoded
29 changes: 29 additions & 0 deletions runtimepy/net/stream/json/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
A module containing useful type definitions for JSON messaging.
"""

# built-in
from typing import Any, Awaitable, Callable, Dict, TypeVar

# third-party
from vcorelib.dict.codec import JsonCodec

JsonMessage = Dict[str, Any]

#
# async def message_handler(response: JsonMessage, data: JsonMessage) -> None:
# """A sample message handler."""
#
MessageHandler = Callable[[JsonMessage, JsonMessage], Awaitable[None]]
MessageHandlers = Dict[str, MessageHandler]
RESERVED_KEYS = {"keys_ignored", "__id__", "__log_messages__"}

#
# async def message_handler(response: JsonMessage, data: JsonCodec) -> None:
# """A sample message handler."""
#
T = TypeVar("T", bound=JsonCodec)
TypedHandler = Callable[[JsonMessage, T], Awaitable[None]]

DEFAULT_LOOPBACK = {"a": 1, "b": 2, "c": 3}
DEFAULT_TIMEOUT = 3
32 changes: 32 additions & 0 deletions tests/net/stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from vcorelib.dict.codec import BasicDictCodec

# module under test
from runtimepy import PKG_NAME
from runtimepy.net.arbiter.info import AppInfo
from runtimepy.net.stream import StringMessageConnection
from runtimepy.net.stream.json import JsonMessage, JsonMessageConnection
Expand All @@ -31,6 +32,35 @@ async def stream_test(app: AppInfo) -> int:
return 0


async def json_client_find_file(client: JsonMessageConnection) -> None:
"""Test JSON-message client file finding."""

file_result = await client.wait_json(
{
"find_file": {
"path": "schemas",
"parts": ["BitFields.yaml"],
"package": PKG_NAME,
"decode": True,
}
}
)
assert file_result["find_file"]["path"] is not None
assert file_result["find_file"]["decoded"]["success"]
assert file_result["find_file"]["decoded"]["data"]

file_result = await client.wait_json(
{
"find_file": {
"path": "schemas",
"parts": ["not_a_file"],
"package": PKG_NAME,
}
}
)
assert file_result["find_file"]["path"] is None


async def json_client_test(client: JsonMessageConnection) -> int:
"""Test a single JSON client."""

Expand All @@ -40,6 +70,8 @@ async def json_client_test(client: JsonMessageConnection) -> int:
client.stage_remote_log("Hello, world!")
client.stage_remote_log("Hello, world! %d", 2)

await json_client_find_file(client)

assert await client.wait_json({"unknown": 0, "command": 1}) == {
"keys_ignored": ["command", "unknown"]
}
Expand Down