binomic 0.1.0__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.
- binomic/__init__.py +0 -0
- binomic/base/__init__.py +13 -0
- binomic/base/constants.py +3 -0
- binomic/base/exceptions.py +17 -0
- binomic/base/schemas.py +39 -0
- binomic/broker/__init__.py +11 -0
- binomic/broker/handlers.py +33 -0
- binomic/broker/protocols.py +27 -0
- binomic/broker/redis.py +165 -0
- binomic/broker/types.py +21 -0
- binomic/client.py +114 -0
- binomic/config.py +16 -0
- binomic/message/__init__.py +3 -0
- binomic/message/schemas.py +54 -0
- binomic/plugin/__init__.py +0 -0
- binomic/plugin/litestar.py +100 -0
- binomic/py.typed +0 -0
- binomic/task/__init__.py +12 -0
- binomic/task/exceptions.py +14 -0
- binomic/task/registry.py +68 -0
- binomic/task/wrappers.py +94 -0
- binomic/tasks.py +9 -0
- binomic/worker/__init__.py +13 -0
- binomic/worker/master.py +114 -0
- binomic/worker/presence.py +65 -0
- binomic/worker/schemas.py +27 -0
- binomic/worker/worker.py +173 -0
- binomic-0.1.0.dist-info/METADATA +179 -0
- binomic-0.1.0.dist-info/RECORD +31 -0
- binomic-0.1.0.dist-info/WHEEL +4 -0
- binomic-0.1.0.dist-info/licenses/LICENSE +21 -0
binomic/__init__.py
ADDED
|
File without changes
|
binomic/base/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__all__ = (
|
|
2
|
+
"BinomicError",
|
|
3
|
+
"DeserializationError",
|
|
4
|
+
"SerializationError",
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BinomicError(Exception):
|
|
9
|
+
"""Base class for binomic exceptions."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SerializationError(BinomicError):
|
|
13
|
+
"""Exception raised for errors during serialization."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeserializationError(BinomicError):
|
|
17
|
+
"""Exception raised for errors during deserialization."""
|
binomic/base/schemas.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from msgspec import Struct, json
|
|
4
|
+
|
|
5
|
+
from .exceptions import DeserializationError, SerializationError
|
|
6
|
+
|
|
7
|
+
__all__ = ("BaseStruct",)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseStruct(Struct):
|
|
11
|
+
"""Base data structure."""
|
|
12
|
+
|
|
13
|
+
def to_json(self) -> str:
|
|
14
|
+
"""Convert the data structure to a JSON string."""
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
return json.encode(self).decode("utf-8")
|
|
18
|
+
|
|
19
|
+
except Exception as exc:
|
|
20
|
+
raise SerializationError(
|
|
21
|
+
f"Failed to serialize data structure: {exc}",
|
|
22
|
+
) from exc
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_json(cls, json_str: str) -> Self:
|
|
26
|
+
"""Create a data structure from a JSON string."""
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
return json.decode(json_str, type=cls)
|
|
30
|
+
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
raise DeserializationError(
|
|
33
|
+
f"Failed to deserialize data structure: {exc}",
|
|
34
|
+
) from exc
|
|
35
|
+
|
|
36
|
+
def __repr__(self) -> str:
|
|
37
|
+
"""Return a string representation of the data structure."""
|
|
38
|
+
|
|
39
|
+
return self.to_json()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from .redis import AsyncredisBroker
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .protocols import Broker
|
|
7
|
+
|
|
8
|
+
__all__ = ("BrokerFactory",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BrokerFactory:
|
|
12
|
+
"""A factory class for creating broker instances."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, dsn: str, queues: list[str]) -> None:
|
|
15
|
+
|
|
16
|
+
self._dsn = dsn
|
|
17
|
+
self._queues = queues
|
|
18
|
+
|
|
19
|
+
def __call__(self) -> "Broker":
|
|
20
|
+
|
|
21
|
+
match self._dsn.split(":")[0]:
|
|
22
|
+
case "redis":
|
|
23
|
+
return AsyncredisBroker(
|
|
24
|
+
dsn=self._dsn,
|
|
25
|
+
queues=self._queues,
|
|
26
|
+
decode_responses=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
case "amqp":
|
|
30
|
+
raise NotImplementedError("AMQP is not supported yet.")
|
|
31
|
+
|
|
32
|
+
case _:
|
|
33
|
+
raise ValueError("Unsupported broker type.")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Protocol
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from binomic.message import Message
|
|
7
|
+
|
|
8
|
+
from .types import Entry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ("Broker",)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Broker(Protocol):
|
|
15
|
+
"""Broker protocol class."""
|
|
16
|
+
|
|
17
|
+
async def initialize(self) -> None: ...
|
|
18
|
+
|
|
19
|
+
async def enqueue(self, msg: "Message") -> "UUID": ...
|
|
20
|
+
|
|
21
|
+
async def acquire(self, consumer: str, *, count: int) -> list["Entry"]: ...
|
|
22
|
+
|
|
23
|
+
async def ack(self, entry: "Entry") -> int: ...
|
|
24
|
+
|
|
25
|
+
async def reclaim(self, consumer: str, *, min_idle_ms: int, count: int) -> int: ...
|
|
26
|
+
|
|
27
|
+
async def aclose(self) -> None: ...
|
binomic/broker/redis.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Final, cast
|
|
3
|
+
|
|
4
|
+
from redis.asyncio import BlockingConnectionPool
|
|
5
|
+
from redis.asyncio import Redis as AsyncRedis
|
|
6
|
+
from redis.exceptions import ResponseError
|
|
7
|
+
|
|
8
|
+
from binomic.base.constants import APP_NAME
|
|
9
|
+
from binomic.message import Message
|
|
10
|
+
|
|
11
|
+
from .protocols import Broker
|
|
12
|
+
from .types import Entry
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
from redis.asyncio import ConnectionPool
|
|
18
|
+
|
|
19
|
+
from .types import Fields
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ("AsyncredisBroker",)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
GROUP_NAMESPACE: Final[str] = APP_NAME
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncredisBroker(Broker):
|
|
29
|
+
"""Broker based on async redis."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
dsn: str,
|
|
34
|
+
queues: Sequence[str],
|
|
35
|
+
**config: Any,
|
|
36
|
+
) -> None:
|
|
37
|
+
|
|
38
|
+
self._dsn = dsn
|
|
39
|
+
self._queues = queues
|
|
40
|
+
self._config = config
|
|
41
|
+
self._group = GROUP_NAMESPACE
|
|
42
|
+
|
|
43
|
+
self._connpool: ConnectionPool | None = None
|
|
44
|
+
|
|
45
|
+
def get_stream_key(self, queue: str) -> str:
|
|
46
|
+
"""Namespace a queue name into its Redis stream key."""
|
|
47
|
+
|
|
48
|
+
return f"{self._group}:{queue}"
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def client(self) -> "AsyncRedis":
|
|
52
|
+
|
|
53
|
+
if self._connpool is None:
|
|
54
|
+
self._connpool = BlockingConnectionPool.from_url(
|
|
55
|
+
url=self._dsn,
|
|
56
|
+
**self._config,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return AsyncRedis.from_pool(self._connpool)
|
|
60
|
+
|
|
61
|
+
async def initialize(self) -> None:
|
|
62
|
+
|
|
63
|
+
for queue in self._queues:
|
|
64
|
+
try:
|
|
65
|
+
await self.client.xgroup_create(
|
|
66
|
+
self.get_stream_key(queue),
|
|
67
|
+
groupname=self._group,
|
|
68
|
+
id="0",
|
|
69
|
+
mkstream=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
except ResponseError as err:
|
|
73
|
+
if "BUSYGROUP" not in str(err):
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
async def enqueue(self, msg: "Message") -> "UUID":
|
|
77
|
+
|
|
78
|
+
await self.client.xadd(
|
|
79
|
+
name=self.get_stream_key(msg.queue),
|
|
80
|
+
fields={
|
|
81
|
+
"id": str(msg.id),
|
|
82
|
+
"message": msg.to_json(),
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return msg.id
|
|
87
|
+
|
|
88
|
+
async def acquire(self, consumer: str, *, count: int) -> list["Entry"]:
|
|
89
|
+
|
|
90
|
+
result = await self.client.xreadgroup(
|
|
91
|
+
groupname=self._group,
|
|
92
|
+
consumername=consumer,
|
|
93
|
+
streams={self.get_stream_key(q): ">" for q in self._queues},
|
|
94
|
+
count=count,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
entries: list[Entry] = []
|
|
98
|
+
if not result:
|
|
99
|
+
return entries
|
|
100
|
+
|
|
101
|
+
queue_by_key = {self.get_stream_key(q): q for q in self._queues}
|
|
102
|
+
|
|
103
|
+
for key, msgs in cast(
|
|
104
|
+
"list[tuple[str, list[tuple[str, Fields]]]]",
|
|
105
|
+
result.items() if isinstance(result, dict) else result,
|
|
106
|
+
):
|
|
107
|
+
queue = queue_by_key[key]
|
|
108
|
+
for msg_id, fields in msgs:
|
|
109
|
+
entries.append(Entry(queue, msg_id, fields))
|
|
110
|
+
|
|
111
|
+
return entries
|
|
112
|
+
|
|
113
|
+
async def ack(self, entry: "Entry") -> int:
|
|
114
|
+
|
|
115
|
+
queue, msg_id, _ = entry
|
|
116
|
+
return await self.client.xack(
|
|
117
|
+
self.get_stream_key(queue),
|
|
118
|
+
self._group,
|
|
119
|
+
msg_id,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def reclaim(
|
|
123
|
+
self,
|
|
124
|
+
consumer: str,
|
|
125
|
+
*,
|
|
126
|
+
min_idle_ms: int,
|
|
127
|
+
count: int,
|
|
128
|
+
) -> int:
|
|
129
|
+
|
|
130
|
+
reclaimed = 0
|
|
131
|
+
for queue in self._queues:
|
|
132
|
+
try:
|
|
133
|
+
result = await self.client.xautoclaim(
|
|
134
|
+
name=self.get_stream_key(queue),
|
|
135
|
+
groupname=self._group,
|
|
136
|
+
consumername=consumer,
|
|
137
|
+
min_idle_time=min_idle_ms,
|
|
138
|
+
start_id="0-0",
|
|
139
|
+
count=count,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
except ResponseError as err:
|
|
143
|
+
if "NOGROUP" in str(err):
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
raise
|
|
147
|
+
|
|
148
|
+
msgs = result[1] if result else []
|
|
149
|
+
for msg_id, fields in cast(
|
|
150
|
+
"list[tuple[str, Fields]]",
|
|
151
|
+
msgs,
|
|
152
|
+
):
|
|
153
|
+
msg = Message.from_json(fields.get("message"))
|
|
154
|
+
await self.enqueue(msg)
|
|
155
|
+
await self.ack(Entry(queue, msg_id, fields))
|
|
156
|
+
|
|
157
|
+
reclaimed += 1
|
|
158
|
+
|
|
159
|
+
return reclaimed
|
|
160
|
+
|
|
161
|
+
async def aclose(self) -> None:
|
|
162
|
+
|
|
163
|
+
if self._connpool is not None:
|
|
164
|
+
await self._connpool.disconnect()
|
|
165
|
+
self._connpool = None
|
binomic/broker/types.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import NamedTuple, TypedDict
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
"Entry",
|
|
5
|
+
"Fields",
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Fields(TypedDict):
|
|
10
|
+
"""Fields in the Entry."""
|
|
11
|
+
|
|
12
|
+
id: str
|
|
13
|
+
message: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Entry(NamedTuple):
|
|
17
|
+
"""Entry in the broker."""
|
|
18
|
+
|
|
19
|
+
queue: str
|
|
20
|
+
msg_id: str
|
|
21
|
+
fields: Fields
|
binomic/client.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import TYPE_CHECKING, Self
|
|
4
|
+
|
|
5
|
+
import anyio
|
|
6
|
+
from anyio import AsyncContextManagerMixin
|
|
7
|
+
|
|
8
|
+
from binomic.broker import Broker, BrokerFactory
|
|
9
|
+
from binomic.worker import Master, MasterPolicy, WorkerPolicy
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from binomic.config import BinomicConfig
|
|
13
|
+
from binomic.message import Message
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
"Binomic",
|
|
18
|
+
"BinomicFactory",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Binomic(AsyncContextManagerMixin):
|
|
23
|
+
"""Binomic working client.
|
|
24
|
+
|
|
25
|
+
Entering the async context starts the master process group and keeps
|
|
26
|
+
it running until the context exits.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
broker_dsn: str,
|
|
32
|
+
redis_dsn: str,
|
|
33
|
+
module_name: str,
|
|
34
|
+
config: "BinomicConfig",
|
|
35
|
+
) -> None:
|
|
36
|
+
|
|
37
|
+
self._broker_dsn = broker_dsn
|
|
38
|
+
self._redis_dsn = redis_dsn
|
|
39
|
+
self._module_name = module_name
|
|
40
|
+
self._config = config
|
|
41
|
+
|
|
42
|
+
self._broker: Broker | None = None
|
|
43
|
+
|
|
44
|
+
async def submit(self, msg: "Message") -> None:
|
|
45
|
+
"""Enqueue a message to the broker stream.
|
|
46
|
+
|
|
47
|
+
The broker is created lazily on the first submit, so constructing
|
|
48
|
+
the client performs no IO.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if self._broker is None:
|
|
52
|
+
self._broker = BrokerFactory(
|
|
53
|
+
self._broker_dsn,
|
|
54
|
+
self._config.queues,
|
|
55
|
+
)()
|
|
56
|
+
|
|
57
|
+
await self._broker.enqueue(msg)
|
|
58
|
+
|
|
59
|
+
@asynccontextmanager
|
|
60
|
+
async def __asynccontextmanager__(self) -> AsyncGenerator[Self, None]:
|
|
61
|
+
|
|
62
|
+
master = Master(
|
|
63
|
+
broker_dsn=self._broker_dsn,
|
|
64
|
+
redis_dsn=self._redis_dsn,
|
|
65
|
+
module_name=self._module_name,
|
|
66
|
+
policy=MasterPolicy(
|
|
67
|
+
workers=self._config.workers,
|
|
68
|
+
worker=WorkerPolicy(
|
|
69
|
+
queues=self._config.queues,
|
|
70
|
+
concurrency=self._config.concurrency,
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
async with anyio.create_task_group() as tg:
|
|
77
|
+
tg.start_soon(master.arun)
|
|
78
|
+
yield self
|
|
79
|
+
tg.cancel_scope.cancel()
|
|
80
|
+
|
|
81
|
+
finally:
|
|
82
|
+
await master.aclose()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class BinomicFactory:
|
|
86
|
+
"""Binomic working client factory."""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
broker_dsn: str,
|
|
91
|
+
redis_dsn: str,
|
|
92
|
+
module_name: str,
|
|
93
|
+
config: "BinomicConfig",
|
|
94
|
+
) -> None:
|
|
95
|
+
|
|
96
|
+
self._broker_dsn = broker_dsn
|
|
97
|
+
self._redis_dsn = redis_dsn
|
|
98
|
+
self._module_name = module_name
|
|
99
|
+
self._config = config
|
|
100
|
+
|
|
101
|
+
self._binomic: Binomic | None = None
|
|
102
|
+
|
|
103
|
+
def create(self) -> "Binomic":
|
|
104
|
+
"""Return the client, creating it on the first call."""
|
|
105
|
+
|
|
106
|
+
if self._binomic is None:
|
|
107
|
+
self._binomic = Binomic(
|
|
108
|
+
broker_dsn=self._broker_dsn,
|
|
109
|
+
redis_dsn=self._redis_dsn,
|
|
110
|
+
module_name=self._module_name,
|
|
111
|
+
config=self._config,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return self._binomic
|
binomic/config.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from binomic.base import BaseStruct
|
|
2
|
+
|
|
3
|
+
__all__ = ("BinomicConfig",)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BinomicConfig(BaseStruct):
|
|
7
|
+
"""Binomic configuration.
|
|
8
|
+
|
|
9
|
+
``workers`` is the number of worker subprocesses the master spawns and
|
|
10
|
+
``concurrency`` the number of tasks each worker executes concurrently;
|
|
11
|
+
every stream in ``queues`` is consumed by all workers.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
queues: list[str]
|
|
15
|
+
workers: int = 1
|
|
16
|
+
concurrency: int = 5
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from msgspec import field, json
|
|
5
|
+
from uuid_utils.compat import uuid7
|
|
6
|
+
|
|
7
|
+
from binomic.base import BaseStruct
|
|
8
|
+
|
|
9
|
+
__all__ = ("Message",)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Message(BaseStruct):
|
|
13
|
+
"""Message for binomic.
|
|
14
|
+
|
|
15
|
+
``name`` is the registered task name, ``queue`` the target stream and
|
|
16
|
+
``enqueued_at`` the submission timestamp; ``args`` and ``kwargs`` carry
|
|
17
|
+
the task call arguments.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
queue: str
|
|
22
|
+
enqueued_at: float
|
|
23
|
+
|
|
24
|
+
id: "UUID" = field(default_factory=uuid7)
|
|
25
|
+
args: list[Any] = field(default_factory=list)
|
|
26
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
def to_json(self) -> str:
|
|
29
|
+
|
|
30
|
+
return json.encode(
|
|
31
|
+
{
|
|
32
|
+
"id": str(self.id),
|
|
33
|
+
"name": self.name,
|
|
34
|
+
"queue": self.queue,
|
|
35
|
+
"enqueued_at": self.enqueued_at,
|
|
36
|
+
"args": self.args,
|
|
37
|
+
"kwargs": self.kwargs,
|
|
38
|
+
},
|
|
39
|
+
order="sorted",
|
|
40
|
+
).decode("utf-8")
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_json(cls, json_str: str) -> "Message":
|
|
44
|
+
|
|
45
|
+
data = json.decode(json_str)
|
|
46
|
+
|
|
47
|
+
return cls(
|
|
48
|
+
id=UUID(data["id"]),
|
|
49
|
+
name=data["name"],
|
|
50
|
+
queue=data["queue"],
|
|
51
|
+
enqueued_at=data["enqueued_at"],
|
|
52
|
+
args=data["args"],
|
|
53
|
+
kwargs=data["kwargs"],
|
|
54
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, ClassVar, cast
|
|
2
|
+
|
|
3
|
+
from binomic.client import Binomic, BinomicFactory
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from binomic.config import BinomicConfig
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import litestar # noqa: F401
|
|
10
|
+
from litestar.di import Provide
|
|
11
|
+
from litestar.plugins import InitPluginProtocol
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from litestar.config.app import AppConfig
|
|
15
|
+
from litestar.datastructures import State
|
|
16
|
+
|
|
17
|
+
except ImportError:
|
|
18
|
+
raise ImportError( # noqa: B904
|
|
19
|
+
"litestar is not installed. Please install it using pip install litestar."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ("BinomicPlugin",)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BinomicPlugin(InitPluginProtocol):
|
|
27
|
+
"""Binomic plugin for litestar."""
|
|
28
|
+
|
|
29
|
+
_binomic_factory_state_key: ClassVar[str] = "binomic_factory"
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
app_name: str,
|
|
34
|
+
broker_dsn: str,
|
|
35
|
+
redis_dsn: str,
|
|
36
|
+
config: "BinomicConfig",
|
|
37
|
+
) -> None:
|
|
38
|
+
|
|
39
|
+
self._app_name = app_name
|
|
40
|
+
self._broker_dsn = broker_dsn
|
|
41
|
+
self._redis_dsn = redis_dsn
|
|
42
|
+
self._config = config
|
|
43
|
+
|
|
44
|
+
def on_app_init(self, app_config: "AppConfig") -> "AppConfig":
|
|
45
|
+
|
|
46
|
+
self.setup_signature_namespaces(app_config)
|
|
47
|
+
self.setup_states(app_config)
|
|
48
|
+
self.setup_dependencies(app_config)
|
|
49
|
+
|
|
50
|
+
return app_config
|
|
51
|
+
|
|
52
|
+
def provide_binomic(self, state: "State") -> "Binomic":
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
factory := cast(
|
|
56
|
+
"BinomicFactory | None",
|
|
57
|
+
state.get(self._binomic_factory_state_key),
|
|
58
|
+
)
|
|
59
|
+
) is None:
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
"BinomicFactory is not provided. "
|
|
62
|
+
"Please ensure that the BinomicPlugin is properly configured."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return factory.create()
|
|
66
|
+
|
|
67
|
+
def setup_signature_namespaces(self, app_config: "AppConfig") -> None:
|
|
68
|
+
|
|
69
|
+
app_config.signature_namespace.update(
|
|
70
|
+
{
|
|
71
|
+
"Binomic": Binomic,
|
|
72
|
+
"BinomicFactory": BinomicFactory,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def setup_states(self, app_config: "AppConfig") -> None:
|
|
77
|
+
|
|
78
|
+
need_setup = {}
|
|
79
|
+
|
|
80
|
+
state = app_config.state
|
|
81
|
+
if self._binomic_factory_state_key not in state:
|
|
82
|
+
need_setup[self._binomic_factory_state_key] = BinomicFactory(
|
|
83
|
+
broker_dsn=self._broker_dsn,
|
|
84
|
+
redis_dsn=self._redis_dsn,
|
|
85
|
+
module_name=self._app_name,
|
|
86
|
+
config=self._config,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
state.update(need_setup)
|
|
90
|
+
|
|
91
|
+
def setup_dependencies(self, app_config: "AppConfig") -> None:
|
|
92
|
+
|
|
93
|
+
app_config.dependencies.update(
|
|
94
|
+
{
|
|
95
|
+
"binomic": Provide(
|
|
96
|
+
dependency=self.provide_binomic,
|
|
97
|
+
sync_to_thread=True,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
)
|
binomic/py.typed
ADDED
|
File without changes
|
binomic/task/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .exceptions import DuplicateTaskError, TaskNotFoundError
|
|
2
|
+
from .registry import TaskRegistry, TaskSpec
|
|
3
|
+
from .wrappers import autodiscover, task
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"DuplicateTaskError",
|
|
7
|
+
"TaskNotFoundError",
|
|
8
|
+
"TaskRegistry",
|
|
9
|
+
"TaskSpec",
|
|
10
|
+
"autodiscover",
|
|
11
|
+
"task",
|
|
12
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from binomic.base import BinomicError
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
"DuplicateTaskError",
|
|
5
|
+
"TaskNotFoundError",
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DuplicateTaskError(BinomicError):
|
|
10
|
+
"""Duplicate task error."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskNotFoundError(BinomicError):
|
|
14
|
+
"""Task not found error."""
|