hio-sdk 1.0.0__tar.gz

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.
hio_sdk-1.0.0/LICENSE ADDED
@@ -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.
hio_sdk-1.0.0/PKG-INFO ADDED
@@ -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,18 @@
1
+ # hio-sdk
2
+
3
+ The SDK HenglosIO components are written with: the base classes and decorators
4
+ for actions and triggers (`from henglosio.sdk import HIOAction, action, ...`), plus
5
+ `henglosio.sdk.testing` to run an action under pytest without a board.
6
+
7
+ ```sh
8
+ pip install hio-sdk
9
+ ```
10
+
11
+ The same code runs on the board: every `@henglosio/hio-system` release ships
12
+ the sdk inside it, so a component never installs it on the device. This package
13
+ is for writing and testing components on your machine (types, autocomplete,
14
+ `henglosio.sdk.testing`).
15
+
16
+ ## License
17
+
18
+ Part of Henglos IO, distributed under the [Sustainable Use License](LICENSE).
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "hio-sdk"
3
+ version = "1.0.0"
4
+ description = "SDK for writing HenglosIO component actions and triggers in MicroPython."
5
+ readme = "README.md"
6
+ license-files = ["LICENSE"]
7
+ requires-python = ">=3.10"
8
+ dependencies = []
9
+ keywords = [
10
+ "henglosio",
11
+ "micropython",
12
+ "iot",
13
+ "sdk",
14
+ ]
15
+
16
+ [[project.authors]]
17
+ name = "Henglos"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.13.8,<0.14.0"]
21
+ build-backend = "uv_build"
22
+
23
+ [tool.uv.build-backend]
24
+ module-name = "henglosio.sdk"
25
+
26
+ [dependency-groups]
27
+ dev = ["pytest>=8"]
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "hio-sdk"
3
+ version = "1.0.0"
4
+ description = "SDK for writing HenglosIO component actions and triggers in MicroPython."
5
+ readme = "README.md"
6
+ license-files = ["LICENSE"]
7
+ authors = [
8
+ { name = "Henglos" }
9
+ ]
10
+ requires-python = ">=3.10"
11
+ dependencies = []
12
+ keywords = ["henglosio", "micropython", "iot", "sdk"]
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.13.8,<0.14.0"]
16
+ build-backend = "uv_build"
17
+
18
+ [tool.uv.build-backend]
19
+ # `henglosio` is a namespace (no henglosio/__init__.py): the sdk is
20
+ # henglosio.sdk, leaving room for other henglosio.* packages.
21
+ module-name = "henglosio.sdk"
22
+
23
+ [dependency-groups]
24
+ dev = ["pytest>=8"]
@@ -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
File without changes
@@ -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))