h-message-bus 0.0.38__py3-none-any.whl → 0.0.40__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- h_message_bus/domain/models/request_message_topic.py +3 -0
- h_message_bus/domain/request_messages/twitter_search_request_message.py +73 -0
- h_message_bus/domain/request_messages/twitter_search_response_message.py +73 -0
- h_message_bus/infrastructure/nats_client_repository.py +70 -54
- {h_message_bus-0.0.38.dist-info → h_message_bus-0.0.40.dist-info}/METADATA +1 -1
- {h_message_bus-0.0.38.dist-info → h_message_bus-0.0.40.dist-info}/RECORD +8 -6
- {h_message_bus-0.0.38.dist-info → h_message_bus-0.0.40.dist-info}/WHEEL +1 -1
- {h_message_bus-0.0.38.dist-info → h_message_bus-0.0.40.dist-info}/top_level.txt +0 -0
@@ -59,6 +59,9 @@ class RequestMessageTopic(str, Enum):
|
|
59
59
|
TWITTER_GET_TWEET= "hai.twitter.get.tweet"
|
60
60
|
TWITTER_GET_TWEET_RESPONSE = "hai.twitter.get.tweet.response"
|
61
61
|
|
62
|
+
TWITTER_SEARCH= "hai.twitter.search"
|
63
|
+
TWITTER_SEARCH_RESPONSE = "hai.twitter.search.response"
|
64
|
+
|
62
65
|
# tools
|
63
66
|
WEB_SEARCH = "hai.tools.web.search"
|
64
67
|
WEB_SEARCH_RESPONSE = "hai.tools.web.search.response"
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from typing import Dict, Any, Type, TypeVar, Optional, Literal, cast
|
2
|
+
|
3
|
+
from ..models.request_message_topic import RequestMessageTopic
|
4
|
+
from ...domain.models.hai_message import HaiMessage
|
5
|
+
|
6
|
+
T = TypeVar('T', bound='HaiMessage')
|
7
|
+
|
8
|
+
|
9
|
+
class TwitterSearchRequestMessage(HaiMessage):
|
10
|
+
"""Message to request Twitter search"""
|
11
|
+
|
12
|
+
@classmethod
|
13
|
+
def create(cls: Type[T], topic: str, payload: Dict[Any, Any]) -> T:
|
14
|
+
"""Create a message - inherited from HaiMessage"""
|
15
|
+
return super().create(topic=topic, payload=payload)
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
def create_message(
|
19
|
+
cls,
|
20
|
+
query: str,
|
21
|
+
max_results: int = 10,
|
22
|
+
min_view_count: int = 50,
|
23
|
+
sort_order: Literal["relevancy", "recency"] = "relevancy"
|
24
|
+
) -> 'TwitterSearchRequestMessage':
|
25
|
+
"""Create a message requesting Twitter search"""
|
26
|
+
return cls.create(
|
27
|
+
topic=RequestMessageTopic.TWITTER_SEARCH,
|
28
|
+
payload={
|
29
|
+
"query": query,
|
30
|
+
"max_results": max_results,
|
31
|
+
"min_view_count": min_view_count,
|
32
|
+
"sort_order": sort_order
|
33
|
+
},
|
34
|
+
)
|
35
|
+
|
36
|
+
@property
|
37
|
+
def query(self) -> str:
|
38
|
+
"""Get the search query from the payload"""
|
39
|
+
return self.payload.get("query", "")
|
40
|
+
|
41
|
+
@property
|
42
|
+
def max_results(self) -> int:
|
43
|
+
"""Get the maximum number of results from the payload"""
|
44
|
+
return self.payload.get("max_results", 10)
|
45
|
+
|
46
|
+
@property
|
47
|
+
def min_view_count(self) -> int:
|
48
|
+
"""Get the minimum view count from the payload"""
|
49
|
+
return self.payload.get("min_view_count", 50)
|
50
|
+
|
51
|
+
@property
|
52
|
+
def sort_order(self) -> Literal["relevancy", "recency"]:
|
53
|
+
"""Get the sort order from the payload"""
|
54
|
+
sort_order_str = self.payload.get("sort_order", "relevancy")
|
55
|
+
if sort_order_str not in ("relevancy", "recency"):
|
56
|
+
sort_order_str = "relevancy"
|
57
|
+
return cast(Literal["relevancy", "recency"], sort_order_str)
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
def from_hai_message(cls, message: HaiMessage) -> 'TwitterSearchRequestMessage':
|
61
|
+
"""Create a TwitterSearchRequestMessage from a HaiMessage"""
|
62
|
+
payload = message.payload
|
63
|
+
query = payload.get("query", "")
|
64
|
+
max_results = payload.get("max_results", 10)
|
65
|
+
min_view_count = payload.get("min_view_count", 50)
|
66
|
+
sort_order = payload.get("sort_order", "relevancy")
|
67
|
+
|
68
|
+
return cls.create_message(
|
69
|
+
query=query,
|
70
|
+
max_results=max_results,
|
71
|
+
min_view_count=min_view_count,
|
72
|
+
sort_order=sort_order
|
73
|
+
)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from typing import Dict, Any, Type, TypeVar, List, Optional
|
2
|
+
import json
|
3
|
+
|
4
|
+
from ..models.request_message_topic import RequestMessageTopic
|
5
|
+
from ...domain.models.hai_message import HaiMessage
|
6
|
+
|
7
|
+
T = TypeVar('T', bound='HaiMessage')
|
8
|
+
|
9
|
+
|
10
|
+
class TwitterSearchResponseMessage(HaiMessage):
|
11
|
+
"""Message containing Twitter search results"""
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def create(cls: Type[T], topic: str, payload: Dict[Any, Any]) -> T:
|
15
|
+
"""Create a message - inherited from HaiMessage"""
|
16
|
+
return super().create(topic=topic, payload=payload)
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def create_message(
|
20
|
+
cls,
|
21
|
+
query: str,
|
22
|
+
results: List[Dict[str, Any]],
|
23
|
+
result_count: int,
|
24
|
+
request_id: str
|
25
|
+
) -> 'TwitterSearchResponseMessage':
|
26
|
+
"""Create a message containing Twitter search results"""
|
27
|
+
return cls.create(
|
28
|
+
topic=RequestMessageTopic.TWITTER_SEARCH_RESPONSE,
|
29
|
+
payload={
|
30
|
+
"query": query,
|
31
|
+
"results": json.dumps(results),
|
32
|
+
"result_count": result_count,
|
33
|
+
"request_id": request_id
|
34
|
+
},
|
35
|
+
)
|
36
|
+
|
37
|
+
@property
|
38
|
+
def query(self) -> str:
|
39
|
+
"""Get the search query from the payload"""
|
40
|
+
return self.payload.get("query", "")
|
41
|
+
|
42
|
+
@property
|
43
|
+
def result_count(self) -> int:
|
44
|
+
"""Get the number of results from the payload"""
|
45
|
+
return self.payload.get("result_count", 0)
|
46
|
+
|
47
|
+
@property
|
48
|
+
def request_id(self) -> str:
|
49
|
+
"""Get the original request ID from the payload"""
|
50
|
+
return self.payload.get("request_id", "")
|
51
|
+
|
52
|
+
@property
|
53
|
+
def results(self) -> List[Dict[str, Any]]:
|
54
|
+
"""Get the search results from the payload"""
|
55
|
+
results_json = self.payload.get("results", "[]")
|
56
|
+
return json.loads(results_json)
|
57
|
+
|
58
|
+
@classmethod
|
59
|
+
def from_hai_message(cls, message: HaiMessage) -> 'TwitterSearchResponseMessage':
|
60
|
+
"""Create a TwitterSearchResponseMessage from a HaiMessage"""
|
61
|
+
payload = message.payload
|
62
|
+
query = payload.get("query", "")
|
63
|
+
results_json = payload.get("results", "[]")
|
64
|
+
results = json.loads(results_json)
|
65
|
+
result_count = payload.get("result_count", 0)
|
66
|
+
request_id = payload.get("request_id", "")
|
67
|
+
|
68
|
+
return cls.create_message(
|
69
|
+
query=query,
|
70
|
+
results=results,
|
71
|
+
result_count=result_count,
|
72
|
+
request_id=request_id
|
73
|
+
)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import logging
|
2
|
+
import asyncio
|
2
3
|
from typing import Optional, Callable, Any
|
3
4
|
|
4
5
|
import nats
|
@@ -6,37 +7,34 @@ from nats.aio.client import Client as NatsClient
|
|
6
7
|
|
7
8
|
from ..infrastructure.nats_config import NatsConfig
|
8
9
|
|
9
|
-
|
10
10
|
class NatsClientRepository:
|
11
11
|
"""
|
12
12
|
Repository for managing connection and interaction with a NATS server.
|
13
|
-
|
14
|
-
This class provides methods to establish a connection with a NATS server,
|
15
|
-
publish and subscribe to subjects, send requests and handle responses, and
|
16
|
-
cleanly disconnect from the server. It abstracts the connection and ensures
|
17
|
-
seamless communication with the NATS server.
|
18
|
-
|
19
|
-
:ivar config: Configuration details for the NATS client, including server
|
20
|
-
connection parameters, timeouts, and limits.
|
21
|
-
:type config: NatsConfig
|
22
|
-
:ivar client: Instance of the NATS client used for communication. Initialized
|
23
|
-
as None and assigned upon connecting to a NATS server.
|
24
|
-
:type client: Optional[NatsClient]
|
25
|
-
:ivar subscriptions: List of active subscriptions for NATS subjects.
|
26
|
-
:type subscriptions: list
|
27
13
|
"""
|
28
14
|
|
29
15
|
def __init__(self, config: NatsConfig):
|
30
16
|
self.config = config
|
31
|
-
self.client: NatsClient
|
32
|
-
|
17
|
+
self.client: Optional[NatsClient] = None
|
33
18
|
self.subscriptions = []
|
34
19
|
self.logger = logging.getLogger(__name__)
|
35
20
|
|
36
21
|
async def connect(self) -> None:
|
37
|
-
"""Connect to NATS server."""
|
22
|
+
"""Connect to NATS server with event handlers and logging."""
|
38
23
|
if self.client and self.client.is_connected:
|
39
24
|
return
|
25
|
+
|
26
|
+
async def error_cb(e):
|
27
|
+
self.logger.error(f"NATS client error: {e}")
|
28
|
+
|
29
|
+
async def disconnect_cb():
|
30
|
+
self.logger.warning("NATS client disconnected.")
|
31
|
+
|
32
|
+
async def reconnect_cb():
|
33
|
+
self.logger.info("NATS client reconnected.")
|
34
|
+
|
35
|
+
async def closed_cb():
|
36
|
+
self.logger.warning("NATS client connection closed.")
|
37
|
+
|
40
38
|
self.logger.info(f"Connecting to NATS server at {self.config.server}")
|
41
39
|
|
42
40
|
self.client = await nats.connect(
|
@@ -45,49 +43,67 @@ class NatsClientRepository:
|
|
45
43
|
reconnect_time_wait=self.config.reconnect_time_wait,
|
46
44
|
connect_timeout=self.config.connection_timeout,
|
47
45
|
ping_interval=self.config.ping_interval,
|
48
|
-
max_outstanding_pings=self.config.max_outstanding_pings
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
46
|
+
max_outstanding_pings=self.config.max_outstanding_pings,
|
47
|
+
error_cb=error_cb,
|
48
|
+
disconnected_cb=disconnect_cb,
|
49
|
+
reconnected_cb=reconnect_cb,
|
50
|
+
closed_cb=closed_cb,
|
51
|
+
) # type: ignore
|
52
|
+
|
53
|
+
async def _ensure_connected(self, retries: int = 5):
|
54
|
+
"""Wait and retry until the client is connected."""
|
55
|
+
for attempt in range(retries):
|
56
|
+
if self.client and self.client.is_connected:
|
57
|
+
return
|
58
|
+
self.logger.info(f"Waiting for NATS connection (attempt {attempt+1})...")
|
59
|
+
await asyncio.sleep(2 ** attempt)
|
60
|
+
raise ConnectionError("Could not establish connection to NATS server.")
|
61
|
+
|
62
|
+
async def publish(self, subject: str, payload: bytes, retries: int = 3) -> None:
|
63
|
+
"""Publish raw message to NATS with retries and backoff."""
|
64
|
+
for attempt in range(retries):
|
65
|
+
try:
|
66
|
+
await self._ensure_connected()
|
67
|
+
await self.client.publish(subject, payload)
|
68
|
+
return
|
69
|
+
except Exception as e:
|
70
|
+
self.logger.error(f"Failed to publish message (attempt {attempt+1}): {e}")
|
71
|
+
await asyncio.sleep(2 ** attempt)
|
72
|
+
self.logger.error("Giving up publishing after retries.")
|
61
73
|
|
62
74
|
async def subscribe(self, subject: str, callback: Callable) -> Any:
|
63
75
|
"""Subscribe to a subject with a callback."""
|
64
|
-
|
65
|
-
await self.connect()
|
66
|
-
|
67
|
-
subscription = await self.client.subscribe(subject, cb=callback)
|
68
|
-
self.subscriptions.append(subscription)
|
69
|
-
return subscription
|
70
|
-
|
71
|
-
async def request(self, subject: str, payload: bytes, timeout: float = 2.0) -> Optional[bytes]:
|
72
|
-
"""Send a request and get raw response."""
|
73
|
-
if not self.client or not self.client.is_connected:
|
74
|
-
await self.connect()
|
75
|
-
|
76
|
+
await self._ensure_connected()
|
76
77
|
try:
|
77
|
-
|
78
|
-
|
78
|
+
subscription = await self.client.subscribe(subject, cb=callback)
|
79
|
+
self.subscriptions.append(subscription)
|
80
|
+
return subscription
|
79
81
|
except Exception as e:
|
80
|
-
|
82
|
+
self.logger.error(f"Failed to subscribe to {subject}: {e}")
|
81
83
|
return None
|
82
84
|
|
85
|
+
async def request(self, subject: str, payload: bytes, timeout: float = 2.0, retries: int = 3) -> Optional[bytes]:
|
86
|
+
"""Send a request and get raw response with retries and backoff."""
|
87
|
+
for attempt in range(retries):
|
88
|
+
try:
|
89
|
+
await self._ensure_connected()
|
90
|
+
response = await self.client.request(subject, payload, timeout=timeout)
|
91
|
+
return response.data
|
92
|
+
except Exception as e:
|
93
|
+
self.logger.error(f"NATS request failed (attempt {attempt+1}): {e}")
|
94
|
+
await asyncio.sleep(2 ** attempt)
|
95
|
+
self.logger.error("Giving up request after retries.")
|
96
|
+
return None
|
97
|
+
|
83
98
|
async def close(self) -> None:
|
84
99
|
"""Close all subscriptions and NATS connection."""
|
85
100
|
if self.client and self.client.is_connected:
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
101
|
+
try:
|
102
|
+
for sub in self.subscriptions:
|
103
|
+
await sub.unsubscribe()
|
104
|
+
await self.client.drain()
|
105
|
+
except Exception as e:
|
106
|
+
self.logger.error(f"Error during NATS cleanup: {e}")
|
107
|
+
finally:
|
108
|
+
self.client = None
|
109
|
+
self.subscriptions = []
|
@@ -13,7 +13,7 @@ h_message_bus/domain/event_messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQ
|
|
13
13
|
h_message_bus/domain/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
14
|
h_message_bus/domain/models/event_message_topic.py,sha256=fSjRMBwMD2RE9GBUud38XMrgLZcngURERrbuoAhQ0Hk,717
|
15
15
|
h_message_bus/domain/models/hai_message.py,sha256=b5CfX7hi5uNq77IVnZzEi9iotc4b_U2MNYwV6JY7JcU,2146
|
16
|
-
h_message_bus/domain/models/request_message_topic.py,sha256=
|
16
|
+
h_message_bus/domain/models/request_message_topic.py,sha256=a4V_D5tZX6iWUwl9hdfcplxvTzqK1GkKDDlhfN-4wE8,4105
|
17
17
|
h_message_bus/domain/models/twitter_message_metadata.py,sha256=Cj0dSVQi0Com9nPlJeEM0c-ZAQKGqIq_txvdzYg0myw,1203
|
18
18
|
h_message_bus/domain/models/twitter_user_metadata.py,sha256=N7GVyn2txmlMQ19aatNh7lvzO6FNSj4dKVujestha9o,2827
|
19
19
|
h_message_bus/domain/models/vector_collection_metadata.py,sha256=fSy3ZQ-zkYISTpc-_M6XWf1JkufVAlzlS2ZT60TzA2s,891
|
@@ -56,6 +56,8 @@ h_message_bus/domain/request_messages/twitter_reply_request_message.py,sha256=7m
|
|
56
56
|
h_message_bus/domain/request_messages/twitter_reply_response_message.py,sha256=6_yQzPSnbueYM3aDsh5GNPr_QRSonY4VA5LqB5ZpCtQ,2061
|
57
57
|
h_message_bus/domain/request_messages/twitter_retweet_request_message.py,sha256=UHnLrgnqgZixb2q5ugFEHcCJxNkuTzrToGCEVoXm4gU,1423
|
58
58
|
h_message_bus/domain/request_messages/twitter_retweet_response_message.py,sha256=OGanRbX7AcKJ35H01wvp0XvuL-Chhm7f5J6NSpGq7uQ,1616
|
59
|
+
h_message_bus/domain/request_messages/twitter_search_request_message.py,sha256=9gKYMAO0CFVY3aS6Il2iQ5VLFo8GMXeuVdcVutJf-dk,2651
|
60
|
+
h_message_bus/domain/request_messages/twitter_search_response_message.py,sha256=P0MagAKNIXQrhT68oeOFoogY1p5IbDYkKloguX4O9vg,2493
|
59
61
|
h_message_bus/domain/request_messages/twitter_user_tweets_request_message.py,sha256=9vAhRxbCCDTqQNNKgp5cDIkiGIsnHwRSOwc6PK6TmcA,2397
|
60
62
|
h_message_bus/domain/request_messages/twitter_user_tweets_response_message.py,sha256=1oMoefXWq8C3_3xEQ3FBWQU-ATiUTYxEzOmqDuidtDU,3171
|
61
63
|
h_message_bus/domain/request_messages/vector_query_collection_request_message.py,sha256=x1J8SLVBlNS4TFzVJY9UG7mo94l9i4azndwzAlyktZw,1800
|
@@ -66,9 +68,9 @@ h_message_bus/domain/request_messages/vector_save_request_message.py,sha256=Xqrd
|
|
66
68
|
h_message_bus/domain/request_messages/web_get_docs_request_message.py,sha256=MkJySkRBlQ2CacMoPCd3FwXQt1tVVFdrZKxsA6yMsrk,1576
|
67
69
|
h_message_bus/domain/request_messages/web_search_request_message.py,sha256=ZoB4idrFEs7HQ6IRxmwKrrfHWe3GlGF4LuOK68K5NEM,1000
|
68
70
|
h_message_bus/infrastructure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
69
|
-
h_message_bus/infrastructure/nats_client_repository.py,sha256=
|
71
|
+
h_message_bus/infrastructure/nats_client_repository.py,sha256=crxWCGkBS-DcUm5Ii4ngCCgQR32tedCEXVk9Tdq0ErY,4501
|
70
72
|
h_message_bus/infrastructure/nats_config.py,sha256=Yzqqd1bCfmUv_4FOnA1dvqIpakzV0BUL2_nXQcndWvo,1304
|
71
|
-
h_message_bus-0.0.
|
72
|
-
h_message_bus-0.0.
|
73
|
-
h_message_bus-0.0.
|
74
|
-
h_message_bus-0.0.
|
73
|
+
h_message_bus-0.0.40.dist-info/METADATA,sha256=UgqzgrRDT29K82kJkbnalFCnBq_SoHBhI0dx8OFPZ8s,8834
|
74
|
+
h_message_bus-0.0.40.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
75
|
+
h_message_bus-0.0.40.dist-info/top_level.txt,sha256=BArjhm_lwFR9yJJEIf-LT_X64psuLkXFdbpQRJUreFE,23
|
76
|
+
h_message_bus-0.0.40.dist-info/RECORD,,
|
File without changes
|