celery-bittensor 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- celery_bittensor/__init__.py +9 -0
- celery_bittensor/client.py +106 -0
- celery_bittensor/schedules.py +185 -0
- celery_bittensor/version.py +1 -0
- celery_bittensor-0.1.dist-info/METADATA +53 -0
- celery_bittensor-0.1.dist-info/RECORD +8 -0
- celery_bittensor-0.1.dist-info/WHEEL +4 -0
- celery_bittensor-0.1.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Block(NamedTuple):
|
|
12
|
+
number: int
|
|
13
|
+
timestamp: datetime.datetime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BittensorSynced:
|
|
17
|
+
RETRY = datetime.timedelta(seconds=5)
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
network: str,
|
|
22
|
+
finalized: bool,
|
|
23
|
+
):
|
|
24
|
+
self.network = network
|
|
25
|
+
self.finalized = finalized
|
|
26
|
+
|
|
27
|
+
self.block: Block | None = None
|
|
28
|
+
self.block_time: float | None = None
|
|
29
|
+
|
|
30
|
+
self._thread = threading.Thread(
|
|
31
|
+
daemon=True,
|
|
32
|
+
name=f"BittensorSynced[{self.network}]",
|
|
33
|
+
target=self._run,
|
|
34
|
+
)
|
|
35
|
+
self._stop = threading.Event()
|
|
36
|
+
|
|
37
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
38
|
+
self._task: asyncio.Task | None = None
|
|
39
|
+
|
|
40
|
+
def start(self) -> None:
|
|
41
|
+
self._thread.start()
|
|
42
|
+
|
|
43
|
+
def stop(self) -> None:
|
|
44
|
+
self._stop.set()
|
|
45
|
+
|
|
46
|
+
loop, task = self._loop, self._task
|
|
47
|
+
|
|
48
|
+
if loop and task:
|
|
49
|
+
with contextlib.suppress(RuntimeError):
|
|
50
|
+
loop.call_soon_threadsafe(task.cancel)
|
|
51
|
+
|
|
52
|
+
def join(self) -> None:
|
|
53
|
+
self._thread.join()
|
|
54
|
+
|
|
55
|
+
def _run(self) -> None:
|
|
56
|
+
try:
|
|
57
|
+
asyncio.run(self._main())
|
|
58
|
+
except asyncio.CancelledError:
|
|
59
|
+
pass
|
|
60
|
+
finally:
|
|
61
|
+
self._loop = None
|
|
62
|
+
self._task = None
|
|
63
|
+
|
|
64
|
+
async def _main(self) -> None:
|
|
65
|
+
self._loop = asyncio.get_running_loop()
|
|
66
|
+
self._task = asyncio.current_task()
|
|
67
|
+
|
|
68
|
+
if self._stop.is_set():
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
while True:
|
|
72
|
+
try:
|
|
73
|
+
await self._blocks_generator()
|
|
74
|
+
except Exception:
|
|
75
|
+
self.block = None
|
|
76
|
+
|
|
77
|
+
logger.exception(
|
|
78
|
+
"Error occured during processing on %s. Reconnecting in %ds",
|
|
79
|
+
self.network,
|
|
80
|
+
self.RETRY.total_seconds(),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
await asyncio.sleep(self.RETRY.total_seconds())
|
|
84
|
+
|
|
85
|
+
async def _blocks_generator(self) -> None:
|
|
86
|
+
import bittensor
|
|
87
|
+
|
|
88
|
+
logger.info("Connecting to %s", self.network)
|
|
89
|
+
|
|
90
|
+
async with bittensor.Subtensor(
|
|
91
|
+
network=self.network,
|
|
92
|
+
retry_forever=True,
|
|
93
|
+
) as client:
|
|
94
|
+
logger.info("Connected to %s. Checking block time", self.network)
|
|
95
|
+
|
|
96
|
+
self.block_time = await client.block_time()
|
|
97
|
+
|
|
98
|
+
logger.info("Awaiting for blocks on %s", self.network)
|
|
99
|
+
|
|
100
|
+
async for block in client.blocks(finalized=self.finalized):
|
|
101
|
+
logger.info("New block received on %s: %d", self.network, block.number)
|
|
102
|
+
|
|
103
|
+
self.block = Block(
|
|
104
|
+
block.number,
|
|
105
|
+
datetime.datetime.now(datetime.timezone.utc),
|
|
106
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import datetime
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
import celery.schedules
|
|
8
|
+
|
|
9
|
+
from .client import BittensorSynced, Block
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseBittensorSchedule(celery.schedules.BaseSchedule):
|
|
13
|
+
INIT_PULL = datetime.timedelta(microseconds=100)
|
|
14
|
+
|
|
15
|
+
_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
16
|
+
_clients: ClassVar[dict[tuple[str, bool], BittensorSynced]] = {}
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
network: str = "finney",
|
|
21
|
+
finalized: bool = True,
|
|
22
|
+
nowfun: Callable | None = None,
|
|
23
|
+
app: celery.Celery | None = None,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(nowfun, app)
|
|
26
|
+
|
|
27
|
+
self.network = network
|
|
28
|
+
self.finalized = finalized
|
|
29
|
+
self.last_run_at = Block(0, self.maybe_make_aware(datetime.datetime.min))
|
|
30
|
+
|
|
31
|
+
self._bittensor: BittensorSynced | None = None
|
|
32
|
+
|
|
33
|
+
def __eq__(self, other: object) -> bool:
|
|
34
|
+
if isinstance(other, BaseBittensorSchedule):
|
|
35
|
+
return (
|
|
36
|
+
self.network == other.network
|
|
37
|
+
and self.finalized == other.finalized
|
|
38
|
+
and super().__eq__(other)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return NotImplemented
|
|
42
|
+
|
|
43
|
+
def __getstate__(self) -> dict:
|
|
44
|
+
return {
|
|
45
|
+
"finalized": self.finalized,
|
|
46
|
+
"last_run_at": self.last_run_at,
|
|
47
|
+
"network": self.network,
|
|
48
|
+
"nowfun": self.nowfun,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def __setstate__(self, state: dict) -> None:
|
|
52
|
+
self.__dict__.update(state)
|
|
53
|
+
self._app = None
|
|
54
|
+
self._bittensor = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def bittensor(self) -> BittensorSynced:
|
|
58
|
+
if not self._bittensor:
|
|
59
|
+
with BaseBittensorSchedule._lock:
|
|
60
|
+
if (self.network, self.finalized) not in BaseBittensorSchedule._clients:
|
|
61
|
+
BaseBittensorSchedule._clients[(self.network, self.finalized)] = (
|
|
62
|
+
BittensorSynced(
|
|
63
|
+
network=self.network,
|
|
64
|
+
finalized=self.finalized,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
BaseBittensorSchedule._clients[
|
|
68
|
+
(self.network, self.finalized)
|
|
69
|
+
].start()
|
|
70
|
+
|
|
71
|
+
self._bittensor = BaseBittensorSchedule._clients[
|
|
72
|
+
(self.network, self.finalized)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
return self._bittensor
|
|
76
|
+
|
|
77
|
+
def is_due(self, last_run_at: datetime.datetime) -> celery.schedules.schedstate:
|
|
78
|
+
if not self.bittensor.block_time:
|
|
79
|
+
return celery.schedules.schedstate(
|
|
80
|
+
is_due=False, next=self.INIT_PULL.total_seconds()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
block = self.bittensor.block
|
|
84
|
+
|
|
85
|
+
if not block:
|
|
86
|
+
return celery.schedules.schedstate(
|
|
87
|
+
is_due=False, next=self.INIT_PULL.total_seconds()
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if self.last_run_at.number == 0:
|
|
91
|
+
self.last_run_at = block
|
|
92
|
+
|
|
93
|
+
is_due = self._prev_block(block.number) > self.last_run_at.number
|
|
94
|
+
|
|
95
|
+
if is_due:
|
|
96
|
+
self.last_run_at = block
|
|
97
|
+
|
|
98
|
+
return celery.schedules.schedstate(
|
|
99
|
+
is_due=is_due,
|
|
100
|
+
next=self._time_until_next(block).total_seconds(),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _next_block(self, block: int) -> int:
|
|
104
|
+
raise NotImplementedError
|
|
105
|
+
|
|
106
|
+
def _prev_block(self, block: int) -> int:
|
|
107
|
+
raise NotImplementedError
|
|
108
|
+
|
|
109
|
+
def _time_until_next(self, block: Block) -> datetime.timedelta:
|
|
110
|
+
assert self.bittensor.block_time is not None
|
|
111
|
+
|
|
112
|
+
target = self._next_block(block.number)
|
|
113
|
+
elapsed = self.maybe_make_aware(self.now()) - block.timestamp
|
|
114
|
+
remaining = (target - block.number) * datetime.timedelta(
|
|
115
|
+
seconds=self.bittensor.block_time
|
|
116
|
+
) - elapsed
|
|
117
|
+
|
|
118
|
+
return max(remaining, self.INIT_PULL)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class schedule(BaseBittensorSchedule):
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
run_every: int,
|
|
125
|
+
relative: bool = False,
|
|
126
|
+
network: str = "finney",
|
|
127
|
+
finalized: bool = True,
|
|
128
|
+
nowfun: Callable | None = None,
|
|
129
|
+
app: celery.Celery | None = None,
|
|
130
|
+
):
|
|
131
|
+
if not isinstance(run_every, int):
|
|
132
|
+
raise TypeError("run_every must be an integer")
|
|
133
|
+
|
|
134
|
+
if run_every <= 0:
|
|
135
|
+
raise ValueError("run_every must be a positive integer")
|
|
136
|
+
|
|
137
|
+
super().__init__(
|
|
138
|
+
network,
|
|
139
|
+
finalized,
|
|
140
|
+
nowfun,
|
|
141
|
+
app,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
self.run_every = run_every
|
|
145
|
+
self.relative = relative
|
|
146
|
+
|
|
147
|
+
def __eq__(self, other: object) -> bool:
|
|
148
|
+
if isinstance(other, schedule):
|
|
149
|
+
return (
|
|
150
|
+
self.run_every == other.run_every
|
|
151
|
+
and self.relative == other.relative
|
|
152
|
+
and super().__eq__(other)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return self.run_every == other
|
|
156
|
+
|
|
157
|
+
def __getstate__(self) -> dict:
|
|
158
|
+
state = super().__getstate__()
|
|
159
|
+
state.update({
|
|
160
|
+
"run_every": self.run_every,
|
|
161
|
+
"relative": self.relative,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
return state
|
|
165
|
+
|
|
166
|
+
def _next_block(self, block: int) -> int:
|
|
167
|
+
return self._prev_block(block) + self.run_every
|
|
168
|
+
|
|
169
|
+
def _prev_block(self, block: int) -> int:
|
|
170
|
+
if self.relative:
|
|
171
|
+
base = self.last_run_at.number
|
|
172
|
+
return base + (block - base) // self.run_every * self.run_every
|
|
173
|
+
|
|
174
|
+
return block - block % self.run_every
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def stop_watchers() -> None:
|
|
178
|
+
for watcher in BaseBittensorSchedule._clients.values():
|
|
179
|
+
watcher.stop()
|
|
180
|
+
|
|
181
|
+
for watcher in BaseBittensorSchedule._clients.values():
|
|
182
|
+
watcher.join()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
atexit.register(stop_watchers)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: celery-bittensor
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Celery's Bittensor Schedule
|
|
5
|
+
Project-URL: Homepage, https://github.com/zyzniewski/celery-bittensor
|
|
6
|
+
Project-URL: Issues, https://github.com/zyzniewski/celery-bittensor/issues
|
|
7
|
+
Author-email: Tomasz Żyźniewski <zyzniewski@gmail.com>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: bittensor==11.*
|
|
14
|
+
Requires-Dist: celery==5.*
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# celery-bittensor
|
|
18
|
+
Bittensor's Schedule for Celery
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
pip install celery-bittensor
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.10+.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import celery
|
|
33
|
+
import celery_bittensor
|
|
34
|
+
|
|
35
|
+
app = celery.Celery()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.on_after_configure.connect
|
|
39
|
+
def setup_periodic_tasks(sender: celery.Celery, **kwargs):
|
|
40
|
+
# Calls test('hello') every 10 blocks.
|
|
41
|
+
sender.add_periodic_task(
|
|
42
|
+
celery_bittensor.schedule(10), test.s("hello"), name="add every 10"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.task
|
|
47
|
+
def test(arg):
|
|
48
|
+
print(arg)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
BSD 3-Clause License, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
celery_bittensor/__init__.py,sha256=ZhV5pfKa7xb3hGP0nc9rrIhn2rxLJ6NYH8pg2oBuGeI,124
|
|
2
|
+
celery_bittensor/client.py,sha256=PPqWvigm76FyTVRYGfDs7MsHBmLrvGAqEm7-MViqyEo,2782
|
|
3
|
+
celery_bittensor/schedules.py,sha256=cQGkw3pknLlI9ccXBJlL0uYGDdoxHshrPB_kWMnNPnI,5476
|
|
4
|
+
celery_bittensor/version.py,sha256=Lgf9keBX7aG_JEFt6UYSyiFKcm-BYGtmk9ohpQhYpyk,20
|
|
5
|
+
celery_bittensor-0.1.dist-info/METADATA,sha256=9U73-H_zo6cnfwV2yIefoNrXHCyb07FN2RnXLWwMoxA,1138
|
|
6
|
+
celery_bittensor-0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
7
|
+
celery_bittensor-0.1.dist-info/licenses/LICENSE,sha256=OnbdkWVXvRm9va0E0Q0DESJIxEM-wrmwVTku_pEwGdM,1506
|
|
8
|
+
celery_bittensor-0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Tomasz Żyźniewski
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|