asyncapi-python 0.3.0rc2__tar.gz → 0.3.0rc4__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.
Files changed (129) hide show
  1. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/PKG-INFO +2 -2
  2. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/README.md +1 -1
  3. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/pyproject.toml +1 -1
  4. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/codec/__init__.py +0 -1
  5. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/codec/json.py +1 -1
  6. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/codec/registry.py +4 -2
  7. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/amqp/factory.py +18 -10
  8. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/amqp/producer.py +34 -6
  9. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/amqp/resolver.py +29 -13
  10. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/amqp/utils.py +1 -0
  11. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/in_memory.py +26 -4
  12. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/application.py +5 -3
  13. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/codec.py +1 -0
  14. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/document/channel.py +3 -2
  15. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/document/message.py +3 -1
  16. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/document/operation.py +3 -2
  17. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/__init__.py +3 -1
  18. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/abc.py +13 -9
  19. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/publisher.py +4 -2
  20. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/rpc_client.py +34 -14
  21. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/rpc_reply_handler.py +38 -14
  22. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/rpc_server.py +31 -17
  23. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/subscriber.py +5 -9
  24. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/typing.py +3 -2
  25. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/wire/__init__.py +7 -4
  26. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/wire/typing.py +9 -7
  27. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/generators/messages.py +3 -2
  28. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/generators/parameters.py +1 -0
  29. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/generators/routers.py +2 -1
  30. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/parser/context.py +1 -0
  31. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/parser/document_loader.py +4 -2
  32. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/parser/extractors.py +11 -10
  33. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/parser/references.py +4 -2
  34. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/templates/application.py.j2 +33 -15
  35. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_pants/rules.py +17 -15
  36. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_pants/targets.py +8 -8
  37. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/codegen/test_parser.py +4 -3
  38. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/conftest.py +1 -0
  39. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/batch_processing.py +5 -3
  40. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/error_handling.py +6 -4
  41. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/fan_in_logging.py +12 -6
  42. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/fan_out_broadcasting.py +12 -6
  43. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/malformed_messages.py +7 -5
  44. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/producer_consumer.py +5 -3
  45. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/reply_channel.py +4 -3
  46. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/test_wire_codec_scenarios.py +10 -12
  47. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/kernel/endpoint/test_batch_processing.py +8 -7
  48. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/kernel/endpoint/test_exception_handling.py +6 -5
  49. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/kernel/endpoint/test_handler_enforcement.py +5 -4
  50. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/kernel/endpoint/test_rpc_endpoints.py +142 -12
  51. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/uv.lock +1 -1
  52. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/.asyncapi-tool +0 -0
  53. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/.devcontainer/Dockerfile +0 -0
  54. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/.devcontainer/devcontainer.json +0 -0
  55. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/.devcontainer/docker-compose.yml +0 -0
  56. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/.github/workflows/release.yml +0 -0
  57. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/.github/workflows/test.yml +0 -0
  58. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/.gitignore +0 -0
  59. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/LICENSE +0 -0
  60. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-pub-sub/.gitignore +0 -0
  61. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-pub-sub/Makefile +0 -0
  62. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-pub-sub/README.md +0 -0
  63. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-pub-sub/main-publisher.py +0 -0
  64. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-pub-sub/main-subscriber.py +0 -0
  65. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-pub-sub/spec/common.asyncapi.yaml +0 -0
  66. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-pub-sub/spec/publisher.asyncapi.yaml +0 -0
  67. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-pub-sub/spec/subscriber.asyncapi.yaml +0 -0
  68. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-rpc/.gitignore +0 -0
  69. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-rpc/Makefile +0 -0
  70. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-rpc/README.md +0 -0
  71. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-rpc/main-client.py +0 -0
  72. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-rpc/main-server.py +0 -0
  73. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-rpc/spec/client.asyncapi.yaml +0 -0
  74. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-rpc/spec/common.asyncapi.yaml +0 -0
  75. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-rpc/spec/server.asyncapi.yaml +0 -0
  76. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-work-queue/.gitignore +0 -0
  77. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-work-queue/Makefile +0 -0
  78. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-work-queue/README.md +0 -0
  79. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-work-queue/main-producer.py +0 -0
  80. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-work-queue/main-worker.py +0 -0
  81. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-work-queue/spec/common.asyncapi.yaml +0 -0
  82. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-work-queue/spec/producer.asyncapi.yaml +0 -0
  83. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/amqp-work-queue/spec/worker.asyncapi.yaml +0 -0
  84. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/examples/specs/financial-trading-system.yaml +0 -0
  85. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/__init__.py +0 -0
  86. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/__init__.py +0 -0
  87. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/__init__.py +0 -0
  88. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/amqp/__init__.py +0 -0
  89. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/amqp/config.py +0 -0
  90. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/amqp/consumer.py +2 -2
  91. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/contrib/wire/amqp/message.py +0 -0
  92. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/__init__.py +0 -0
  93. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/document/__init__.py +7 -7
  94. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/document/bindings.py +1 -1
  95. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/document/common.py +0 -0
  96. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/exceptions.py +0 -0
  97. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/endpoint/message.py +0 -0
  98. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/kernel/exceptions.py +0 -0
  99. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/py.typed +0 -0
  100. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python/utils.py +0 -0
  101. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/__init__.py +3 -3
  102. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/cli.py +0 -0
  103. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/generators/__init__.py +0 -0
  104. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/generators/main.py +3 -3
  105. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/generators/templates.py +0 -0
  106. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/parser/__init__.py +1 -1
  107. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/parser/types.py +1 -1
  108. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/templates/__init__.py.j2 +0 -0
  109. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/templates/messages.py.j2 +0 -0
  110. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/templates/messages_datamodel.py.j2 +0 -0
  111. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/templates/parameters.py.j2 +0 -0
  112. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_codegen/templates/router.py.j2 +0 -0
  113. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_pants/__init__.py +0 -0
  114. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_pants/py.typed +0 -0
  115. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/src/asyncapi_python_pants/register.py +2 -2
  116. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/__init__.py +0 -0
  117. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/codegen/__init__.py +0 -0
  118. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/codegen/specs/relative_refs/common/channels.yaml +0 -0
  119. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/codegen/specs/relative_refs/main.yaml +0 -0
  120. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/codegen/specs/relative_refs/shared/messages.yaml +0 -0
  121. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/codegen/specs/relative_refs/shared/notifications.yaml +0 -0
  122. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/codegen/specs/rpc.yaml +0 -0
  123. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/codegen/specs/simple.yaml +0 -0
  124. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/__init__.py +0 -0
  125. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/__init__.py +3 -3
  126. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/scenarios/many_to_many_microservices.py +7 -7
  127. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/test_app/__init__.py +0 -0
  128. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/test_app/messages/__init__.py +0 -0
  129. {asyncapi_python-0.3.0rc2 → asyncapi_python-0.3.0rc4}/tests/integration/test_app/messages/json.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asyncapi-python
3
- Version: 0.3.0rc2
3
+ Version: 0.3.0rc4
4
4
  Summary: Easily generate type-safe and async Python applications from AsyncAPI 3 specifications.
5
5
  Author-email: Yaroslav Petrov <yaroslav.v.petrov@gmail.com>
6
6
  License: Apache-2.0
@@ -21,7 +21,7 @@ Description-Content-Type: text/markdown
21
21
  # AsyncAPI Python Code Generator
22
22
  >
23
23
  > [!IMPORTANT]
24
- > Although commits to dev branch might seem infrequent, the project is under active development **as of August 2025**.
24
+ > Although commits to dev branch might seem infrequent, the project is under active development **as of November 2025**.
25
25
  >
26
26
  > We currently produce only those changes that are required to satisfy our personal use cases.
27
27
  >
@@ -1,7 +1,7 @@
1
1
  # AsyncAPI Python Code Generator
2
2
  >
3
3
  > [!IMPORTANT]
4
- > Although commits to dev branch might seem infrequent, the project is under active development **as of August 2025**.
4
+ > Although commits to dev branch might seem infrequent, the project is under active development **as of November 2025**.
5
5
  >
6
6
  > We currently produce only those changes that are required to satisfy our personal use cases.
7
7
  >
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asyncapi-python"
3
- version = "0.3.0rc2"
3
+ version = "0.3.0rc4"
4
4
  license = { text = "Apache-2.0" }
5
5
  description = "Easily generate type-safe and async Python applications from AsyncAPI 3 specifications."
6
6
  authors = [{ name = "Yaroslav Petrov", email = "yaroslav.v.petrov@gmail.com" }]
@@ -2,5 +2,4 @@
2
2
 
3
3
  from .registry import CodecRegistry
4
4
 
5
-
6
5
  __all__ = ["CodecRegistry"]
@@ -1,6 +1,6 @@
1
1
  import json
2
- from typing import Type, ClassVar
3
2
  from types import ModuleType
3
+ from typing import ClassVar, Type
4
4
 
5
5
  from pydantic import BaseModel, ValidationError
6
6
 
@@ -1,7 +1,9 @@
1
- from typing import ClassVar, Any
2
1
  from types import ModuleType
3
- from asyncapi_python.kernel.codec import CodecFactory, Codec
2
+ from typing import Any, ClassVar
3
+
4
+ from asyncapi_python.kernel.codec import Codec, CodecFactory
4
5
  from asyncapi_python.kernel.document.message import Message
6
+
5
7
  from .json import JsonCodecFactory
6
8
 
7
9
 
@@ -1,7 +1,8 @@
1
1
  """AMQP wire factory implementation"""
2
2
 
3
3
  import secrets
4
- from typing import Optional, Callable, Any, cast
4
+ from typing import Any, Callable, Optional, cast
5
+
5
6
  from typing_extensions import Unpack
6
7
 
7
8
  try:
@@ -13,11 +14,11 @@ except ImportError as e:
13
14
  ) from e
14
15
 
15
16
  from asyncapi_python.kernel.wire import AbstractWireFactory, EndpointParams
16
- from asyncapi_python.kernel.wire.typing import Producer, Consumer
17
+ from asyncapi_python.kernel.wire.typing import Consumer, Producer
17
18
 
18
- from .message import AmqpWireMessage, AmqpIncomingMessage
19
- from .producer import AmqpProducer
20
19
  from .consumer import AmqpConsumer
20
+ from .message import AmqpIncomingMessage, AmqpWireMessage
21
+ from .producer import AmqpProducer
21
22
  from .resolver import resolve_amqp_config
22
23
 
23
24
 
@@ -31,7 +32,6 @@ class AmqpWire(AbstractWireFactory[AmqpWireMessage, AmqpIncomingMessage]):
31
32
  def __init__(
32
33
  self,
33
34
  connection_url: str,
34
- service_name: str = "app",
35
35
  robust: bool = False,
36
36
  reconnect_interval: float = 1.0,
37
37
  max_reconnect_interval: float = 60.0,
@@ -45,7 +45,6 @@ class AmqpWire(AbstractWireFactory[AmqpWireMessage, AmqpIncomingMessage]):
45
45
 
46
46
  Args:
47
47
  connection_url: AMQP connection URL
48
- service_name: Service name prefix for app_id
49
48
  robust: Enable robust connection with auto-reconnect (default: False)
50
49
  reconnect_interval: Initial reconnect interval in seconds (for robust mode)
51
50
  max_reconnect_interval: Maximum reconnect interval in seconds (for robust mode)
@@ -55,9 +54,10 @@ class AmqpWire(AbstractWireFactory[AmqpWireMessage, AmqpIncomingMessage]):
55
54
  on_connection_lost: Callback when connection is lost (for non-robust mode)
56
55
  """
57
56
  self._connection_url = connection_url
58
- # Generate app_id with service name plus 8 random hex characters
57
+ # Generate fallback app_id with random hex characters
58
+ # Note: For RPC, app_id should be provided via EndpointParams from application level
59
59
  random_hex = secrets.token_hex(4) # 4 bytes = 8 hex chars
60
- self._app_id = f"{service_name}-{random_hex}"
60
+ self._app_id = f"wire-{random_hex}"
61
61
  self._connection: AbstractConnection | None = None
62
62
  self._robust = robust
63
63
  self._reconnect_interval = reconnect_interval
@@ -135,8 +135,12 @@ class AmqpWire(AbstractWireFactory[AmqpWireMessage, AmqpIncomingMessage]):
135
135
  # Generate operation name from available information
136
136
  operation_name = self._generate_operation_name(kwargs)
137
137
 
138
+ # Use provided app_id if available, otherwise use instance app_id
139
+ # This allows application-level control over queue naming
140
+ app_id = kwargs.get("app_id", self._app_id)
141
+
138
142
  # Resolve AMQP configuration using pattern matching
139
- config = resolve_amqp_config(kwargs, operation_name, self._app_id)
143
+ config = resolve_amqp_config(kwargs, operation_name, app_id)
140
144
 
141
145
  connection = await self._get_connection()
142
146
 
@@ -154,8 +158,12 @@ class AmqpWire(AbstractWireFactory[AmqpWireMessage, AmqpIncomingMessage]):
154
158
  # Generate operation name from available information
155
159
  operation_name = self._generate_operation_name(kwargs)
156
160
 
161
+ # Use provided app_id if available, otherwise use instance app_id
162
+ # This allows application-level control over queue naming
163
+ app_id = kwargs.get("app_id", self._app_id)
164
+
157
165
  # Resolve AMQP configuration using pattern matching
158
- config = resolve_amqp_config(kwargs, operation_name, self._app_id)
166
+ config = resolve_amqp_config(kwargs, operation_name, app_id)
159
167
 
160
168
  connection = await self._get_connection()
161
169
 
@@ -3,10 +3,11 @@
3
3
  from typing import Any
4
4
 
5
5
  try:
6
- from aio_pika import Message as AmqpMessage, ExchangeType # type: ignore[import-not-found]
6
+ from aio_pika import ExchangeType
7
+ from aio_pika import Message as AmqpMessage # type: ignore[import-not-found]
7
8
  from aio_pika.abc import ( # type: ignore[import-not-found]
8
- AbstractConnection,
9
9
  AbstractChannel,
10
+ AbstractConnection,
10
11
  AbstractExchange,
11
12
  )
12
13
  except ImportError as e:
@@ -100,11 +101,38 @@ class AmqpProducer(Producer[AmqpWireMessage]):
100
101
 
101
102
  self._started = False
102
103
 
103
- async def send_batch(self, messages: list[AmqpWireMessage]) -> None:
104
- """Send a batch of messages using the configured exchange"""
104
+ async def send_batch(
105
+ self, messages: list[AmqpWireMessage], *, address_override: str | None = None
106
+ ) -> None:
107
+ """Send a batch of messages using the configured exchange
108
+
109
+ Args:
110
+ messages: Messages to send
111
+ address_override: Optional dynamic routing key/queue to override static config.
112
+ If provided, overrides self._routing_key for this send operation.
113
+ If None, uses static routing_key from configuration/bindings.
114
+ """
105
115
  if not self._started or not self._channel or not self._target_exchange:
106
116
  raise RuntimeError("Producer not started")
107
117
 
118
+ # Determine effective routing key: override takes precedence over static config
119
+ effective_routing_key = (
120
+ address_override if address_override is not None else self._routing_key
121
+ )
122
+
123
+ # Validate we have a destination
124
+ # Fail ONLY if both are truly missing:
125
+ # - address_override is None (not provided by caller)
126
+ # - AND self._routing_key is "" (no static config was derived from channel/bindings/operation)
127
+ # Note: empty string IS valid when explicitly configured (fanout exchanges, default exchange)
128
+ if address_override is None and not self._routing_key:
129
+ raise ValueError(
130
+ f"Cannot send: no routing destination available. "
131
+ f"RPC replies require reply_to from the request, or the channel must "
132
+ f"have address/bindings/operation-name to derive destination. "
133
+ f"(address_override={address_override}, routing_key={self._routing_key!r})"
134
+ )
135
+
108
136
  for message in messages:
109
137
  amqp_message = AmqpMessage(
110
138
  body=message.payload,
@@ -113,8 +141,8 @@ class AmqpProducer(Producer[AmqpWireMessage]):
113
141
  reply_to=message.reply_to,
114
142
  )
115
143
 
116
- # Publish to the configured target exchange (not always default)
144
+ # Publish to the configured target exchange with dynamic or static routing key
117
145
  await self._target_exchange.publish(
118
146
  amqp_message,
119
- routing_key=self._routing_key,
147
+ routing_key=effective_routing_key,
120
148
  )
@@ -1,12 +1,13 @@
1
1
  """Binding resolution with comprehensive pattern matching"""
2
2
 
3
3
  from typing import Any
4
- from asyncapi_python.kernel.wire import EndpointParams
5
- from asyncapi_python.kernel.document.channel import Channel
4
+
6
5
  from asyncapi_python.kernel.document.bindings import AmqpChannelBinding
6
+ from asyncapi_python.kernel.document.channel import Channel
7
+ from asyncapi_python.kernel.wire import EndpointParams
7
8
 
8
- from .config import AmqpConfig, AmqpBindingType
9
- from .utils import validate_parameters_strict, substitute_parameters
9
+ from .config import AmqpBindingType, AmqpConfig
10
+ from .utils import substitute_parameters, validate_parameters_strict
10
11
 
11
12
 
12
13
  def resolve_amqp_config(
@@ -57,17 +58,32 @@ def resolve_amqp_config(
57
58
  },
58
59
  )
59
60
 
60
- # Reply channel with explicit address - shared channel with filtering
61
+ # Reply channel with explicit address - check if direct queue or topic exchange
61
62
  case (True, _, address, _) if address:
62
63
  resolved_address = substitute_parameters(address, param_values)
63
- return AmqpConfig(
64
- queue_name=f"reply-{app_id}", # App-specific reply queue
65
- exchange_name=resolved_address, # Shared exchange for replies
66
- exchange_type="topic", # Enable pattern matching for filtering
67
- routing_key=app_id, # Filter messages by app_id
68
- binding_type=AmqpBindingType.REPLY,
69
- queue_properties={"durable": True, "exclusive": False},
70
- )
64
+ # If address starts with "reply-", treat it as a direct queue name (RPC pattern)
65
+ if resolved_address.startswith("reply-"):
66
+ return AmqpConfig(
67
+ queue_name=resolved_address, # Use address as queue name
68
+ exchange_name="", # Default exchange for direct routing
69
+ routing_key=resolved_address, # Route directly to queue
70
+ binding_type=AmqpBindingType.REPLY,
71
+ queue_properties={
72
+ "durable": False,
73
+ "exclusive": True,
74
+ "auto_delete": True,
75
+ },
76
+ )
77
+ else:
78
+ # Topic-based reply pattern - shared exchange with filtering
79
+ return AmqpConfig(
80
+ queue_name=f"reply-{app_id}", # App-specific reply queue
81
+ exchange_name=resolved_address, # Shared exchange for replies
82
+ exchange_type="topic", # Enable pattern matching for filtering
83
+ routing_key=app_id, # Filter messages by app_id
84
+ binding_type=AmqpBindingType.REPLY,
85
+ queue_properties={"durable": True, "exclusive": False},
86
+ )
71
87
 
72
88
  # Reply channel with binding - defer to binding resolution
73
89
  case (True, binding, _, _) if binding and binding.type == "queue":
@@ -3,6 +3,7 @@
3
3
  # TODO: This thing should be general wire utils, not tied to specific wire
4
4
 
5
5
  import re
6
+
6
7
  from asyncapi_python.kernel.document.channel import Channel
7
8
 
8
9
 
@@ -4,10 +4,11 @@ import asyncio
4
4
  from collections import defaultdict, deque
5
5
  from dataclasses import dataclass, field
6
6
  from typing import Any, AsyncGenerator
7
+
7
8
  from typing_extensions import Unpack
8
9
 
9
10
  from asyncapi_python.kernel.wire import AbstractWireFactory, EndpointParams
10
- from asyncapi_python.kernel.wire.typing import Producer, Consumer
11
+ from asyncapi_python.kernel.wire.typing import Consumer, Producer
11
12
 
12
13
 
13
14
  @dataclass
@@ -142,13 +143,34 @@ class InMemoryProducer(Producer[InMemoryMessage]):
142
143
  """Stop the producer"""
143
144
  self._started = False
144
145
 
145
- async def send_batch(self, messages: list[InMemoryMessage]) -> None:
146
- """Send a batch of messages to the channel"""
146
+ async def send_batch(
147
+ self, messages: list[InMemoryMessage], *, address_override: str | None = None
148
+ ) -> None:
149
+ """Send a batch of messages to the channel
150
+
151
+ Args:
152
+ messages: Messages to send
153
+ address_override: Optional dynamic channel name to override static config.
154
+ If provided, overrides self._channel_name for this send operation.
155
+ If None, uses static channel_name from configuration.
156
+ """
147
157
  if not self._started:
148
158
  raise RuntimeError("Producer not started")
149
159
 
160
+ # Determine effective channel: override takes precedence over static config
161
+ effective_channel = (
162
+ address_override if address_override is not None else self._channel_name
163
+ )
164
+
165
+ # Validate we have a destination
166
+ if not effective_channel:
167
+ raise ValueError(
168
+ f"Cannot send: no channel specified. "
169
+ f"address_override={address_override}, channel_name={self._channel_name}"
170
+ )
171
+
150
172
  for message in messages:
151
- await _bus.publish(self._channel_name, message)
173
+ await _bus.publish(effective_channel, message)
152
174
 
153
175
 
154
176
  class InMemoryConsumer(Consumer[InMemoryIncomingMessage]):
@@ -1,12 +1,14 @@
1
1
  import asyncio
2
- from typing import TypedDict, Any
3
- from typing_extensions import Unpack, Required, NotRequired
2
+ from typing import Any, TypedDict
3
+
4
+ from typing_extensions import NotRequired, Required, Unpack
4
5
 
5
6
  from asyncapi_python.kernel.document.operation import Operation
6
7
  from asyncapi_python.kernel.wire import AbstractWireFactory
8
+
9
+ from .codec import CodecFactory
7
10
  from .endpoint import AbstractEndpoint, EndpointFactory
8
11
  from .endpoint.abc import EndpointParams
9
- from .codec import CodecFactory
10
12
 
11
13
 
12
14
  class BaseApplication:
@@ -3,6 +3,7 @@ from types import ModuleType
3
3
  from typing import Generic, Protocol
4
4
 
5
5
  from asyncapi_python.kernel.document.message import Message
6
+
6
7
  from .typing import T_DecodedPayload, T_EncodedPayload
7
8
 
8
9
 
@@ -1,8 +1,9 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Any
3
- from .message import Message
4
- from .common import *
3
+
5
4
  from .bindings import AmqpChannelBinding
5
+ from .common import *
6
+ from .message import Message
6
7
 
7
8
  __all__ = ["AddressParameter", "ChannelBindings", "Channel"]
8
9
 
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
4
  from typing import Any
4
- from .common import *
5
+
5
6
  from .bindings import AmqpMessageBinding
7
+ from .common import *
6
8
 
7
9
  __all__ = [
8
10
  "CorrelationId",
@@ -1,9 +1,10 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Any, Literal
3
- from .common import *
3
+
4
+ from .bindings import AmqpOperationBinding
4
5
  from .channel import Channel
6
+ from .common import *
5
7
  from .message import Message
6
- from .bindings import AmqpOperationBinding
7
8
 
8
9
  __all__ = [
9
10
  "SecurityScheme",
@@ -1,10 +1,12 @@
1
1
  from typing import ClassVar, Literal
2
+
2
3
  from typing_extensions import Unpack
4
+
3
5
  from .abc import AbstractEndpoint
4
6
  from .publisher import Publisher
5
- from .subscriber import Subscriber
6
7
  from .rpc_client import RpcClient
7
8
  from .rpc_server import RpcServer
9
+ from .subscriber import Subscriber
8
10
 
9
11
  __all__ = [
10
12
  "AbstractEndpoint",
@@ -1,19 +1,23 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, Callable, Generic, TypedDict, overload, Union
3
- from typing_extensions import Unpack, Required, NotRequired
2
+ from typing import Any, Callable, Generic, TypedDict, Union, overload
3
+
4
+ from typing_extensions import NotRequired, Required, Unpack
4
5
 
5
- from ..typing import Handler, T_Input, T_Output, BatchConfig
6
- from asyncapi_python.kernel.wire import AbstractWireFactory
7
- from asyncapi_python.kernel.document import Operation
8
6
  from asyncapi_python.kernel.codec import Codec, CodecFactory
7
+ from asyncapi_python.kernel.document import Operation
8
+ from asyncapi_python.kernel.wire import AbstractWireFactory
9
+
10
+ from ..typing import BatchConfig, Handler, T_Input, T_Output
9
11
 
10
12
 
11
- class EndpointParams(TypedDict):
13
+ class EndpointParams(TypedDict, total=False):
12
14
  """Optional parameters for endpoint configuration"""
13
15
 
14
- disable_handler_validation: NotRequired[
15
- bool
16
- ] # Opt-out of handler enforcement for testing
16
+ service_name: str # Service name for generating app_id
17
+ default_rpc_timeout: (
18
+ float | None
19
+ ) # Default timeout in seconds for RPC client requests (default: 180.0), or None to disable
20
+ disable_handler_validation: bool # Opt-out of handler enforcement for testing
17
21
 
18
22
 
19
23
  class HandlerParams(TypedDict):
@@ -1,11 +1,13 @@
1
1
  from typing import Generic
2
+
2
3
  from typing_extensions import Unpack
3
4
 
5
+ from asyncapi_python.kernel.wire import Producer
6
+
7
+ from ..typing import T_Input
4
8
  from .abc import AbstractEndpoint, Send
5
9
  from .exceptions import UninitializedError
6
10
  from .message import WireMessage
7
- from ..typing import T_Input
8
- from asyncapi_python.kernel.wire import Producer
9
11
 
10
12
 
11
13
  class Publisher(AbstractEndpoint, Send[T_Input, None], Generic[T_Input]):
@@ -1,15 +1,15 @@
1
1
  import asyncio
2
2
  from typing import Generic
3
- from typing_extensions import Unpack
4
3
  from uuid import uuid4
5
4
 
6
- from .abc import AbstractEndpoint, Send
7
- from .exceptions import UninitializedError, TimeoutError
8
- from .message import WireMessage
9
- from ..typing import T_Input, T_Output, IncomingMessage
10
- from asyncapi_python.kernel.wire import Producer
5
+ from typing_extensions import NotRequired, Unpack
11
6
 
7
+ from asyncapi_python.kernel.wire import Producer
12
8
 
9
+ from ..typing import IncomingMessage, T_Input, T_Output
10
+ from .abc import AbstractEndpoint, Send
11
+ from .exceptions import TimeoutError, UninitializedError
12
+ from .message import WireMessage
13
13
  from .rpc_reply_handler import global_reply_handler
14
14
 
15
15
 
@@ -21,6 +21,13 @@ class RpcClient(AbstractEndpoint, Send[T_Input, T_Output], Generic[T_Input, T_Ou
21
21
  a single reply consumer and background task for efficiency.
22
22
  """
23
23
 
24
+ class RouterInputs(Send.RouterInputs):
25
+ """Router inputs for RPC client, extending Send.RouterInputs with timeout"""
26
+
27
+ timeout: NotRequired[
28
+ float | None
29
+ ] # Timeout in seconds for this RPC request, or None to disable timeout
30
+
24
31
  def __init__(self, **kwargs: Unpack[AbstractEndpoint.Inputs]):
25
32
  super().__init__(**kwargs)
26
33
  # Instance-specific state
@@ -44,7 +51,12 @@ class RpcClient(AbstractEndpoint, Send[T_Input, T_Output], Generic[T_Input, T_Ou
44
51
  global_reply_handler.increment_instance_count()
45
52
 
46
53
  # Ensure global reply handling is set up (only happens once)
47
- await global_reply_handler.ensure_reply_handler(self._wire, self._operation)
54
+ await global_reply_handler.ensure_reply_handler(
55
+ self._wire, self._operation, self._endpoint_params
56
+ )
57
+
58
+ # Extract service_name from endpoint_params for app_id
59
+ service_name = self._endpoint_params.get("service_name", "app")
48
60
 
49
61
  # Create instance-specific producer for sending requests
50
62
  self._producer = await self._wire.create_producer(
@@ -52,6 +64,7 @@ class RpcClient(AbstractEndpoint, Send[T_Input, T_Output], Generic[T_Input, T_Ou
52
64
  parameters={},
53
65
  op_bindings=self._operation.bindings,
54
66
  is_reply=False,
67
+ app_id=service_name,
55
68
  )
56
69
 
57
70
  # Start producer
@@ -70,18 +83,17 @@ class RpcClient(AbstractEndpoint, Send[T_Input, T_Output], Generic[T_Input, T_Ou
70
83
  if remaining_count == 0:
71
84
  await global_reply_handler.cleanup_if_last_instance()
72
85
 
73
- async def __call__(
74
- self,
75
- payload: T_Input,
76
- /,
77
- timeout: float = 30.0,
78
- **kwargs: Unpack[Send.RouterInputs],
86
+ async def __call__( # type: ignore[override]
87
+ self, payload: T_Input, /, **kwargs: Unpack[RouterInputs]
79
88
  ) -> T_Output:
80
89
  """Send an RPC request and wait for response using global reply handling
81
90
 
82
91
  Args:
83
92
  payload: The request payload to send
84
- timeout: Maximum time to wait for response (default 30 seconds)
93
+ **kwargs: Router inputs including optional timeout:
94
+ - Not provided: uses default_rpc_timeout from endpoint_params (default: 180.0)
95
+ - float: uses the specified timeout in seconds
96
+ - None: disables timeout (waits indefinitely)
85
97
 
86
98
  Returns:
87
99
  The response payload
@@ -93,6 +105,14 @@ class RpcClient(AbstractEndpoint, Send[T_Input, T_Output], Generic[T_Input, T_Ou
93
105
  if not self._producer:
94
106
  raise UninitializedError()
95
107
 
108
+ # Determine timeout: use provided value, or fall back to endpoint_params default
109
+ if "timeout" in kwargs:
110
+ # Explicitly provided (could be float or None)
111
+ timeout = kwargs["timeout"]
112
+ else:
113
+ # Not provided, use default from endpoint_params
114
+ timeout = self._endpoint_params.get("default_rpc_timeout", 180.0)
115
+
96
116
  # Generate correlation ID for this request
97
117
  correlation_id: str = str(uuid4())
98
118
 
@@ -2,11 +2,13 @@
2
2
 
3
3
  import asyncio
4
4
  import secrets
5
-
6
- from ..typing import IncomingMessage
7
5
  from typing import Any
8
- from asyncapi_python.kernel.wire import Consumer, AbstractWireFactory
6
+
9
7
  from asyncapi_python.kernel.document import Channel, Operation
8
+ from asyncapi_python.kernel.wire import AbstractWireFactory, Consumer
9
+
10
+ from ..typing import IncomingMessage
11
+ from .abc import EndpointParams
10
12
 
11
13
 
12
14
  class GlobalRpcReplyHandler:
@@ -25,37 +27,59 @@ class GlobalRpcReplyHandler:
25
27
  self._instance_count: int = 0
26
28
 
27
29
  async def ensure_reply_handler(
28
- self, wire_factory: AbstractWireFactory[Any, Any], operation: Operation
30
+ self,
31
+ wire_factory: AbstractWireFactory[Any, Any],
32
+ operation: Operation,
33
+ endpoint_params: EndpointParams,
29
34
  ) -> None:
30
- """Ensure reply consumer and task are running"""
35
+ """Ensure reply consumer and task are running
36
+
37
+ Args:
38
+ wire_factory: Wire factory for creating consumer
39
+ operation: Operation definition
40
+ endpoint_params: Endpoint parameters including service_name
41
+ """
31
42
  if self._reply_consumer is None:
32
- # Create reply consumer (only once for all instances)
33
- reply_channel = self._get_or_create_reply_channel(operation)
43
+ # Extract service_name from endpoint_params
44
+ service_name = endpoint_params.get("service_name", "app")
34
45
 
46
+ # Generate app_id with service name + random hex (same format as AmqpWire)
47
+ random_hex = secrets.token_hex(4) # 4 bytes = 8 hex chars
48
+ app_id = f"{service_name}-{random_hex}"
49
+
50
+ # Use app_id as the reply queue name
51
+ self._reply_queue_name = f"reply-{app_id}"
52
+
53
+ # Create reply channel with the generated queue name as address
54
+ reply_channel = self._get_or_create_reply_channel(
55
+ operation, self._reply_queue_name
56
+ )
57
+
58
+ # Create reply consumer with the channel (wire factory will use the address)
35
59
  self._reply_consumer = await wire_factory.create_consumer(
36
60
  channel=reply_channel,
37
61
  parameters={},
38
62
  op_bindings=None,
39
63
  is_reply=True,
64
+ app_id=app_id,
40
65
  )
41
66
 
42
- # Generate unique reply queue name for all clients
43
- self._reply_queue_name = f"reply-{secrets.token_hex(8)}"
44
-
45
67
  # Start the consumer
46
68
  await self._reply_consumer.start()
47
69
 
48
70
  # Start background task
49
71
  self._consume_task = asyncio.create_task(self._consume_all_replies())
50
72
 
51
- def _get_or_create_reply_channel(self, operation: Operation) -> Channel:
52
- """Get reply channel from operation or create default one"""
73
+ def _get_or_create_reply_channel(
74
+ self, operation: Operation, queue_name: str
75
+ ) -> Channel:
76
+ """Get reply channel from operation or create default one with specified queue name"""
53
77
  if operation.reply and operation.reply.channel:
54
78
  return operation.reply.channel
55
79
  else:
56
- # Create a default reply channel for global use
80
+ # Create a default reply channel with the generated queue name as address
57
81
  return Channel(
58
- address=None, # Use default/null address for global reply queue
82
+ address=queue_name, # Use the generated queue name as address
59
83
  title="Global RPC Reply Queue",
60
84
  summary=None,
61
85
  description=None,