queue-func 0.1__py3-none-any.whl → 0.1.2__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.
- queue_func/__init__.py +6 -0
- queue_func/broker/base.py +12 -14
- queue_func/broker/queue.py +7 -7
- queue_func/config/general.py +2 -6
- queue_func/config/queue.py +3 -0
- queue_func/consumer/base.py +5 -5
- queue_func/consumer/queue.py +7 -7
- queue_func/errors.py +7 -0
- queue_func/factory/deco.py +4 -6
- queue_func/producer/base.py +17 -9
- queue_func/producer/queue.py +12 -10
- queue_func/serializer/base.py +10 -21
- queue_func/serializer/orjson_ser.py +14 -17
- queue_func/serializer/raw.py +24 -0
- queue_func-0.1.2.dist-info/METADATA +183 -0
- queue_func-0.1.2.dist-info/RECORD +37 -0
- queue_func/message/__init__.py +0 -1
- queue_func/message/base.py +0 -39
- queue_func/message/general.py +0 -30
- queue_func/serializer/json_ser.py +0 -25
- queue_func/serializer/utils.py +0 -16
- queue_func-0.1.dist-info/METADATA +0 -121
- queue_func-0.1.dist-info/RECORD +0 -40
- {queue_func-0.1.dist-info → queue_func-0.1.2.dist-info}/WHEEL +0 -0
- {queue_func-0.1.dist-info → queue_func-0.1.2.dist-info}/licenses/LICENSE +0 -0
queue_func/__init__.py
CHANGED
queue_func/broker/base.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import abc
|
|
4
4
|
import typing as t
|
|
5
5
|
|
|
6
|
-
from queue_func.
|
|
6
|
+
from queue_func.serializer.base import BaseSerializer, Message
|
|
7
7
|
|
|
8
8
|
if t.TYPE_CHECKING:
|
|
9
9
|
from queue_func.config.general import Config
|
|
@@ -14,16 +14,16 @@ if t.TYPE_CHECKING:
|
|
|
14
14
|
ConfigType = t.TypeVar("ConfigType", bound="Config")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class BaseBroker(t.Generic[ConfigType,
|
|
17
|
+
class BaseBroker(t.Generic[ConfigType, Message], metaclass=abc.ABCMeta):
|
|
18
18
|
"""一条通道的装配者。
|
|
19
19
|
|
|
20
|
-
创建时准备好双方共用的
|
|
20
|
+
创建时准备好双方共用的 Serializer,并持有配置与 Producer/Consumer。
|
|
21
21
|
对外通过 publish、start_consumer 等入口操作;运行期业务逻辑仍在 Producer/Consumer 内完成。
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
__slots__ = (
|
|
25
25
|
"_config",
|
|
26
|
-
"
|
|
26
|
+
"_serializer",
|
|
27
27
|
"_producer",
|
|
28
28
|
"_consumer",
|
|
29
29
|
)
|
|
@@ -31,35 +31,33 @@ class BaseBroker(t.Generic[ConfigType, Packed], metaclass=abc.ABCMeta):
|
|
|
31
31
|
def __init__(self, config: ConfigType):
|
|
32
32
|
|
|
33
33
|
self._config = config
|
|
34
|
-
self.
|
|
35
|
-
message_serializer_cls=self._config.message_serializer_cls,
|
|
36
|
-
enable_serializer=self._config.enable_message_serializer,
|
|
37
|
-
)
|
|
34
|
+
self._serializer: type[BaseSerializer[Message]] = self._config.serializer_cls
|
|
38
35
|
self._producer = self._create_producer()
|
|
39
36
|
self._consumer = self._create_consumer()
|
|
40
37
|
|
|
41
38
|
@abc.abstractmethod
|
|
42
|
-
def _create_producer(self) -> "BaseProducer[
|
|
39
|
+
def _create_producer(self) -> "BaseProducer[Message]":
|
|
43
40
|
|
|
44
41
|
pass
|
|
45
42
|
|
|
46
43
|
@abc.abstractmethod
|
|
47
|
-
def _create_consumer(self) -> "BaseConsumer[
|
|
44
|
+
def _create_consumer(self) -> "BaseConsumer[Message]":
|
|
48
45
|
|
|
49
46
|
pass
|
|
50
47
|
|
|
51
48
|
@property
|
|
52
|
-
def
|
|
49
|
+
def serializer(self) -> type[BaseSerializer[Message]]:
|
|
53
50
|
|
|
54
|
-
return self.
|
|
51
|
+
return self._serializer
|
|
55
52
|
|
|
56
53
|
async def publish(
|
|
57
54
|
self,
|
|
58
55
|
coro_args: tuple[t.Any, ...],
|
|
59
56
|
coro_kwargs: dict[str, t.Any] | None = None,
|
|
60
|
-
|
|
57
|
+
*,
|
|
58
|
+
timeout: float | None = None) -> None:
|
|
61
59
|
|
|
62
|
-
await self._producer.publish(coro_args, coro_kwargs,
|
|
60
|
+
await self._producer.publish(coro_args, coro_kwargs, timeout=timeout)
|
|
63
61
|
|
|
64
62
|
async def start_consumer(self) -> None:
|
|
65
63
|
|
queue_func/broker/queue.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import typing as t
|
|
5
5
|
|
|
6
|
-
from queue_func.
|
|
6
|
+
from queue_func.serializer.base import Message
|
|
7
7
|
from queue_func.broker.base import BaseBroker as _BaseBroker
|
|
8
8
|
from queue_func.producer.queue import Producer
|
|
9
9
|
from queue_func.consumer.queue import Consumer
|
|
@@ -12,7 +12,7 @@ if t.TYPE_CHECKING:
|
|
|
12
12
|
from queue_func.config.queue import Config
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class Broker(_BaseBroker["Config",
|
|
15
|
+
class Broker(_BaseBroker["Config", Message]):
|
|
16
16
|
"""内存队列通道。队列由这里创建,再交给 Producer 和 Consumer。"""
|
|
17
17
|
|
|
18
18
|
__slots__ = (
|
|
@@ -20,23 +20,23 @@ class Broker(_BaseBroker["Config", Packed]):
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
def __init__(self, config: "Config"):
|
|
23
|
-
self._queue: asyncio.Queue[
|
|
23
|
+
self._queue: asyncio.Queue[Message] = asyncio.Queue(maxsize=config.maxsize)
|
|
24
24
|
super().__init__(config)
|
|
25
25
|
|
|
26
|
-
def _create_producer(self) -> Producer[
|
|
26
|
+
def _create_producer(self) -> Producer[Message]:
|
|
27
27
|
|
|
28
28
|
return Producer(
|
|
29
29
|
queue=self._queue,
|
|
30
30
|
publish_timeout=self._config.publish_timeout,
|
|
31
|
-
|
|
31
|
+
serializer=self._serializer,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
def _create_consumer(self) -> Consumer[
|
|
34
|
+
def _create_consumer(self) -> Consumer[Message]:
|
|
35
35
|
|
|
36
36
|
return Consumer(
|
|
37
37
|
queue=self._queue,
|
|
38
38
|
consume_timeout=self._config.consume_timeout,
|
|
39
|
-
|
|
39
|
+
serializer=self._serializer,
|
|
40
40
|
coro=self._config.coro,
|
|
41
41
|
enable_consumer=self._config.enable_consumer,
|
|
42
42
|
pool_executor_cls=self._config.consumer_pool_executor_cls,
|
queue_func/config/general.py
CHANGED
|
@@ -5,10 +5,8 @@ import typing as t
|
|
|
5
5
|
from pydantic.dataclasses import dataclass as pyd_dataclass
|
|
6
6
|
|
|
7
7
|
from queue_func.config.utils import ExtraIgnoreConfig
|
|
8
|
-
from queue_func.message.base import BaseMessage
|
|
9
|
-
from queue_func.message.general import Message
|
|
10
8
|
from queue_func.serializer.base import BaseSerializer
|
|
11
|
-
from queue_func.serializer.orjson_ser import
|
|
9
|
+
from queue_func.serializer.orjson_ser import Serializer
|
|
12
10
|
from queue_func.pool_executor.base import BasePoolExecutor
|
|
13
11
|
from queue_func.pool_executor.general import PoolExecutor
|
|
14
12
|
|
|
@@ -24,9 +22,7 @@ class Config:
|
|
|
24
22
|
consumer_pool_maxsize: int = 200
|
|
25
23
|
|
|
26
24
|
# broker
|
|
27
|
-
|
|
28
|
-
message_serializer_cls: type[BaseSerializer] = OrJSONSerializer
|
|
29
|
-
enable_message_serializer: bool = True
|
|
25
|
+
serializer_cls: type[BaseSerializer] = Serializer
|
|
30
26
|
|
|
31
27
|
# producer
|
|
32
28
|
publish_timeout: float | None = None
|
queue_func/config/queue.py
CHANGED
|
@@ -5,6 +5,8 @@ from pydantic.dataclasses import dataclass as pyd_dataclass
|
|
|
5
5
|
|
|
6
6
|
from queue_func.config.utils import ExtraIgnoreConfig
|
|
7
7
|
from queue_func.config.general import Config as _Config
|
|
8
|
+
from queue_func.serializer.base import BaseSerializer
|
|
9
|
+
from queue_func.serializer.raw import Serializer
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
@pyd_dataclass(frozen=True, config=ExtraIgnoreConfig)
|
|
@@ -12,3 +14,4 @@ class Config(_Config):
|
|
|
12
14
|
|
|
13
15
|
# broker
|
|
14
16
|
maxsize: int = 200
|
|
17
|
+
serializer_cls: type[BaseSerializer] = Serializer
|
queue_func/consumer/base.py
CHANGED
|
@@ -7,14 +7,14 @@ import contextlib
|
|
|
7
7
|
import typing as t
|
|
8
8
|
from functools import wraps
|
|
9
9
|
|
|
10
|
-
from queue_func.
|
|
10
|
+
from queue_func.serializer.base import BaseSerializer, Message
|
|
11
11
|
from queue_func.pool_executor.base import BasePoolExecutor
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class BaseConsumer(t.Generic[
|
|
14
|
+
class BaseConsumer(t.Generic[Message], metaclass=abc.ABCMeta):
|
|
15
15
|
|
|
16
16
|
__slots__ = (
|
|
17
|
-
"
|
|
17
|
+
"_serializer",
|
|
18
18
|
"_coro",
|
|
19
19
|
"_enable_consumer",
|
|
20
20
|
"_pool_executor",
|
|
@@ -25,13 +25,13 @@ class BaseConsumer(t.Generic[Packed], metaclass=abc.ABCMeta):
|
|
|
25
25
|
|
|
26
26
|
def __init__(
|
|
27
27
|
self,
|
|
28
|
-
|
|
28
|
+
serializer: type[BaseSerializer[Message]],
|
|
29
29
|
coro: t.Callable[..., t.Coroutine],
|
|
30
30
|
enable_consumer: bool,
|
|
31
31
|
pool_executor_cls: type[BasePoolExecutor],
|
|
32
32
|
pool_maxsize: int):
|
|
33
33
|
|
|
34
|
-
self.
|
|
34
|
+
self._serializer = serializer
|
|
35
35
|
self._coro = coro
|
|
36
36
|
self._enable_consumer = enable_consumer
|
|
37
37
|
self._pool_executor = self._init_pool_executor(pool_executor_cls, pool_maxsize)
|
queue_func/consumer/queue.py
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import typing as t
|
|
5
5
|
|
|
6
|
-
from queue_func.
|
|
6
|
+
from queue_func.serializer.base import BaseSerializer, Message
|
|
7
7
|
from queue_func.pool_executor.base import BasePoolExecutor
|
|
8
8
|
from queue_func.consumer.base import BaseConsumer as _BaseConsumer
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class Consumer(_BaseConsumer[
|
|
11
|
+
class Consumer(_BaseConsumer[Message]):
|
|
12
12
|
|
|
13
13
|
__slots__ = (
|
|
14
14
|
"_queue",
|
|
@@ -17,16 +17,16 @@ class Consumer(_BaseConsumer[Packed]):
|
|
|
17
17
|
|
|
18
18
|
def __init__(
|
|
19
19
|
self,
|
|
20
|
-
queue: asyncio.Queue[
|
|
20
|
+
queue: asyncio.Queue[Message],
|
|
21
21
|
consume_timeout: float,
|
|
22
|
-
|
|
22
|
+
serializer: type[BaseSerializer[Message]],
|
|
23
23
|
coro: t.Callable[..., t.Coroutine],
|
|
24
24
|
enable_consumer: bool,
|
|
25
25
|
pool_executor_cls: type[BasePoolExecutor],
|
|
26
26
|
pool_maxsize: int):
|
|
27
27
|
|
|
28
28
|
super().__init__(
|
|
29
|
-
|
|
29
|
+
serializer,
|
|
30
30
|
coro,
|
|
31
31
|
enable_consumer,
|
|
32
32
|
pool_executor_cls,
|
|
@@ -39,13 +39,13 @@ class Consumer(_BaseConsumer[Packed]):
|
|
|
39
39
|
|
|
40
40
|
while (not self._stop_flag) or (not self._queue.empty()):
|
|
41
41
|
try:
|
|
42
|
-
|
|
42
|
+
packed: Message = await asyncio.wait_for(
|
|
43
43
|
self._queue.get(),
|
|
44
44
|
timeout=self._consume_timeout,
|
|
45
45
|
)
|
|
46
46
|
except asyncio.TimeoutError:
|
|
47
47
|
continue
|
|
48
|
-
coro_args, coro_kwargs = self.
|
|
48
|
+
coro_args, coro_kwargs = self._serializer.unpack(packed)
|
|
49
49
|
await self._dispatch(coro_args, coro_kwargs)
|
|
50
50
|
self._queue.task_done()
|
|
51
51
|
|
queue_func/errors.py
ADDED
queue_func/factory/deco.py
CHANGED
|
@@ -9,7 +9,6 @@ from queue_func.factory.registry import build_broker
|
|
|
9
9
|
if t.TYPE_CHECKING:
|
|
10
10
|
from queue_func.broker.base import BaseBroker
|
|
11
11
|
from queue_func.config.general import Config as GeneralConfig
|
|
12
|
-
from queue_func.message.base import BaseMessage
|
|
13
12
|
from queue_func.pool_executor.base import BasePoolExecutor
|
|
14
13
|
from queue_func.serializer.base import BaseSerializer
|
|
15
14
|
|
|
@@ -46,9 +45,10 @@ class QueueFunc(t.Generic[_P, _R]):
|
|
|
46
45
|
self,
|
|
47
46
|
*,
|
|
48
47
|
args: tuple[t.Any, ...] = (),
|
|
49
|
-
kwargs: dict[str, t.Any] | None = None
|
|
48
|
+
kwargs: dict[str, t.Any] | None = None,
|
|
49
|
+
timeout: float | None = None) -> None:
|
|
50
50
|
|
|
51
|
-
await self._broker.publish(coro_args=args, coro_kwargs=kwargs)
|
|
51
|
+
await self._broker.publish(coro_args=args, coro_kwargs=kwargs, timeout=timeout)
|
|
52
52
|
|
|
53
53
|
@property
|
|
54
54
|
def broker(self) -> "BaseBroker[t.Any, t.Any]":
|
|
@@ -76,9 +76,7 @@ def queue(
|
|
|
76
76
|
consume_timeout: float = ...,
|
|
77
77
|
consumer_pool_executor_cls: type["BasePoolExecutor"] = ...,
|
|
78
78
|
consumer_pool_maxsize: int = ...,
|
|
79
|
-
|
|
80
|
-
message_serializer_cls: type["BaseSerializer"] = ...,
|
|
81
|
-
enable_message_serializer: bool = ...,
|
|
79
|
+
serializer_cls: type["BaseSerializer"] = ...,
|
|
82
80
|
publish_timeout: float | None = ...,
|
|
83
81
|
maxsize: int = ...,
|
|
84
82
|
) -> t.Callable[[t.Callable[_P, t.Coroutine[t.Any, t.Any, _R]]], QueueFunc[_P, _R]]:
|
queue_func/producer/base.py
CHANGED
|
@@ -3,30 +3,38 @@
|
|
|
3
3
|
import abc
|
|
4
4
|
import typing as t
|
|
5
5
|
|
|
6
|
-
from queue_func.
|
|
6
|
+
from queue_func.serializer.base import BaseSerializer, Message
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class BaseProducer(t.Generic[
|
|
9
|
+
class BaseProducer(t.Generic[Message], metaclass=abc.ABCMeta):
|
|
10
10
|
|
|
11
11
|
__slots__ = (
|
|
12
|
-
"
|
|
12
|
+
"_serializer",
|
|
13
|
+
"_publish_timeout",
|
|
13
14
|
)
|
|
14
15
|
|
|
15
|
-
def __init__(
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
serializer: type[BaseSerializer[Message]],
|
|
19
|
+
publish_timeout: float | None):
|
|
16
20
|
|
|
17
|
-
self.
|
|
21
|
+
self._serializer = serializer
|
|
22
|
+
self._publish_timeout = publish_timeout
|
|
18
23
|
|
|
19
24
|
async def publish(
|
|
20
25
|
self,
|
|
21
26
|
coro_args: tuple[t.Any],
|
|
22
27
|
coro_kwargs: dict[str, t.Any] | None = None,
|
|
23
|
-
|
|
28
|
+
*,
|
|
29
|
+
timeout: float | None = None) -> None:
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
if timeout is None:
|
|
32
|
+
timeout = self._publish_timeout
|
|
33
|
+
packed = self._serializer.pack(coro_args, coro_kwargs)
|
|
34
|
+
await self._publish(packed, timeout=timeout)
|
|
27
35
|
|
|
28
36
|
@abc.abstractmethod
|
|
29
|
-
async def _publish(self, data:
|
|
37
|
+
async def _publish(self, data: Message, *, timeout: float | None) -> None:
|
|
30
38
|
pass
|
|
31
39
|
|
|
32
40
|
async def stop(self) -> None:
|
queue_func/producer/queue.py
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
import asyncio
|
|
3
3
|
|
|
4
|
-
from queue_func.
|
|
4
|
+
from queue_func.errors import PublishTimeoutError
|
|
5
|
+
from queue_func.serializer.base import BaseSerializer, Message
|
|
5
6
|
from queue_func.producer.base import BaseProducer
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
class Producer(BaseProducer[
|
|
9
|
+
class Producer(BaseProducer[Message]):
|
|
9
10
|
|
|
10
11
|
__slots__ = (
|
|
11
12
|
"_queue",
|
|
12
|
-
"_publish_timeout",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
def __init__(
|
|
16
16
|
self,
|
|
17
|
-
queue: asyncio.Queue[
|
|
17
|
+
queue: asyncio.Queue[Message],
|
|
18
18
|
publish_timeout: float | None,
|
|
19
|
-
|
|
19
|
+
serializer: type[BaseSerializer[Message]]):
|
|
20
20
|
|
|
21
|
-
super().__init__(
|
|
21
|
+
super().__init__(serializer, publish_timeout)
|
|
22
22
|
self._queue = queue
|
|
23
|
-
self._publish_timeout = publish_timeout
|
|
24
23
|
|
|
25
|
-
async def _publish(self, data:
|
|
24
|
+
async def _publish(self, data: Message, *, timeout: float | None) -> None:
|
|
26
25
|
|
|
27
|
-
if timeout
|
|
28
|
-
|
|
26
|
+
if timeout:
|
|
27
|
+
try:
|
|
28
|
+
await asyncio.wait_for(self._queue.put(data), timeout)
|
|
29
|
+
except asyncio.TimeoutError as exc:
|
|
30
|
+
raise PublishTimeoutError("publish timed out") from exc
|
|
29
31
|
return None
|
|
30
32
|
await self._queue.put(data)
|
queue_func/serializer/base.py
CHANGED
|
@@ -3,36 +3,25 @@
|
|
|
3
3
|
import abc
|
|
4
4
|
import typing as t
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
"BaseSerializer",
|
|
8
|
-
)
|
|
6
|
+
Message = t.TypeVar("Message")
|
|
9
7
|
|
|
8
|
+
type CoroParams = tuple[tuple[t.Any], dict[str, t.Any]]
|
|
10
9
|
|
|
11
|
-
class BaseSerializer(metaclass=abc.ABCMeta):
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
def dumps(self, data: t.Any, return_bytes: t.Literal[True]) -> bytes: ...
|
|
15
|
-
@t.overload
|
|
16
|
-
def dumps(self, data: t.Any, return_bytes: t.Literal[False]) -> str: ...
|
|
17
|
-
@t.overload
|
|
18
|
-
def dumps(self, data: t.Any, return_bytes: bool) -> t.Union[str, bytes]: ...
|
|
11
|
+
class BaseSerializer(t.Generic[Message], metaclass=abc.ABCMeta):
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if return_bytes:
|
|
23
|
-
return self._dumps_to_bytes(data)
|
|
24
|
-
return self._dumps_to_str(data)
|
|
25
|
-
|
|
26
|
-
@abc.abstractmethod
|
|
27
|
-
def _dumps_to_str(self, data: t.Any) -> str:
|
|
28
|
-
pass
|
|
13
|
+
__slots__ = ()
|
|
29
14
|
|
|
15
|
+
@staticmethod
|
|
30
16
|
@abc.abstractmethod
|
|
31
|
-
def
|
|
17
|
+
def pack(
|
|
18
|
+
coro_args: tuple[t.Any],
|
|
19
|
+
coro_kwargs: dict[str, t.Any] | None = None) -> Message:
|
|
32
20
|
|
|
33
21
|
pass
|
|
34
22
|
|
|
23
|
+
@staticmethod
|
|
35
24
|
@abc.abstractmethod
|
|
36
|
-
def
|
|
25
|
+
def unpack(data: Message) -> CoroParams:
|
|
37
26
|
|
|
38
27
|
pass
|
|
@@ -4,27 +4,24 @@ import typing as t
|
|
|
4
4
|
|
|
5
5
|
import orjson
|
|
6
6
|
|
|
7
|
-
from queue_func.serializer.base import BaseSerializer
|
|
8
|
-
from queue_func.serializer.utils import default as _default
|
|
7
|
+
from queue_func.serializer.base import BaseSerializer, CoroParams
|
|
9
8
|
|
|
10
|
-
__all__ = (
|
|
11
|
-
"OrJSONSerializer",
|
|
12
|
-
)
|
|
13
9
|
|
|
10
|
+
class Serializer(BaseSerializer[bytes]):
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
__slots__ = ()
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
@staticmethod
|
|
15
|
+
def pack(
|
|
16
|
+
coro_args: tuple[t.Any],
|
|
17
|
+
coro_kwargs: dict[str, t.Any] | None = None) -> bytes:
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
if coro_kwargs is None:
|
|
20
|
+
coro_kwargs = {}
|
|
21
|
+
return orjson.dumps((coro_args, coro_kwargs))
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
@staticmethod
|
|
24
|
+
def unpack(data: bytes) -> CoroParams:
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return orjson.dumps(data, default=self._default)
|
|
27
|
-
|
|
28
|
-
def loads(self, data: t.Union[str, bytes]) -> t.Any:
|
|
29
|
-
|
|
30
|
-
return orjson.loads(data)
|
|
26
|
+
args, kwargs = orjson.loads(data)
|
|
27
|
+
return tuple(args), kwargs
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from queue_func.serializer.base import CoroParams, BaseSerializer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Serializer(BaseSerializer[CoroParams]):
|
|
9
|
+
|
|
10
|
+
__slots__ = ()
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def pack(
|
|
14
|
+
coro_args: tuple[t.Any],
|
|
15
|
+
coro_kwargs: dict[str, t.Any] | None = None) -> CoroParams:
|
|
16
|
+
|
|
17
|
+
if coro_kwargs is None:
|
|
18
|
+
coro_kwargs = {}
|
|
19
|
+
return coro_args, coro_kwargs
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def unpack(data: CoroParams) -> CoroParams:
|
|
23
|
+
|
|
24
|
+
return data
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: queue-func
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Async coroutine functions as queue-backed tasks (QueueFunc).
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: liuseki
|
|
8
|
+
Author-email: liuseki@163.com
|
|
9
|
+
Requires-Python: >=3.12,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Requires-Dist: orjson
|
|
17
|
+
Requires-Dist: pydantic (>=2.14.1,<3.0.0)
|
|
18
|
+
Project-URL: Changelog, https://github.com/sekil9529/queue-func/blob/master/Changelog.rst
|
|
19
|
+
Project-URL: Documentation, https://github.com/sekil9529/queue-func#readme
|
|
20
|
+
Project-URL: Homepage, https://github.com/sekil9529/queue-func
|
|
21
|
+
Project-URL: Issues, https://github.com/sekil9529/queue-func/issues
|
|
22
|
+
Project-URL: Repository, https://github.com/sekil9529/queue-func
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# queue-func
|
|
26
|
+
|
|
27
|
+
把 **`async def` 协程** 变成可 **`push`** 的队列任务:在 asyncio 里投递、消费,并按通道控制并发与背压。
|
|
28
|
+
|
|
29
|
+
需要 **Python 3.12+**;任务入口必须是协程,不支持普通 `def`。
|
|
30
|
+
|
|
31
|
+
仓库:[github.com/sekil9529/queue-func](https://github.com/sekil9529/queue-func)
|
|
32
|
+
|
|
33
|
+
## 特点
|
|
34
|
+
|
|
35
|
+
- **asyncio 原生**:consumer 在同一事件循环里池化 `await` 任务,与 FastAPI / Starlette 等同进程组合很自然。
|
|
36
|
+
- **运行时聚焦队列链路**:`pack` → 投递 → `unpack` → 执行;生命周期由 `broker` / `QueueFuncRuntime` 显式管理。
|
|
37
|
+
- **环节可扩展**:通过 `register_backend` 接入新队列;`Serializer.pack` / `unpack` 决定队列里的数据结构。
|
|
38
|
+
- **每个 `QueueFunc` 独立消费并发**:各自 `consumer_pool_maxsize`。一个进程里可用 `QueueFuncRuntime` 同时跑多段流水线(例如 count → list → detail),**各段并发互不影响**。
|
|
39
|
+
|
|
40
|
+
## 架构设计
|
|
41
|
+
|
|
42
|
+
### Func
|
|
43
|
+
|
|
44
|
+
业务协程:一个 `async def`,用 `@queue` 装饰。
|
|
45
|
+
|
|
46
|
+
### Queue
|
|
47
|
+
|
|
48
|
+
装饰之后,这个 Func 就是一条队列,`push` 投到它自己这条队列上。
|
|
49
|
+
|
|
50
|
+
### Broker
|
|
51
|
+
|
|
52
|
+
组装 Producer、Consumer 的公有对象,提供对外接口。
|
|
53
|
+
|
|
54
|
+
### Serializer
|
|
55
|
+
|
|
56
|
+
控制队列中的数据结构:`pack` 写入,`unpack` 读出后还原成 `(args, kwargs)`。
|
|
57
|
+
|
|
58
|
+
- `raw.Serializer`:队列中保持 `(args, kwargs)`。
|
|
59
|
+
- `orjson_ser.Serializer`:队列中是 `bytes`。
|
|
60
|
+
|
|
61
|
+
### Producer
|
|
62
|
+
|
|
63
|
+
调用 `Serializer.pack` 后把数据写入队列。
|
|
64
|
+
|
|
65
|
+
### Consumer
|
|
66
|
+
|
|
67
|
+
从队列得到数据,`Serializer.unpack` 后交给 PoolExecutor 执行。
|
|
68
|
+
|
|
69
|
+
### PoolExecutor
|
|
70
|
+
|
|
71
|
+
按并发上限执行业务协程。
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### 组件关系
|
|
76
|
+
|
|
77
|
+
```mermaid
|
|
78
|
+
flowchart TB
|
|
79
|
+
PUSH["push"] --> B["Broker"]
|
|
80
|
+
B --> PRO["Producer"]
|
|
81
|
+
B -.->|启停| CON["Consumer"]
|
|
82
|
+
|
|
83
|
+
PRO -->|Serializer.pack| Q[("QUEUE")]
|
|
84
|
+
Q -->|Serializer.unpack| CON
|
|
85
|
+
CON --> POOL["PoolExecutor"]
|
|
86
|
+
POOL --> FN["async def"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 当前实现
|
|
90
|
+
|
|
91
|
+
| Backend | 说明 |
|
|
92
|
+
|---------|------|
|
|
93
|
+
| **`Backend.QUEUE`** | 进程内 `asyncio.Queue`(已可用) |
|
|
94
|
+
| RabbitMQ、Kafka 等 | 通过同一套 Broker / Producer / Consumer 扩展点接入(见 [Changelog.rst](Changelog.rst)) |
|
|
95
|
+
|
|
96
|
+
接入 MQ 后端后,producer / consumer 可部署在不同进程或机器;**现阶段发行版仅包含内存队列实现**。
|
|
97
|
+
|
|
98
|
+
## 快速开始
|
|
99
|
+
|
|
100
|
+
安装(开发):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git clone https://github.com/sekil9529/queue-func.git
|
|
104
|
+
cd queue-func
|
|
105
|
+
poetry install
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
单个 `QueueFunc`:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import asyncio
|
|
112
|
+
|
|
113
|
+
from queue_func.factory import Backend, queue
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@queue(Backend.QUEUE, consume_timeout=0.5, maxsize=1000)
|
|
117
|
+
async def process_order(order_id: int):
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def main():
|
|
122
|
+
await process_order.broker.start_consumer()
|
|
123
|
+
await process_order.push(args=(42,))
|
|
124
|
+
await process_order.broker.stop_consumer()
|
|
125
|
+
await process_order.broker.join_consumer()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
asyncio.run(main())
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `push` 等价于 `broker.publish`。不传 `timeout` 时用 `publish_timeout`;传入数字只改这一次。`None` 表示不限时。超时抛出 `queue_func.PublishTimeoutError`
|
|
132
|
+
- 仍可直接 `await process_order(1)`,不经过队列
|
|
133
|
+
|
|
134
|
+
多段流水线(各段独立队列与并发上限):
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from queue_func.factory import Backend, QueueFuncRuntime, queue
|
|
138
|
+
|
|
139
|
+
# task_count 内 await task_list.push(...),task_list 内 await task_detail.push(...)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def main():
|
|
143
|
+
runtime = QueueFuncRuntime([task_count, task_list, task_detail])
|
|
144
|
+
await runtime.start() # 按列表反序启动 consumer,下游先就绪
|
|
145
|
+
await task_count.push(args=(...,))
|
|
146
|
+
await runtime.stop()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
链式示例(含日志):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
poetry run python examples/demo_queue.py
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
常用配置:`maxsize` / `publish_timeout`(队列背压与投递等待)、`consumer_pool_maxsize`(同时执行的协程数)、`serializer_cls`(队列中的数据结构)。低级组装见 `queue_func.config.queue.Config` 与 `queue_func.broker.queue.Broker`。
|
|
156
|
+
|
|
157
|
+
## 项目结构
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
queue_func/
|
|
161
|
+
├── broker/
|
|
162
|
+
├── producer/
|
|
163
|
+
├── consumer/
|
|
164
|
+
├── serializer/ # raw、orjson
|
|
165
|
+
├── config/
|
|
166
|
+
├── factory/ # @queue、QueueFunc、QueueFuncRuntime
|
|
167
|
+
└── pool_executor/
|
|
168
|
+
examples/
|
|
169
|
+
└── demo_queue.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 开发与测试
|
|
173
|
+
|
|
174
|
+
版本与路线图:[Changelog.rst](Changelog.rst)
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pytest tests/ -q
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 许可证
|
|
181
|
+
|
|
182
|
+
Apache-2.0
|
|
183
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
queue_func/__init__.py,sha256=hL8i9WF-A8geNk7K4ZpMCxB0yIVSuNVRhGghksg-sk0,116
|
|
2
|
+
queue_func/broker/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
3
|
+
queue_func/broker/base.py,sha256=6S4cVIbAAC60CVAfj5yfuN0eRJbqRlXNFuUcy1oZfLY,2120
|
|
4
|
+
queue_func/broker/queue.py,sha256=k82CKXmZZOcALH0P1xxiidrVkgJpelwHCPnMpw30Fqg,1373
|
|
5
|
+
queue_func/config/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
6
|
+
queue_func/config/general.py,sha256=CMB2_ivstZoRW6Y0QY7hBxR3GdgTP3wS-Y2PepXhNBk,832
|
|
7
|
+
queue_func/config/queue.py,sha256=yXETTp4nLPkaQ38J2G5iHsmRHREEVu50oWsbqsNU5u0,497
|
|
8
|
+
queue_func/config/utils.py,sha256=DOZnwPkjMsfBVfZVkHEEK1sa-ZVbPjn9gwHFAjTn2UA,263
|
|
9
|
+
queue_func/consumer/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
10
|
+
queue_func/consumer/base.py,sha256=VtfuEhKY2QgQBwDrc_YD1oTT2RWf7KWGw-LfsNs87Ww,2919
|
|
11
|
+
queue_func/consumer/queue.py,sha256=v40AG2IkLSlALhH4JAf9EuToVaB2pQHaw7MIrrxxlVo,1606
|
|
12
|
+
queue_func/errors.py,sha256=3KPNWM0HxC-hwYdrt9UNGrTHLxWYVYOCNEKW_08z6o0,120
|
|
13
|
+
queue_func/factory/__init__.py,sha256=7sSCKz_-rx29nJuNFAZlvKro8PuruvZ2ArGcDFFKNb8,269
|
|
14
|
+
queue_func/factory/backend.py,sha256=dWfxozQsm_uTaVyCgVRL8llUzTFQLmYeJRx3fjq1rng,148
|
|
15
|
+
queue_func/factory/builders/__init__.py,sha256=149GwvM0jyWl-CmdjBb8XRH1LFibofGA_6MbxuaE1Mc,187
|
|
16
|
+
queue_func/factory/builders/queue.py,sha256=Yxa8ySiqZurFrCSl5m9OBS6yKv6MF-joyTepkYbDdio,534
|
|
17
|
+
queue_func/factory/deco.py,sha256=g0lwkh58J3GLfIS4rk08fxgAzrWT1AhrKA56Bh5KOpo,2736
|
|
18
|
+
queue_func/factory/registry.py,sha256=RN8PgcfkpM1pDFCYqOab-AYq6379IVEyAurtkgtDHwM,1027
|
|
19
|
+
queue_func/factory/runtime.py,sha256=A5CUSVRi4YtlRnroOc-FVeWEbPgEGQ8A-gR49ZgRz8c,958
|
|
20
|
+
queue_func/pool_executor/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
21
|
+
queue_func/pool_executor/base.py,sha256=reYgENOa9bRMBKfzvlPP_aTMPSXFCZaNStn6vPwnWak,585
|
|
22
|
+
queue_func/pool_executor/general.py,sha256=MzYZUKNom-xcoop_BBJpma2LPuV3pnnpyWKno5KWa-s,1110
|
|
23
|
+
queue_func/pool_executor/utils.py,sha256=FQR6nE2IVPk_rgfEzQEh6ZeelTc3iqeJegKZxN7EM0c,1042
|
|
24
|
+
queue_func/producer/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
25
|
+
queue_func/producer/base.py,sha256=mqzoJPhZwGJ3n1h-_AzHvMzMhSyHZYYP_sTItrQZaMg,1123
|
|
26
|
+
queue_func/producer/queue.py,sha256=ezbIqGEq40DiDxO8NZ3sr_8gEwqlWvkwwe8-dYvD_O0,942
|
|
27
|
+
queue_func/serializer/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
28
|
+
queue_func/serializer/base.py,sha256=B22gUVHW34dFwn8t1c2ruc6rVNGfQApr20OOBmmCo3M,529
|
|
29
|
+
queue_func/serializer/orjson_ser.py,sha256=8DlCHFQzg46yPubIdSTKS6qp452QAzDeceTqXChQb5E,601
|
|
30
|
+
queue_func/serializer/raw.py,sha256=KkwgkfbqRKSIHDLawFpTR-wafjdY_K4qba2tNgkoOxg,525
|
|
31
|
+
queue_func/task/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
32
|
+
queue_func/task/base.py,sha256=JblRVlT5go9ztFXsv0-e25U7Lz27yUIOHe4oszCNBHA,649
|
|
33
|
+
queue_func/task/thread.py,sha256=OIGG0H_z_pDWYHvjSCX-PyvGovuob8wJaaUaUg6PD1M,443
|
|
34
|
+
queue_func-0.1.2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
35
|
+
queue_func-0.1.2.dist-info/METADATA,sha256=-TmQbE2RxRLo6dpvPrT5NYt9nkHjJwUX8yKt3V1rUvk,5378
|
|
36
|
+
queue_func-0.1.2.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
37
|
+
queue_func-0.1.2.dist-info/RECORD,,
|
queue_func/message/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
queue_func/message/base.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
|
|
3
|
-
import abc
|
|
4
|
-
import typing as t
|
|
5
|
-
|
|
6
|
-
from queue_func.serializer.base import BaseSerializer
|
|
7
|
-
|
|
8
|
-
Packed = t.TypeVar("Packed")
|
|
9
|
-
|
|
10
|
-
type ArgsMessage = tuple[tuple[t.Any], dict[str, t.Any]]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class BaseMessage(t.Generic[Packed], metaclass=abc.ABCMeta):
|
|
14
|
-
|
|
15
|
-
__slots__ = (
|
|
16
|
-
"_enable_serializer",
|
|
17
|
-
"_serializer",
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
message_serializer_cls: type[BaseSerializer],
|
|
23
|
-
enable_serializer: bool):
|
|
24
|
-
|
|
25
|
-
self._serializer = message_serializer_cls()
|
|
26
|
-
self._enable_serializer = enable_serializer
|
|
27
|
-
|
|
28
|
-
@abc.abstractmethod
|
|
29
|
-
def pack(
|
|
30
|
-
self,
|
|
31
|
-
coro_args: tuple[t.Any],
|
|
32
|
-
coro_kwargs: dict[str, t.Any] | None = None) -> Packed:
|
|
33
|
-
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
@abc.abstractmethod
|
|
37
|
-
def unpack(self, message: Packed) -> ArgsMessage:
|
|
38
|
-
|
|
39
|
-
pass
|
queue_func/message/general.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
|
|
3
|
-
import typing as t
|
|
4
|
-
|
|
5
|
-
from queue_func.message.base import ArgsMessage, BaseMessage
|
|
6
|
-
|
|
7
|
-
type PackedMessage = ArgsMessage | bytes
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Message(BaseMessage[PackedMessage]):
|
|
11
|
-
|
|
12
|
-
__slots__ = ()
|
|
13
|
-
|
|
14
|
-
def pack(
|
|
15
|
-
self,
|
|
16
|
-
coro_args: tuple[t.Any],
|
|
17
|
-
coro_kwargs: dict[str, t.Any] | None = None) -> PackedMessage:
|
|
18
|
-
|
|
19
|
-
if coro_kwargs is None:
|
|
20
|
-
coro_kwargs = {}
|
|
21
|
-
message: ArgsMessage = (coro_args, coro_kwargs)
|
|
22
|
-
if self._enable_serializer:
|
|
23
|
-
return self._serializer.dumps(message, return_bytes=True)
|
|
24
|
-
return message
|
|
25
|
-
|
|
26
|
-
def unpack(self, message: PackedMessage) -> ArgsMessage:
|
|
27
|
-
|
|
28
|
-
if self._enable_serializer:
|
|
29
|
-
return t.cast(ArgsMessage, self._serializer.loads(message))
|
|
30
|
-
return t.cast(ArgsMessage, message)
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import typing as t
|
|
5
|
-
|
|
6
|
-
from queue_func.serializer.base import BaseSerializer
|
|
7
|
-
from queue_func.serializer.utils import default as _default
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class JSONSerializer(BaseSerializer):
|
|
11
|
-
|
|
12
|
-
def __init__(self, default: t.Optional[t.Callable[[t.Any], t.Any]] = None):
|
|
13
|
-
self._default = default or _default
|
|
14
|
-
|
|
15
|
-
def _dumps_to_str(self, data: t.Any) -> str:
|
|
16
|
-
|
|
17
|
-
return json.dumps(data, default=self._default)
|
|
18
|
-
|
|
19
|
-
def _dumps_to_bytes(self, data: t.Any) -> bytes:
|
|
20
|
-
|
|
21
|
-
return self._dumps_to_str(data).encode("utf-8")
|
|
22
|
-
|
|
23
|
-
def loads(self, data: t.Union[str, bytes]) -> t.Any:
|
|
24
|
-
|
|
25
|
-
return json.loads(data)
|
queue_func/serializer/utils.py
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
|
|
3
|
-
import typing as t
|
|
4
|
-
from decimal import Decimal
|
|
5
|
-
from datetime import datetime, date
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def default(o: t.Any) -> t.Any:
|
|
9
|
-
|
|
10
|
-
if isinstance(o, Decimal):
|
|
11
|
-
return str(o)
|
|
12
|
-
if isinstance(o, datetime):
|
|
13
|
-
return o.strftime("%Y-%m-%d %H:%M:%S")
|
|
14
|
-
if isinstance(o, date):
|
|
15
|
-
return o.strftime("%Y-%m-%d")
|
|
16
|
-
raise TypeError(f"Object of type {o.__class__.__name__} is not JSON serializable")
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: queue-func
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: Async coroutine functions as queue-backed tasks (QueueFunc).
|
|
5
|
-
License: Apache-2.0
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Author: liuseki
|
|
8
|
-
Author-email: liuseki@163.com
|
|
9
|
-
Requires-Python: >=3.12,<4.0
|
|
10
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
-
Requires-Dist: orjson
|
|
17
|
-
Requires-Dist: pydantic (>=2.14.1,<3.0.0)
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
|
|
20
|
-
# queue-func
|
|
21
|
-
|
|
22
|
-
**把 async 协程当成队列里的任务来投递与消费**——进程内用内存队列跑通链路,并为 RabbitMQ、Kafka 等后端预留同一套 Broker / Producer / Consumer 抽象。
|
|
23
|
-
|
|
24
|
-
> 仅支持 **`async def`**,不支持普通同步函数。需要 **Python 3.12+**。
|
|
25
|
-
|
|
26
|
-
## 关键概念
|
|
27
|
-
|
|
28
|
-
1. **`@queue`**:把协程包成 `QueueFunc`,提供 `push(args=..., kwargs=...)` 与 `broker`。
|
|
29
|
-
2. **消息**:`(args, kwargs)` 经 `Message` 打包;可选 JSON / orjson 序列化。
|
|
30
|
-
3. **消费**:Consumer 从通道取消息,经池化 `await` 你的协程(`consumer_pool_maxsize` 限并发)。
|
|
31
|
-
|
|
32
|
-
运行期的 `publish` / `consume` 在 Producer / Consumer 内完成;Broker 主要负责装配与生命周期,不做「万事中转」。
|
|
33
|
-
|
|
34
|
-
## 快速开始
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
poetry install
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
单个任务:
|
|
41
|
-
|
|
42
|
-
```python
|
|
43
|
-
import asyncio
|
|
44
|
-
|
|
45
|
-
from queue_func.factory import Backend, queue
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@queue(Backend.QUEUE, enable_message_serializer=False, consume_timeout=0.5, maxsize=1000)
|
|
49
|
-
async def process_order(order_id: int):
|
|
50
|
-
...
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
async def main():
|
|
54
|
-
await process_order.broker.start_consumer()
|
|
55
|
-
await process_order.push(args=(42,))
|
|
56
|
-
await process_order.broker.stop_consumer()
|
|
57
|
-
await process_order.broker.join_consumer()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
asyncio.run(main())
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
- `push` → `broker.publish(coro_args=..., coro_kwargs=...)`
|
|
64
|
-
- 仍可直接 `await process_order(1)` 做本地调用(不经队列)
|
|
65
|
-
|
|
66
|
-
多个 `QueueFunc`(各自独立队列)用 **`QueueFuncRuntime`** 统一启停;`start()` 按注册列表**反序**启动 consumer(下游先就绪,便于链式 `push`):
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
from queue_func.factory import Backend, QueueFuncRuntime, queue
|
|
70
|
-
|
|
71
|
-
# task_a 内 await task_b.push(...),task_b 内 await task_c.push(...) 等
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async def main():
|
|
75
|
-
runtime = QueueFuncRuntime([task_a, task_b, task_c])
|
|
76
|
-
await runtime.start()
|
|
77
|
-
await task_a.push(args=("job-1",))
|
|
78
|
-
await runtime.stop()
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
可运行示例(a → b → c 链式投递 + 控制台日志):
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
poetry run python examples/demo_queue.py
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
需要自行组装通道时,使用 `Config` + `queue_func.broker.queue.Broker`(见 `queue_func/config/`)。
|
|
88
|
-
|
|
89
|
-
## 使用场景
|
|
90
|
-
|
|
91
|
-
| 场景 | 说明 |
|
|
92
|
-
|------|------|
|
|
93
|
-
| 同进程解耦 + 控并发 | Handler 里 `push`,后台 consumer 跑重逻辑;`maxsize` / `publish_timeout` 做背压 |
|
|
94
|
-
| 多段流水线 | 多个 `QueueFunc`,在协程里继续 `push` 到下一环;用 `QueueFuncRuntime` 管生命周期 |
|
|
95
|
-
| 先单机、后换 MQ | 打开序列化后消息已是 bytes,后续可换 Broker 实现,协程签名尽量不变 |
|
|
96
|
-
| 可测 | Producer / Consumer / `push` 副作用分开测;`tests/` 有集成示例 |
|
|
97
|
-
|
|
98
|
-
**规划中**:`Backend` 除内存队列外的 RabbitMQ / Kafka(**当前仅 `Backend.QUEUE` 可用**)。
|
|
99
|
-
|
|
100
|
-
**不太适合**:同步任务、分布式调度/结果后端/Celery 级平台、无界堆积不限流、纯 CPU 密集(应另配进程池或 worker)。
|
|
101
|
-
|
|
102
|
-
## 项目结构
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
queue_func/
|
|
106
|
-
broker/ producer/ consumer/ message/ config/
|
|
107
|
-
factory/ # Backend、queue、QueueFunc、QueueFuncRuntime
|
|
108
|
-
pool_executor/ serializer/
|
|
109
|
-
examples/demo_queue.py
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## 开发与测试
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
pytest tests/ -q
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## 许可证
|
|
119
|
-
|
|
120
|
-
Apache-2.0
|
|
121
|
-
|
queue_func-0.1.dist-info/RECORD
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
queue_func/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
2
|
-
queue_func/broker/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
3
|
-
queue_func/broker/base.py,sha256=Y0MMZGSHfm54LOWkso6OaP5UmRD0Gs94QYa6eyY8aHk,2192
|
|
4
|
-
queue_func/broker/queue.py,sha256=yG2C_DQ5FhxTTPDw3lNI-jKLRAGEkQeSG750lzg2uoc,1353
|
|
5
|
-
queue_func/config/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
6
|
-
queue_func/config/general.py,sha256=_G-gfatX8Sfhb9vbEo-RPgOjcn_ctTgn5yDA0xdbZ2Y,1039
|
|
7
|
-
queue_func/config/queue.py,sha256=A8pTbKUaee-653eeby4jqpxdwEwg6EowzPRrHh-1fRo,337
|
|
8
|
-
queue_func/config/utils.py,sha256=DOZnwPkjMsfBVfZVkHEEK1sa-ZVbPjn9gwHFAjTn2UA,263
|
|
9
|
-
queue_func/consumer/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
10
|
-
queue_func/consumer/base.py,sha256=wuCPoB0zUgxhF3GBoKF33Zq43LJFIugzoiTRQeccNrE,2889
|
|
11
|
-
queue_func/consumer/queue.py,sha256=bDf3qYCCTnT3CFazWdz3ivvWUsvqm0UpKa9qibrqi54,1579
|
|
12
|
-
queue_func/factory/__init__.py,sha256=7sSCKz_-rx29nJuNFAZlvKro8PuruvZ2ArGcDFFKNb8,269
|
|
13
|
-
queue_func/factory/backend.py,sha256=dWfxozQsm_uTaVyCgVRL8llUzTFQLmYeJRx3fjq1rng,148
|
|
14
|
-
queue_func/factory/builders/__init__.py,sha256=149GwvM0jyWl-CmdjBb8XRH1LFibofGA_6MbxuaE1Mc,187
|
|
15
|
-
queue_func/factory/builders/queue.py,sha256=Yxa8ySiqZurFrCSl5m9OBS6yKv6MF-joyTepkYbDdio,534
|
|
16
|
-
queue_func/factory/deco.py,sha256=cD_aQssAGgvOgC9ioJNTLBT_JaHTnYYnKgNAXdJc100,2834
|
|
17
|
-
queue_func/factory/registry.py,sha256=RN8PgcfkpM1pDFCYqOab-AYq6379IVEyAurtkgtDHwM,1027
|
|
18
|
-
queue_func/factory/runtime.py,sha256=A5CUSVRi4YtlRnroOc-FVeWEbPgEGQ8A-gR49ZgRz8c,958
|
|
19
|
-
queue_func/message/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
20
|
-
queue_func/message/base.py,sha256=cVQwFDKWpX5v7MVYzPYko5LPHg9txsJ2vBqkHtjwyNo,877
|
|
21
|
-
queue_func/message/general.py,sha256=GU9Enr1UMyM1cFV4yIdM0qVTFPlG3260AWIMxkfnC1U,842
|
|
22
|
-
queue_func/pool_executor/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
23
|
-
queue_func/pool_executor/base.py,sha256=reYgENOa9bRMBKfzvlPP_aTMPSXFCZaNStn6vPwnWak,585
|
|
24
|
-
queue_func/pool_executor/general.py,sha256=MzYZUKNom-xcoop_BBJpma2LPuV3pnnpyWKno5KWa-s,1110
|
|
25
|
-
queue_func/pool_executor/utils.py,sha256=FQR6nE2IVPk_rgfEzQEh6ZeelTc3iqeJegKZxN7EM0c,1042
|
|
26
|
-
queue_func/producer/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
27
|
-
queue_func/producer/base.py,sha256=oVJvFTz1nCGGSEl76Xj-_Z03_pxIZSveU2Zstk1pH-E,809
|
|
28
|
-
queue_func/producer/queue.py,sha256=dlrTh88j0344PG5vhhobyKLrTG825H5dSZI3DW5HOfc,791
|
|
29
|
-
queue_func/serializer/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
30
|
-
queue_func/serializer/base.py,sha256=yBIVEZ3XImKOuFAqnlu5trrbV9pDvTC_2nF7tkBHawI,913
|
|
31
|
-
queue_func/serializer/json_ser.py,sha256=S3HTxWDLSuyKi5g9SBLigNxUCpbmGyJwBqmOYqIcVMM,661
|
|
32
|
-
queue_func/serializer/orjson_ser.py,sha256=NAYd74TCjg8lPhbcCrNNO_qVHEPt89SJqgR3j0Lk7xI,716
|
|
33
|
-
queue_func/serializer/utils.py,sha256=ozUCUw6-8eMqYqljFcykoEb-NaLAEyBj3dj0YcAnuyY,436
|
|
34
|
-
queue_func/task/__init__.py,sha256=7DxiEpT1g6hMwW1hBF17OOFRJ3DW03giEu6rgvRCTHY,17
|
|
35
|
-
queue_func/task/base.py,sha256=JblRVlT5go9ztFXsv0-e25U7Lz27yUIOHe4oszCNBHA,649
|
|
36
|
-
queue_func/task/thread.py,sha256=OIGG0H_z_pDWYHvjSCX-PyvGovuob8wJaaUaUg6PD1M,443
|
|
37
|
-
queue_func-0.1.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
38
|
-
queue_func-0.1.dist-info/METADATA,sha256=xnUtrdkPN84tLNrQjRKxELL6O1C2KhIvscLIcwvWuKk,3876
|
|
39
|
-
queue_func-0.1.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
40
|
-
queue_func-0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|