asyncapi-python 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.
Files changed (31) hide show
  1. asyncapi_python/__init__.py +0 -0
  2. asyncapi_python/amqp/__init__.py +19 -0
  3. asyncapi_python/amqp/base_application.py +61 -0
  4. asyncapi_python/amqp/connection.py +42 -0
  5. asyncapi_python/amqp/consumer.py +80 -0
  6. asyncapi_python/amqp/message_handler.py +89 -0
  7. asyncapi_python/amqp/message_handler_params.py +62 -0
  8. asyncapi_python/amqp/producer.py +103 -0
  9. asyncapi_python/amqp/utils.py +27 -0
  10. asyncapi_python/py.typed +0 -0
  11. asyncapi_python-0.1.0.dist-info/LICENSE +201 -0
  12. asyncapi_python-0.1.0.dist-info/METADATA +38 -0
  13. asyncapi_python-0.1.0.dist-info/RECORD +31 -0
  14. asyncapi_python-0.1.0.dist-info/WHEEL +4 -0
  15. asyncapi_python-0.1.0.dist-info/entry_points.txt +3 -0
  16. asyncapi_python_codegen/__init__.py +54 -0
  17. asyncapi_python_codegen/document/__init__.py +18 -0
  18. asyncapi_python_codegen/document/base.py +16 -0
  19. asyncapi_python_codegen/document/bindings/__init__.py +21 -0
  20. asyncapi_python_codegen/document/bindings/amqp.py +46 -0
  21. asyncapi_python_codegen/document/components.py +83 -0
  22. asyncapi_python_codegen/document/document.py +56 -0
  23. asyncapi_python_codegen/document/document_context.py +38 -0
  24. asyncapi_python_codegen/document/ref.py +95 -0
  25. asyncapi_python_codegen/generators/__init__.py +16 -0
  26. asyncapi_python_codegen/generators/amqp/__init__.py +16 -0
  27. asyncapi_python_codegen/generators/amqp/generate.py +160 -0
  28. asyncapi_python_codegen/generators/amqp/templates/__init__.py.j2 +14 -0
  29. asyncapi_python_codegen/generators/amqp/templates/application.py.j2 +100 -0
  30. asyncapi_python_codegen/generators/amqp/utils.py +42 -0
  31. asyncapi_python_codegen/py.typed +0 -0
File without changes
@@ -0,0 +1,19 @@
1
+ # Copyright 2024 Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from .connection import channel_pool, AmqpPool
17
+ from .consumer import Consumer
18
+ from .producer import Producer
19
+ from .base_application import BaseApplication
@@ -0,0 +1,61 @@
1
+ # Copyright 2024 Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from .connection import channel_pool, AmqpPool
17
+ from .consumer import Consumer
18
+ from .producer import Producer
19
+ from abc import ABC, abstractmethod
20
+ from typing import Literal, TypedDict
21
+
22
+
23
+ class Queue(TypedDict):
24
+ name: str | None
25
+ durable: bool
26
+ exclusive: bool
27
+ auto_delete: bool
28
+
29
+
30
+ class Exchange(TypedDict):
31
+ name: str | None
32
+ type: Literal["topic", "direct", "fanout", "default", "headers"]
33
+ durable: bool
34
+ auto_delete: bool
35
+
36
+
37
+ class BaseApplication(ABC):
38
+ def __init__(self, amqp_uri: str):
39
+ self._uri = amqp_uri
40
+ self._has_started = False
41
+ self._pool = channel_pool(self._uri)
42
+ self._consumer = Consumer(self._pool)
43
+
44
+ def _assert_started(self):
45
+ if not self._has_started:
46
+ cls_name = self.__class__.__name__
47
+ raise AssertionError(
48
+ f"Invoke of {cls_name}::request or {cls_name}::publish "
49
+ + "occurred before {cls_name}::start"
50
+ )
51
+
52
+ async def start(self, blocking: bool = True):
53
+ async with self._pool.acquire() as ch:
54
+ reply_queue = await ch.declare_queue(exclusive=True)
55
+ self._producer = Producer(self._pool, reply_queue)
56
+ await self._producer.run()
57
+ self._has_started = True
58
+ if blocking:
59
+ await self._consumer.run_blocking(timeout=None)
60
+ else:
61
+ await self._consumer.run()
@@ -0,0 +1,42 @@
1
+ # Copyright 2024 Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from functools import cache
17
+ from aio_pika.robust_connection import (
18
+ AbstractRobustConnection,
19
+ AbstractRobustChannel,
20
+ connect_robust,
21
+ )
22
+ from aio_pika.pool import Pool
23
+
24
+
25
+ @cache
26
+ def connection_pool(amqp_uri: str) -> Pool[AbstractRobustConnection]:
27
+ async def get_connection():
28
+ return await connect_robust(amqp_uri)
29
+
30
+ return Pool(get_connection, max_size=2)
31
+
32
+
33
+ @cache
34
+ def channel_pool(amqp_uri: str) -> Pool[AbstractRobustChannel]:
35
+ async def get_channel():
36
+ async with connection_pool(amqp_uri).acquire() as connection:
37
+ return await connection.channel()
38
+
39
+ return Pool(get_channel, max_size=10)
40
+
41
+
42
+ AmqpPool = Pool[AbstractRobustChannel]
@@ -0,0 +1,80 @@
1
+ # Copyright 2024 Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from .message_handler import AbstractMessageHandler, MessageHandler, RpcMessageHandler
17
+ from .message_handler_params import MessageHandlerParams
18
+ from .utils import encode_message, decode_message
19
+
20
+ import asyncio
21
+ from aio_pika import Message
22
+ from aio_pika.pool import Pool
23
+ from aio_pika.abc import AbstractRobustChannel
24
+ from asyncio import Future
25
+ from typing import Callable, TypeVar
26
+ from pydantic import BaseModel
27
+ from logging import getLogger
28
+
29
+ T = TypeVar("T", bound=BaseModel)
30
+ U = TypeVar("U", bound=BaseModel)
31
+
32
+
33
+ class Consumer:
34
+ def __init__(self, channel_pool: Pool[AbstractRobustChannel]):
35
+ self._handlers: dict[MessageHandlerParams, AbstractMessageHandler] = {}
36
+ self._logger = getLogger(__name__)
37
+ self._pool = channel_pool
38
+
39
+ async def run_blocking(self, timeout: int | float | None):
40
+ await self.run()
41
+ if timeout is not None:
42
+ await asyncio.sleep(timeout)
43
+ else:
44
+ await Future()
45
+
46
+ async def run(self):
47
+ async with self._pool.acquire() as channel:
48
+ for params, handler in self._handlers.items():
49
+ await params.setup_consume(handler, channel)
50
+
51
+ async def _reply_callback(self, message: Message, routing_key: str):
52
+ async with self._pool.acquire() as channel:
53
+ await channel.default_exchange.publish(message, routing_key)
54
+
55
+ def on(
56
+ self,
57
+ *,
58
+ params: MessageHandlerParams,
59
+ input_type: type[T],
60
+ output_type: type[U] | None,
61
+ callback: Callable,
62
+ ):
63
+ handler: AbstractMessageHandler
64
+ if params in self._handlers:
65
+ raise AssertionError(f"Only one handler for `{params}` is allowed")
66
+ if output_type is None:
67
+ handler = MessageHandler(
68
+ name=params.root.name,
69
+ callback=callback,
70
+ decode_message=lambda x: decode_message(x, input_type),
71
+ )
72
+ else:
73
+ handler = RpcMessageHandler(
74
+ name=params.root.name,
75
+ callback=callback,
76
+ reply_callback=self._reply_callback,
77
+ encode_message=encode_message,
78
+ decode_message=lambda x: decode_message(x, input_type),
79
+ )
80
+ self._handlers[params] = handler
@@ -0,0 +1,89 @@
1
+ # Copyright 2024 Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from abc import ABC, abstractmethod
17
+ from aio_pika.message import AbstractIncomingMessage, Message
18
+ from typing import Awaitable, Callable, Generic, TypeVar
19
+ from pydantic import BaseModel
20
+ from logging import getLogger
21
+
22
+
23
+ T = TypeVar("T", bound=BaseModel)
24
+ U = TypeVar("U", bound=BaseModel | None)
25
+ V = TypeVar("V", bound=BaseModel)
26
+
27
+
28
+ class AbstractMessageHandler(ABC, Generic[T, U]):
29
+ def __init__(self, name: str, callback: Callable[[T], Awaitable[U]]):
30
+ self._logger = getLogger(__name__)
31
+ self._name = name
32
+ self._callback = callback
33
+
34
+ async def __call__(self, message: AbstractIncomingMessage) -> None:
35
+ cls = f"{self.__class__.__name__}#{self._name}"
36
+ self._logger.info(f"{cls}: got message: {message.info()}")
37
+ self._logger.debug(f"content: {message.body!r}")
38
+ await self.on_call(message)
39
+ await message.ack()
40
+ self._logger.info(f"{cls} finished message: {message.info()}")
41
+
42
+ @abstractmethod
43
+ async def on_call(self, message: AbstractIncomingMessage) -> None:
44
+ raise NotImplementedError
45
+
46
+
47
+ class MessageHandler(AbstractMessageHandler[T, None]):
48
+ def __init__(
49
+ self,
50
+ name: str,
51
+ callback: Callable[[T], Awaitable[None]],
52
+ decode_message: Callable[[bytes], T],
53
+ ):
54
+ super().__init__(name, callback)
55
+ self._decode_message = decode_message
56
+
57
+ async def on_call(self, message: AbstractIncomingMessage) -> None:
58
+ message_body = self._decode_message(message.body)
59
+ await self._callback(message_body)
60
+
61
+
62
+ class RpcMessageHandler(AbstractMessageHandler[T, V]):
63
+ def __init__(
64
+ self,
65
+ name: str,
66
+ callback: Callable[[T], Awaitable[V]],
67
+ reply_callback: Callable[[Message, str], Awaitable[None]],
68
+ decode_message: Callable[[bytes], T],
69
+ encode_message: Callable[[V], bytes],
70
+ ):
71
+ super().__init__(name, callback)
72
+ self._reply_callback = reply_callback
73
+ self._decode_message = decode_message
74
+ self._encode_message = encode_message
75
+
76
+ async def on_call(self, message: AbstractIncomingMessage) -> None:
77
+ if message.correlation_id is None:
78
+ raise AssertionError("RPC Call got empty correlation_id")
79
+ if message.reply_to is None:
80
+ raise AssertionError("RPC Call got empty reply_to header")
81
+ message_body = self._decode_message(message.body)
82
+ result = await self._callback(message_body)
83
+ await self._reply_callback(
84
+ Message(
85
+ self._encode_message(result),
86
+ correlation_id=message.correlation_id,
87
+ ),
88
+ message.reply_to,
89
+ )
@@ -0,0 +1,62 @@
1
+ # Copyright 2024 Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from pydantic import RootModel, BaseModel, ConfigDict, computed_field
17
+ from typing import Literal
18
+ from aio_pika.abc import AbstractRobustChannel
19
+ from .message_handler import AbstractMessageHandler
20
+
21
+
22
+ class ExchangeHandlerParams(BaseModel):
23
+ model_config = ConfigDict(frozen=True)
24
+ kind: Literal["exchange"] = "exchange"
25
+ type: Literal["direct", "fanout", "topic", "headers"]
26
+ name: str
27
+ routing_key: str | None
28
+ auto_delete: bool = False
29
+
30
+
31
+ class QueueHandlerParams(BaseModel):
32
+ model_config = ConfigDict(frozen=True)
33
+ kind: Literal["queue"] = "queue"
34
+ name: str
35
+ exclusive: bool = False
36
+ auto_delete: bool = False
37
+ durable: bool = False
38
+
39
+
40
+ class MessageHandlerParams(RootModel):
41
+ model_config = ConfigDict(frozen=True)
42
+ root: QueueHandlerParams | ExchangeHandlerParams
43
+
44
+ async def setup_consume(
45
+ self,
46
+ handler: AbstractMessageHandler,
47
+ channel: AbstractRobustChannel,
48
+ ):
49
+ match self.root:
50
+ case ExchangeHandlerParams(
51
+ name=name, routing_key=rk, type=et, auto_delete=ad
52
+ ):
53
+ exchange = await channel.declare_exchange(name, type=et, auto_delete=ad)
54
+ queue = await channel.declare_queue(exclusive=True)
55
+ await queue.bind(exchange, rk)
56
+ case QueueHandlerParams(name=name, exclusive=ex, auto_delete=ad, durable=d):
57
+ queue = await channel.declare_queue(
58
+ name, exclusive=ex, auto_delete=ad, durable=d
59
+ )
60
+ case _:
61
+ raise NotImplementedError
62
+ await queue.consume(handler)
@@ -0,0 +1,103 @@
1
+ # Copyright 2024 Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from aio_pika import Message
17
+ from aio_pika.pool import Pool
18
+ from aio_pika.abc import (
19
+ AbstractRobustChannel,
20
+ AbstractRobustQueue,
21
+ AbstractIncomingMessage,
22
+ )
23
+ from logging import getLogger
24
+ from pydantic import BaseModel
25
+ from typing import TypeVar
26
+ from asyncio import Future
27
+ from uuid import uuid4
28
+ from .utils import encode_message, decode_message
29
+
30
+ T = TypeVar("T", bound=BaseModel)
31
+ U = TypeVar("U", bound=BaseModel)
32
+
33
+
34
+ class Producer:
35
+ def __init__(
36
+ self,
37
+ channel_pool: Pool[AbstractRobustChannel],
38
+ reply_queue: AbstractRobustQueue,
39
+ ):
40
+ if not reply_queue.exclusive:
41
+ raise AssertionError("Reply queue must be exclusive")
42
+
43
+ self._logger = getLogger(__name__)
44
+ self._pool = channel_pool
45
+ self._replies: dict[str, Future[AbstractIncomingMessage]] = {}
46
+ self._reply_queue = reply_queue
47
+ self._reply_consumer_tag: str | None = None
48
+
49
+ async def _on_reply(self, msg: AbstractIncomingMessage):
50
+ await msg.ack()
51
+ if msg.correlation_id is None or msg.correlation_id not in self._replies:
52
+ return
53
+ future = self._replies.pop(msg.correlation_id)
54
+ future.set_result(msg)
55
+
56
+ async def run(self):
57
+ self._reply_consumer_tag = await self._reply_queue.consume(self._on_reply)
58
+
59
+ async def publish(
60
+ self,
61
+ message: T,
62
+ exchange: str | None,
63
+ routing_key: str | None,
64
+ ):
65
+ outbound_message = Message(
66
+ body=encode_message(message),
67
+ )
68
+ async with self._pool.acquire() as channel:
69
+ await (
70
+ await channel.get_exchange(exchange)
71
+ if exchange is not None
72
+ else channel.default_exchange
73
+ ).publish(outbound_message, routing_key or "")
74
+
75
+ async def request(
76
+ self,
77
+ message: T,
78
+ exchange: str | None,
79
+ routing_key: str | None,
80
+ output_type: type[U],
81
+ ) -> U:
82
+ if not self._reply_consumer_tag:
83
+ raise AssertionError(
84
+ "Cannot make requests that expect replies before Consumer::start is called"
85
+ )
86
+ corr_id = str(uuid4())
87
+ outbound_message = Message(
88
+ body=encode_message(message),
89
+ correlation_id=corr_id,
90
+ reply_to=self._reply_queue.name,
91
+ )
92
+ reply_future = Future[AbstractIncomingMessage]()
93
+ async with self._pool.acquire() as channel:
94
+ await (
95
+ await channel.get_exchange(exchange)
96
+ if exchange is not None
97
+ else channel.default_exchange
98
+ ).publish(outbound_message, routing_key or "")
99
+ self._logger.info(f"Sent request {message}")
100
+ self._replies[corr_id] = reply_future
101
+ res = decode_message((await reply_future).body, output_type)
102
+ self._logger.info(f"Got response {res}")
103
+ return res
@@ -0,0 +1,27 @@
1
+ # Copyright 2024 Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from pydantic import BaseModel
17
+ from typing import TypeVar
18
+
19
+ T = TypeVar("T", bound=BaseModel)
20
+
21
+
22
+ def encode_message(message: T) -> bytes:
23
+ return message.model_dump_json().encode()
24
+
25
+
26
+ def decode_message(message: bytes, schema: type[T]) -> T:
27
+ return schema.model_validate_json(message)
File without changes