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.
- chumicro_runner-0.1.0/PKG-INFO +218 -0
- chumicro_runner-0.1.0/README.md +204 -0
- chumicro_runner-0.1.0/VERSION +1 -0
- chumicro_runner-0.1.0/pyproject.toml +32 -0
- chumicro_runner-0.1.0/setup.cfg +4 -0
- chumicro_runner-0.1.0/src/chumicro_runner/__init__.py +11 -0
- chumicro_runner-0.1.0/src/chumicro_runner/core.py +338 -0
- chumicro_runner-0.1.0/src/chumicro_runner/testing.py +43 -0
- chumicro_runner-0.1.0/src/chumicro_runner.egg-info/PKG-INFO +218 -0
- chumicro_runner-0.1.0/src/chumicro_runner.egg-info/SOURCES.txt +12 -0
- chumicro_runner-0.1.0/src/chumicro_runner.egg-info/dependency_links.txt +1 -0
- chumicro_runner-0.1.0/src/chumicro_runner.egg-info/requires.txt +1 -0
- chumicro_runner-0.1.0/src/chumicro_runner.egg-info/top_level.txt +1 -0
- chumicro_runner-0.1.0/tests/test_core.py +965 -0
|
@@ -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"]
|