pobblebonk 0.0.1__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.
- pobblebonk-0.0.1/.gitignore +9 -0
- pobblebonk-0.0.1/PKG-INFO +234 -0
- pobblebonk-0.0.1/README.md +212 -0
- pobblebonk-0.0.1/pobblebonk/__init__.py +2 -0
- pobblebonk-0.0.1/pobblebonk/_modidx.py +37 -0
- pobblebonk-0.0.1/pobblebonk/core.py +306 -0
- pobblebonk-0.0.1/pyproject.toml +52 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pobblebonk
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: the clock and the notebook for an agent: schedules, lists and notes on honker, in one file
|
|
5
|
+
Project-URL: Repository, https://github.com/vedicreader/pobblebonk
|
|
6
|
+
Project-URL: Documentation, https://vedicreader.github.io/pobblebonk/
|
|
7
|
+
Author-email: Karthik <karthik.rajgopal@hotmail.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: acp,agents,cron,honker,nbdev,reminders,scheduler,sqlite
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: fastcore>=2.2.14
|
|
20
|
+
Requires-Dist: honker>=0.5.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# pobblebonk
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
27
|
+
|
|
28
|
+
> the clock and the notebook for an agent
|
|
29
|
+
|
|
30
|
+
`pobblebonk` adds callbacks, durable lists, and per-reader notes to [honker](https://github.com/russellromney/honker). Schedules, queued work, list items, retries, and notes share one SQLite file.
|
|
31
|
+
|
|
32
|
+
There is no scheduler daemon. Call [`Pob.tick()`](https://vedicreader.github.io/pobblebonk/core.html#pob.tick) from cron, launchd, or a systemd timer. Each tick asks honker for due fires and runs their callbacks.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
``` sh
|
|
37
|
+
uv add pobblebonk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Python 3.12 or later is required.
|
|
41
|
+
|
|
42
|
+
## Schedule a callback
|
|
43
|
+
|
|
44
|
+
Register a callback, add its schedule, then call `tick`. The callback return value becomes a note.
|
|
45
|
+
|
|
46
|
+
``` python
|
|
47
|
+
import time
|
|
48
|
+
|
|
49
|
+
pob = Pob()
|
|
50
|
+
beats = []
|
|
51
|
+
|
|
52
|
+
@pob.on('heartbeat')
|
|
53
|
+
def beat(fire):
|
|
54
|
+
beats.append(fire.fire_at)
|
|
55
|
+
return f'beat {len(beats)} at {fire.fire_at}'
|
|
56
|
+
|
|
57
|
+
pob.add('heartbeat', every='1s')
|
|
58
|
+
for _ in range(3):
|
|
59
|
+
time.sleep(1.05)
|
|
60
|
+
pob.tick()
|
|
61
|
+
|
|
62
|
+
beats
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`fire.fire_at` is the scheduled boundary, not the time the callback happened. The one-second gaps show that the cadence held.
|
|
66
|
+
|
|
67
|
+
``` python
|
|
68
|
+
[b-a for a, b in zip(beats, beats[1:])]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`drain` returns the notes a reader has not seen. Each reader has an independent cursor.
|
|
72
|
+
|
|
73
|
+
``` python
|
|
74
|
+
pob.drain('leela').attrgot('body')
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
``` python
|
|
78
|
+
pob.drain('leela'), len(pob.drain('phone'))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Run ordinary Python
|
|
82
|
+
|
|
83
|
+
A callback is a Python function. It can call a library, write a file, or run a command.
|
|
84
|
+
|
|
85
|
+
``` python
|
|
86
|
+
import subprocess
|
|
87
|
+
|
|
88
|
+
pob2 = Pob()
|
|
89
|
+
|
|
90
|
+
@pob2.on('git roundup')
|
|
91
|
+
def roundup(fire):
|
|
92
|
+
out = subprocess.run(['git', 'log', '--oneline', '--since=30 days ago'],
|
|
93
|
+
capture_output=True, text=True).stdout.strip().splitlines()
|
|
94
|
+
return f'{len(out)} commits in the last 30 days'
|
|
95
|
+
|
|
96
|
+
pob2.add('git roundup', cron='0 17 * * *') # 5pm daily
|
|
97
|
+
time.sleep(1.05)
|
|
98
|
+
pob2.tick(at=int(time.time()) + 86400).ran[0].result
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Give a callback a durable list
|
|
102
|
+
|
|
103
|
+
`push` adds an item to a named list. A schedule with `needs` runs only when that list is not empty. Its callback receives the open items as `fire.list`.
|
|
104
|
+
|
|
105
|
+
The callback marks its items used when it returns. If it raises, the items remain open for the retry. A `key` makes repeated open items idempotent.
|
|
106
|
+
|
|
107
|
+
``` python
|
|
108
|
+
pob3 = Pob()
|
|
109
|
+
|
|
110
|
+
@pob3.on('cart')
|
|
111
|
+
def cart(fire):
|
|
112
|
+
return 'added: ' + ', '.join(i.text for i in fire.list)
|
|
113
|
+
|
|
114
|
+
pob3.add('cart', cron='0 20 * * 3', needs='shopping') # 8pm on Wednesdays
|
|
115
|
+
pob3.push('shopping', 'yoghurt, the greek one', key='img_2201.heic')
|
|
116
|
+
pob3.push('shopping', 'yoghurt, the greek one', key='img_2201.heic') # the same photo twice
|
|
117
|
+
pob3.push('shopping', 'oat milk')
|
|
118
|
+
pob3.items('shopping').attrgot('text')
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
``` python
|
|
122
|
+
time.sleep(1.05)
|
|
123
|
+
got = pob3.tick(at=int(time.time()) + 7*86400).ran[0]
|
|
124
|
+
got.status, got.result, got.used
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When the list is empty, the next fire is `skipped`. It is not a callback failure and produces no note.
|
|
128
|
+
|
|
129
|
+
``` python
|
|
130
|
+
time.sleep(1.05)
|
|
131
|
+
nxt = pob3.tick(at=int(time.time()) + 14*86400).ran[0]
|
|
132
|
+
nxt.status, nxt.why
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Use a model callback
|
|
136
|
+
|
|
137
|
+
A model is another callable dependency. The documentation build does not run this example because it needs a LiteRT model.
|
|
138
|
+
|
|
139
|
+
``` python
|
|
140
|
+
import rishi
|
|
141
|
+
|
|
142
|
+
pob4 = Pob()
|
|
143
|
+
|
|
144
|
+
@pob4.on('news')
|
|
145
|
+
def digest(fire):
|
|
146
|
+
chat = rishi.Chat('litert-community/gemma-4-E2B-it-litert-lm')
|
|
147
|
+
topics = ', '.join(i.text for i in fire.list)
|
|
148
|
+
return str(chat(f'Name one thing worth reading about each of: {topics}. One line each.'))
|
|
149
|
+
|
|
150
|
+
pob4.add('news', cron='0 7 * * *', needs='interests')
|
|
151
|
+
pob4.push('interests', 'sanskrit grammar')
|
|
152
|
+
pob4.push('interests', 'sqlite internals')
|
|
153
|
+
pob4.tick()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Operate schedules
|
|
157
|
+
|
|
158
|
+
Use `pause`, `resume`, `update`, and `drop` to maintain schedules. `update` changes only the fields you pass. `drop` also unregisters the callback in the current process.
|
|
159
|
+
|
|
160
|
+
``` python
|
|
161
|
+
pob4.pause('news')
|
|
162
|
+
pob4.update('news', cron='30 7 * * *', retries=5)
|
|
163
|
+
pob4.resume('news')
|
|
164
|
+
pob4.drop('news')
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
``` python
|
|
168
|
+
# Queue due fires without running callbacks.
|
|
169
|
+
queued = pob.tick(run=False)
|
|
170
|
+
|
|
171
|
+
# Run queued fires separately, with a bounded batch.
|
|
172
|
+
results = pob.work(worker='scheduler', limit=100)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Failures and missed fires
|
|
176
|
+
|
|
177
|
+
A fire retries with exponential backoff when its callback raises. The default attempt budget is three. Set `retries` on [`Pob`](https://vedicreader.github.io/pobblebonk/core.html#pob) or on one schedule. After the final attempt, the fire is dead-lettered and a note records the error.
|
|
178
|
+
|
|
179
|
+
`catchup='once'` keeps the latest fire missed while the machine was off. This is the default. `catchup='all'` keeps every missed fire.
|
|
180
|
+
|
|
181
|
+
Use `tick(run=False)` when scheduling and callback execution belong in separate processes. It queues due fires without running them. `work` claims and runs queued fires.
|
|
182
|
+
|
|
183
|
+
``` python
|
|
184
|
+
pob = Pob('~/.pobblebonk/pob.db', retries=3)
|
|
185
|
+
pob.add('roundup', cron='0 17 * * *', catchup='once', retries=5)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Run the tick
|
|
189
|
+
|
|
190
|
+
The process that calls `tick` must register the callbacks first. A small script is enough.
|
|
191
|
+
|
|
192
|
+
``` python
|
|
193
|
+
# tick.py
|
|
194
|
+
from pobblebonk.core import Pob
|
|
195
|
+
|
|
196
|
+
pob = Pob('~/.pobblebonk/pob.db')
|
|
197
|
+
|
|
198
|
+
@pob.on('cart')
|
|
199
|
+
def cart(fire):
|
|
200
|
+
return 'added: ' + ', '.join(item.text for item in fire.list)
|
|
201
|
+
|
|
202
|
+
if __name__ == '__main__':
|
|
203
|
+
print(pob.tick())
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Run it once a minute. A tick with nothing due is one SQL call.
|
|
207
|
+
|
|
208
|
+
``` cron
|
|
209
|
+
* * * * * cd ~/myapp && uv run python tick.py
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Use a launchd `StartInterval` of 60 on macOS or a systemd timer with `OnCalendar=minutely` on Linux.
|
|
213
|
+
|
|
214
|
+
## Share an existing database
|
|
215
|
+
|
|
216
|
+
Pass an open honker database to keep pobblebonk data beside another application.
|
|
217
|
+
|
|
218
|
+
``` python
|
|
219
|
+
import honker
|
|
220
|
+
|
|
221
|
+
db = honker.open('app.db')
|
|
222
|
+
pob = Pob(db=db)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Develop
|
|
226
|
+
|
|
227
|
+
``` sh
|
|
228
|
+
uv sync --group dev
|
|
229
|
+
uv run nbdev-export
|
|
230
|
+
uv run nbdev-test
|
|
231
|
+
uv run nbdev-clean
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`nbs/00_core.ipynb` contains the implementation and tests. `nbs/index.ipynb` generates this README.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# pobblebonk
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
5
|
+
|
|
6
|
+
> the clock and the notebook for an agent
|
|
7
|
+
|
|
8
|
+
`pobblebonk` adds callbacks, durable lists, and per-reader notes to [honker](https://github.com/russellromney/honker). Schedules, queued work, list items, retries, and notes share one SQLite file.
|
|
9
|
+
|
|
10
|
+
There is no scheduler daemon. Call [`Pob.tick()`](https://vedicreader.github.io/pobblebonk/core.html#pob.tick) from cron, launchd, or a systemd timer. Each tick asks honker for due fires and runs their callbacks.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
``` sh
|
|
15
|
+
uv add pobblebonk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Python 3.12 or later is required.
|
|
19
|
+
|
|
20
|
+
## Schedule a callback
|
|
21
|
+
|
|
22
|
+
Register a callback, add its schedule, then call `tick`. The callback return value becomes a note.
|
|
23
|
+
|
|
24
|
+
``` python
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
pob = Pob()
|
|
28
|
+
beats = []
|
|
29
|
+
|
|
30
|
+
@pob.on('heartbeat')
|
|
31
|
+
def beat(fire):
|
|
32
|
+
beats.append(fire.fire_at)
|
|
33
|
+
return f'beat {len(beats)} at {fire.fire_at}'
|
|
34
|
+
|
|
35
|
+
pob.add('heartbeat', every='1s')
|
|
36
|
+
for _ in range(3):
|
|
37
|
+
time.sleep(1.05)
|
|
38
|
+
pob.tick()
|
|
39
|
+
|
|
40
|
+
beats
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`fire.fire_at` is the scheduled boundary, not the time the callback happened. The one-second gaps show that the cadence held.
|
|
44
|
+
|
|
45
|
+
``` python
|
|
46
|
+
[b-a for a, b in zip(beats, beats[1:])]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`drain` returns the notes a reader has not seen. Each reader has an independent cursor.
|
|
50
|
+
|
|
51
|
+
``` python
|
|
52
|
+
pob.drain('leela').attrgot('body')
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
``` python
|
|
56
|
+
pob.drain('leela'), len(pob.drain('phone'))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Run ordinary Python
|
|
60
|
+
|
|
61
|
+
A callback is a Python function. It can call a library, write a file, or run a command.
|
|
62
|
+
|
|
63
|
+
``` python
|
|
64
|
+
import subprocess
|
|
65
|
+
|
|
66
|
+
pob2 = Pob()
|
|
67
|
+
|
|
68
|
+
@pob2.on('git roundup')
|
|
69
|
+
def roundup(fire):
|
|
70
|
+
out = subprocess.run(['git', 'log', '--oneline', '--since=30 days ago'],
|
|
71
|
+
capture_output=True, text=True).stdout.strip().splitlines()
|
|
72
|
+
return f'{len(out)} commits in the last 30 days'
|
|
73
|
+
|
|
74
|
+
pob2.add('git roundup', cron='0 17 * * *') # 5pm daily
|
|
75
|
+
time.sleep(1.05)
|
|
76
|
+
pob2.tick(at=int(time.time()) + 86400).ran[0].result
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Give a callback a durable list
|
|
80
|
+
|
|
81
|
+
`push` adds an item to a named list. A schedule with `needs` runs only when that list is not empty. Its callback receives the open items as `fire.list`.
|
|
82
|
+
|
|
83
|
+
The callback marks its items used when it returns. If it raises, the items remain open for the retry. A `key` makes repeated open items idempotent.
|
|
84
|
+
|
|
85
|
+
``` python
|
|
86
|
+
pob3 = Pob()
|
|
87
|
+
|
|
88
|
+
@pob3.on('cart')
|
|
89
|
+
def cart(fire):
|
|
90
|
+
return 'added: ' + ', '.join(i.text for i in fire.list)
|
|
91
|
+
|
|
92
|
+
pob3.add('cart', cron='0 20 * * 3', needs='shopping') # 8pm on Wednesdays
|
|
93
|
+
pob3.push('shopping', 'yoghurt, the greek one', key='img_2201.heic')
|
|
94
|
+
pob3.push('shopping', 'yoghurt, the greek one', key='img_2201.heic') # the same photo twice
|
|
95
|
+
pob3.push('shopping', 'oat milk')
|
|
96
|
+
pob3.items('shopping').attrgot('text')
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
``` python
|
|
100
|
+
time.sleep(1.05)
|
|
101
|
+
got = pob3.tick(at=int(time.time()) + 7*86400).ran[0]
|
|
102
|
+
got.status, got.result, got.used
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
When the list is empty, the next fire is `skipped`. It is not a callback failure and produces no note.
|
|
106
|
+
|
|
107
|
+
``` python
|
|
108
|
+
time.sleep(1.05)
|
|
109
|
+
nxt = pob3.tick(at=int(time.time()) + 14*86400).ran[0]
|
|
110
|
+
nxt.status, nxt.why
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Use a model callback
|
|
114
|
+
|
|
115
|
+
A model is another callable dependency. The documentation build does not run this example because it needs a LiteRT model.
|
|
116
|
+
|
|
117
|
+
``` python
|
|
118
|
+
import rishi
|
|
119
|
+
|
|
120
|
+
pob4 = Pob()
|
|
121
|
+
|
|
122
|
+
@pob4.on('news')
|
|
123
|
+
def digest(fire):
|
|
124
|
+
chat = rishi.Chat('litert-community/gemma-4-E2B-it-litert-lm')
|
|
125
|
+
topics = ', '.join(i.text for i in fire.list)
|
|
126
|
+
return str(chat(f'Name one thing worth reading about each of: {topics}. One line each.'))
|
|
127
|
+
|
|
128
|
+
pob4.add('news', cron='0 7 * * *', needs='interests')
|
|
129
|
+
pob4.push('interests', 'sanskrit grammar')
|
|
130
|
+
pob4.push('interests', 'sqlite internals')
|
|
131
|
+
pob4.tick()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Operate schedules
|
|
135
|
+
|
|
136
|
+
Use `pause`, `resume`, `update`, and `drop` to maintain schedules. `update` changes only the fields you pass. `drop` also unregisters the callback in the current process.
|
|
137
|
+
|
|
138
|
+
``` python
|
|
139
|
+
pob4.pause('news')
|
|
140
|
+
pob4.update('news', cron='30 7 * * *', retries=5)
|
|
141
|
+
pob4.resume('news')
|
|
142
|
+
pob4.drop('news')
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
``` python
|
|
146
|
+
# Queue due fires without running callbacks.
|
|
147
|
+
queued = pob.tick(run=False)
|
|
148
|
+
|
|
149
|
+
# Run queued fires separately, with a bounded batch.
|
|
150
|
+
results = pob.work(worker='scheduler', limit=100)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Failures and missed fires
|
|
154
|
+
|
|
155
|
+
A fire retries with exponential backoff when its callback raises. The default attempt budget is three. Set `retries` on [`Pob`](https://vedicreader.github.io/pobblebonk/core.html#pob) or on one schedule. After the final attempt, the fire is dead-lettered and a note records the error.
|
|
156
|
+
|
|
157
|
+
`catchup='once'` keeps the latest fire missed while the machine was off. This is the default. `catchup='all'` keeps every missed fire.
|
|
158
|
+
|
|
159
|
+
Use `tick(run=False)` when scheduling and callback execution belong in separate processes. It queues due fires without running them. `work` claims and runs queued fires.
|
|
160
|
+
|
|
161
|
+
``` python
|
|
162
|
+
pob = Pob('~/.pobblebonk/pob.db', retries=3)
|
|
163
|
+
pob.add('roundup', cron='0 17 * * *', catchup='once', retries=5)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Run the tick
|
|
167
|
+
|
|
168
|
+
The process that calls `tick` must register the callbacks first. A small script is enough.
|
|
169
|
+
|
|
170
|
+
``` python
|
|
171
|
+
# tick.py
|
|
172
|
+
from pobblebonk.core import Pob
|
|
173
|
+
|
|
174
|
+
pob = Pob('~/.pobblebonk/pob.db')
|
|
175
|
+
|
|
176
|
+
@pob.on('cart')
|
|
177
|
+
def cart(fire):
|
|
178
|
+
return 'added: ' + ', '.join(item.text for item in fire.list)
|
|
179
|
+
|
|
180
|
+
if __name__ == '__main__':
|
|
181
|
+
print(pob.tick())
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Run it once a minute. A tick with nothing due is one SQL call.
|
|
185
|
+
|
|
186
|
+
``` cron
|
|
187
|
+
* * * * * cd ~/myapp && uv run python tick.py
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use a launchd `StartInterval` of 60 on macOS or a systemd timer with `OnCalendar=minutely` on Linux.
|
|
191
|
+
|
|
192
|
+
## Share an existing database
|
|
193
|
+
|
|
194
|
+
Pass an open honker database to keep pobblebonk data beside another application.
|
|
195
|
+
|
|
196
|
+
``` python
|
|
197
|
+
import honker
|
|
198
|
+
|
|
199
|
+
db = honker.open('app.db')
|
|
200
|
+
pob = Pob(db=db)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Develop
|
|
204
|
+
|
|
205
|
+
``` sh
|
|
206
|
+
uv sync --group dev
|
|
207
|
+
uv run nbdev-export
|
|
208
|
+
uv run nbdev-test
|
|
209
|
+
uv run nbdev-clean
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`nbs/00_core.ipynb` contains the implementation and tests. `nbs/index.ipynb` generates this README.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Autogenerated by nbdev
|
|
2
|
+
|
|
3
|
+
d = { 'settings': { 'branch': 'main',
|
|
4
|
+
'doc_baseurl': '/pobblebonk',
|
|
5
|
+
'doc_host': 'https://vedicreader.github.io',
|
|
6
|
+
'git_url': 'https://github.com/vedicreader/pobblebonk',
|
|
7
|
+
'lib_path': 'pobblebonk'},
|
|
8
|
+
'syms': { 'pobblebonk.core': { 'pobblebonk.core.Pob': ('core.html#pob', 'pobblebonk/core.py'),
|
|
9
|
+
'pobblebonk.core.Pob.__init__': ('core.html#pob.__init__', 'pobblebonk/core.py'),
|
|
10
|
+
'pobblebonk.core.Pob.__repr__': ('core.html#pob.__repr__', 'pobblebonk/core.py'),
|
|
11
|
+
'pobblebonk.core.Pob._done': ('core.html#pob._done', 'pobblebonk/core.py'),
|
|
12
|
+
'pobblebonk.core.Pob._failed': ('core.html#pob._failed', 'pobblebonk/core.py'),
|
|
13
|
+
'pobblebonk.core.Pob._fire': ('core.html#pob._fire', 'pobblebonk/core.py'),
|
|
14
|
+
'pobblebonk.core.Pob._row': ('core.html#pob._row', 'pobblebonk/core.py'),
|
|
15
|
+
'pobblebonk.core.Pob._run': ('core.html#pob._run', 'pobblebonk/core.py'),
|
|
16
|
+
'pobblebonk.core.Pob.add': ('core.html#pob.add', 'pobblebonk/core.py'),
|
|
17
|
+
'pobblebonk.core.Pob.all': ('core.html#pob.all', 'pobblebonk/core.py'),
|
|
18
|
+
'pobblebonk.core.Pob.drain': ('core.html#pob.drain', 'pobblebonk/core.py'),
|
|
19
|
+
'pobblebonk.core.Pob.drop': ('core.html#pob.drop', 'pobblebonk/core.py'),
|
|
20
|
+
'pobblebonk.core.Pob.due': ('core.html#pob.due', 'pobblebonk/core.py'),
|
|
21
|
+
'pobblebonk.core.Pob.get': ('core.html#pob.get', 'pobblebonk/core.py'),
|
|
22
|
+
'pobblebonk.core.Pob.items': ('core.html#pob.items', 'pobblebonk/core.py'),
|
|
23
|
+
'pobblebonk.core.Pob.note': ('core.html#pob.note', 'pobblebonk/core.py'),
|
|
24
|
+
'pobblebonk.core.Pob.notes': ('core.html#pob.notes', 'pobblebonk/core.py'),
|
|
25
|
+
'pobblebonk.core.Pob.on': ('core.html#pob.on', 'pobblebonk/core.py'),
|
|
26
|
+
'pobblebonk.core.Pob.pause': ('core.html#pob.pause', 'pobblebonk/core.py'),
|
|
27
|
+
'pobblebonk.core.Pob.push': ('core.html#pob.push', 'pobblebonk/core.py'),
|
|
28
|
+
'pobblebonk.core.Pob.resume': ('core.html#pob.resume', 'pobblebonk/core.py'),
|
|
29
|
+
'pobblebonk.core.Pob.tick': ('core.html#pob.tick', 'pobblebonk/core.py'),
|
|
30
|
+
'pobblebonk.core.Pob.update': ('core.html#pob.update', 'pobblebonk/core.py'),
|
|
31
|
+
'pobblebonk.core.Pob.used': ('core.html#pob.used', 'pobblebonk/core.py'),
|
|
32
|
+
'pobblebonk.core.Pob.work': ('core.html#pob.work', 'pobblebonk/core.py'),
|
|
33
|
+
'pobblebonk.core._note': ('core.html#_note', 'pobblebonk/core.py'),
|
|
34
|
+
'pobblebonk.core.backoff': ('core.html#backoff', 'pobblebonk/core.py'),
|
|
35
|
+
'pobblebonk.core.clip': ('core.html#clip', 'pobblebonk/core.py'),
|
|
36
|
+
'pobblebonk.core.secs': ('core.html#secs', 'pobblebonk/core.py'),
|
|
37
|
+
'pobblebonk.core.superseded': ('core.html#superseded', 'pobblebonk/core.py')}}}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""schedules, the lists they read, and the notes they leave
|
|
2
|
+
|
|
3
|
+
Docs: https://vedicreader.github.io/pobblebonk/core.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['FIRES', 'NOTES', 'RETRIES', 'BASE', 'MAX_TEXT', 'CATCHUP', 'secs', 'backoff', 'clip', 'Pob', 'superseded']
|
|
9
|
+
|
|
10
|
+
# %% ../nbs/00_core.ipynb #8b4162b7
|
|
11
|
+
import json, re, time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from tempfile import mkdtemp
|
|
14
|
+
|
|
15
|
+
import honker
|
|
16
|
+
from fastcore.all import AttrDict, L, first, ifnone, patch, dict2obj
|
|
17
|
+
from honker import crontab, every_s
|
|
18
|
+
|
|
19
|
+
# %% ../nbs/00_core.ipynb #2dc2cc67
|
|
20
|
+
_DUR = re.compile(r'([\d.]+)\s*([smhdwMy])', re.I)
|
|
21
|
+
_MULT = dict(s=1, m=60, h=3600, d=86400, w=604800, M=2629800, y=31557600)
|
|
22
|
+
|
|
23
|
+
# %% ../nbs/00_core.ipynb #a807caa9
|
|
24
|
+
def secs(every) -> int:
|
|
25
|
+
"Whole seconds from `'30m'`, `'6h'`, `'1w'`, `'1h30m'`, or a number."
|
|
26
|
+
try: return int(float(every))
|
|
27
|
+
except (TypeError, ValueError): pass
|
|
28
|
+
if not (ms := _DUR.findall(str(every))): raise ValueError(f'not a duration: {every!r}')
|
|
29
|
+
return int(sum(float(n) * _MULT[u.lower()] for n, u in ms))
|
|
30
|
+
|
|
31
|
+
# %% ../nbs/00_core.ipynb #57b9ddaf
|
|
32
|
+
FIRES = 'pob.fires' # the honker queue every schedule fires into
|
|
33
|
+
NOTES = 'pob.notes' # the honker stream a person reads
|
|
34
|
+
RETRIES = 5 # attempts a fire gets before it is dead-lettered
|
|
35
|
+
BASE = 30 # seconds before the first retry; doubles per attempt
|
|
36
|
+
|
|
37
|
+
# %% ../nbs/00_core.ipynb #6ecca23b
|
|
38
|
+
def backoff(attempt:int, # failures so far; 1 on the first
|
|
39
|
+
base:float=BASE, # the first delay, in seconds
|
|
40
|
+
cap:float=3600, # longest a retry ever waits
|
|
41
|
+
) -> float:
|
|
42
|
+
'Doubling delay: 30s, 60s, 120s, 240s, capped. A bot wall clears on its own or it does not.'
|
|
43
|
+
return min(base * 2 ** max(0, attempt - 1), cap)
|
|
44
|
+
|
|
45
|
+
# %% ../nbs/00_core.ipynb #755f5560
|
|
46
|
+
MAX_TEXT = 20_000 # chars kept of a note body; an answer is a note, not a document
|
|
47
|
+
def clip(s, mx:int=MAX_TEXT) -> str:
|
|
48
|
+
'Text short enough to store, saying what it dropped rather than losing it quietly.'
|
|
49
|
+
s = '' if s is None else str(s)
|
|
50
|
+
return s if len(s) <= mx else s[:mx] + f'\n... [{len(s)-mx} more chars]'
|
|
51
|
+
|
|
52
|
+
# %% ../nbs/00_core.ipynb #25758930
|
|
53
|
+
class Pob:
|
|
54
|
+
"A honker database, the one table honker has not got, and the callbacks a fire runs."
|
|
55
|
+
def __init__(self,
|
|
56
|
+
path=None, # the file; None makes a temporary one
|
|
57
|
+
db=None, # or a honker database something else already opened
|
|
58
|
+
retries:int=RETRIES,# attempts a failing fire gets before it is dead-lettered
|
|
59
|
+
base:float=BASE): # seconds before the first retry; doubles per attempt
|
|
60
|
+
# honker watches `PRAGMA data_version` on a file, so `:memory:` is not an option
|
|
61
|
+
if db is None and path is None: path = Path(mkdtemp())/'pob.db'
|
|
62
|
+
self.db = db if db is not None else honker.open(str(path))
|
|
63
|
+
self.retries, self.base = max(1, int(retries)), float(base)
|
|
64
|
+
self.q, self.stream = self.db.queue(FIRES), self.db.stream(NOTES)
|
|
65
|
+
self.sched, self.runners = honker.Scheduler(self.db), {}
|
|
66
|
+
with self.db.transaction() as tx:
|
|
67
|
+
tx.execute('CREATE TABLE IF NOT EXISTS pob_items (id INTEGER PRIMARY KEY, topic TEXT, '
|
|
68
|
+
'text TEXT, key TEXT, at REAL, used_at REAL, used_by TEXT)')
|
|
69
|
+
tx.execute('CREATE INDEX IF NOT EXISTS pob_items_open ON pob_items(topic, at) '
|
|
70
|
+
'WHERE used_at IS NULL')
|
|
71
|
+
tx.execute('CREATE UNIQUE INDEX IF NOT EXISTS pob_items_key ON pob_items(topic, key) '
|
|
72
|
+
'WHERE key IS NOT NULL AND used_at IS NULL')
|
|
73
|
+
|
|
74
|
+
# %% ../nbs/00_core.ipynb #4a1865e6
|
|
75
|
+
@patch
|
|
76
|
+
def note(self:Pob, title:str, body:str='', **meta):
|
|
77
|
+
'Leave one note.'
|
|
78
|
+
self.stream.publish(dict(title=str(title), body=clip(body), **meta))
|
|
79
|
+
|
|
80
|
+
def _note(r): return AttrDict(json.loads(r['payload']), offset=r['offset'])
|
|
81
|
+
@patch
|
|
82
|
+
def notes(self:Pob, limit:int=50) -> L:
|
|
83
|
+
'Every note, newest first, whatever any reader has seen.'
|
|
84
|
+
return L(self.db.query('SELECT offset, payload FROM _honker_stream WHERE topic=? '
|
|
85
|
+
'ORDER BY offset DESC LIMIT ?', [NOTES, limit])).map(_note)
|
|
86
|
+
|
|
87
|
+
@patch
|
|
88
|
+
def drain(self:Pob, reader:str, limit:int=50) -> L:
|
|
89
|
+
'Every note this reader has not had. What a turn calls before it answers you.'
|
|
90
|
+
got = L(self.db.query('SELECT offset, payload FROM _honker_stream WHERE topic=? AND offset>? '
|
|
91
|
+
'ORDER BY offset LIMIT ?',
|
|
92
|
+
[NOTES, self.stream.get_offset(str(reader)), limit]))
|
|
93
|
+
if got: self.stream.save_offset(str(reader), got[-1]['offset'])
|
|
94
|
+
return got.map(_note)
|
|
95
|
+
|
|
96
|
+
# %% ../nbs/00_core.ipynb #0396b45d138a
|
|
97
|
+
@patch
|
|
98
|
+
def due(self:Pob) -> int:
|
|
99
|
+
'Fires queued and not yet run.'
|
|
100
|
+
return self.db.query("SELECT count(*) n FROM _honker_live WHERE queue=? AND state='pending' AND run_at <= unixepoch()",
|
|
101
|
+
[FIRES])[0]['n']
|
|
102
|
+
@patch
|
|
103
|
+
def all(self:Pob) -> L:
|
|
104
|
+
'Every schedule, with its payload parsed.'
|
|
105
|
+
return L(self.sched.list()).map(lambda r: AttrDict(r, payload=json.loads(r['payload'] or 'null')))
|
|
106
|
+
|
|
107
|
+
@patch
|
|
108
|
+
def __repr__(self:Pob):
|
|
109
|
+
return f'Pob({len(self.all())} schedules, {self.due()} due, {len(self.notes())} notes)'
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# %% ../nbs/00_core.ipynb #21d054f1
|
|
113
|
+
CATCHUP = ('once', 'all') # what to do with the fires missed while the machine was off
|
|
114
|
+
@patch
|
|
115
|
+
def add(self:Pob,
|
|
116
|
+
name:str, # what you call it; unique, and what a note is titled with
|
|
117
|
+
cron:str=None, # a five-field cron expression
|
|
118
|
+
every=None, # or a duration: `'30m'`, `'1w'`, or seconds
|
|
119
|
+
needs:str=None, # a list topic this cannot run without
|
|
120
|
+
catchup:str='once', # `once` runs the latest missed fire, `all` runs every one
|
|
121
|
+
retries:int=None, # attempts a failing fire gets; None -> the Pob's own
|
|
122
|
+
**payload # anything the callback wants
|
|
123
|
+
) -> AttrDict:
|
|
124
|
+
'Write down a standing instruction.'
|
|
125
|
+
if bool(cron) == bool(every): raise ValueError('a schedule needs one of `cron` or `every`')
|
|
126
|
+
if catchup not in CATCHUP: raise ValueError(f'catchup must be one of {CATCHUP}: {catchup!r}')
|
|
127
|
+
self.sched.add(name=str(name), queue=FIRES, payload=dict(
|
|
128
|
+
schedule=str(name), needs=needs, catchup=catchup, **payload),
|
|
129
|
+
schedule=crontab(cron) if cron else every_s(secs(every)),
|
|
130
|
+
max_attempts=max(1, int(ifnone(retries, self.retries))))
|
|
131
|
+
return self.get(name)
|
|
132
|
+
|
|
133
|
+
@patch
|
|
134
|
+
def on(self:Pob, name:str):
|
|
135
|
+
'Decorator naming the callback a fire of `name` runs.'
|
|
136
|
+
def _f(fn):
|
|
137
|
+
self.runners[str(name)] = fn
|
|
138
|
+
return fn
|
|
139
|
+
return _f
|
|
140
|
+
|
|
141
|
+
@patch
|
|
142
|
+
def get(self:Pob, name:str) -> AttrDict: return first(r for r in self.all() if r['name'] == str(name))
|
|
143
|
+
|
|
144
|
+
@patch
|
|
145
|
+
def pause(self:Pob, name:str) -> bool: return self.sched.pause(str(name))
|
|
146
|
+
|
|
147
|
+
@patch
|
|
148
|
+
def resume(self:Pob, name:str) -> bool: return self.sched.resume(str(name))
|
|
149
|
+
|
|
150
|
+
@patch
|
|
151
|
+
def drop(self:Pob, name:str) -> bool:
|
|
152
|
+
self.runners.pop(str(name), None)
|
|
153
|
+
return self.sched.remove(str(name))
|
|
154
|
+
|
|
155
|
+
@patch
|
|
156
|
+
def update(self:Pob,
|
|
157
|
+
name:str, # schedule to change
|
|
158
|
+
cron:str=None, # new cron expression
|
|
159
|
+
every=None, # or a new duration
|
|
160
|
+
priority:int=None, # new queue priority
|
|
161
|
+
retries:int=None, # new attempt budget
|
|
162
|
+
**payload # payload fields to add or replace
|
|
163
|
+
) -> AttrDict:
|
|
164
|
+
'Change an existing schedule without replacing fields that are not given.'
|
|
165
|
+
if cron is not None and every is not None: raise ValueError('a schedule update accepts only one of `cron` or `every`')
|
|
166
|
+
if 'catchup' in payload and payload['catchup'] not in CATCHUP:
|
|
167
|
+
raise ValueError(f'catchup must be one of {CATCHUP}: {payload["catchup"]!r}')
|
|
168
|
+
if (was := self.get(name)) is None: return None
|
|
169
|
+
changes = {}
|
|
170
|
+
if cron is not None: changes['schedule'] = crontab(cron)
|
|
171
|
+
elif every is not None: changes['schedule'] = every_s(secs(every))
|
|
172
|
+
if priority is not None: changes['priority'] = int(priority)
|
|
173
|
+
if retries is not None: changes['max_attempts'] = max(1, int(retries))
|
|
174
|
+
if payload: changes['payload'] = {**(was.payload or {}), **payload, 'schedule': str(name)}
|
|
175
|
+
self.sched.update(str(name), **changes)
|
|
176
|
+
return self.get(name)
|
|
177
|
+
|
|
178
|
+
# %% ../nbs/00_core.ipynb #1014ad54
|
|
179
|
+
@patch
|
|
180
|
+
def push(self:Pob,
|
|
181
|
+
topic:str, # which list
|
|
182
|
+
text:str, # the line the callback reads
|
|
183
|
+
key:str=None, # send the same thing twice and it stays one item
|
|
184
|
+
at:float=None
|
|
185
|
+
) -> AttrDict:
|
|
186
|
+
'Add one item, or give back the open one that `key` already names.'
|
|
187
|
+
at = ifnone(at, time.time())
|
|
188
|
+
with self.db.transaction() as tx:
|
|
189
|
+
if key is not None and (was := tx.query(
|
|
190
|
+
'SELECT * FROM pob_items WHERE topic=? AND key=? AND used_at IS NULL',
|
|
191
|
+
[str(topic), key])): return AttrDict(was[0])
|
|
192
|
+
got = tx.query('INSERT INTO pob_items (topic, text, key, at) VALUES (?,?,?,?) RETURNING *',
|
|
193
|
+
[str(topic), clip(text), key, at])
|
|
194
|
+
return AttrDict(got[0])
|
|
195
|
+
|
|
196
|
+
@patch
|
|
197
|
+
def items(self:Pob, topic:str, limit:int=200) -> L:
|
|
198
|
+
'The open items on `topic`, oldest first.'
|
|
199
|
+
return L(self.db.query('SELECT * FROM pob_items WHERE topic=? AND used_at IS NULL '
|
|
200
|
+
'ORDER BY at LIMIT ?', [str(topic), limit])).map(AttrDict)
|
|
201
|
+
|
|
202
|
+
# %% ../nbs/00_core.ipynb #f156da8b
|
|
203
|
+
@patch
|
|
204
|
+
def used(self:Pob, ids, by:str='', at:float=None) -> int:
|
|
205
|
+
'Mark items done, once the work that consumed them succeeded.'
|
|
206
|
+
if not (ids := L(ids)): return 0
|
|
207
|
+
qs = ','.join('?'*len(ids))
|
|
208
|
+
with self.db.transaction() as tx:
|
|
209
|
+
return len(tx.query(f'UPDATE pob_items SET used_at=?, used_by=? WHERE id IN ({qs}) '
|
|
210
|
+
'AND used_at IS NULL RETURNING id',
|
|
211
|
+
[ifnone(at, time.time()), str(by), *ids]))
|
|
212
|
+
|
|
213
|
+
# %% ../nbs/00_core.ipynb #708046dc
|
|
214
|
+
def superseded(fires, keep_latest) -> set:
|
|
215
|
+
'Older job IDs for schedules that keep only their latest missed fire.'
|
|
216
|
+
fires, keep_latest = L(fires), set(keep_latest)
|
|
217
|
+
return {
|
|
218
|
+
f['job_id']
|
|
219
|
+
for name in {f['name'] for f in fires} & keep_latest
|
|
220
|
+
for f in fires.filter(lambda f: f['name'] == name)[:-1]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# %% ../nbs/00_core.ipynb #b5e1bce7
|
|
224
|
+
@patch
|
|
225
|
+
def tick(
|
|
226
|
+
self:Pob,
|
|
227
|
+
at:float=None, # when this tick is happening; now when unset
|
|
228
|
+
worker:str='tick', # the lease holder recorded on each job
|
|
229
|
+
run:bool=True, # False queues the fires and runs none of them
|
|
230
|
+
) -> AttrDict:
|
|
231
|
+
'Create due fires, apply catch-up policy, then run the remaining jobs.'
|
|
232
|
+
at = int(ifnone(at, time.time()))
|
|
233
|
+
with self.db.transaction() as tx: fires = L(json.loads(tx.query('SELECT honker_scheduler_tick(?) AS f',[at])[0]['f']))
|
|
234
|
+
keep_latest = {schedule.name for schedule in self.all() if schedule.payload.get('catchup', 'once') == 'once'}
|
|
235
|
+
dropped = {job_id for job_id in superseded(fires, keep_latest) if self.q.cancel(job_id)}
|
|
236
|
+
kept = fires.filter(lambda fire: fire['job_id'] not in dropped)
|
|
237
|
+
fire_times = {fire['job_id']: fire['fire_at'] for fire in kept}
|
|
238
|
+
return dict2obj(dict(fired=kept,cancelled=len(dropped),ran=self.work(worker, at, when=fire_times) if run else L()))
|
|
239
|
+
|
|
240
|
+
# %% ../nbs/00_core.ipynb #65b5766a
|
|
241
|
+
@patch
|
|
242
|
+
def _row(self:Pob, job) -> AttrDict:
|
|
243
|
+
'fetch specific honker job'
|
|
244
|
+
return AttrDict(first(self.db.query('SELECT * FROM _honker_live WHERE id=?', [job.id])) or {})
|
|
245
|
+
|
|
246
|
+
# %% ../nbs/00_core.ipynb #9e7d9518
|
|
247
|
+
@patch
|
|
248
|
+
def _failed(self:Pob, job, row, name:str, exc:Exception) -> AttrDict:
|
|
249
|
+
'Retry a failed callback while attempts remain, else dead-letter it.'
|
|
250
|
+
err = f'{type(exc).__name__}: {exc}'
|
|
251
|
+
left = max(0, int(row.get('max_attempts') or job.max_attempts or 1) - job.attempts)
|
|
252
|
+
if left:
|
|
253
|
+
delay = backoff(job.attempts, self.base)
|
|
254
|
+
if job.retry(int(delay),err): return AttrDict(name=name,status='retry',error=err,attempt=job.attempts,attempts_left=left,after=delay)
|
|
255
|
+
job.fail(err)
|
|
256
|
+
self.note(name, f'gave up after {job.attempts} tries. {err}', ref=job.id, dead=True)
|
|
257
|
+
return AttrDict(name=name, status='error', error=err, attempt=job.attempts)
|
|
258
|
+
|
|
259
|
+
# %% ../nbs/00_core.ipynb #83ba6c2c
|
|
260
|
+
@patch
|
|
261
|
+
def _done(self:Pob, job, name:str, got, res) -> AttrDict:
|
|
262
|
+
'Store a successful callback result and finish its fire.'
|
|
263
|
+
self.q.save_result(job.id, res)
|
|
264
|
+
if got: self.used(got.attrgot('id'), by=name)
|
|
265
|
+
body = res if isinstance(res, str) else json.dumps(res, default=str)
|
|
266
|
+
self.note(name, body, ref=job.id, used=len(got))
|
|
267
|
+
job.ack()
|
|
268
|
+
return AttrDict(name=name, status='ok', result=res, used=len(got))
|
|
269
|
+
|
|
270
|
+
# %% ../nbs/00_core.ipynb #b034d25c
|
|
271
|
+
@patch
|
|
272
|
+
def _fire(self:Pob, job, at:int, fire_at:int=None):
|
|
273
|
+
'Build the value passed to a fire callback.'
|
|
274
|
+
p, row = AttrDict(job.payload or {}), self._row(job)
|
|
275
|
+
got = self.items(p.needs) if p.get('needs') else L()
|
|
276
|
+
fire_at = ifnone(fire_at, row.get('run_at') or at)
|
|
277
|
+
return AttrDict(p, at=at, fire_at=fire_at, list=got, job=job.id), row
|
|
278
|
+
|
|
279
|
+
# %% ../nbs/00_core.ipynb #df9981a3
|
|
280
|
+
@patch
|
|
281
|
+
def _run(self:Pob, job, at:int, fire_at:int=None) -> AttrDict:
|
|
282
|
+
'Run one claimed fire.'
|
|
283
|
+
p = AttrDict(job.payload or {})
|
|
284
|
+
name = p.get('schedule') or ''
|
|
285
|
+
if (fn := self.runners.get(name)) is None:
|
|
286
|
+
job.fail(err := f'no callback registered for {name!r}')
|
|
287
|
+
self.note(name, err, ref=job.id, dead=True)
|
|
288
|
+
return AttrDict(name=name, status='error', error=err)
|
|
289
|
+
fire, row = self._fire(job, at, fire_at)
|
|
290
|
+
got = fire.list
|
|
291
|
+
if fire.get('needs') and not got:
|
|
292
|
+
job.ack()
|
|
293
|
+
return AttrDict(name=name, status='skipped', why=f'the {fire.needs} list is empty')
|
|
294
|
+
try: res = fn(fire)
|
|
295
|
+
except Exception as e: return self._failed(job, row, name, e)
|
|
296
|
+
return self._done(job, name, got, res)
|
|
297
|
+
|
|
298
|
+
# %% ../nbs/00_core.ipynb #f8d9c278
|
|
299
|
+
@patch
|
|
300
|
+
def work(self:Pob, worker:str='tick', at:float=None, limit:int=100, when:dict=None) -> L:
|
|
301
|
+
'Claim and run every fire that is due.'
|
|
302
|
+
at, when, out = int(ifnone(at, time.time())), when or {}, L()
|
|
303
|
+
while len(out) < limit:
|
|
304
|
+
if (job := self.q.claim_one(worker)) is None: break
|
|
305
|
+
out.append(self._run(job, at, when.get(job.id)))
|
|
306
|
+
return out
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pobblebonk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "the clock and the notebook for an agent: schedules, lists and notes on honker, in one file"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
authors = [{name = "Karthik", email = "karthik.rajgopal@hotmail.com"}]
|
|
13
|
+
keywords = ['nbdev', 'sqlite', 'honker', 'cron', 'scheduler', 'reminders', 'agents', 'acp']
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"honker>=0.5.0",
|
|
26
|
+
"fastcore>=2.2.14",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.entry-points.nbdev]
|
|
30
|
+
pobblebonk = "pobblebonk._modidx:d"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/vedicreader/pobblebonk"
|
|
34
|
+
Documentation = "https://vedicreader.github.io/pobblebonk/"
|
|
35
|
+
|
|
36
|
+
[tool.nbdev]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["pobblebonk"]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
include = ["/pobblebonk", "/README.md", "/pyproject.toml"]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.version]
|
|
45
|
+
path = "pobblebonk/__init__.py"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"nbdev>=3.3.12",
|
|
50
|
+
"notebook>=7.6.1",
|
|
51
|
+
"slopometer>=0.0.1",
|
|
52
|
+
]
|