microagent 1.8.2__tar.gz → 1.8.3__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 (80) hide show
  1. {microagent-1.8.2 → microagent-1.8.3}/CHANGELOG.rst +7 -0
  2. {microagent-1.8.2 → microagent-1.8.3}/PKG-INFO +1 -1
  3. {microagent-1.8.2 → microagent-1.8.3}/microagent/__init__.py +1 -1
  4. {microagent-1.8.2 → microagent-1.8.3}/microagent/abc.py +3 -0
  5. {microagent-1.8.2 → microagent-1.8.3}/microagent/agent.py +2 -0
  6. {microagent-1.8.2 → microagent-1.8.3}/microagent/broker.py +12 -0
  7. {microagent-1.8.2 → microagent-1.8.3}/microagent/queue.py +1 -1
  8. {microagent-1.8.2 → microagent-1.8.3}/microagent/signal.py +1 -1
  9. {microagent-1.8.2 → microagent-1.8.3}/microagent/tools/amqp.py +44 -18
  10. {microagent-1.8.2 → microagent-1.8.3}/microagent/tools/kafka.py +15 -12
  11. {microagent-1.8.2 → microagent-1.8.3}/microagent/tools/mocks.py +3 -2
  12. {microagent-1.8.2 → microagent-1.8.3}/microagent/tools/redis.py +22 -24
  13. {microagent-1.8.2 → microagent-1.8.3}/microagent.egg-info/PKG-INFO +1 -1
  14. {microagent-1.8.2 → microagent-1.8.3}/tests/test_tools_amqp.py +5 -2
  15. {microagent-1.8.2 → microagent-1.8.3}/tests/test_tools_kafka.py +11 -4
  16. {microagent-1.8.2 → microagent-1.8.3}/tests/test_tools_redis.py +1 -1
  17. {microagent-1.8.2 → microagent-1.8.3}/.github/workflows/python-app.yml +0 -0
  18. {microagent-1.8.2 → microagent-1.8.3}/.gitignore +0 -0
  19. {microagent-1.8.2 → microagent-1.8.3}/.readthedocs.yml +0 -0
  20. {microagent-1.8.2 → microagent-1.8.3}/.travis.yml +0 -0
  21. {microagent-1.8.2 → microagent-1.8.3}/LICENSE.md +0 -0
  22. {microagent-1.8.2 → microagent-1.8.3}/Makefile +0 -0
  23. {microagent-1.8.2 → microagent-1.8.3}/README.rst +0 -0
  24. {microagent-1.8.2 → microagent-1.8.3}/docs/Makefile +0 -0
  25. {microagent-1.8.2 → microagent-1.8.3}/docs/agent.rst +0 -0
  26. {microagent-1.8.2 → microagent-1.8.3}/docs/amqp.rst +0 -0
  27. {microagent-1.8.2 → microagent-1.8.3}/docs/broker.rst +0 -0
  28. {microagent-1.8.2 → microagent-1.8.3}/docs/bus.rst +0 -0
  29. {microagent-1.8.2 → microagent-1.8.3}/docs/conf.py +0 -0
  30. {microagent-1.8.2 → microagent-1.8.3}/docs/example_agents.rst +0 -0
  31. {microagent-1.8.2 → microagent-1.8.3}/docs/example_run.rst +0 -0
  32. {microagent-1.8.2 → microagent-1.8.3}/docs/hooks.rst +0 -0
  33. {microagent-1.8.2 → microagent-1.8.3}/docs/index.rst +0 -0
  34. {microagent-1.8.2 → microagent-1.8.3}/docs/kafka.rst +0 -0
  35. {microagent-1.8.2 → microagent-1.8.3}/docs/launcher.rst +0 -0
  36. {microagent-1.8.2 → microagent-1.8.3}/docs/ma-bus.dia +0 -0
  37. {microagent-1.8.2 → microagent-1.8.3}/docs/ma-bus.svg +0 -0
  38. {microagent-1.8.2 → microagent-1.8.3}/docs/ma-logo.dia +0 -0
  39. {microagent-1.8.2 → microagent-1.8.3}/docs/ma-logo.svg +0 -0
  40. {microagent-1.8.2 → microagent-1.8.3}/docs/ma-periodic.dia +0 -0
  41. {microagent-1.8.2 → microagent-1.8.3}/docs/ma-periodic.svg +0 -0
  42. {microagent-1.8.2 → microagent-1.8.3}/docs/ma-queue.dia +0 -0
  43. {microagent-1.8.2 → microagent-1.8.3}/docs/ma-queue.svg +0 -0
  44. {microagent-1.8.2 → microagent-1.8.3}/docs/mocks.rst +0 -0
  45. {microagent-1.8.2 → microagent-1.8.3}/docs/periodic.rst +0 -0
  46. {microagent-1.8.2 → microagent-1.8.3}/docs/redis.rst +0 -0
  47. {microagent-1.8.2 → microagent-1.8.3}/docs/requirements.txt +0 -0
  48. {microagent-1.8.2 → microagent-1.8.3}/examples/__init__.py +0 -0
  49. {microagent-1.8.2 → microagent-1.8.3}/examples/comment_agent.py +0 -0
  50. {microagent-1.8.2 → microagent-1.8.3}/examples/email_agent.py +0 -0
  51. {microagent-1.8.2 → microagent-1.8.3}/examples/push_agent.py +0 -0
  52. {microagent-1.8.2 → microagent-1.8.3}/examples/redis_server.py +0 -0
  53. {microagent-1.8.2 → microagent-1.8.3}/examples/requirements.txt +0 -0
  54. {microagent-1.8.2 → microagent-1.8.3}/examples/settings.py +0 -0
  55. {microagent-1.8.2 → microagent-1.8.3}/examples/signals.json +0 -0
  56. {microagent-1.8.2 → microagent-1.8.3}/examples/user_agent.py +0 -0
  57. {microagent-1.8.2 → microagent-1.8.3}/microagent/bus.py +0 -0
  58. {microagent-1.8.2 → microagent-1.8.3}/microagent/hooks.py +0 -0
  59. {microagent-1.8.2 → microagent-1.8.3}/microagent/launcher.py +0 -0
  60. {microagent-1.8.2 → microagent-1.8.3}/microagent/py.typed +0 -0
  61. {microagent-1.8.2 → microagent-1.8.3}/microagent/timer.py +0 -0
  62. {microagent-1.8.2 → microagent-1.8.3}/microagent/tools/__init__.py +0 -0
  63. {microagent-1.8.2 → microagent-1.8.3}/microagent/utils.py +0 -0
  64. {microagent-1.8.2 → microagent-1.8.3}/microagent.egg-info/SOURCES.txt +0 -0
  65. {microagent-1.8.2 → microagent-1.8.3}/microagent.egg-info/dependency_links.txt +0 -0
  66. {microagent-1.8.2 → microagent-1.8.3}/microagent.egg-info/entry_points.txt +0 -0
  67. {microagent-1.8.2 → microagent-1.8.3}/microagent.egg-info/requires.txt +0 -0
  68. {microagent-1.8.2 → microagent-1.8.3}/microagent.egg-info/top_level.txt +0 -0
  69. {microagent-1.8.2 → microagent-1.8.3}/pyproject.toml +0 -0
  70. {microagent-1.8.2 → microagent-1.8.3}/setup.cfg +0 -0
  71. {microagent-1.8.2 → microagent-1.8.3}/tests/__init__.py +0 -0
  72. {microagent-1.8.2 → microagent-1.8.3}/tests/conftest.py +0 -0
  73. {microagent-1.8.2 → microagent-1.8.3}/tests/stuff.json +0 -0
  74. {microagent-1.8.2 → microagent-1.8.3}/tests/test_agent.py +0 -0
  75. {microagent-1.8.2 → microagent-1.8.3}/tests/test_api.py +0 -0
  76. {microagent-1.8.2 → microagent-1.8.3}/tests/test_broker.py +0 -0
  77. {microagent-1.8.2 → microagent-1.8.3}/tests/test_bus.py +0 -0
  78. {microagent-1.8.2 → microagent-1.8.3}/tests/test_hook.py +0 -0
  79. {microagent-1.8.2 → microagent-1.8.3}/tests/test_launcher.py +0 -0
  80. {microagent-1.8.2 → microagent-1.8.3}/tests/test_timer.py +0 -0
@@ -3,6 +3,13 @@ Changelog
3
3
  =========
4
4
 
5
5
 
6
+ 1.8.3 (2026-06-18)
7
+ ==================
8
+
9
+ - Broker graceful shutdown with task tracking
10
+ - Broker fixes and updates
11
+ - Signal and queue hash over name
12
+
6
13
  1.8.2 (2026-06-11)
7
14
  ==================
8
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microagent
3
- Version: 1.8.2
3
+ Version: 1.8.3
4
4
  Summary: Tool for agent ecosystem
5
5
  Author-email: Dmitriy Vlasov <scailer@yandex.ru>
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
- __version__ = '1.8.2'
1
+ __version__ = '1.8.3'
2
2
 
3
3
  import importlib
4
4
  import json
@@ -39,5 +39,8 @@ class BrokerProtocol(Protocol):
39
39
  uid: str
40
40
  log: logging.Logger
41
41
 
42
+ async def close(self) -> None:
43
+ ...
44
+
42
45
  def __getattr__(self, name: str) -> QueueProtocol:
43
46
  ...
@@ -210,6 +210,8 @@ class MicroAgent:
210
210
  cron_task.cancel()
211
211
  if self.bus:
212
212
  await self.bus.close()
213
+ if self.broker:
214
+ await self.broker.close()
213
215
  await self.hook.pre_stop()
214
216
 
215
217
  @staticmethod
@@ -56,6 +56,7 @@ Using with MicroAgent
56
56
  email_agent = EmailAgent(broker=broker)
57
57
  await email_agent.start()
58
58
  '''
59
+ import asyncio
59
60
  import logging
60
61
  import uuid
61
62
  from abc import abstractmethod
@@ -108,6 +109,17 @@ class AbstractQueueBroker(BrokerProtocol):
108
109
  log: logging.Logger = logging.getLogger('microagent.broker')
109
110
 
110
111
  _bindings: dict[str, Consumer] = field(default_factory=dict)
112
+ _background_tasks: set[asyncio.Task] = field(default_factory=set)
113
+
114
+ async def close(self) -> None:
115
+ '''
116
+ Graceful shutdown. Cancel all background tasks.
117
+ '''
118
+ tasks = list(self._background_tasks)
119
+ for task in tasks:
120
+ task.cancel()
121
+ if tasks:
122
+ await asyncio.wait(tasks)
111
123
 
112
124
  def __getattr__(self, name: str) -> 'BoundQueue':
113
125
  return BoundQueue(self, Queue.get(name))
@@ -87,7 +87,7 @@ class Queue:
87
87
  return self.name == other.name
88
88
 
89
89
  def __hash__(self) -> int:
90
- return id(self)
90
+ return id(self.name)
91
91
 
92
92
  @classmethod
93
93
  def set_jsonlib(cls, jsonlib: ModuleType) -> None:
@@ -89,7 +89,7 @@ class Signal:
89
89
  return self.name == other.name
90
90
 
91
91
  def __hash__(self) -> int:
92
- return id(self)
92
+ return id(self.name)
93
93
 
94
94
  @classmethod
95
95
  def set_jsonlib(cls, jsonlib: ModuleType) -> None:
@@ -60,15 +60,30 @@ class AMQPBroker(AbstractQueueBroker):
60
60
  '''
61
61
  connection: AbstractConnection = field(init=False)
62
62
  sending_channel: AbstractChannel | None = None
63
+ _managed_connections: list['ManagedConnection'] = field(default_factory=list, init=False)
64
+ _closing: bool = field(default=False, init=False)
63
65
 
64
66
  def __post_init__(self) -> None:
65
67
  self.connection = ReConnection(self.reconnect, self.dsn)
66
68
 
67
69
  async def reconnect(self) -> None:
70
+ if self._closing:
71
+ return
68
72
  self.connection = ReConnection(self.reconnect, self.dsn)
69
73
  await self.connection.connect()
70
74
  log.info('Reconnect "%s"', self.connection)
71
75
 
76
+ async def close(self) -> None:
77
+ self._closing = True
78
+ for mc in self._managed_connections:
79
+ mc.closed = True
80
+ await mc.close()
81
+ if self.sending_channel and not self.sending_channel.is_closed:
82
+ await self.sending_channel.close()
83
+ if self.connection.is_opened:
84
+ await self.connection.close()
85
+ await super().close()
86
+
72
87
  async def get_channel(self) -> AbstractChannel:
73
88
  '''
74
89
  Takes a channel from the pool or a new one, performs a lazy connection if required.
@@ -103,17 +118,17 @@ class AMQPBroker(AbstractQueueBroker):
103
118
 
104
119
  async def bind(self, name: str) -> None:
105
120
  consumer = self._bindings[name]
106
- await ManagedConnection(
121
+ mc = ManagedConnection(
107
122
  dsn=self.dsn,
108
123
  consumer=consumer,
109
124
  handler=self._amqp_wrapper(consumer)
110
- ).bind()
125
+ )
126
+ self._managed_connections.append(mc)
127
+ await mc.bind()
111
128
 
112
129
  def _amqp_wrapper(self, consumer: Consumer) -> Callable[[DeliveredMessage], Awaitable[None]]:
113
130
  async def _wrapper(message: DeliveredMessage) -> None:
114
- if not (data := self.prepared_data(consumer, message.body)):
115
- log.debug('Calling %s by %s without data', consumer, consumer.queue.name)
116
- return
131
+ data = self.prepared_data(consumer, message.body)
117
132
 
118
133
  log.debug('Calling %s by %s with %s', consumer,
119
134
  consumer.queue.name, str(data).encode('utf-8'))
@@ -126,7 +141,7 @@ class AMQPBroker(AbstractQueueBroker):
126
141
  if consumer.options.get('autoack', True) and message.delivery_tag:
127
142
  await message.channel.basic_ack(delivery_tag=message.delivery_tag)
128
143
 
129
- except TypeError:
144
+ except TypeError: # when data mismatch handler args (stay data in queue)
130
145
  log.exception('Call %s failed', consumer)
131
146
 
132
147
  except asyncio.TimeoutError:
@@ -185,12 +200,15 @@ class ManagedConnection:
185
200
  handler: Callable
186
201
  bind_attempts: int = 0
187
202
  bind_running: bool = False
203
+ closed: bool = False
204
+ _connection: AbstractConnection | None = None
205
+ _channel: AbstractChannel | None = None
188
206
 
189
207
  async def bind(self) -> None:
190
208
  ''' Start connection and bind consumer '''
191
- connection = ReConnection(self.rebind, self.dsn)
192
- await connection.connect()
193
- channel = await connection.channel()
209
+ self._connection = ReConnection(self.rebind, self.dsn)
210
+ await self._connection.connect()
211
+ self._channel = await self._connection.channel()
194
212
 
195
213
  queue_name = self.consumer.queue.name
196
214
  exchange_name = self.consumer.queue.exchange
@@ -199,27 +217,35 @@ class ManagedConnection:
199
217
  log.warning('Declare queue "%s" with exchange_name "%s" topics "%s"',
200
218
  queue_name, exchange_name, topics)
201
219
 
202
- await channel.queue_declare(queue_name)
220
+ await self._channel.queue_declare(queue_name)
203
221
 
204
222
  if topics: # topics
205
- await channel.exchange_declare(exchange=exchange_name, exchange_type='topic')
223
+ await self._channel.exchange_declare(exchange=exchange_name, exchange_type='topic')
206
224
  for topic in topics:
207
- await channel.queue_bind(queue=queue_name, exchange=exchange_name, routing_key=topic)
225
+ await self._channel.queue_bind(queue=queue_name, exchange=exchange_name, routing_key=topic)
208
226
  elif exchange_name: # fanout
209
- await channel.exchange_declare(exchange=exchange_name, exchange_type='fanout')
210
- await channel.queue_bind(queue=queue_name, exchange=exchange_name)
227
+ await self._channel.exchange_declare(exchange=exchange_name, exchange_type='fanout')
228
+ await self._channel.queue_bind(queue=queue_name, exchange=exchange_name)
229
+
230
+ await self._channel.basic_consume(queue_name, self.handler)
211
231
 
212
- await channel.basic_consume(queue_name, self.handler)
232
+ async def close(self) -> None:
233
+ if self._channel and not self._channel.is_closed:
234
+ await self._channel.close()
235
+ if self._connection and self._connection.is_opened:
236
+ await self._connection.close()
213
237
 
214
238
  async def rebind(self) -> bool:
215
- if self.bind_running:
216
- log.exception('Already rebinding queue "%s"', self.consumer.queue.name)
239
+ if self.closed or self.bind_running:
240
+ log.warning('Already rebinding queue "%s"', self.consumer.queue.name)
217
241
  return False
218
242
 
219
243
  if self.bind_attempts > REBIND_ATTEMPTS:
220
- log.exception('Failed all attempts to rebind queue "%s"', self.consumer.queue.name)
244
+ log.error('Failed all attempts to rebind queue "%s"', self.consumer.queue.name)
221
245
  return False
222
246
 
247
+ self.bind_running = True
248
+
223
249
  await asyncio.sleep((self.bind_attempts ** 2) * REBIND_BASE_DELAY)
224
250
  self.bind_attempts += 1
225
251
 
@@ -44,26 +44,27 @@ class KafkaBroker(AbstractQueueBroker):
44
44
  '''
45
45
  addr: str = field(init=False)
46
46
  producer: aiokafka.AIOKafkaProducer = field(init=False)
47
+ _producer_started: bool = field(default=False, init=False)
47
48
 
48
49
  def __post_init__(self) -> None:
49
50
  self.addr = parse.urlparse(self.dsn).netloc
50
51
  self.producer = aiokafka.AIOKafkaProducer(bootstrap_servers=self.addr)
51
52
 
52
53
  async def send(self, name: str, message: str, **kwargs: Any) -> None:
53
- await self.producer.start()
54
+ if not self._producer_started:
55
+ await self.producer.start()
56
+ self._producer_started = True
57
+ await self.producer.send_and_wait(name, bytes(message, 'utf8'), **kwargs)
54
58
 
55
- try:
56
- await self.producer.send_and_wait(name, bytes(message, 'utf8'), **kwargs)
57
-
58
- finally:
59
- await self.producer.stop()
60
- _loop = asyncio.get_running_loop()
61
- self.producer = aiokafka.AIOKafkaProducer(loop=_loop, bootstrap_servers=self.addr)
59
+ async def close(self) -> None:
60
+ await self.producer.stop()
61
+ await super().close()
62
62
 
63
63
  async def bind(self, name: str) -> None:
64
- loop = asyncio.get_running_loop()
65
- kafka_consumer = aiokafka.AIOKafkaConsumer(name, loop=loop, bootstrap_servers=self.addr)
66
- asyncio.create_task(self._kafka_wrapper(kafka_consumer, name))
64
+ kafka_consumer = aiokafka.AIOKafkaConsumer(name, bootstrap_servers=self.addr)
65
+ task = asyncio.create_task(self._kafka_wrapper(kafka_consumer, name))
66
+ self._background_tasks.add(task)
67
+ task.add_done_callback(self._background_tasks.discard)
67
68
 
68
69
  async def _kafka_wrapper(self, kafka_consumer: aiokafka.AIOKafkaConsumer, name: str) -> None:
69
70
  consumer = self._bindings[name]
@@ -73,7 +74,9 @@ class KafkaBroker(AbstractQueueBroker):
73
74
  async for msg in kafka_consumer:
74
75
  data = self.prepared_data(consumer, msg.value)
75
76
  data['kafka'] = msg
76
- asyncio.create_task(self._handle(consumer, data))
77
+ htask = asyncio.create_task(self._handle(consumer, data))
78
+ self._background_tasks.add(htask)
79
+ htask.add_done_callback(self._background_tasks.discard)
77
80
 
78
81
  finally:
79
82
  await kafka_consumer.stop()
@@ -68,14 +68,15 @@ class BrokerMock(MagicMock):
68
68
  self._stuff: dict[str, BoundQueueMock] = {}
69
69
  self.bind_consumer = AsyncMock()
70
70
  self.send = AsyncMock()
71
+ self.close = AsyncMock()
71
72
  self.dsn = ''
72
73
  self.uid = ''
73
74
 
74
75
  def __str__(self) -> str:
75
- return f'<BrokerMock id={id(self)}'
76
+ return f'<BrokerMock id={id(self)}>'
76
77
 
77
78
  def __getattr__(self, name: str) -> BoundQueueMock:
78
- if name.startswith('_') or name in {'bind_consumer', 'send'}:
79
+ if name.startswith('_') or name in {'bind_consumer', 'send', 'close'}:
79
80
  return super().__getattr__(name)
80
81
  self._stuff[name] = self._stuff.get(name, BoundQueueMock())
81
82
  return self._stuff[name]
@@ -2,7 +2,6 @@
2
2
  :ref:`Signal Bus <bus>` and :ref:`Queue Broker <broker>` based on :redis:`redis <>`.
3
3
  '''
4
4
  import asyncio
5
- import inspect
6
5
  import time
7
6
  from collections import defaultdict
8
7
  from dataclasses import dataclass, field
@@ -110,33 +109,32 @@ class RedisBroker(AbstractQueueBroker):
110
109
  return Redis.from_url(self.dsn, decode_responses=True)
111
110
 
112
111
  async def send(self, name: str, message: str, **kwargs: Any) -> None:
113
- ret = self.connection.rpush(name, message)
114
-
115
- if inspect.isawaitable(ret):
116
- await ret
112
+ await self.connection.rpush(name, message)
117
113
 
118
114
  async def queue_length(self, name: str, **options: Any) -> int:
119
- ret = self.connection.llen(name)
120
-
121
- if inspect.isawaitable(ret):
122
- return await ret
123
-
124
- return ret
115
+ return await self.connection.llen(name)
125
116
 
126
117
  async def bind(self, name: str) -> None:
127
- _loop = asyncio.get_running_loop()
128
- _loop.call_later(self.BIND_TIME, lambda: asyncio.create_task(self._wait(name)))
118
+ task = asyncio.create_task(self._wait(name))
119
+ self._background_tasks.add(task)
120
+ task.add_done_callback(self._background_tasks.discard)
129
121
 
130
122
  async def _wait(self, name: str) -> None:
131
- conn = await self.new_connection()
132
- while True:
133
- if data := await conn.blpop(name, self.WAIT_TIME):
134
- _, data = data
135
- asyncio.create_task(self._handler(name, data)) # type: ignore[arg-type, unused-ignore]
123
+ await asyncio.sleep(self.BIND_TIME)
124
+ conn = self.new_connection()
125
+ try:
126
+ while True:
127
+ if data := await conn.blpop(name, self.WAIT_TIME):
128
+ _, data = data
129
+ htask = asyncio.create_task(self._handler(name, data))
130
+ self._background_tasks.add(htask)
131
+ htask.add_done_callback(self._background_tasks.discard)
132
+ finally:
133
+ await conn.aclose()
136
134
 
137
135
  async def rollback(self, name: str, data: str) -> None:
138
- _hash = str(hash(name)) + str(hash(data))
139
- attempt = self._rollbacks[_hash]
136
+ _key = (name, data)
137
+ attempt = self._rollbacks[_key]
140
138
 
141
139
  if attempt > self.ROLLBACK_ATTEMPTS:
142
140
  self.log.error('Rollback limit exceeded on queue "%s" with data: %s', name, data)
@@ -147,7 +145,7 @@ class RedisBroker(AbstractQueueBroker):
147
145
  _loop = asyncio.get_running_loop()
148
146
  _loop.call_later(attempt ** 2, lambda: asyncio.create_task(self.send(name, data)))
149
147
 
150
- self._rollbacks[_hash] += 1
148
+ self._rollbacks[_key] += 1
151
149
 
152
150
  async def _handler(self, name: str, data: str) -> None:
153
151
  consumer = self._bindings[name]
@@ -156,9 +154,9 @@ class RedisBroker(AbstractQueueBroker):
156
154
 
157
155
  try:
158
156
  await asyncio.wait_for(consumer.handler(**_data), consumer.timeout)
159
- except Exception:
160
- self.log.exception('Call %s failed', consumer.queue.name)
161
- await self.rollback(consumer.queue.name, data)
162
157
  except asyncio.TimeoutError:
163
158
  self.log.error('TimeoutError: %s %.2f', consumer, time.monotonic() - timer)
164
159
  await self.rollback(name, data)
160
+ except Exception:
161
+ self.log.exception('Call %s failed', consumer.queue.name)
162
+ await self.rollback(consumer.queue.name, data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microagent
3
- Version: 1.8.2
3
+ Version: 1.8.3
4
4
  Summary: Tool for agent ecosystem
5
5
  Author-email: Dmitriy Vlasov <scailer@yandex.ru>
6
6
  License: MIT License
@@ -136,6 +136,9 @@ async def test_broker_rebind_ok_already(monkeypatch, channel):
136
136
 
137
137
  assert not await conn.rebind()
138
138
 
139
+ conn.bind_running = False
140
+ assert await conn.rebind()
141
+
139
142
 
140
143
  async def test_broker_rebind_ok_attempts(monkeypatch, channel):
141
144
  queue = Queue(name='test_queue')
@@ -204,8 +207,8 @@ async def test_broker_wrapper_ok_nodata(channel):
204
207
 
205
208
  await broker._amqp_wrapper(consumer)(message)
206
209
 
207
- channel.basic_ack.assert_not_called()
208
- handler.assert_not_called()
210
+ channel.basic_ack.assert_called()
211
+ handler.assert_called_once()
209
212
 
210
213
 
211
214
  async def test_broker_wrapper_ok_type_err(channel):
@@ -71,10 +71,17 @@ async def test_broker_send_ok(kafka_producer):
71
71
  broker = KafkaBroker('kafka://localhost')
72
72
  await broker.send(queue.name, '{}')
73
73
 
74
- broker.producer.start.assert_called()
74
+ broker.producer.start.assert_called_once()
75
75
  broker.producer.send_and_wait.assert_called_once_with(queue.name, b'{}')
76
- broker.producer.stop.assert_called()
76
+ broker.producer.stop.assert_not_called()
77
77
 
78
- broker.producer._closed = False
79
78
  await broker.send(queue.name, '{}')
80
- broker.producer.start.assert_called()
79
+ assert broker.producer.send_and_wait.call_count == 2
80
+
81
+
82
+ async def test_broker_close_stops_producer_ok(kafka_producer):
83
+ broker = KafkaBroker('kafka://localhost')
84
+ await broker.send('test_queue', '{}')
85
+
86
+ await broker.close()
87
+ broker.producer.stop.assert_called_once()
@@ -142,7 +142,7 @@ async def test_broker_rollback_many_attempts_ok():
142
142
  queue = Queue(name='test_queue')
143
143
  broker = RedisBroker('redis://localhost')
144
144
  broker.send = AsyncMock()
145
- broker._rollbacks[str(hash(queue.name)) + str(hash('{}'))] = 4
145
+ broker._rollbacks[(queue.name, '{}')] = 4
146
146
  await broker.rollback(queue.name, '{}')
147
147
  broker.send.assert_not_called()
148
148
  await asyncio.sleep(.01)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes