chumicro-runner 0.1.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.
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: chumicro-runner
3
+ Version: 0.1.0
4
+ Summary: Tick-based task runner with shared timestamps for Chumicro applications.
5
+ Author: Chumicro
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 2 - Pre-Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: chumicro-timing
14
+
15
+ # chumicro-runner
16
+
17
+ A tick-based service pattern for Chumicro libraries.
18
+
19
+ Components implement a `check(now_ms) -> bool` check that gates when a handler fires. A `Runner` captures time once per tick, checks each service, and batch-fires all due handlers — replacing ad-hoc polling loops with a single standard contract.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # CPython (pip)
25
+ pip install chumicro-runner
26
+
27
+ # CircuitPython (circup)
28
+ circup bundle-add ChuMicro/chumicro-bundle
29
+ circup install chumicro-runner
30
+
31
+ # MicroPython (mip)
32
+ mpremote mip install github:ChuMicro/chumicro-bundle/chumicro_runner
33
+ ```
34
+
35
+ For experimental (pre-release) versions from the develop branch:
36
+
37
+ ```bash
38
+ pip install chumicro-runner-experimental
39
+ circup install chumicro-runner-experimental
40
+ mpremote mip install github:ChuMicro/chumicro-bundle/chumicro_runner_experimental
41
+ ```
42
+
43
+ ## Quick example
44
+
45
+ ```python
46
+ from chumicro_runner import Runner
47
+
48
+
49
+ class TemperatureSensor:
50
+ """Alert when temperature exceeds a threshold."""
51
+
52
+ def __init__(self, threshold=30.0):
53
+ self._threshold = threshold
54
+ self._last_reading = 0.0
55
+
56
+ def read_temperature(self):
57
+ """Read from hardware — fast I2C or ADC operation."""
58
+ # On a real board: return self._i2c_device.temperature
59
+ return self._last_reading
60
+
61
+ def check(self, now_ms):
62
+ self._last_reading = self.read_temperature()
63
+ return self._last_reading > self._threshold
64
+
65
+ def handle(self, now_ms):
66
+ print(f"ALERT: {self._last_reading}°C exceeds {self._threshold}°C")
67
+
68
+
69
+ runner = Runner()
70
+ sensor = TemperatureSensor(threshold=30.0)
71
+ runner.add(sensor, period_ms=5000) # check every 5 seconds
72
+
73
+ while True:
74
+ runner.tick()
75
+ ```
76
+
77
+ For simple periodic tasks, no service class is needed:
78
+
79
+ ```python
80
+ from chumicro_runner import Runner
81
+
82
+ runner = Runner()
83
+ runner.add_periodic(lambda now_ms: print("blink!"), period_ms=500)
84
+
85
+ while True:
86
+ runner.tick()
87
+ ```
88
+
89
+ ## What's included
90
+
91
+ ### Core
92
+
93
+ | Symbol | Description |
94
+ |---|---|
95
+ | `Runner(ticks=None)` | Tick-based service loop with shared timestamps |
96
+ | `Runner.add(task, handler=None, period_ms=None, start_after_ms=None, run_count=None)` | Register a task; returns a `TaskHandle` |
97
+ | `Runner.add_periodic(handler, period_ms, start_after_ms=None, run_count=None)` | Register a periodic handler; returns a `TaskHandle` |
98
+ | `Runner.tick()` | Capture time, check services, batch-fire handlers; returns `now_ms` |
99
+ | `TaskHandle` | Opaque handle for runtime mutation of a registered service |
100
+ | `TaskHandle.set_period(period_ms)` | Add, change, or remove the period (`None` to remove) |
101
+ | `TaskHandle.remove()` | Remove this service from the runner |
102
+ | `TaskHandle.period_ms` | Read-only: the service period, or `None` |
103
+ | `TaskHandle.run_count` | Read-only: remaining run count, or `None` if unlimited |
104
+ | `TaskHandle.active` | Read-only: whether the service is still registered |
105
+
106
+ ### Testing
107
+
108
+ | Symbol | Description |
109
+ |---|---|
110
+ | `CallRecorder()` | Callable that records handler invocations for test assertions |
111
+ | `CallRecorder.calls` | Direct access to the list of recorded `now_ms` values |
112
+
113
+ ## Registration patterns
114
+
115
+ ### Object-based (with `.check()` and `.handle()`)
116
+
117
+ Pass an object that has `check(now_ms) -> bool` and `handle(now_ms)` methods. The runner calls `.check()`; if it returns `True`, `.handle()` is queued:
118
+
119
+ ```python
120
+ class MotionDetector:
121
+ def __init__(self):
122
+ # On a real board: self._pin = digitalio.DigitalInOut(board.D5)
123
+ pass
124
+
125
+ def detect_motion(self):
126
+ """Read PIR sensor pin — fast digital read."""
127
+ # On a real board: return self._pin.value
128
+ return False
129
+
130
+ def check(self, now_ms):
131
+ return self.detect_motion()
132
+
133
+ def handle(self, now_ms):
134
+ print("Motion!")
135
+
136
+ runner.add(MotionDetector())
137
+ ```
138
+
139
+ ### Callable-based (check function + handler)
140
+
141
+ Pass a callable check function and a handler. Both can be lambdas, bound methods, or regular functions:
142
+
143
+ ```python
144
+ runner.add(
145
+ lambda now_ms: sensor.ready(),
146
+ handler=lambda now_ms: process(sensor.read()),
147
+ )
148
+ ```
149
+
150
+ ### Handler-only (no check, fires every tick)
151
+
152
+ Pass just a handler with no service check:
153
+
154
+ ```python
155
+ runner.add(handler=lambda now_ms: poll_buttons(now_ms))
156
+ ```
157
+
158
+ ### Periodic (fires every N milliseconds)
159
+
160
+ No service check needed — the handler fires on schedule:
161
+
162
+ ```python
163
+ handle = runner.add_periodic(toggle_led, period_ms=500)
164
+ handle.set_period(1000) # change rate at runtime
165
+ ```
166
+
167
+ ## Runtime mutation
168
+
169
+ `add()` and `add_periodic()` return a `TaskHandle` for runtime changes:
170
+
171
+ ```python
172
+ handle = runner.add(sensor, period_ms=5000)
173
+
174
+ # Speed up.
175
+ handle.set_period(1000)
176
+
177
+ # Remove the period — service runs every tick.
178
+ handle.set_period(None)
179
+
180
+ # Remove entirely.
181
+ handle.remove()
182
+ ```
183
+
184
+ ## Testing your components
185
+
186
+ The `chumicro_runner.testing` module provides `CallRecorder` for verifying that handlers fire at the right times:
187
+
188
+ ```python
189
+ from chumicro_runner.testing import CallRecorder
190
+ from chumicro_timing.testing import FakeTicks
191
+
192
+ fake = FakeTicks()
193
+ recorder = CallRecorder()
194
+ runner = Runner(ticks=fake)
195
+ runner.add_periodic(recorder, period_ms=100)
196
+
197
+ runner.tick()
198
+ assert len(recorder) == 0 # not due yet
199
+
200
+ fake.advance(100)
201
+ runner.tick()
202
+ assert recorder.calls == [100]
203
+ ```
204
+
205
+ ## Platform support
206
+
207
+ All classes use only basic Python features. Works identically on CPython, MicroPython, and CircuitPython.
208
+
209
+ ## Memory notes
210
+
211
+ - `_TaskEntry` and `TaskHandle` use `__slots__` to minimise per-instance memory.
212
+ - Handlers are collected into a pre-allocated list and batch-fired, avoiding per-tick allocation.
213
+
214
+ ## Docs
215
+
216
+ - [User guide](docs/guide.md) — the pattern, getting started, writing components
217
+ - [API reference](docs/api.md) — full API documentation
218
+ - [Testing helpers](docs/testing.md) — using `CallRecorder` in your tests
@@ -0,0 +1,204 @@
1
+ # chumicro-runner
2
+
3
+ A tick-based service pattern for Chumicro libraries.
4
+
5
+ Components implement a `check(now_ms) -> bool` check that gates when a handler fires. A `Runner` captures time once per tick, checks each service, and batch-fires all due handlers — replacing ad-hoc polling loops with a single standard contract.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # CPython (pip)
11
+ pip install chumicro-runner
12
+
13
+ # CircuitPython (circup)
14
+ circup bundle-add ChuMicro/chumicro-bundle
15
+ circup install chumicro-runner
16
+
17
+ # MicroPython (mip)
18
+ mpremote mip install github:ChuMicro/chumicro-bundle/chumicro_runner
19
+ ```
20
+
21
+ For experimental (pre-release) versions from the develop branch:
22
+
23
+ ```bash
24
+ pip install chumicro-runner-experimental
25
+ circup install chumicro-runner-experimental
26
+ mpremote mip install github:ChuMicro/chumicro-bundle/chumicro_runner_experimental
27
+ ```
28
+
29
+ ## Quick example
30
+
31
+ ```python
32
+ from chumicro_runner import Runner
33
+
34
+
35
+ class TemperatureSensor:
36
+ """Alert when temperature exceeds a threshold."""
37
+
38
+ def __init__(self, threshold=30.0):
39
+ self._threshold = threshold
40
+ self._last_reading = 0.0
41
+
42
+ def read_temperature(self):
43
+ """Read from hardware — fast I2C or ADC operation."""
44
+ # On a real board: return self._i2c_device.temperature
45
+ return self._last_reading
46
+
47
+ def check(self, now_ms):
48
+ self._last_reading = self.read_temperature()
49
+ return self._last_reading > self._threshold
50
+
51
+ def handle(self, now_ms):
52
+ print(f"ALERT: {self._last_reading}°C exceeds {self._threshold}°C")
53
+
54
+
55
+ runner = Runner()
56
+ sensor = TemperatureSensor(threshold=30.0)
57
+ runner.add(sensor, period_ms=5000) # check every 5 seconds
58
+
59
+ while True:
60
+ runner.tick()
61
+ ```
62
+
63
+ For simple periodic tasks, no service class is needed:
64
+
65
+ ```python
66
+ from chumicro_runner import Runner
67
+
68
+ runner = Runner()
69
+ runner.add_periodic(lambda now_ms: print("blink!"), period_ms=500)
70
+
71
+ while True:
72
+ runner.tick()
73
+ ```
74
+
75
+ ## What's included
76
+
77
+ ### Core
78
+
79
+ | Symbol | Description |
80
+ |---|---|
81
+ | `Runner(ticks=None)` | Tick-based service loop with shared timestamps |
82
+ | `Runner.add(task, handler=None, period_ms=None, start_after_ms=None, run_count=None)` | Register a task; returns a `TaskHandle` |
83
+ | `Runner.add_periodic(handler, period_ms, start_after_ms=None, run_count=None)` | Register a periodic handler; returns a `TaskHandle` |
84
+ | `Runner.tick()` | Capture time, check services, batch-fire handlers; returns `now_ms` |
85
+ | `TaskHandle` | Opaque handle for runtime mutation of a registered service |
86
+ | `TaskHandle.set_period(period_ms)` | Add, change, or remove the period (`None` to remove) |
87
+ | `TaskHandle.remove()` | Remove this service from the runner |
88
+ | `TaskHandle.period_ms` | Read-only: the service period, or `None` |
89
+ | `TaskHandle.run_count` | Read-only: remaining run count, or `None` if unlimited |
90
+ | `TaskHandle.active` | Read-only: whether the service is still registered |
91
+
92
+ ### Testing
93
+
94
+ | Symbol | Description |
95
+ |---|---|
96
+ | `CallRecorder()` | Callable that records handler invocations for test assertions |
97
+ | `CallRecorder.calls` | Direct access to the list of recorded `now_ms` values |
98
+
99
+ ## Registration patterns
100
+
101
+ ### Object-based (with `.check()` and `.handle()`)
102
+
103
+ Pass an object that has `check(now_ms) -> bool` and `handle(now_ms)` methods. The runner calls `.check()`; if it returns `True`, `.handle()` is queued:
104
+
105
+ ```python
106
+ class MotionDetector:
107
+ def __init__(self):
108
+ # On a real board: self._pin = digitalio.DigitalInOut(board.D5)
109
+ pass
110
+
111
+ def detect_motion(self):
112
+ """Read PIR sensor pin — fast digital read."""
113
+ # On a real board: return self._pin.value
114
+ return False
115
+
116
+ def check(self, now_ms):
117
+ return self.detect_motion()
118
+
119
+ def handle(self, now_ms):
120
+ print("Motion!")
121
+
122
+ runner.add(MotionDetector())
123
+ ```
124
+
125
+ ### Callable-based (check function + handler)
126
+
127
+ Pass a callable check function and a handler. Both can be lambdas, bound methods, or regular functions:
128
+
129
+ ```python
130
+ runner.add(
131
+ lambda now_ms: sensor.ready(),
132
+ handler=lambda now_ms: process(sensor.read()),
133
+ )
134
+ ```
135
+
136
+ ### Handler-only (no check, fires every tick)
137
+
138
+ Pass just a handler with no service check:
139
+
140
+ ```python
141
+ runner.add(handler=lambda now_ms: poll_buttons(now_ms))
142
+ ```
143
+
144
+ ### Periodic (fires every N milliseconds)
145
+
146
+ No service check needed — the handler fires on schedule:
147
+
148
+ ```python
149
+ handle = runner.add_periodic(toggle_led, period_ms=500)
150
+ handle.set_period(1000) # change rate at runtime
151
+ ```
152
+
153
+ ## Runtime mutation
154
+
155
+ `add()` and `add_periodic()` return a `TaskHandle` for runtime changes:
156
+
157
+ ```python
158
+ handle = runner.add(sensor, period_ms=5000)
159
+
160
+ # Speed up.
161
+ handle.set_period(1000)
162
+
163
+ # Remove the period — service runs every tick.
164
+ handle.set_period(None)
165
+
166
+ # Remove entirely.
167
+ handle.remove()
168
+ ```
169
+
170
+ ## Testing your components
171
+
172
+ The `chumicro_runner.testing` module provides `CallRecorder` for verifying that handlers fire at the right times:
173
+
174
+ ```python
175
+ from chumicro_runner.testing import CallRecorder
176
+ from chumicro_timing.testing import FakeTicks
177
+
178
+ fake = FakeTicks()
179
+ recorder = CallRecorder()
180
+ runner = Runner(ticks=fake)
181
+ runner.add_periodic(recorder, period_ms=100)
182
+
183
+ runner.tick()
184
+ assert len(recorder) == 0 # not due yet
185
+
186
+ fake.advance(100)
187
+ runner.tick()
188
+ assert recorder.calls == [100]
189
+ ```
190
+
191
+ ## Platform support
192
+
193
+ All classes use only basic Python features. Works identically on CPython, MicroPython, and CircuitPython.
194
+
195
+ ## Memory notes
196
+
197
+ - `_TaskEntry` and `TaskHandle` use `__slots__` to minimise per-instance memory.
198
+ - Handlers are collected into a pre-allocated list and batch-fired, avoiding per-tick allocation.
199
+
200
+ ## Docs
201
+
202
+ - [User guide](docs/guide.md) — the pattern, getting started, writing components
203
+ - [API reference](docs/api.md) — full API documentation
204
+ - [Testing helpers](docs/testing.md) — using `CallRecorder` in your tests
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chumicro-runner"
7
+ dynamic = ["version"]
8
+ description = "Tick-based task runner with shared timestamps for Chumicro applications."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Chumicro" },
14
+ ]
15
+ dependencies = [
16
+ "chumicro-timing",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 2 - Pre-Alpha",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ ]
24
+
25
+ [tool.setuptools]
26
+ package-dir = {"" = "src"}
27
+
28
+ [tool.setuptools.dynamic]
29
+ version = {file = "VERSION"}
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """Public exports for the chumicro-runner package."""
2
+
3
+ from .core import (
4
+ Runner,
5
+ TaskHandle,
6
+ )
7
+
8
+ __all__ = [
9
+ "Runner",
10
+ "TaskHandle",
11
+ ]