agstack 1.17.0__tar.gz → 1.17.1__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 (76) hide show
  1. {agstack-1.17.0 → agstack-1.17.1}/PKG-INFO +1 -1
  2. {agstack-1.17.0 → agstack-1.17.1}/agstack/messagebus/redis.py +12 -5
  3. {agstack-1.17.0 → agstack-1.17.1}/agstack.egg-info/PKG-INFO +1 -1
  4. {agstack-1.17.0 → agstack-1.17.1}/pyproject.toml +1 -1
  5. {agstack-1.17.0 → agstack-1.17.1}/tests/test_messagebus_redis.py +16 -12
  6. {agstack-1.17.0 → agstack-1.17.1}/LICENSE +0 -0
  7. {agstack-1.17.0 → agstack-1.17.1}/README.md +0 -0
  8. {agstack-1.17.0 → agstack-1.17.1}/agstack/__init__.py +0 -0
  9. {agstack-1.17.0 → agstack-1.17.1}/agstack/cache/__init__.py +0 -0
  10. {agstack-1.17.0 → agstack-1.17.1}/agstack/cache/base.py +0 -0
  11. {agstack-1.17.0 → agstack-1.17.1}/agstack/cache/memory.py +0 -0
  12. {agstack-1.17.0 → agstack-1.17.1}/agstack/cache/redis.py +0 -0
  13. {agstack-1.17.0 → agstack-1.17.1}/agstack/config/__init__.py +0 -0
  14. {agstack-1.17.0 → agstack-1.17.1}/agstack/config/logger.py +0 -0
  15. {agstack-1.17.0 → agstack-1.17.1}/agstack/config/manager.py +0 -0
  16. {agstack-1.17.0 → agstack-1.17.1}/agstack/config/types.py +0 -0
  17. {agstack-1.17.0 → agstack-1.17.1}/agstack/contexts.py +0 -0
  18. {agstack-1.17.0 → agstack-1.17.1}/agstack/decorators.py +0 -0
  19. {agstack-1.17.0 → agstack-1.17.1}/agstack/events.py +0 -0
  20. {agstack-1.17.0 → agstack-1.17.1}/agstack/exceptions.py +0 -0
  21. {agstack-1.17.0 → agstack-1.17.1}/agstack/fastapi/__init__.py +0 -0
  22. {agstack-1.17.0 → agstack-1.17.1}/agstack/fastapi/exception.py +0 -0
  23. {agstack-1.17.0 → agstack-1.17.1}/agstack/fastapi/middleware.py +0 -0
  24. {agstack-1.17.0 → agstack-1.17.1}/agstack/fastapi/offline.py +0 -0
  25. {agstack-1.17.0 → agstack-1.17.1}/agstack/fastapi/sse.py +0 -0
  26. {agstack-1.17.0 → agstack-1.17.1}/agstack/infra/db/__init__.py +0 -0
  27. {agstack-1.17.0 → agstack-1.17.1}/agstack/infra/es/__init__.py +0 -0
  28. {agstack-1.17.0 → agstack-1.17.1}/agstack/infra/kg/__init__.py +0 -0
  29. {agstack-1.17.0 → agstack-1.17.1}/agstack/infra/mq/__init__.py +0 -0
  30. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/__init__.py +0 -0
  31. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/client.py +0 -0
  32. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/__init__.py +0 -0
  33. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/agent.py +0 -0
  34. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/context.py +0 -0
  35. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/event.py +0 -0
  36. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/exceptions.py +0 -0
  37. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/factory.py +0 -0
  38. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/flow.py +0 -0
  39. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/loader.py +0 -0
  40. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/__init__.py +0 -0
  41. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/agent_node.py +0 -0
  42. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/base.py +0 -0
  43. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/detect_node.py +0 -0
  44. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/echo_node.py +0 -0
  45. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/llm_chat_node.py +0 -0
  46. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/llm_embed_node.py +0 -0
  47. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/llm_rerank_node.py +0 -0
  48. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/python_node.py +0 -0
  49. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/subflow_node.py +0 -0
  50. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/switch_node.py +0 -0
  51. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/nodes/tool_node.py +0 -0
  52. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/records.py +0 -0
  53. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/registry.py +0 -0
  54. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/sandbox.py +0 -0
  55. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/state.py +0 -0
  56. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/flow/tool.py +0 -0
  57. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/prompts.py +0 -0
  58. {agstack-1.17.0 → agstack-1.17.1}/agstack/llm/token.py +0 -0
  59. {agstack-1.17.0 → agstack-1.17.1}/agstack/messagebus/__init__.py +0 -0
  60. {agstack-1.17.0 → agstack-1.17.1}/agstack/messagebus/base.py +0 -0
  61. {agstack-1.17.0 → agstack-1.17.1}/agstack/messagebus/memory.py +0 -0
  62. {agstack-1.17.0 → agstack-1.17.1}/agstack/schema.py +0 -0
  63. {agstack-1.17.0 → agstack-1.17.1}/agstack/security/__init__.py +0 -0
  64. {agstack-1.17.0 → agstack-1.17.1}/agstack/security/casbin.py +0 -0
  65. {agstack-1.17.0 → agstack-1.17.1}/agstack/security/crypt.py +0 -0
  66. {agstack-1.17.0 → agstack-1.17.1}/agstack/status.py +0 -0
  67. {agstack-1.17.0 → agstack-1.17.1}/agstack.egg-info/SOURCES.txt +0 -0
  68. {agstack-1.17.0 → agstack-1.17.1}/agstack.egg-info/dependency_links.txt +0 -0
  69. {agstack-1.17.0 → agstack-1.17.1}/agstack.egg-info/requires.txt +0 -0
  70. {agstack-1.17.0 → agstack-1.17.1}/agstack.egg-info/top_level.txt +0 -0
  71. {agstack-1.17.0 → agstack-1.17.1}/setup.cfg +0 -0
  72. {agstack-1.17.0 → agstack-1.17.1}/tests/test_cache_memory.py +0 -0
  73. {agstack-1.17.0 → agstack-1.17.1}/tests/test_cache_redis.py +0 -0
  74. {agstack-1.17.0 → agstack-1.17.1}/tests/test_flow_io.py +0 -0
  75. {agstack-1.17.0 → agstack-1.17.1}/tests/test_flow_switch_subflow.py +0 -0
  76. {agstack-1.17.0 → agstack-1.17.1}/tests/test_messagebus_memory.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agstack
3
- Version: 1.17.0
3
+ Version: 1.17.1
4
4
  Summary: Production-ready toolkit for building FastAPI and LLM applications
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  from collections.abc import AsyncIterator
5
+ from typing import Self
5
6
 
6
7
  import redis.asyncio as aioredis
7
8
  from redis.asyncio.client import PubSub
@@ -17,15 +18,21 @@ class RedisSubscription(Subscription):
17
18
  self._channels = channels
18
19
  self._task: asyncio.Task[None] | None = None
19
20
  self._queue: asyncio.Queue[tuple[str, bytes]] = asyncio.Queue()
21
+ self._started = False
20
22
  self._closed = False
21
23
 
22
24
  async def _start(self) -> None:
23
- """订阅 channel 并启动后台监听任务"""
25
+ if self._started:
26
+ return
27
+ self._started = True
24
28
  await self._pubsub.subscribe(*self._channels)
25
29
  self._task = asyncio.create_task(self._listen())
26
30
 
31
+ async def __aenter__(self) -> Self:
32
+ await self._start()
33
+ return self
34
+
27
35
  async def _listen(self) -> None:
28
- """后台监听 Redis Pub/Sub 消息"""
29
36
  try:
30
37
  async for msg in self._pubsub.listen():
31
38
  if self._closed:
@@ -47,6 +54,8 @@ class RedisSubscription(Subscription):
47
54
  return self
48
55
 
49
56
  async def __anext__(self) -> tuple[str, bytes]:
57
+ if not self._started:
58
+ await self._start()
50
59
  if self._closed and self._queue.empty():
51
60
  raise StopAsyncIteration
52
61
  try:
@@ -79,9 +88,7 @@ class RedisMessageBus(MessageBus):
79
88
 
80
89
  def subscribe(self, *channels: str) -> RedisSubscription:
81
90
  pubsub = self._client.pubsub()
82
- sub = RedisSubscription(pubsub, channels)
83
- # 调用方需要在 async with 或手动 await _start() 后使用
84
- return sub
91
+ return RedisSubscription(pubsub, channels)
85
92
 
86
93
  async def close(self) -> None:
87
94
  await self._client.aclose()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agstack
3
- Version: 1.17.0
3
+ Version: 1.17.1
4
4
  Summary: Production-ready toolkit for building FastAPI and LLM applications
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agstack"
3
- version = "1.17.0"
3
+ version = "1.17.1"
4
4
  description = "Production-ready toolkit for building FastAPI and LLM applications"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -26,40 +26,44 @@ async def bus():
26
26
 
27
27
  class TestPublishSubscribe:
28
28
  async def test_single_channel(self, bus: RedisMessageBus) -> None:
29
- sub = bus.subscribe("test:ch1")
30
- await sub._start()
31
- async with sub:
29
+ async with bus.subscribe("test:ch1") as sub:
32
30
  await asyncio.sleep(0.1)
33
31
  await bus.publish("test:ch1", b"hello")
34
- channel, message = await asyncio.wait_for(sub.__anext__(), timeout=3)
32
+ channel, message = await asyncio.wait_for(anext(sub), timeout=3)
35
33
  assert channel == "test:ch1"
36
34
  assert message == b"hello"
37
35
 
38
36
  async def test_multi_channel(self, bus: RedisMessageBus) -> None:
39
- sub = bus.subscribe("test:ch1", "test:ch2")
40
- await sub._start()
41
- async with sub:
37
+ async with bus.subscribe("test:ch1", "test:ch2") as sub:
42
38
  await asyncio.sleep(0.1)
43
39
  await bus.publish("test:ch1", b"msg1")
44
40
  await bus.publish("test:ch2", b"msg2")
45
41
  results = []
46
42
  for _ in range(2):
47
- ch, msg = await asyncio.wait_for(sub.__anext__(), timeout=3)
43
+ ch, msg = await asyncio.wait_for(anext(sub), timeout=3)
48
44
  results.append((ch, msg))
49
45
  channels = [r[0] for r in results]
50
46
  assert "test:ch1" in channels
51
47
  assert "test:ch2" in channels
52
48
 
49
+ async def test_lazy_start_without_context_manager(self, bus: RedisMessageBus) -> None:
50
+ sub = bus.subscribe("test:ch1")
51
+ await asyncio.sleep(0.1)
52
+ await bus.publish("test:ch1", b"world")
53
+ channel, message = await asyncio.wait_for(anext(sub), timeout=3)
54
+ assert channel == "test:ch1"
55
+ assert message == b"world"
56
+ await sub.close()
57
+
53
58
  async def test_publish_no_subscribers(self, bus: RedisMessageBus) -> None:
54
59
  await bus.publish("test:nobody", b"lost message")
55
60
 
56
61
 
57
62
  class TestSubscriptionClose:
58
63
  async def test_close(self, bus: RedisMessageBus) -> None:
59
- sub = bus.subscribe("test:ch1")
60
- await sub._start()
61
- await asyncio.sleep(0.1)
62
- await sub.close()
64
+ async with bus.subscribe("test:ch1") as sub:
65
+ await asyncio.sleep(0.1)
66
+ assert sub._closed
63
67
 
64
68
 
65
69
  class TestBusClose:
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