asyncapi-python 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asyncapi_python-0.1.2/PKG-INFO +97 -0
- asyncapi_python-0.1.2/README.md +70 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/pyproject.toml +15 -12
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/base_application.py +4 -5
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/consumer.py +15 -11
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/message_handler.py +2 -2
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/message_handler_params.py +20 -16
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/producer.py +18 -16
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/utils.py +10 -2
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/__init__.py +6 -7
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/bindings/amqp.py +4 -4
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/components.py +15 -29
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/document.py +2 -2
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/ref.py +40 -22
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/generators/amqp/generate.py +100 -48
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/generators/amqp/templates/application.py.j2 +9 -8
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/generators/amqp/utils.py +9 -8
- asyncapi_python-0.1.0/PKG-INFO +0 -38
- asyncapi_python-0.1.0/README.md +0 -13
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/LICENSE +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/__init__.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/__init__.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/connection.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/py.typed +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/__init__.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/base.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/bindings/__init__.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/document_context.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/generators/__init__.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/generators/amqp/__init__.py +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/generators/amqp/templates/__init__.py.j2 +0 -0
- {asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/py.typed +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: asyncapi-python
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Easily generate type-safe and async Python applications from AsyncAPI 3 specifications.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Author: Yaroslav Petrov
|
|
7
|
+
Author-email: yaroslav.v.petrov@gmail.com
|
|
8
|
+
Requires-Python: >=3.9,<3.13
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Provides-Extra: amqp
|
|
16
|
+
Provides-Extra: codegen
|
|
17
|
+
Requires-Dist: aio-pika ; extra == "amqp"
|
|
18
|
+
Requires-Dist: black ; extra == "codegen"
|
|
19
|
+
Requires-Dist: datamodel-code-generator[http] (>=0.26.1,<0.27.0) ; extra == "codegen"
|
|
20
|
+
Requires-Dist: jinja2 (>=3.1.4,<4.0.0) ; extra == "codegen"
|
|
21
|
+
Requires-Dist: pydantic (>=2)
|
|
22
|
+
Requires-Dist: pytz
|
|
23
|
+
Requires-Dist: pyyaml ; extra == "codegen"
|
|
24
|
+
Requires-Dist: typer[all] (>=0.12.5,<0.13.0) ; extra == "codegen"
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# AsyncAPI Python Code Generator
|
|
28
|
+
|
|
29
|
+
[Link to this github repository](https://github.com/G-USI/asyncapi-python)
|
|
30
|
+
|
|
31
|
+
Easily generate type-safe and async Python applications from AsyncAPI 3 specifications.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- [x] Creates `Application` class from [AsyncAPI 3](https://asyncapi.com) specifications, implementing every operation in the file
|
|
36
|
+
- [x] Generates typed Python code (messages are generated using [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator))
|
|
37
|
+
- [x] Performs dynamic validation of messages with [Pydantic 2](https://docs.pydantic.dev/latest/)
|
|
38
|
+
- [x] Enforces the user code to implement all consumer methods as described by spec
|
|
39
|
+
- [x] Provides async code
|
|
40
|
+
- [x] Spec parser references other files through absolute or relative paths
|
|
41
|
+
- [ ] Spec parser references other files through url
|
|
42
|
+
- [x] Supports request-reply pattern
|
|
43
|
+
- [x] Supports publish-subscribe pattern
|
|
44
|
+
- [ ] AsyncAPI trait support
|
|
45
|
+
- [ ] Customizable message encoder/decoder
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- `python>=3.10`
|
|
50
|
+
- `pydantic>=2`
|
|
51
|
+
- `pytz`
|
|
52
|
+
- For `codegen` extra
|
|
53
|
+
- `jinja2`
|
|
54
|
+
- `typer`
|
|
55
|
+
- `datamodel-code-generator`
|
|
56
|
+
- `pyyaml`
|
|
57
|
+
- For `amqp` extra
|
|
58
|
+
- `aio-pika`
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
For code generation (development env), run:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install asyncapi-python[codegen]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For runtime, run:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install asyncapi-python[amqp]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
You can replace `amqp` with any other supported protocols. For more info, see [Supported Protocols](#supported-protocols--use-cases) section.
|
|
75
|
+
|
|
76
|
+
## Supported Protocols / Use Cases
|
|
77
|
+
|
|
78
|
+
Below, you may see the table of protocols and the supported use cases. The tick signs (✅) contain links to the examples for each implemented protocol-use case pair, while the hammer signs (🔨) contain links to the Issues tracking the progress for protocol-use case pairs. The list of protocols and use cases is expected to increase over the progress of development.
|
|
79
|
+
|
|
80
|
+
| Use Case | AMQP |
|
|
81
|
+
| ---------- | ------------------------------------------------ |
|
|
82
|
+
| Pub-Sub | [✅ amqp-pub-sub](./examples/amqp-pub-sub) |
|
|
83
|
+
| Work Queue | [✅ amqp-work-queue](./examples/amqp-work-queue) |
|
|
84
|
+
| RPC | [✅ amqp-rpc](./examples/amqp-rpc) |
|
|
85
|
+
|
|
86
|
+
## Documentation
|
|
87
|
+
|
|
88
|
+
Although there's no documentation available at the moment, a set of comprehensive examples with comments for each implemented use-case is under the [examples](./examples/) directory.
|
|
89
|
+
|
|
90
|
+
## Contributing
|
|
91
|
+
|
|
92
|
+
Contributions are welcome! Please feel free to open an Issue or submit a Pull Request.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
|
|
97
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# AsyncAPI Python Code Generator
|
|
2
|
+
|
|
3
|
+
[Link to this github repository](https://github.com/G-USI/asyncapi-python)
|
|
4
|
+
|
|
5
|
+
Easily generate type-safe and async Python applications from AsyncAPI 3 specifications.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- [x] Creates `Application` class from [AsyncAPI 3](https://asyncapi.com) specifications, implementing every operation in the file
|
|
10
|
+
- [x] Generates typed Python code (messages are generated using [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator))
|
|
11
|
+
- [x] Performs dynamic validation of messages with [Pydantic 2](https://docs.pydantic.dev/latest/)
|
|
12
|
+
- [x] Enforces the user code to implement all consumer methods as described by spec
|
|
13
|
+
- [x] Provides async code
|
|
14
|
+
- [x] Spec parser references other files through absolute or relative paths
|
|
15
|
+
- [ ] Spec parser references other files through url
|
|
16
|
+
- [x] Supports request-reply pattern
|
|
17
|
+
- [x] Supports publish-subscribe pattern
|
|
18
|
+
- [ ] AsyncAPI trait support
|
|
19
|
+
- [ ] Customizable message encoder/decoder
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- `python>=3.10`
|
|
24
|
+
- `pydantic>=2`
|
|
25
|
+
- `pytz`
|
|
26
|
+
- For `codegen` extra
|
|
27
|
+
- `jinja2`
|
|
28
|
+
- `typer`
|
|
29
|
+
- `datamodel-code-generator`
|
|
30
|
+
- `pyyaml`
|
|
31
|
+
- For `amqp` extra
|
|
32
|
+
- `aio-pika`
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
For code generation (development env), run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install asyncapi-python[codegen]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For runtime, run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install asyncapi-python[amqp]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You can replace `amqp` with any other supported protocols. For more info, see [Supported Protocols](#supported-protocols--use-cases) section.
|
|
49
|
+
|
|
50
|
+
## Supported Protocols / Use Cases
|
|
51
|
+
|
|
52
|
+
Below, you may see the table of protocols and the supported use cases. The tick signs (✅) contain links to the examples for each implemented protocol-use case pair, while the hammer signs (🔨) contain links to the Issues tracking the progress for protocol-use case pairs. The list of protocols and use cases is expected to increase over the progress of development.
|
|
53
|
+
|
|
54
|
+
| Use Case | AMQP |
|
|
55
|
+
| ---------- | ------------------------------------------------ |
|
|
56
|
+
| Pub-Sub | [✅ amqp-pub-sub](./examples/amqp-pub-sub) |
|
|
57
|
+
| Work Queue | [✅ amqp-work-queue](./examples/amqp-work-queue) |
|
|
58
|
+
| RPC | [✅ amqp-rpc](./examples/amqp-rpc) |
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
Although there's no documentation available at the moment, a set of comprehensive examples with comments for each implemented use-case is under the [examples](./examples/) directory.
|
|
63
|
+
|
|
64
|
+
## Contributing
|
|
65
|
+
|
|
66
|
+
Contributions are welcome! Please feel free to open an Issue or submit a Pull Request.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "asyncapi-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
license = "Apache-2.0"
|
|
5
|
-
description = ""
|
|
5
|
+
description = "Easily generate type-safe and async Python applications from AsyncAPI 3 specifications."
|
|
6
6
|
authors = ["Yaroslav Petrov <yaroslav.v.petrov@gmail.com>"]
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
packages = [
|
|
@@ -20,11 +20,12 @@ codegen = [
|
|
|
20
20
|
"asyncapi_python_codegen",
|
|
21
21
|
"pyyaml",
|
|
22
22
|
"datamodel-code-generator",
|
|
23
|
+
"black",
|
|
23
24
|
]
|
|
24
25
|
amqp = ["aio-pika"]
|
|
25
26
|
|
|
26
27
|
[tool.poetry.dependencies]
|
|
27
|
-
python = ">=3.
|
|
28
|
+
python = ">=3.9,<3.13"
|
|
28
29
|
pydantic = ">=2"
|
|
29
30
|
pytz = "*"
|
|
30
31
|
jinja2 = { version = "^3.1.4", optional = true }
|
|
@@ -32,17 +33,19 @@ typer = { extras = ["all"], version = "^0.12.5", optional = true }
|
|
|
32
33
|
datamodel-code-generator = { extras = [
|
|
33
34
|
"http",
|
|
34
35
|
], version = "^0.26.1", optional = true }
|
|
35
|
-
aio-pika = { version = "
|
|
36
|
-
pyyaml = { version = "
|
|
36
|
+
aio-pika = { version = "*", optional = true }
|
|
37
|
+
pyyaml = { version = "*", optional = true }
|
|
38
|
+
black = { version = "*", optional = true }
|
|
37
39
|
|
|
38
40
|
[tool.poetry.group.dev.dependencies]
|
|
39
|
-
black = "
|
|
40
|
-
mypy = "
|
|
41
|
-
isort = "
|
|
42
|
-
types-pyyaml = "
|
|
43
|
-
pytest = "
|
|
44
|
-
types-pytz = "
|
|
45
|
-
pytest-asyncio = "
|
|
41
|
+
black = "*"
|
|
42
|
+
mypy = "*"
|
|
43
|
+
isort = "*"
|
|
44
|
+
types-pyyaml = "*"
|
|
45
|
+
pytest = "*"
|
|
46
|
+
types-pytz = "*"
|
|
47
|
+
pytest-asyncio = "*"
|
|
48
|
+
pex = "*"
|
|
46
49
|
|
|
47
50
|
[build-system]
|
|
48
51
|
requires = ["poetry-core"]
|
{asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/base_application.py
RENAMED
|
@@ -16,25 +16,24 @@
|
|
|
16
16
|
from .connection import channel_pool, AmqpPool
|
|
17
17
|
from .consumer import Consumer
|
|
18
18
|
from .producer import Producer
|
|
19
|
-
from
|
|
20
|
-
from typing import Literal, TypedDict
|
|
19
|
+
from typing import Literal, Optional, TypedDict
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class Queue(TypedDict):
|
|
24
|
-
name: str
|
|
23
|
+
name: Optional[str]
|
|
25
24
|
durable: bool
|
|
26
25
|
exclusive: bool
|
|
27
26
|
auto_delete: bool
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
class Exchange(TypedDict):
|
|
31
|
-
name: str
|
|
30
|
+
name: Optional[str]
|
|
32
31
|
type: Literal["topic", "direct", "fanout", "default", "headers"]
|
|
33
32
|
durable: bool
|
|
34
33
|
auto_delete: bool
|
|
35
34
|
|
|
36
35
|
|
|
37
|
-
class BaseApplication
|
|
36
|
+
class BaseApplication:
|
|
38
37
|
def __init__(self, amqp_uri: str):
|
|
39
38
|
self._uri = amqp_uri
|
|
40
39
|
self._has_started = False
|
|
@@ -15,19 +15,19 @@
|
|
|
15
15
|
|
|
16
16
|
from .message_handler import AbstractMessageHandler, MessageHandler, RpcMessageHandler
|
|
17
17
|
from .message_handler_params import MessageHandlerParams
|
|
18
|
-
from .utils import encode_message, decode_message
|
|
18
|
+
from .utils import encode_message, decode_message, union_model
|
|
19
19
|
|
|
20
20
|
import asyncio
|
|
21
21
|
from aio_pika import Message
|
|
22
22
|
from aio_pika.pool import Pool
|
|
23
23
|
from aio_pika.abc import AbstractRobustChannel
|
|
24
24
|
from asyncio import Future
|
|
25
|
-
from typing import Callable, TypeVar
|
|
26
|
-
from pydantic import BaseModel
|
|
25
|
+
from typing import Callable, Optional, Sequence, TypeVar, Union
|
|
26
|
+
from pydantic import BaseModel, RootModel
|
|
27
27
|
from logging import getLogger
|
|
28
28
|
|
|
29
|
-
T = TypeVar("T"
|
|
30
|
-
U = TypeVar("U"
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
U = TypeVar("U")
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class Consumer:
|
|
@@ -36,7 +36,7 @@ class Consumer:
|
|
|
36
36
|
self._logger = getLogger(__name__)
|
|
37
37
|
self._pool = channel_pool
|
|
38
38
|
|
|
39
|
-
async def run_blocking(self, timeout: int
|
|
39
|
+
async def run_blocking(self, timeout: Union[int, float, None]):
|
|
40
40
|
await self.run()
|
|
41
41
|
if timeout is not None:
|
|
42
42
|
await asyncio.sleep(timeout)
|
|
@@ -56,18 +56,20 @@ class Consumer:
|
|
|
56
56
|
self,
|
|
57
57
|
*,
|
|
58
58
|
params: MessageHandlerParams,
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
input_types: Sequence[type[T]],
|
|
60
|
+
output_types: Union[None, Sequence[type[U]]],
|
|
61
61
|
callback: Callable,
|
|
62
62
|
):
|
|
63
63
|
handler: AbstractMessageHandler
|
|
64
64
|
if params in self._handlers:
|
|
65
65
|
raise AssertionError(f"Only one handler for `{params}` is allowed")
|
|
66
|
-
if
|
|
66
|
+
if output_types is None or len(output_types) == 0:
|
|
67
67
|
handler = MessageHandler(
|
|
68
68
|
name=params.root.name,
|
|
69
69
|
callback=callback,
|
|
70
|
-
decode_message=lambda x: decode_message(
|
|
70
|
+
decode_message=lambda x: decode_message(
|
|
71
|
+
x, union_model(input_types)
|
|
72
|
+
).root,
|
|
71
73
|
)
|
|
72
74
|
else:
|
|
73
75
|
handler = RpcMessageHandler(
|
|
@@ -75,6 +77,8 @@ class Consumer:
|
|
|
75
77
|
callback=callback,
|
|
76
78
|
reply_callback=self._reply_callback,
|
|
77
79
|
encode_message=encode_message,
|
|
78
|
-
decode_message=lambda x: decode_message(
|
|
80
|
+
decode_message=lambda x: decode_message(
|
|
81
|
+
x, union_model(input_types)
|
|
82
|
+
).root,
|
|
79
83
|
)
|
|
80
84
|
self._handlers[params] = handler
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
from abc import ABC, abstractmethod
|
|
17
17
|
from aio_pika.message import AbstractIncomingMessage, Message
|
|
18
|
-
from typing import Awaitable, Callable, Generic, TypeVar
|
|
18
|
+
from typing import Awaitable, Callable, Generic, TypeVar, Union
|
|
19
19
|
from pydantic import BaseModel
|
|
20
20
|
from logging import getLogger
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
T = TypeVar("T", bound=BaseModel)
|
|
24
|
-
U = TypeVar("U", bound=BaseModel
|
|
24
|
+
U = TypeVar("U", bound=Union[BaseModel, None])
|
|
25
25
|
V = TypeVar("V", bound=BaseModel)
|
|
26
26
|
|
|
27
27
|
|
{asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python/amqp/message_handler_params.py
RENAMED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
from pydantic import RootModel, BaseModel, ConfigDict, computed_field
|
|
17
|
-
from typing import Literal
|
|
17
|
+
from typing import Literal, Optional, Union
|
|
18
18
|
from aio_pika.abc import AbstractRobustChannel
|
|
19
19
|
from .message_handler import AbstractMessageHandler
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ class ExchangeHandlerParams(BaseModel):
|
|
|
24
24
|
kind: Literal["exchange"] = "exchange"
|
|
25
25
|
type: Literal["direct", "fanout", "topic", "headers"]
|
|
26
26
|
name: str
|
|
27
|
-
routing_key: str
|
|
27
|
+
routing_key: Optional[str]
|
|
28
28
|
auto_delete: bool = False
|
|
29
29
|
|
|
30
30
|
|
|
@@ -39,24 +39,28 @@ class QueueHandlerParams(BaseModel):
|
|
|
39
39
|
|
|
40
40
|
class MessageHandlerParams(RootModel):
|
|
41
41
|
model_config = ConfigDict(frozen=True)
|
|
42
|
-
root: QueueHandlerParams
|
|
42
|
+
root: Union[QueueHandlerParams, ExchangeHandlerParams]
|
|
43
43
|
|
|
44
44
|
async def setup_consume(
|
|
45
45
|
self,
|
|
46
46
|
handler: AbstractMessageHandler,
|
|
47
47
|
channel: AbstractRobustChannel,
|
|
48
48
|
):
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
name=name,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
if isinstance(self.root, ExchangeHandlerParams):
|
|
50
|
+
exchange = await channel.declare_exchange(
|
|
51
|
+
name=self.root.name,
|
|
52
|
+
type=self.root.type,
|
|
53
|
+
auto_delete=self.root.auto_delete,
|
|
54
|
+
)
|
|
55
|
+
queue = await channel.declare_queue(exclusive=True)
|
|
56
|
+
await queue.bind(exchange, self.root.routing_key)
|
|
57
|
+
elif isinstance(self.root, QueueHandlerParams):
|
|
58
|
+
queue = await channel.declare_queue(
|
|
59
|
+
self.root.name,
|
|
60
|
+
exclusive=self.root.exclusive,
|
|
61
|
+
auto_delete=self.root.auto_delete,
|
|
62
|
+
durable=self.root.durable,
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
raise NotImplementedError
|
|
62
66
|
await queue.consume(handler)
|
|
@@ -21,14 +21,15 @@ from aio_pika.abc import (
|
|
|
21
21
|
AbstractIncomingMessage,
|
|
22
22
|
)
|
|
23
23
|
from logging import getLogger
|
|
24
|
-
from pydantic import BaseModel
|
|
25
|
-
from typing import TypeVar
|
|
24
|
+
from pydantic import BaseModel, RootModel, create_model
|
|
25
|
+
from typing import Optional, Sequence, TypeVar, Union, Type
|
|
26
|
+
from collections.abc import Sequence as Seq
|
|
26
27
|
from asyncio import Future
|
|
27
28
|
from uuid import uuid4
|
|
28
|
-
from .utils import encode_message, decode_message
|
|
29
|
+
from .utils import encode_message, decode_message, union_model
|
|
29
30
|
|
|
30
|
-
T = TypeVar("T"
|
|
31
|
-
U = TypeVar("U"
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
U = TypeVar("U")
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class Producer:
|
|
@@ -44,7 +45,7 @@ class Producer:
|
|
|
44
45
|
self._pool = channel_pool
|
|
45
46
|
self._replies: dict[str, Future[AbstractIncomingMessage]] = {}
|
|
46
47
|
self._reply_queue = reply_queue
|
|
47
|
-
self._reply_consumer_tag: str
|
|
48
|
+
self._reply_consumer_tag: Optional[str] = None
|
|
48
49
|
|
|
49
50
|
async def _on_reply(self, msg: AbstractIncomingMessage):
|
|
50
51
|
await msg.ack()
|
|
@@ -59,12 +60,10 @@ class Producer:
|
|
|
59
60
|
async def publish(
|
|
60
61
|
self,
|
|
61
62
|
message: T,
|
|
62
|
-
exchange: str
|
|
63
|
-
routing_key: str
|
|
63
|
+
exchange: Optional[str],
|
|
64
|
+
routing_key: Optional[str],
|
|
64
65
|
):
|
|
65
|
-
outbound_message = Message(
|
|
66
|
-
body=encode_message(message),
|
|
67
|
-
)
|
|
66
|
+
outbound_message = Message(body=encode_message(RootModel[T](message)))
|
|
68
67
|
async with self._pool.acquire() as channel:
|
|
69
68
|
await (
|
|
70
69
|
await channel.get_exchange(exchange)
|
|
@@ -75,9 +74,9 @@ class Producer:
|
|
|
75
74
|
async def request(
|
|
76
75
|
self,
|
|
77
76
|
message: T,
|
|
78
|
-
exchange: str
|
|
79
|
-
routing_key: str
|
|
80
|
-
|
|
77
|
+
exchange: Optional[str],
|
|
78
|
+
routing_key: Optional[str],
|
|
79
|
+
output_types: Sequence[type[U]],
|
|
81
80
|
) -> U:
|
|
82
81
|
if not self._reply_consumer_tag:
|
|
83
82
|
raise AssertionError(
|
|
@@ -85,7 +84,7 @@ class Producer:
|
|
|
85
84
|
)
|
|
86
85
|
corr_id = str(uuid4())
|
|
87
86
|
outbound_message = Message(
|
|
88
|
-
body=encode_message(message),
|
|
87
|
+
body=encode_message(RootModel[T](message)),
|
|
89
88
|
correlation_id=corr_id,
|
|
90
89
|
reply_to=self._reply_queue.name,
|
|
91
90
|
)
|
|
@@ -98,6 +97,9 @@ class Producer:
|
|
|
98
97
|
).publish(outbound_message, routing_key or "")
|
|
99
98
|
self._logger.info(f"Sent request {message}")
|
|
100
99
|
self._replies[corr_id] = reply_future
|
|
101
|
-
res = decode_message(
|
|
100
|
+
res = decode_message(
|
|
101
|
+
(await reply_future).body,
|
|
102
|
+
union_model(tuple(output_types)),
|
|
103
|
+
).root
|
|
102
104
|
self._logger.info(f"Got response {res}")
|
|
103
105
|
return res
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
from
|
|
17
|
-
from
|
|
16
|
+
from functools import cache
|
|
17
|
+
from pydantic import BaseModel, RootModel, create_model
|
|
18
|
+
from typing import TypeVar, Union
|
|
18
19
|
|
|
19
20
|
T = TypeVar("T", bound=BaseModel)
|
|
21
|
+
U = TypeVar("U")
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def encode_message(message: T) -> bytes:
|
|
@@ -25,3 +27,9 @@ def encode_message(message: T) -> bytes:
|
|
|
25
27
|
|
|
26
28
|
def decode_message(message: bytes, schema: type[T]) -> T:
|
|
27
29
|
return schema.model_validate_json(message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cache
|
|
33
|
+
def union_model(types: tuple[type[U], ...]) -> type[RootModel[U]]:
|
|
34
|
+
UnionType = Union.__getitem__(types)
|
|
35
|
+
return RootModel[UnionType] # type: ignore
|
|
@@ -36,13 +36,12 @@ def generate(
|
|
|
36
36
|
|
|
37
37
|
# Generate code
|
|
38
38
|
generation_result: dict[Path, str]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
raise NotImplementedError(f"Protocol {protocol} is not supported")
|
|
39
|
+
if protocol == "amqp":
|
|
40
|
+
generation_result = g.amqp.generate(
|
|
41
|
+
input_path=input_file, output_path=output_dir
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
raise NotImplementedError(f"Protocol {protocol} is not supported")
|
|
46
45
|
|
|
47
46
|
# Write files
|
|
48
47
|
for path, code in generation_result.items():
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
from typing import Literal
|
|
16
|
+
from typing import Literal, Optional, Union
|
|
17
17
|
|
|
18
18
|
from pydantic import BaseModel, Field, RootModel
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class Exchange(BaseModel):
|
|
22
|
-
name: str
|
|
22
|
+
name: Optional[str] = None
|
|
23
23
|
type: Literal["topic", "direct", "fanout", "default", "headers"] = "default"
|
|
24
24
|
durable: bool = False
|
|
25
25
|
auto_delete: bool = Field(alias="autoDelete", default=False)
|
|
@@ -31,7 +31,7 @@ class ExchangeBinding(BaseModel):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class Queue(BaseModel):
|
|
34
|
-
name: str
|
|
34
|
+
name: Optional[str] = None
|
|
35
35
|
durable: bool = False
|
|
36
36
|
exclusive: bool = False
|
|
37
37
|
auto_delete: bool = Field(alias="autoDelete", default=False)
|
|
@@ -43,4 +43,4 @@ class QueueBinding(BaseModel):
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class AmqpBinding(RootModel):
|
|
46
|
-
root: ExchangeBinding
|
|
46
|
+
root: Union[ExchangeBinding, QueueBinding] = QueueBinding()
|
{asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/components.py
RENAMED
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
-
from
|
|
19
|
-
|
|
20
|
-
from .base import BaseModel
|
|
21
|
-
from typing import Any, Literal
|
|
18
|
+
from .base import BaseModel, RootModel
|
|
19
|
+
from typing import Any, Literal, Optional
|
|
22
20
|
from .ref import MaybeRef, Ref
|
|
23
21
|
from .bindings import Bindings
|
|
24
22
|
|
|
@@ -28,56 +26,44 @@ class Components(BaseModel):
|
|
|
28
26
|
channels: dict[str, MaybeRef[Channel]] = {}
|
|
29
27
|
messages: dict[str, MaybeRef[Message]] = {}
|
|
30
28
|
correlation_ids: dict[str, CorrelationId] = {}
|
|
29
|
+
schemas: dict[str, MaybeRef[JsonSchema]] = {}
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
class JsonSchema(
|
|
32
|
+
class JsonSchema(RootModel):
|
|
34
33
|
# TODO: Create a better parser for JsonSchema
|
|
35
|
-
|
|
36
|
-
properties: dict[str, Any]
|
|
37
|
-
required: list[str] = []
|
|
34
|
+
root: Any
|
|
38
35
|
|
|
39
36
|
|
|
40
37
|
class Message(BaseModel):
|
|
41
|
-
title: str
|
|
42
|
-
headers: MaybeRef[JsonSchema]
|
|
38
|
+
title: Optional[str] = None
|
|
39
|
+
headers: Optional[MaybeRef[JsonSchema]] = None
|
|
43
40
|
payload: MaybeRef[JsonSchema]
|
|
44
41
|
|
|
45
|
-
@model_validator(mode="before")
|
|
46
|
-
@classmethod
|
|
47
|
-
def has_title(cls, data: dict[str, Any]):
|
|
48
|
-
if not "title" in data:
|
|
49
|
-
raise AssertionError(
|
|
50
|
-
"As of now, all Message objects require "
|
|
51
|
-
+ "`title` field to be present to uniquely identify data types. "
|
|
52
|
-
+ "This limitation will be removed in the future."
|
|
53
|
-
)
|
|
54
|
-
return data
|
|
55
|
-
|
|
56
42
|
|
|
57
43
|
class CorrelationId(BaseModel):
|
|
58
|
-
description: str
|
|
44
|
+
description: Optional[str] = None
|
|
59
45
|
location: str
|
|
60
46
|
|
|
61
47
|
|
|
62
48
|
class Operation(BaseModel):
|
|
63
49
|
action: Literal["receive", "send"]
|
|
64
50
|
channel: Ref[Channel]
|
|
65
|
-
reply: OperationReply
|
|
51
|
+
reply: Optional[OperationReply] = None
|
|
66
52
|
|
|
67
53
|
|
|
68
54
|
class OperationReply(BaseModel):
|
|
69
|
-
address: ReplyAddress
|
|
55
|
+
address: Optional[ReplyAddress] = None
|
|
70
56
|
channel: Ref[Channel]
|
|
71
57
|
|
|
72
58
|
|
|
73
59
|
class ReplyAddress(BaseModel):
|
|
74
|
-
description: str
|
|
60
|
+
description: Optional[str] = None
|
|
75
61
|
location: str
|
|
76
62
|
|
|
77
63
|
|
|
78
64
|
class Channel(BaseModel):
|
|
79
|
-
address: str
|
|
80
|
-
title: str
|
|
81
|
-
description: str
|
|
82
|
-
bindings: Bindings
|
|
65
|
+
address: Optional[str] = None
|
|
66
|
+
title: Optional[str] = None
|
|
67
|
+
description: Optional[str] = None
|
|
68
|
+
bindings: Optional[Bindings] = None
|
|
83
69
|
messages: dict[str, MaybeRef[Message]]
|
{asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/document.py
RENAMED
|
@@ -18,7 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
|
|
19
19
|
from pydantic import Field
|
|
20
20
|
from .base import BaseModel
|
|
21
|
-
from typing import Annotated, Any, Literal
|
|
21
|
+
from typing import Annotated, Any, Literal, Optional
|
|
22
22
|
import yaml
|
|
23
23
|
from .components import Channel, Components, Message, Operation
|
|
24
24
|
from .ref import MaybeRef, Ref
|
|
@@ -53,4 +53,4 @@ class Document(BaseModel):
|
|
|
53
53
|
class Info(BaseModel):
|
|
54
54
|
title: str
|
|
55
55
|
version: str
|
|
56
|
-
description: str
|
|
56
|
+
description: Optional[str] = None
|
|
@@ -23,7 +23,8 @@ from .document_context import (
|
|
|
23
23
|
set_current_doc_path,
|
|
24
24
|
DOCUMENT_CONTEXT_STACK,
|
|
25
25
|
)
|
|
26
|
-
from typing import Any, Callable, Generic, TypeVar, Annotated
|
|
26
|
+
from typing import Any, Callable, Generic, TypeVar, Annotated, Union
|
|
27
|
+
from typing_extensions import Self
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
T = TypeVar("T", bound=BaseModel)
|
|
@@ -54,32 +55,49 @@ class Ref(BaseModel, Generic[T]):
|
|
|
54
55
|
def get(self) -> T:
|
|
55
56
|
from .document import Document
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
sub = self.flatten()
|
|
59
|
+
doc = Document.load_yaml(sub.filepath).model_dump(by_alias=True)
|
|
58
60
|
for p in self.doc_path:
|
|
59
61
|
doc = doc[p]
|
|
62
|
+
with set_current_doc_path(sub.filepath):
|
|
63
|
+
return sub.type().model_validate(doc)
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
@cache
|
|
66
|
+
def flatten(self, max_depth: int = 1000) -> Self:
|
|
67
|
+
from .document import Document
|
|
68
|
+
|
|
69
|
+
sub = self
|
|
70
|
+
for _ in range(max_depth):
|
|
71
|
+
doc = Document.load_yaml(sub.filepath).model_dump(by_alias=True)
|
|
72
|
+
try:
|
|
73
|
+
for p in sub.doc_path:
|
|
74
|
+
doc = doc[p]
|
|
75
|
+
except KeyError as e:
|
|
76
|
+
raise KeyError(
|
|
77
|
+
f"$ref `{sub.ref}` is invalid \n"
|
|
78
|
+
+ f"The Error was raised when trying to get key {e.args}"
|
|
79
|
+
)
|
|
80
|
+
if not "$ref" in doc:
|
|
81
|
+
return sub
|
|
82
|
+
sub = self.__class__.model_validate(doc)
|
|
83
|
+
raise RecursionError(
|
|
84
|
+
f"Document Ref[{self.type().__class__}] flattening limit reached"
|
|
85
|
+
)
|
|
65
86
|
|
|
66
87
|
@model_validator(mode="before")
|
|
67
88
|
@classmethod
|
|
68
89
|
def parse_ref(cls, data: Any) -> Any:
|
|
69
|
-
fp: str
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
case x:
|
|
82
|
-
raise ValueError(f"Requires {{$ref: ... }}, given {x} ")
|
|
90
|
+
fp: Union[str, Path]
|
|
91
|
+
|
|
92
|
+
if (ref := data.get("ref")) or (ref := data.get("$ref")):
|
|
93
|
+
fp, dp = ref.split("#")
|
|
94
|
+
if fp == "":
|
|
95
|
+
fp = current_doc_path()
|
|
96
|
+
elif not Path(fp).is_absolute():
|
|
97
|
+
fp = current_doc_path().parent / fp
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(f"Requires {{$ref: ... }}, given {data} ")
|
|
100
|
+
|
|
83
101
|
return {
|
|
84
102
|
**data,
|
|
85
103
|
"$ref": ref,
|
|
@@ -88,8 +106,8 @@ class Ref(BaseModel, Generic[T]):
|
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
|
|
91
|
-
class MaybeRef(RootModel[Ref[T]
|
|
92
|
-
root: Ref[T]
|
|
109
|
+
class MaybeRef(RootModel[Union[Ref[T], T]], Generic[T]):
|
|
110
|
+
root: Union[Ref[T], T]
|
|
93
111
|
|
|
94
112
|
def get(self) -> T:
|
|
95
113
|
return self.root.get() if isinstance(self.root, Ref) else self.root
|
|
@@ -17,11 +17,12 @@ from __future__ import annotations
|
|
|
17
17
|
from contextlib import ExitStack
|
|
18
18
|
import json
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
import
|
|
20
|
+
import tempfile
|
|
21
21
|
import jinja2 as j2
|
|
22
|
-
from typing import
|
|
22
|
+
from typing import Literal, Optional, TypedDict
|
|
23
23
|
from asyncapi_python_codegen import document as d
|
|
24
24
|
from itertools import chain
|
|
25
|
+
from datamodel_code_generator.__main__ import main as datamodel_codegen
|
|
25
26
|
|
|
26
27
|
from .utils import snake_case
|
|
27
28
|
|
|
@@ -46,13 +47,13 @@ def generate(
|
|
|
46
47
|
doc.info.description,
|
|
47
48
|
doc.info.version,
|
|
48
49
|
).items()
|
|
49
|
-
} | {output_path / "models.py": generate_models(ops)}
|
|
50
|
+
} | {output_path / "models.py": generate_models(ops, doc.filepath.parent)}
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
def generate_application(
|
|
53
54
|
ops: list[Operation],
|
|
54
55
|
title: str,
|
|
55
|
-
description: str
|
|
56
|
+
description: Optional[str],
|
|
56
57
|
version: str,
|
|
57
58
|
template_dir: Path = Path(__file__).parent / "templates",
|
|
58
59
|
filenames: list[str] = ["__init__.py", "application.py"],
|
|
@@ -65,16 +66,11 @@ def generate_application(
|
|
|
65
66
|
return {f: t.render(**render_args) for t, f in zip(templates, filenames)}
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def generate_models(schemas: list[Operation]) -> str:
|
|
69
|
-
args = """datamodel-codegen
|
|
70
|
-
--output-model-type pydantic_v2.BaseModel
|
|
71
|
-
--input-file-type jsonschema
|
|
72
|
-
""".split()
|
|
73
|
-
|
|
69
|
+
def generate_models(schemas: list[Operation], cwd: Path) -> str:
|
|
74
70
|
inp = {
|
|
75
71
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
76
72
|
"$defs": {
|
|
77
|
-
type_name: type_schema
|
|
73
|
+
type_name: {"$ref": type_schema}
|
|
78
74
|
for s in schemas
|
|
79
75
|
for type_name, type_schema in chain(
|
|
80
76
|
zip(s["input_types"], s["input_schemas"]),
|
|
@@ -82,31 +78,49 @@ def generate_models(schemas: list[Operation]) -> str:
|
|
|
82
78
|
)
|
|
83
79
|
},
|
|
84
80
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
|
|
82
|
+
with tempfile.TemporaryDirectory() as dir:
|
|
83
|
+
schema_path = Path(dir) / "schema.json"
|
|
84
|
+
models_path = Path(dir) / "models.py"
|
|
85
|
+
|
|
86
|
+
args = f"""
|
|
87
|
+
--input { str(schema_path.absolute()) }
|
|
88
|
+
--output { str(models_path.absolute()) }
|
|
89
|
+
--output-model-type pydantic_v2.BaseModel
|
|
90
|
+
--input-file-type jsonschema
|
|
91
|
+
""".split()
|
|
92
|
+
|
|
93
|
+
with schema_path.open("w") as schema:
|
|
94
|
+
json.dump(inp, schema)
|
|
95
|
+
|
|
96
|
+
datamodel_codegen(args=args)
|
|
97
|
+
|
|
98
|
+
with models_path.open() as f:
|
|
99
|
+
models_code = f.read()
|
|
100
|
+
|
|
101
|
+
return models_code
|
|
88
102
|
|
|
89
103
|
|
|
90
104
|
def get_operation(op_name: str, op: d.Operation) -> Operation:
|
|
91
|
-
exchange: str
|
|
92
|
-
routing_key: str
|
|
105
|
+
exchange: Optional[str]
|
|
106
|
+
routing_key: Optional[str]
|
|
93
107
|
|
|
94
108
|
channel = op.channel.get()
|
|
95
109
|
reply_channel = op.reply.channel.get() if op.reply else None
|
|
96
110
|
addr = lambda x: x or channel.address or op_name
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
|
|
112
|
+
if channel.bindings is None:
|
|
113
|
+
# Default exchange + named queues
|
|
114
|
+
exchange = None
|
|
115
|
+
routing_key = addr(None)
|
|
116
|
+
elif (bind := channel.bindings).amqp.root.type == "queue":
|
|
117
|
+
# Default exchange + named queues
|
|
118
|
+
exchange = None
|
|
119
|
+
routing_key = addr(bind.amqp.root.queue.name)
|
|
120
|
+
elif bind.amqp.root.type == "routingKey":
|
|
121
|
+
# Named exchange + exclusive queues
|
|
122
|
+
exchange = addr(bind.amqp.root.exchange.name)
|
|
123
|
+
routing_key = None
|
|
110
124
|
|
|
111
125
|
# Get reply channel properties
|
|
112
126
|
if reply_channel is not None:
|
|
@@ -124,31 +138,69 @@ def get_operation(op_name: str, op: d.Operation) -> Operation:
|
|
|
124
138
|
"As of now, reply channel must be a queue without name"
|
|
125
139
|
)
|
|
126
140
|
|
|
141
|
+
input_types: list[str]
|
|
142
|
+
input_schemas: list[str]
|
|
143
|
+
output_types: list[str]
|
|
144
|
+
output_schemas: list[str]
|
|
145
|
+
|
|
146
|
+
input_types, input_schemas = get_channel_types(
|
|
147
|
+
channel, op.channel.filepath, op.channel.doc_path
|
|
148
|
+
)
|
|
149
|
+
output_types, output_schemas = (
|
|
150
|
+
get_channel_types(
|
|
151
|
+
op.reply.channel.get(),
|
|
152
|
+
op.reply.channel.filepath,
|
|
153
|
+
op.reply.channel.doc_path,
|
|
154
|
+
)
|
|
155
|
+
if op.reply
|
|
156
|
+
else ([], [])
|
|
157
|
+
)
|
|
158
|
+
|
|
127
159
|
return {
|
|
128
160
|
"field_name": snake_case(op_name),
|
|
129
161
|
"action": op.action,
|
|
130
162
|
"exchange": exchange,
|
|
131
163
|
"routing_key": routing_key,
|
|
132
|
-
"input_types":
|
|
133
|
-
"input_schemas":
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"output_types": (
|
|
137
|
-
[msg.get().title for msg in reply_channel.messages.values()]
|
|
138
|
-
if reply_channel
|
|
139
|
-
else []
|
|
140
|
-
),
|
|
141
|
-
"output_schemas": (
|
|
142
|
-
[
|
|
143
|
-
msg.get().payload.get().model_dump()
|
|
144
|
-
for msg in reply_channel.messages.values()
|
|
145
|
-
]
|
|
146
|
-
if reply_channel
|
|
147
|
-
else []
|
|
148
|
-
),
|
|
164
|
+
"input_types": input_types,
|
|
165
|
+
"input_schemas": input_schemas,
|
|
166
|
+
"output_types": output_types,
|
|
167
|
+
"output_schemas": output_schemas,
|
|
149
168
|
}
|
|
150
169
|
|
|
151
170
|
|
|
171
|
+
def get_channel_types(
|
|
172
|
+
channel: d.Channel,
|
|
173
|
+
channel_filepath: Path,
|
|
174
|
+
channel_doc_path: tuple[str, ...],
|
|
175
|
+
) -> tuple[list[str], list[str]]:
|
|
176
|
+
types, schemas = [], []
|
|
177
|
+
for message_key, message in channel.messages.items():
|
|
178
|
+
|
|
179
|
+
if isinstance(message.root, d.Ref):
|
|
180
|
+
msg_ref = message.root.flatten()
|
|
181
|
+
msg_filepath = msg_ref.filepath
|
|
182
|
+
msg_doc_path = msg_ref.doc_path
|
|
183
|
+
del msg_ref
|
|
184
|
+
else:
|
|
185
|
+
msg_filepath = channel_filepath
|
|
186
|
+
msg_doc_path = (*channel_doc_path, "messages", message_key)
|
|
187
|
+
|
|
188
|
+
message_payload = message.get().payload.root
|
|
189
|
+
if isinstance(message_payload, d.Ref):
|
|
190
|
+
payload_ref = message_payload.flatten()
|
|
191
|
+
pl_filepath = payload_ref.filepath
|
|
192
|
+
pl_doc_path = payload_ref.doc_path
|
|
193
|
+
del payload_ref
|
|
194
|
+
else:
|
|
195
|
+
pl_filepath = msg_filepath
|
|
196
|
+
pl_doc_path = (*msg_doc_path, "payload")
|
|
197
|
+
|
|
198
|
+
types.append(message.get().title or message_key)
|
|
199
|
+
schemas.append(str(pl_filepath) + "#/" + "/".join(pl_doc_path))
|
|
200
|
+
|
|
201
|
+
return types, schemas
|
|
202
|
+
|
|
203
|
+
|
|
152
204
|
class Operation(TypedDict):
|
|
153
205
|
field_name: str
|
|
154
206
|
action: Literal["send", "receive"]
|
|
@@ -156,5 +208,5 @@ class Operation(TypedDict):
|
|
|
156
208
|
routing_key: str | None
|
|
157
209
|
input_types: list[str]
|
|
158
210
|
output_types: list[str]
|
|
159
|
-
input_schemas: list[
|
|
160
|
-
output_schemas: list[
|
|
211
|
+
input_schemas: list[str]
|
|
212
|
+
output_schemas: list[str]
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
from .models import *
|
|
15
15
|
from asyncapi_python.amqp import BaseApplication, Consumer
|
|
16
16
|
from asyncapi_python.amqp.message_handler_params import MessageHandlerParams
|
|
17
|
-
from typing import Callable, Awaitable
|
|
17
|
+
from typing import Callable, Awaitable, Union
|
|
18
|
+
from pydantic import RootModel
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class Application(BaseApplication):
|
|
@@ -34,8 +35,8 @@ class Application(BaseApplication):
|
|
|
34
35
|
await super().start(blocking=blocking)
|
|
35
36
|
|
|
36
37
|
{% for op in ops if op.action == "send" -%}
|
|
37
|
-
{%- set in_type = op.input_types | join(" | ") -%}
|
|
38
|
-
{%- set out_type = op.output_types | join(" | ") if op.output_types else "None" -%}
|
|
38
|
+
{%- set in_type = "Union[" + (op.input_types | join(" | ")) + "]" -%}
|
|
39
|
+
{%- set out_type = "Union[" + (op.output_types | join(" | ") if op.output_types else "None") + "]" -%}
|
|
39
40
|
{%- set callback_type = "Callable[["+ in_type + "], Awaitable[" + out_type + "]" + "]" -%}
|
|
40
41
|
{%- set send_method = "publish" if out_type == "None" else "request" -%}
|
|
41
42
|
{%- set routing_key_literal = '"' + op.routing_key + '"' if op.routing_key else "None" -%}
|
|
@@ -46,7 +47,7 @@ class Application(BaseApplication):
|
|
|
46
47
|
exchange={{ '"' + op.exchange + '"' if op.exchange else "None" }},
|
|
47
48
|
routing_key={{ routing_key_literal }},
|
|
48
49
|
{%- if send_method == "request" %}
|
|
49
|
-
|
|
50
|
+
output_types={{ "[" + op.output_types | join(", ") + "]" }},
|
|
50
51
|
{%- endif %}
|
|
51
52
|
)
|
|
52
53
|
{% endfor %}
|
|
@@ -72,8 +73,8 @@ class ConsumerWrapper:
|
|
|
72
73
|
|
|
73
74
|
|
|
74
75
|
{% for op in ops if op.action == "receive" %}
|
|
75
|
-
{%- set in_type = op.input_types | join("
|
|
76
|
-
{%- set out_type = op.output_types | join("
|
|
76
|
+
{%- set in_type = "Union[" + (op.input_types | join(", ")) + "]" -%}
|
|
77
|
+
{%- set out_type = "Union[" + (op.output_types | join(", ") if op.output_types else "None") + "]" -%}
|
|
77
78
|
{%- set callback_type = "Callable[["+ in_type + "], Awaitable[" + out_type + "]" + "]" -%}
|
|
78
79
|
{%- set routing_key_literal = '"' + op.routing_key + '"' if op.routing_key else "None" -%}
|
|
79
80
|
def {{ op.field_name }}(self, callback: {{ callback_type }}) -> None:
|
|
@@ -93,8 +94,8 @@ class ConsumerWrapper:
|
|
|
93
94
|
{"kind": "queue", "name": "{{op.routing_key}}"}
|
|
94
95
|
{%- endif %}
|
|
95
96
|
),
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
input_types={{ "[" + op.input_types | join(", ") + "]" }},
|
|
98
|
+
output_types={{ "[" + op.output_types | join(", ") + "]" }},
|
|
98
99
|
callback=callback,
|
|
99
100
|
)
|
|
100
101
|
{% endfor %}
|
{asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/generators/amqp/utils.py
RENAMED
|
@@ -31,12 +31,13 @@ def snake_case(s: str) -> str:
|
|
|
31
31
|
).lower()
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def camel_case(kind: Literal["upper", "lower"], string: str):
|
|
34
|
+
def camel_case(kind: Literal["upper", "lower"], string: str) -> str:
|
|
35
|
+
if not string:
|
|
36
|
+
return ""
|
|
37
|
+
|
|
35
38
|
string = sub(r"(_|-)+", " ", string).title().replace(" ", "")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
case _, "upper":
|
|
42
|
-
return string[0].upper() + string[1:]
|
|
39
|
+
|
|
40
|
+
if kind == "lower":
|
|
41
|
+
return string[0].lower() + string[1:]
|
|
42
|
+
elif kind == "upper":
|
|
43
|
+
return string[0].upper() + string[1:]
|
asyncapi_python-0.1.0/PKG-INFO
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: asyncapi-python
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary:
|
|
5
|
-
License: Apache-2.0
|
|
6
|
-
Author: Yaroslav Petrov
|
|
7
|
-
Author-email: yaroslav.v.petrov@gmail.com
|
|
8
|
-
Requires-Python: >=3.10,<3.13
|
|
9
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Provides-Extra: amqp
|
|
15
|
-
Provides-Extra: codegen
|
|
16
|
-
Requires-Dist: aio-pika (>=9.4.3,<10.0.0) ; extra == "amqp"
|
|
17
|
-
Requires-Dist: datamodel-code-generator[http] (>=0.26.1,<0.27.0) ; extra == "codegen"
|
|
18
|
-
Requires-Dist: jinja2 (>=3.1.4,<4.0.0) ; extra == "codegen"
|
|
19
|
-
Requires-Dist: pydantic (>=2)
|
|
20
|
-
Requires-Dist: pytz
|
|
21
|
-
Requires-Dist: pyyaml (>=6.0.2,<7.0.0) ; extra == "codegen"
|
|
22
|
-
Requires-Dist: typer[all] (>=0.12.5,<0.13.0) ; extra == "codegen"
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
|
|
25
|
-
# AsyncAPI Python Code Generator
|
|
26
|
-
|
|
27
|
-
> This README is a work in progress
|
|
28
|
-
|
|
29
|
-
A command line interface to generate Python code from AsyncAPI specifications. This tool helps you automatically create Python implementations of AsyncAPI services, reducing boilerplate code and ensuring consistency with your API specifications.
|
|
30
|
-
|
|
31
|
-
## Features
|
|
32
|
-
|
|
33
|
-
- Generates Python code from AsyncAPI specifications
|
|
34
|
-
- Creates both consumer and producer implementations
|
|
35
|
-
- Supports AMQP protocol
|
|
36
|
-
- Includes connection pooling and management
|
|
37
|
-
- Provides base application structure
|
|
38
|
-
|
asyncapi_python-0.1.0/README.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# AsyncAPI Python Code Generator
|
|
2
|
-
|
|
3
|
-
> This README is a work in progress
|
|
4
|
-
|
|
5
|
-
A command line interface to generate Python code from AsyncAPI specifications. This tool helps you automatically create Python implementations of AsyncAPI services, reducing boilerplate code and ensuring consistency with your API specifications.
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- Generates Python code from AsyncAPI specifications
|
|
10
|
-
- Creates both consumer and producer implementations
|
|
11
|
-
- Supports AMQP protocol
|
|
12
|
-
- Includes connection pooling and management
|
|
13
|
-
- Provides base application structure
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/__init__.py
RENAMED
|
File without changes
|
{asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/document/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{asyncapi_python-0.1.0 → asyncapi_python-0.1.2}/src/asyncapi_python_codegen/generators/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|