hio-sdk 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- henglosio/sdk/__init__.py +7 -0
- henglosio/sdk/core/action.py +54 -0
- henglosio/sdk/core/clock.py +31 -0
- henglosio/sdk/core/context.py +52 -0
- henglosio/sdk/core/identifier.py +15 -0
- henglosio/sdk/core/registry.py +67 -0
- henglosio/sdk/core/stream.py +56 -0
- henglosio/sdk/core/trigger.py +55 -0
- henglosio/sdk/py.typed +0 -0
- henglosio/sdk/testing.py +98 -0
- hio_sdk-1.0.0.dist-info/METADATA +28 -0
- hio_sdk-1.0.0.dist-info/RECORD +14 -0
- hio_sdk-1.0.0.dist-info/WHEEL +4 -0
- hio_sdk-1.0.0.dist-info/licenses/LICENSE +80 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .core.action import HIOAction, SUCCESS, FALLBACK
|
|
2
|
+
from .core.trigger import HIOTrigger
|
|
3
|
+
from .core.context import HIOContext
|
|
4
|
+
from .core.stream import HIOStream, HIOStreamError
|
|
5
|
+
from .core.registry import action, trigger
|
|
6
|
+
from .core.identifier import generate_id
|
|
7
|
+
from .core.clock import sleep_ms, ticks_ms, ticks_diff
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from .context import HIOContext
|
|
2
|
+
from .stream import HIOStream
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Outputs an action can end on — execute() returns one of these, or the name
|
|
6
|
+
# of one of its own custom outputs (see HIOAction.execute).
|
|
7
|
+
SUCCESS = "success"
|
|
8
|
+
FALLBACK = "fallback"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HIOAction:
|
|
12
|
+
# Base of every action. Subclass it, implement execute() and register the
|
|
13
|
+
# class with @action("<component>/<action>") (see registry.py):
|
|
14
|
+
#
|
|
15
|
+
# @action("leds/set")
|
|
16
|
+
# class SetLeds(HIOAction):
|
|
17
|
+
#
|
|
18
|
+
# def initialize(self) -> None:
|
|
19
|
+
# self.strip = Strip(pin=5)
|
|
20
|
+
#
|
|
21
|
+
# async def execute(self, context) -> str:
|
|
22
|
+
# self.strip.brightness(context.get("brightness"))
|
|
23
|
+
# async for chunk in self.batch("leds"):
|
|
24
|
+
# self.strip.write(chunk)
|
|
25
|
+
# return SUCCESS
|
|
26
|
+
#
|
|
27
|
+
# One instance per action type, created the first time it runs and reused
|
|
28
|
+
# for every execution after that.
|
|
29
|
+
|
|
30
|
+
# Set by the runtime before each execute() — the same object execute()
|
|
31
|
+
# receives, kept here so helpers like batch() can reach it.
|
|
32
|
+
context: HIOContext
|
|
33
|
+
|
|
34
|
+
# Names of configs this action reads that a node may leave empty (an
|
|
35
|
+
# optional field). The runtime drops them from the run's values before
|
|
36
|
+
# each execution, so get() returns None instead of what an earlier action
|
|
37
|
+
# left under the same name.
|
|
38
|
+
clears = ()
|
|
39
|
+
|
|
40
|
+
def initialize(self) -> None:
|
|
41
|
+
# Called once, right after the instance is created. Optional.
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
async def execute(self, context: HIOContext) -> "str | None":
|
|
45
|
+
# Runs the action. Returns the output it ended on: SUCCESS (or None),
|
|
46
|
+
# FALLBACK, or the name of a custom output. Values published with
|
|
47
|
+
# context.set() go back to the host and to the next actions.
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
def batch(self, name: str) -> HIOStream:
|
|
51
|
+
# The STREAM config `name`, arriving in chunks while this runs:
|
|
52
|
+
# async for chunk in self.batch("leds"): ...
|
|
53
|
+
# See HIOStream for how long each chunk stays valid.
|
|
54
|
+
return self.context.stream(name)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Time for actions: MicroPython's millisecond clock and non-blocking sleep,
|
|
2
|
+
# with CPython stand-ins so the same code runs under the host-side tests.
|
|
3
|
+
#
|
|
4
|
+
# start = ticks_ms()
|
|
5
|
+
# await sleep_ms(250) # lets the rest of the runtime run
|
|
6
|
+
# elapsed = ticks_diff(ticks_ms(), start) # wrap-safe
|
|
7
|
+
#
|
|
8
|
+
# ticks_ms() wraps around (on MicroPython after ~12.4 days): never compare or
|
|
9
|
+
# subtract two of them directly — always through ticks_diff().
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from asyncio import sleep_ms # type: ignore # MicroPython only
|
|
15
|
+
except ImportError:
|
|
16
|
+
async def sleep_ms(ms: int) -> None: # CPython (tests)
|
|
17
|
+
await asyncio.sleep(ms / 1000)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from time import ticks_ms, ticks_diff # type: ignore # MicroPython only
|
|
21
|
+
except ImportError:
|
|
22
|
+
import time as _time
|
|
23
|
+
|
|
24
|
+
# Same period as MicroPython's ESP32 port (2^30), so wrap-around behaves alike.
|
|
25
|
+
_PERIOD = 1 << 30
|
|
26
|
+
|
|
27
|
+
def ticks_ms() -> int: # CPython (tests)
|
|
28
|
+
return int(_time.monotonic() * 1000) % _PERIOD
|
|
29
|
+
|
|
30
|
+
def ticks_diff(new: int, old: int) -> int:
|
|
31
|
+
return ((new - old + _PERIOD // 2) % _PERIOD) - _PERIOD // 2
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from .stream import HIOStream
|
|
2
|
+
|
|
3
|
+
class HIOContext:
|
|
4
|
+
# What an action sees while it runs.
|
|
5
|
+
#
|
|
6
|
+
# Values are MicroPython's own types — what the workflow calls integer,
|
|
7
|
+
# float, bool, string, list and object: int, float, bool, str, list, dict
|
|
8
|
+
# (and bytes for a file's or a color's raw bytes).
|
|
9
|
+
|
|
10
|
+
# The trigger whose branch the action runs in (its HIOTrigger instance) —
|
|
11
|
+
# None in Start's. Branches run side by side: an action answering its
|
|
12
|
+
# trigger (HTTP's Respond, its request) finds it here.
|
|
13
|
+
origin = None
|
|
14
|
+
|
|
15
|
+
def get(self, name: str) -> "int | float | bool | str | list | dict | bytes | None":
|
|
16
|
+
# The value of `name`: this action's own configs first, then whatever
|
|
17
|
+
# previous actions published, then the workflow variable `name`. A
|
|
18
|
+
# config bound to a variable already reads it here — the action never
|
|
19
|
+
# needs to know whether a value is a literal, a bind or a variable.
|
|
20
|
+
# None when there's no such value.
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
def set(self, name: str, value: "int | float | bool | str | list | dict | bytes") -> None:
|
|
24
|
+
# Publishes `value` under `name`: sent back to the host with this
|
|
25
|
+
# action's response and visible to the next actions (binds).
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
def remove(self, name: str) -> bool:
|
|
29
|
+
# Drops `name` from the values shared between the actions of the run.
|
|
30
|
+
# Variables aren't touched — see remove_variable.
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def variable(self, name: str) -> "int | float | bool | str | list | dict | bytes | None":
|
|
34
|
+
# Only the workflow variable `name` (kept across loops of the program)
|
|
35
|
+
# — for when a config or output of the same name must not shadow it.
|
|
36
|
+
# None while it has no value.
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
def set_variable(self, name: str, value: "int | float | bool | str | list | dict | bytes") -> bool:
|
|
40
|
+
# Writes the workflow variable `name` — every action and script reads
|
|
41
|
+
# it from then on, across loops. False when there's no room for
|
|
42
|
+
# another variable (the workflow's MAX_VARIABLES).
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
def remove_variable(self, name: str) -> bool:
|
|
46
|
+
# The workflow variable `name` back to no value. False when it had none.
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
def stream(self, name: str) -> HIOStream:
|
|
50
|
+
# The STREAM config `name` — see HIOStream (HIOAction.batch is the
|
|
51
|
+
# shortcut for this).
|
|
52
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 16-bit id of a name (config, output, "<component>/<action>") — the same hash
|
|
2
|
+
# as @henglosio/workflow's generateId (src/utils/identifier.ts), which is what
|
|
3
|
+
# the host puts on the wire. Keep the two identical.
|
|
4
|
+
def generate_id(name: str) -> int:
|
|
5
|
+
hash = 0x811C
|
|
6
|
+
for byte in name.encode():
|
|
7
|
+
hash ^= byte
|
|
8
|
+
hash = (hash * 0x0101) & 0xFFFF
|
|
9
|
+
return hash
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def variable_id(name: str) -> int:
|
|
13
|
+
# VARIABLE_ID of a workflow variable — @henglosio/workflow's
|
|
14
|
+
# computeWireVariableId. Case-insensitive, like the studio's names.
|
|
15
|
+
return generate_id("var/" + name.lower())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from .action import HIOAction
|
|
2
|
+
from .trigger import HIOTrigger
|
|
3
|
+
from .identifier import generate_id
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ACTION_ID -> action (or trigger) class. ACTION_ID is
|
|
7
|
+
# generate_id("<component>/<action>"), the same id the host puts in every EXECUTE.
|
|
8
|
+
_actions = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def action(name: str):
|
|
12
|
+
# Class decorator registering an action under "<component>/<action>":
|
|
13
|
+
#
|
|
14
|
+
# @action("leds/set")
|
|
15
|
+
# class SetLeds(HIOAction): ...
|
|
16
|
+
#
|
|
17
|
+
# Rejects, at import time, a class that doesn't inherit HIOAction or
|
|
18
|
+
# doesn't implement execute() — MicroPython has no abc to do it for us.
|
|
19
|
+
def register(cls):
|
|
20
|
+
|
|
21
|
+
if not issubclass(cls, HIOAction):
|
|
22
|
+
raise TypeError("@action('%s'): %s must inherit from HIOAction" % (name, cls.__name__))
|
|
23
|
+
|
|
24
|
+
if cls.execute is HIOAction.execute:
|
|
25
|
+
raise TypeError("@action('%s'): %s must implement execute()" % (name, cls.__name__))
|
|
26
|
+
|
|
27
|
+
_actions[generate_id(name)] = cls
|
|
28
|
+
return cls
|
|
29
|
+
|
|
30
|
+
return register
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def trigger(name: str):
|
|
34
|
+
# Class decorator registering a trigger under "<component>/<trigger>" —
|
|
35
|
+
# in the same id space as actions: its node's EXECUTE carries this id, and
|
|
36
|
+
# the runtime arms it instead of running it (see TriggerService).
|
|
37
|
+
#
|
|
38
|
+
# @trigger("http/request trigger")
|
|
39
|
+
# class RequestTrigger(HIOTrigger): ...
|
|
40
|
+
def register(cls):
|
|
41
|
+
|
|
42
|
+
if not issubclass(cls, HIOTrigger):
|
|
43
|
+
raise TypeError("@trigger('%s'): %s must inherit from HIOTrigger" % (name, cls.__name__))
|
|
44
|
+
|
|
45
|
+
if cls.poll is HIOTrigger.poll:
|
|
46
|
+
raise TypeError("@trigger('%s'): %s must implement poll()" % (name, cls.__name__))
|
|
47
|
+
|
|
48
|
+
_actions[generate_id(name)] = cls
|
|
49
|
+
return cls
|
|
50
|
+
|
|
51
|
+
return register
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_trigger(action_id: int) -> bool:
|
|
55
|
+
# Whether the class registered under `action_id` is a trigger.
|
|
56
|
+
cls = _actions.get(action_id)
|
|
57
|
+
return cls is not None and issubclass(cls, HIOTrigger)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def find(action_id: int):
|
|
61
|
+
# The action class registered under `action_id`, or None.
|
|
62
|
+
return _actions.get(action_id)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def unregister(action_id: int) -> None:
|
|
66
|
+
# Forgets the class under `action_id` — its component was removed.
|
|
67
|
+
_actions.pop(action_id, None)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class HIOStreamError(Exception):
|
|
2
|
+
# The stream ended before all its data arrived: timeout, an invalid chunk,
|
|
3
|
+
# or the host gave up.
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HIOStream:
|
|
8
|
+
# A config whose value arrives in chunks while the action is already
|
|
9
|
+
# running, instead of inside the EXECUTE — for values too big for one frame
|
|
10
|
+
# (e.g. the state of every LED in a strip). Consumed with `async for`:
|
|
11
|
+
#
|
|
12
|
+
# async def execute(self, ctx):
|
|
13
|
+
# async for chunk in self.batch("leds"):
|
|
14
|
+
# self.strip.write(chunk)
|
|
15
|
+
#
|
|
16
|
+
# Each chunk is a memoryview holding a whole number of `data_type`
|
|
17
|
+
# elements. It's only valid until the loop asks for the next one — copy
|
|
18
|
+
# with bytes(chunk) anything that must outlive that. The host only sends
|
|
19
|
+
# the next chunk after this one is consumed, so a slow loop body slows the
|
|
20
|
+
# host down instead of losing data. While the action waits for a chunk,
|
|
21
|
+
# nothing else runs on the board.
|
|
22
|
+
#
|
|
23
|
+
# The loop ends normally after the last chunk (TOTAL_SIZE bytes, or the
|
|
24
|
+
# empty end chunk of an open-ended stream), or raises HIOStreamError.
|
|
25
|
+
# A stream can be read once.
|
|
26
|
+
|
|
27
|
+
config_id: int
|
|
28
|
+
data_type: int # DataType — chunks hold whole values of it (a byte of a sized one)
|
|
29
|
+
total_size: int # bytes across every chunk — 0 when open-ended
|
|
30
|
+
received: int # bytes delivered so far
|
|
31
|
+
|
|
32
|
+
def __aiter__(self) -> "HIOStream":
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
async def __anext__(self) -> memoryview:
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
async def read(self) -> bytearray:
|
|
39
|
+
# The whole value at once, for one that fits in RAM:
|
|
40
|
+
# data = await self.batch("text").read()
|
|
41
|
+
# Filled in place into a buffer sized from TOTAL_SIZE up front — no
|
|
42
|
+
# reallocation per chunk. An open-ended stream has no size to start
|
|
43
|
+
# from, so it grows as chunks arrive.
|
|
44
|
+
if not self.total_size:
|
|
45
|
+
data = bytearray()
|
|
46
|
+
async for chunk in self:
|
|
47
|
+
data.extend(chunk)
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
data = bytearray(self.total_size)
|
|
51
|
+
view = memoryview(data)
|
|
52
|
+
offset = 0
|
|
53
|
+
async for chunk in self:
|
|
54
|
+
view[offset:offset + len(chunk)] = chunk
|
|
55
|
+
offset += len(chunk)
|
|
56
|
+
return data
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from .context import HIOContext
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HIOTrigger:
|
|
5
|
+
# Base of every trigger — a node that starts a branch of the flow when
|
|
6
|
+
# something happens (a request comes in, a pin changes, a code is
|
|
7
|
+
# received), instead of being reached by a wire. Subclass it, implement
|
|
8
|
+
# poll() and register the class with @trigger("<component>/<trigger>"):
|
|
9
|
+
#
|
|
10
|
+
# @trigger("ir/code trigger")
|
|
11
|
+
# class CodeTrigger(HIOTrigger):
|
|
12
|
+
#
|
|
13
|
+
# def arm(self, context) -> None:
|
|
14
|
+
# self.receiver = Receiver(context.get("code trigger/rx pin"))
|
|
15
|
+
#
|
|
16
|
+
# def poll(self, context) -> bool:
|
|
17
|
+
# code = self.receiver.code()
|
|
18
|
+
# if code is None:
|
|
19
|
+
# return False
|
|
20
|
+
# context.set("code", code)
|
|
21
|
+
# return True
|
|
22
|
+
#
|
|
23
|
+
# One instance per trigger NODE (two Timers are two instances, each with
|
|
24
|
+
# its own configs), created when the program arms it. The runtime runs one
|
|
25
|
+
# branch at a time: while a branch runs, an event waits in the trigger
|
|
26
|
+
# itself (a pin's edge flag, a request in the socket's queue) until poll()
|
|
27
|
+
# is asked again.
|
|
28
|
+
|
|
29
|
+
# Set by the runtime before each call — the same object the call receives.
|
|
30
|
+
context: HIOContext
|
|
31
|
+
|
|
32
|
+
def initialize(self) -> None:
|
|
33
|
+
# Called once, right after the instance is created. Optional.
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def arm(self, context: HIOContext) -> None:
|
|
37
|
+
# The program starts listening: read the configs (context.get), open
|
|
38
|
+
# what it listens on (a socket, an IRQ). Called again when the host
|
|
39
|
+
# re-arms it with other configs — after disarm(). Optional.
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def poll(self, context: HIOContext) -> bool:
|
|
43
|
+
# Whether it fired since the last call — publishing what happened with
|
|
44
|
+
# context.set(), which the branch's actions read like any output.
|
|
45
|
+
# Must NOT block: it's asked over and over while the board is idle.
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
def finish(self, context: HIOContext) -> None:
|
|
49
|
+
# The branch it started is over (its last action had nothing wired
|
|
50
|
+
# after it) — e.g. answer a request nobody answered. Optional.
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def disarm(self) -> None:
|
|
54
|
+
# Stop listening: close what arm() opened. Optional.
|
|
55
|
+
pass
|
henglosio/sdk/py.typed
ADDED
|
File without changes
|
henglosio/sdk/testing.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from .core.action import HIOAction
|
|
4
|
+
from .core.context import HIOContext
|
|
5
|
+
from .core.stream import HIOStream, HIOStreamError
|
|
6
|
+
|
|
7
|
+
# Runs an action without the runtime — in a component's unit tests, or by hand
|
|
8
|
+
# on the board's REPL:
|
|
9
|
+
#
|
|
10
|
+
# from henglosio.sdk.testing import MemoryContext, run
|
|
11
|
+
# from components.http.request import RequestAction
|
|
12
|
+
#
|
|
13
|
+
# ctx = MemoryContext({"request/method": "GET", "request/url": "https://example.com"})
|
|
14
|
+
# run(RequestAction, ctx) # -> "ok"
|
|
15
|
+
# ctx.published # {"status": 200, "body": "...", ...}
|
|
16
|
+
#
|
|
17
|
+
# Values go in as an action receives them from the runtime: a string or
|
|
18
|
+
# options config as a str, an integer config as an int, a float config as a
|
|
19
|
+
# float, a toggle as a bool, a list / object as a list / dict, a file as
|
|
20
|
+
# bytes. Nothing imports this module at runtime — it only costs what a test
|
|
21
|
+
# that uses it loads.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MemoryStream(HIOStream):
|
|
25
|
+
# A STREAM config whose whole value is already here: handed out in chunks
|
|
26
|
+
# of `chunk_size` bytes, like the host would send them. Read once.
|
|
27
|
+
|
|
28
|
+
def __init__(self, data: "bytes | bytearray", chunk_size: int = 64, data_type: int = 0) -> None:
|
|
29
|
+
self._data = bytes(data)
|
|
30
|
+
self._chunk_size = max(1, chunk_size)
|
|
31
|
+
self.config_id = 0
|
|
32
|
+
self.data_type = data_type
|
|
33
|
+
self.total_size = len(self._data)
|
|
34
|
+
self.received = 0
|
|
35
|
+
|
|
36
|
+
async def __anext__(self) -> memoryview:
|
|
37
|
+
if self.received >= self.total_size:
|
|
38
|
+
raise StopAsyncIteration
|
|
39
|
+
chunk = memoryview(self._data)[self.received:self.received + self._chunk_size]
|
|
40
|
+
self.received += len(chunk)
|
|
41
|
+
return chunk
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MemoryContext(HIOContext):
|
|
45
|
+
# An HIOContext over plain dicts, looked up like the runtime's
|
|
46
|
+
# ActionContext: `values` (the configs given, and what the action
|
|
47
|
+
# published — set() replaces a config of the same name) first, then the
|
|
48
|
+
# workflow `variables`. What the action published is in `published`.
|
|
49
|
+
|
|
50
|
+
def __init__(self, configs: "dict | None" = None, variables: "dict | None" = None, streams: "dict | None" = None) -> None:
|
|
51
|
+
self.values = dict(configs or {})
|
|
52
|
+
self.variables = dict(variables or {})
|
|
53
|
+
# name -> the stream's bytes (or a MemoryStream, for its own chunk size).
|
|
54
|
+
self.streams = dict(streams or {})
|
|
55
|
+
self.published = {}
|
|
56
|
+
|
|
57
|
+
def get(self, name: str):
|
|
58
|
+
if name in self.values:
|
|
59
|
+
return self.values[name]
|
|
60
|
+
return self.variables.get(name)
|
|
61
|
+
|
|
62
|
+
def set(self, name: str, value) -> None:
|
|
63
|
+
self.values[name] = value
|
|
64
|
+
self.published[name] = value
|
|
65
|
+
|
|
66
|
+
def variable(self, name: str):
|
|
67
|
+
return self.variables.get(name)
|
|
68
|
+
|
|
69
|
+
def set_variable(self, name: str, value) -> bool:
|
|
70
|
+
self.variables[name] = value
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def remove_variable(self, name: str) -> bool:
|
|
74
|
+
return self.variables.pop(name, None) is not None
|
|
75
|
+
|
|
76
|
+
def remove(self, name: str) -> bool:
|
|
77
|
+
# Like the runtime: the run's values only, never a variable.
|
|
78
|
+
return self.values.pop(name, None) is not None
|
|
79
|
+
|
|
80
|
+
def stream(self, name: str) -> HIOStream:
|
|
81
|
+
value = self.streams.get(name)
|
|
82
|
+
if value is None:
|
|
83
|
+
raise HIOStreamError("'%s' is not a STREAM config of this execution" % name)
|
|
84
|
+
if not isinstance(value, HIOStream):
|
|
85
|
+
value = self.streams[name] = MemoryStream(value)
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run(action: "type | HIOAction", context: "MemoryContext | None" = None) -> "str | None":
|
|
90
|
+
# Runs `action` once with `context` (an empty one when left out) and
|
|
91
|
+
# returns the output it ended on. A class gets an instance, initialized
|
|
92
|
+
# like the runtime does the first time; an instance is reused as it is.
|
|
93
|
+
if isinstance(action, type):
|
|
94
|
+
action = action()
|
|
95
|
+
action.initialize()
|
|
96
|
+
context = context if context is not None else MemoryContext()
|
|
97
|
+
action.context = context
|
|
98
|
+
return asyncio.run(action.execute(context))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hio-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: SDK for writing HenglosIO component actions and triggers in MicroPython.
|
|
5
|
+
Keywords: henglosio,micropython,iot,sdk
|
|
6
|
+
Author: Henglos
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# hio-sdk
|
|
12
|
+
|
|
13
|
+
The SDK HenglosIO components are written with: the base classes and decorators
|
|
14
|
+
for actions and triggers (`from henglosio.sdk import HIOAction, action, ...`), plus
|
|
15
|
+
`henglosio.sdk.testing` to run an action under pytest without a board.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pip install hio-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The same code runs on the board: every `@henglosio/hio-system` release ships
|
|
22
|
+
the sdk inside it, so a component never installs it on the device. This package
|
|
23
|
+
is for writing and testing components on your machine (types, autocomplete,
|
|
24
|
+
`henglosio.sdk.testing`).
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
Part of Henglos IO, distributed under the [Sustainable Use License](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
henglosio/sdk/__init__.py,sha256=M1GLdcmrhr851YW7THFBje5qmvzqcNn1VzSXxieTYVg,325
|
|
2
|
+
henglosio/sdk/core/action.py,sha256=_pG_LGQLN0hO9zUXnr3MgTngBsjzYS6a1eIZLFGjPqo,2010
|
|
3
|
+
henglosio/sdk/core/clock.py,sha256=ph_a97f9Nin8K0Agm6NoKxrMNZx2t8MrfG8HLHRiHmw,1174
|
|
4
|
+
henglosio/sdk/core/context.py,sha256=G3jXoFH4_BP0cWlff5QFdZ4XThHTtVTzmWqoHRkAqZQ,2385
|
|
5
|
+
henglosio/sdk/core/identifier.py,sha256=tpls-S4ubpJZ_s3ZHuSLZwGewsiTzGeouQSMKa4vM1Y,581
|
|
6
|
+
henglosio/sdk/core/registry.py,sha256=KukFdBHq_BShkyx_LONp5rGx76k1UiE8LiBZYa3SoMw,2095
|
|
7
|
+
henglosio/sdk/core/stream.py,sha256=U5KoM4-JnK4qYVjyM1ELNhcp_hRmAB5iovtiQIhQKqk,2116
|
|
8
|
+
henglosio/sdk/core/trigger.py,sha256=U7AJWXRw92FiEGAYtbOnnma7Xl_RsnpFDJOnMM8G_z0,2145
|
|
9
|
+
henglosio/sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
henglosio/sdk/testing.py,sha256=FAZ978J14_liUzhzo8G6VmvrOh08nZGTnB1DA_DnumQ,3652
|
|
11
|
+
hio_sdk-1.0.0.dist-info/licenses/LICENSE,sha256=psVVsuL8XpB9Tsr0gg6MZ7p58oD_mMhgcr8CPTBBCKk,4080
|
|
12
|
+
hio_sdk-1.0.0.dist-info/WHEEL,sha256=3OmCxFA0ffBIsX4x0o8CdfSvZAOvO3mzEG8U_z5TvV4,80
|
|
13
|
+
hio_sdk-1.0.0.dist-info/METADATA,sha256=q17H9u2tHZFbszXpE4GxEBd1LZaqeFnyDTWp3dKHkDY,903
|
|
14
|
+
hio_sdk-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Sustainable Use License
|
|
2
|
+
|
|
3
|
+
Version 1.0
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 Henglos — the licensor of Henglos IO.
|
|
6
|
+
|
|
7
|
+
Henglos IO is fair-code: these terms let you use and modify it for your own or your company's
|
|
8
|
+
internal use, and share it free of charge for non-commercial purposes. Any other use — selling it,
|
|
9
|
+
or offering it to others as a paid product or service — needs a separate commercial agreement with
|
|
10
|
+
the licensor.
|
|
11
|
+
|
|
12
|
+
## Acceptance
|
|
13
|
+
|
|
14
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
15
|
+
|
|
16
|
+
## Copyright License
|
|
17
|
+
|
|
18
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
|
19
|
+
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
|
20
|
+
to the limitations below.
|
|
21
|
+
|
|
22
|
+
## Limitations
|
|
23
|
+
|
|
24
|
+
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
|
25
|
+
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
|
26
|
+
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
|
27
|
+
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
|
28
|
+
|
|
29
|
+
## Patents
|
|
30
|
+
|
|
31
|
+
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
|
32
|
+
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
|
33
|
+
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
|
34
|
+
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
|
35
|
+
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
|
36
|
+
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
|
37
|
+
license ends immediately for work on behalf of your company.
|
|
38
|
+
|
|
39
|
+
## Notices
|
|
40
|
+
|
|
41
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
|
42
|
+
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
|
43
|
+
stating that you have modified the software.
|
|
44
|
+
|
|
45
|
+
## No Other Rights
|
|
46
|
+
|
|
47
|
+
These terms do not imply any licenses other than those expressly granted in these terms.
|
|
48
|
+
|
|
49
|
+
## Termination
|
|
50
|
+
|
|
51
|
+
If you use the software in violation of these terms, such use is not licensed, and your license will
|
|
52
|
+
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
|
53
|
+
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
|
54
|
+
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
|
55
|
+
terms will cause your license to terminate automatically and permanently.
|
|
56
|
+
|
|
57
|
+
## No Liability
|
|
58
|
+
|
|
59
|
+
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
|
60
|
+
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
|
61
|
+
any kind of legal claim.
|
|
62
|
+
|
|
63
|
+
## Definitions
|
|
64
|
+
|
|
65
|
+
The “licensor” is the entity offering these terms.
|
|
66
|
+
|
|
67
|
+
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
|
68
|
+
|
|
69
|
+
“You” refers to the individual or entity agreeing to these terms.
|
|
70
|
+
|
|
71
|
+
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
|
72
|
+
all organizations that have control over, are under the control of, or are under common control with that
|
|
73
|
+
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
|
74
|
+
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
75
|
+
|
|
76
|
+
“Your license” is the license granted to you for the software under these terms.
|
|
77
|
+
|
|
78
|
+
“Use” means anything you do with the software requiring your license.
|
|
79
|
+
|
|
80
|
+
“Trademark” means trademarks, service marks, and similar rights.
|