fustor-sender-http 0.8.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.
- fustor_sender_http-0.8.1/PKG-INFO +67 -0
- fustor_sender_http-0.8.1/README.md +52 -0
- fustor_sender_http-0.8.1/pyproject.toml +41 -0
- fustor_sender_http-0.8.1/setup.cfg +4 -0
- fustor_sender_http-0.8.1/src/fustor_sender_http/__init__.py +233 -0
- fustor_sender_http-0.8.1/src/fustor_sender_http.egg-info/PKG-INFO +67 -0
- fustor_sender_http-0.8.1/src/fustor_sender_http.egg-info/SOURCES.txt +10 -0
- fustor_sender_http-0.8.1/src/fustor_sender_http.egg-info/dependency_links.txt +1 -0
- fustor_sender_http-0.8.1/src/fustor_sender_http.egg-info/entry_points.txt +2 -0
- fustor_sender_http-0.8.1/src/fustor_sender_http.egg-info/requires.txt +2 -0
- fustor_sender_http-0.8.1/src/fustor_sender_http.egg-info/top_level.txt +1 -0
- fustor_sender_http-0.8.1/tests/test_http_sender.py +108 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fustor-sender-http
|
|
3
|
+
Version: 0.8.1
|
|
4
|
+
Summary: Fustor HTTP Sender - Transport layer for Agent to Fusion communication
|
|
5
|
+
Author-email: Huajin Wang <wanghuajin999@163.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/excelwang/fustor/tree/master/extensions/sender-http
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/excelwang/fustor/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: fustor-core
|
|
14
|
+
Requires-Dist: fustor-fusion-sdk
|
|
15
|
+
|
|
16
|
+
# fustor-sender-http
|
|
17
|
+
|
|
18
|
+
HTTP Sender for Fustor Agent - implements the transport layer for Agent to Fusion communication.
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
This package provides an HTTP-based implementation of the `Sender` transport abstraction. It uses the Fusion SDK client to communicate with Fusion's REST API.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install fustor-sender-http
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from fustor_sender_http import HTTPSender
|
|
34
|
+
|
|
35
|
+
sender = HTTPSender(
|
|
36
|
+
sender_id="my-sender",
|
|
37
|
+
endpoint="http://fusion.example.com:8000",
|
|
38
|
+
credential={"api_key": "your-api-key"}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Create session
|
|
42
|
+
await sender.connect()
|
|
43
|
+
session = await sender.create_session("my-task-id")
|
|
44
|
+
|
|
45
|
+
# Send events
|
|
46
|
+
await sender.send_events(events, source_type="message")
|
|
47
|
+
|
|
48
|
+
# Heartbeat
|
|
49
|
+
await sender.heartbeat()
|
|
50
|
+
|
|
51
|
+
# Cleanup
|
|
52
|
+
await sender.close()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Entry Points
|
|
56
|
+
|
|
57
|
+
This package registers itself as:
|
|
58
|
+
- `fustor.senders:http` - New sender registry
|
|
59
|
+
- `fustor_agent.drivers.senders:fusion` - Legacy sender registry (backward compat)
|
|
60
|
+
|
|
61
|
+
## Migration from fustor-sender-fusion
|
|
62
|
+
|
|
63
|
+
The `fustor-sender-fusion` package is deprecated. To migrate:
|
|
64
|
+
|
|
65
|
+
1. Replace `from fustor_sender_fusion import FusionDriver` with `from fustor_sender_http import HTTPSender`
|
|
66
|
+
2. Update configuration to use `sender` instead of `sender` terminology
|
|
67
|
+
3. The `HTTPSender` class implements the new `Sender` interface but maintains API compatibility
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# fustor-sender-http
|
|
2
|
+
|
|
3
|
+
HTTP Sender for Fustor Agent - implements the transport layer for Agent to Fusion communication.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides an HTTP-based implementation of the `Sender` transport abstraction. It uses the Fusion SDK client to communicate with Fusion's REST API.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install fustor-sender-http
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from fustor_sender_http import HTTPSender
|
|
19
|
+
|
|
20
|
+
sender = HTTPSender(
|
|
21
|
+
sender_id="my-sender",
|
|
22
|
+
endpoint="http://fusion.example.com:8000",
|
|
23
|
+
credential={"api_key": "your-api-key"}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Create session
|
|
27
|
+
await sender.connect()
|
|
28
|
+
session = await sender.create_session("my-task-id")
|
|
29
|
+
|
|
30
|
+
# Send events
|
|
31
|
+
await sender.send_events(events, source_type="message")
|
|
32
|
+
|
|
33
|
+
# Heartbeat
|
|
34
|
+
await sender.heartbeat()
|
|
35
|
+
|
|
36
|
+
# Cleanup
|
|
37
|
+
await sender.close()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Entry Points
|
|
41
|
+
|
|
42
|
+
This package registers itself as:
|
|
43
|
+
- `fustor.senders:http` - New sender registry
|
|
44
|
+
- `fustor_agent.drivers.senders:fusion` - Legacy sender registry (backward compat)
|
|
45
|
+
|
|
46
|
+
## Migration from fustor-sender-fusion
|
|
47
|
+
|
|
48
|
+
The `fustor-sender-fusion` package is deprecated. To migrate:
|
|
49
|
+
|
|
50
|
+
1. Replace `from fustor_sender_fusion import FusionDriver` with `from fustor_sender_http import HTTPSender`
|
|
51
|
+
2. Update configuration to use `sender` instead of `sender` terminology
|
|
52
|
+
3. The `HTTPSender` class implements the new `Sender` interface but maintains API compatibility
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fustor-sender-http"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Fustor HTTP Sender - Transport layer for Agent to Fusion communication"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"fustor-core",
|
|
18
|
+
"fustor-fusion-sdk",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[[project.authors]]
|
|
22
|
+
name = "Huajin Wang"
|
|
23
|
+
email = "wanghuajin999@163.com"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/excelwang/fustor/tree/master/extensions/sender-http"
|
|
27
|
+
"Bug Tracker" = "https://github.com/excelwang/fustor/issues"
|
|
28
|
+
|
|
29
|
+
# New entry point for V2 architecture
|
|
30
|
+
[project.entry-points."fustor_agent.drivers.senders"]
|
|
31
|
+
fusion = "fustor_sender_http:HTTPSender"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
[tool.setuptools_scm]
|
|
36
|
+
root = "../.."
|
|
37
|
+
version_scheme = "post-release"
|
|
38
|
+
local_scheme = "dirty-tag"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fustor HTTP Sender - Transport layer for Agent to Fusion communication.
|
|
3
|
+
|
|
4
|
+
This package implements the HTTP transport protocol for sending events
|
|
5
|
+
from Fustor Agent to Fustor Fusion.
|
|
6
|
+
"""
|
|
7
|
+
try:
|
|
8
|
+
from ._version import version as __version__
|
|
9
|
+
except ImportError:
|
|
10
|
+
__version__ = "unknown"
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from fustor_core.transport import Sender
|
|
17
|
+
from fustor_core.exceptions import FusionConnectionError
|
|
18
|
+
from fustor_core.event import EventBase
|
|
19
|
+
from fustor_core.exceptions import SessionObsoletedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HTTPSender(Sender):
|
|
23
|
+
"""
|
|
24
|
+
HTTP-based Sender implementation for Fustor.
|
|
25
|
+
|
|
26
|
+
Uses the Fusion SDK client to communicate with Fusion's REST API.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
sender_id: str,
|
|
32
|
+
endpoint: str,
|
|
33
|
+
credential: Dict[str, Any],
|
|
34
|
+
config: Optional[Dict[str, Any]] = None
|
|
35
|
+
):
|
|
36
|
+
super().__init__(sender_id, endpoint, credential, config)
|
|
37
|
+
self.logger = logging.getLogger(f"fustor.sender.http.{sender_id}")
|
|
38
|
+
|
|
39
|
+
# Lazy import to avoid circular dependency
|
|
40
|
+
from fustor_fusion_sdk.client import FusionClient
|
|
41
|
+
|
|
42
|
+
api_key = credential.get("key") or credential.get("api_key")
|
|
43
|
+
|
|
44
|
+
# Extended configuration
|
|
45
|
+
timeout = self.config.get("timeout", 30.0)
|
|
46
|
+
api_version = self.config.get("api_version", "pipe")
|
|
47
|
+
|
|
48
|
+
self.client = FusionClient(
|
|
49
|
+
base_url=endpoint,
|
|
50
|
+
api_key=api_key,
|
|
51
|
+
timeout=timeout,
|
|
52
|
+
api_version=api_version
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def connect(self) -> None:
|
|
56
|
+
"""Establish connection (for HTTP, this is a no-op as we use stateless requests)."""
|
|
57
|
+
self.logger.debug(f"HTTP Sender {self.id} ready for endpoint {self.endpoint}")
|
|
58
|
+
|
|
59
|
+
async def create_session(
|
|
60
|
+
self,
|
|
61
|
+
task_id: str,
|
|
62
|
+
source_type: Optional[str] = None,
|
|
63
|
+
session_timeout_seconds: Optional[int] = None,
|
|
64
|
+
**kwargs
|
|
65
|
+
) -> Dict[str, Any]:
|
|
66
|
+
"""
|
|
67
|
+
Create a new session with Fusion.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
task_id: Identifier for this sync task
|
|
71
|
+
source_type: Type of source
|
|
72
|
+
session_timeout_seconds: Requested timeout
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Session metadata including session_id, timeout, role
|
|
76
|
+
"""
|
|
77
|
+
self.logger.info(f"Creating session for task {task_id}...")
|
|
78
|
+
try:
|
|
79
|
+
session_data = await self.client.create_session(
|
|
80
|
+
task_id,
|
|
81
|
+
source_type=source_type,
|
|
82
|
+
session_timeout_seconds=session_timeout_seconds,
|
|
83
|
+
client_info=kwargs
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if session_data and session_data.get("session_id"):
|
|
87
|
+
session_id = session_data["session_id"]
|
|
88
|
+
self.session_id = session_id
|
|
89
|
+
self.logger.info(
|
|
90
|
+
f"Session created: {self.session_id}, "
|
|
91
|
+
f"Role: {session_data.get('role')}, "
|
|
92
|
+
f"Timeout: {session_data.get('session_timeout_seconds')}s"
|
|
93
|
+
)
|
|
94
|
+
return session_id, session_data
|
|
95
|
+
else:
|
|
96
|
+
# Should not happen if client raises exception on error, but handling just in case
|
|
97
|
+
self.logger.error("Failed to create session: Empty response.")
|
|
98
|
+
raise RuntimeError("Failed to create session with Fusion service: Empty response.")
|
|
99
|
+
|
|
100
|
+
except httpx.ConnectError as e:
|
|
101
|
+
self.logger.warning(f"Failed to create session (Connection Error): {e}")
|
|
102
|
+
raise FusionConnectionError(f"Failed to create session with Fusion service: {e}") from e
|
|
103
|
+
except httpx.HTTPStatusError as e:
|
|
104
|
+
self.logger.error(f"Failed to create session: HTTP {e.response.status_code} - {e.response.text}")
|
|
105
|
+
raise RuntimeError(f"Failed to create session with Fusion service: HTTP {e.response.status_code} - {e.response.text}") from e
|
|
106
|
+
except Exception as e:
|
|
107
|
+
self.logger.error(f"Failed to create session: {e!r}")
|
|
108
|
+
raise RuntimeError(f"Failed to create session with Fusion service: {e}") from e
|
|
109
|
+
|
|
110
|
+
async def _send_events_impl(
|
|
111
|
+
self,
|
|
112
|
+
events: List[EventBase],
|
|
113
|
+
source_type: str = "message",
|
|
114
|
+
is_end: bool = False,
|
|
115
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
116
|
+
) -> Dict[str, Any]:
|
|
117
|
+
"""
|
|
118
|
+
Implementation of sending events to Fusion.
|
|
119
|
+
Called by the base class template method.
|
|
120
|
+
"""
|
|
121
|
+
if not self.session_id:
|
|
122
|
+
self.logger.error("Cannot send events: session_id is not set.")
|
|
123
|
+
return {"success": False, "error": "No session"}
|
|
124
|
+
|
|
125
|
+
# Convert events to dictionaries for JSON serialization
|
|
126
|
+
event_dicts = []
|
|
127
|
+
for event in events:
|
|
128
|
+
if hasattr(event, 'model_dump'):
|
|
129
|
+
event_dicts.append(event.model_dump(mode='json'))
|
|
130
|
+
elif isinstance(event, dict):
|
|
131
|
+
event_dicts.append(event)
|
|
132
|
+
else:
|
|
133
|
+
event_dicts.append(dict(event))
|
|
134
|
+
|
|
135
|
+
total_rows = sum(len(e.get("rows", [])) for e in event_dicts)
|
|
136
|
+
self.logger.debug(f"[{source_type}] Attempting to push {len(events)} events ({total_rows} rows) to {self.endpoint}")
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
success = await self.client.push_events(
|
|
140
|
+
session_id=self.session_id,
|
|
141
|
+
events=event_dicts,
|
|
142
|
+
source_type=source_type,
|
|
143
|
+
is_snapshot_end=is_end,
|
|
144
|
+
metadata=metadata
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if success:
|
|
148
|
+
self.logger.info(f"[{source_type}] Sent {len(events)} events ({total_rows} rows).")
|
|
149
|
+
return {"success": True}
|
|
150
|
+
else:
|
|
151
|
+
self.logger.error(f"[{source_type}] Failed to send {len(events)} events.")
|
|
152
|
+
return {"success": False, "error": "Push failed"}
|
|
153
|
+
except httpx.HTTPStatusError as e:
|
|
154
|
+
if e.response.status_code == 419:
|
|
155
|
+
raise SessionObsoletedError(f"Session {self.session_id} is obsolete (419)")
|
|
156
|
+
self.logger.error(f"[{source_type}] Failed to send {len(events)} events: {e}")
|
|
157
|
+
return {"success": False, "error": f"Push failed: {e}"}
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self.logger.error(f"[{source_type}] Failed to send {len(events)} events: {e}")
|
|
160
|
+
return {"success": False, "error": "Push failed"}
|
|
161
|
+
|
|
162
|
+
async def heartbeat(self, can_realtime: bool = False) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Send a heartbeat to maintain session.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Response including current role status
|
|
168
|
+
"""
|
|
169
|
+
if not self.session_id:
|
|
170
|
+
self.logger.error("Cannot send heartbeat: session_id is not set.")
|
|
171
|
+
return {"status": "error", "message": "Session ID not set"}
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
result = await self.client.send_heartbeat(self.session_id, can_realtime=can_realtime)
|
|
175
|
+
|
|
176
|
+
if result and result.get("status") == "ok":
|
|
177
|
+
self.logger.debug("Heartbeat sent successfully.")
|
|
178
|
+
return result
|
|
179
|
+
else:
|
|
180
|
+
msg = result.get("message") if result else "Unknown error"
|
|
181
|
+
# Fallback: some legacy or non-FastAPI paths might still return 200 with error body
|
|
182
|
+
if result and result.get("status") == "error":
|
|
183
|
+
raise SessionObsoletedError(f"Session {self.session_id} is no longer valid: {msg}")
|
|
184
|
+
return {"status": "error", "message": msg}
|
|
185
|
+
except httpx.HTTPStatusError as e:
|
|
186
|
+
if e.response.status_code == 419:
|
|
187
|
+
raise SessionObsoletedError(f"Session {self.session_id} is obsolete (419)")
|
|
188
|
+
self.logger.error(f"Failed to send heartbeat: {e}")
|
|
189
|
+
return {"status": "error", "message": f"Heartbeat failed: {e}"}
|
|
190
|
+
|
|
191
|
+
async def close_session(self) -> None:
|
|
192
|
+
"""Close the current session gracefully."""
|
|
193
|
+
if self.session_id:
|
|
194
|
+
try:
|
|
195
|
+
await self.client.terminate_session(self.session_id)
|
|
196
|
+
self.logger.info(f"Session {self.session_id} terminated.")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self.logger.warning(f"Failed to terminate session: {e}")
|
|
199
|
+
finally:
|
|
200
|
+
self.session_id = None
|
|
201
|
+
|
|
202
|
+
async def close(self) -> None:
|
|
203
|
+
"""Close the sender and release resources."""
|
|
204
|
+
await self.close_session()
|
|
205
|
+
if hasattr(self.client, 'close'):
|
|
206
|
+
await self.client.close()
|
|
207
|
+
|
|
208
|
+
# --- Consistency signals ---
|
|
209
|
+
|
|
210
|
+
async def signal_audit_start(self) -> bool:
|
|
211
|
+
"""Signal the start of an audit cycle."""
|
|
212
|
+
return await self.client.signal_audit_start(self.id)
|
|
213
|
+
|
|
214
|
+
async def signal_audit_end(self) -> bool:
|
|
215
|
+
"""Signal the end of an audit cycle."""
|
|
216
|
+
return await self.client.signal_audit_end(self.id)
|
|
217
|
+
|
|
218
|
+
async def get_sentinel_tasks(self) -> Optional[Dict[str, Any]]:
|
|
219
|
+
"""Query for sentinel verification tasks."""
|
|
220
|
+
try:
|
|
221
|
+
return await self.client.get_sentinel_tasks()
|
|
222
|
+
except Exception as e:
|
|
223
|
+
self.logger.debug(f"Failed to get sentinel tasks: {e}")
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
async def submit_sentinel_results(self, results: Dict[str, Any]) -> bool:
|
|
227
|
+
"""Submit sentinel verification results."""
|
|
228
|
+
try:
|
|
229
|
+
return await self.client.submit_sentinel_feedback(results)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
self.logger.error(f"Failed to submit sentinel results: {e}")
|
|
232
|
+
return False
|
|
233
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fustor-sender-http
|
|
3
|
+
Version: 0.8.1
|
|
4
|
+
Summary: Fustor HTTP Sender - Transport layer for Agent to Fusion communication
|
|
5
|
+
Author-email: Huajin Wang <wanghuajin999@163.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/excelwang/fustor/tree/master/extensions/sender-http
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/excelwang/fustor/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: fustor-core
|
|
14
|
+
Requires-Dist: fustor-fusion-sdk
|
|
15
|
+
|
|
16
|
+
# fustor-sender-http
|
|
17
|
+
|
|
18
|
+
HTTP Sender for Fustor Agent - implements the transport layer for Agent to Fusion communication.
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
This package provides an HTTP-based implementation of the `Sender` transport abstraction. It uses the Fusion SDK client to communicate with Fusion's REST API.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install fustor-sender-http
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from fustor_sender_http import HTTPSender
|
|
34
|
+
|
|
35
|
+
sender = HTTPSender(
|
|
36
|
+
sender_id="my-sender",
|
|
37
|
+
endpoint="http://fusion.example.com:8000",
|
|
38
|
+
credential={"api_key": "your-api-key"}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Create session
|
|
42
|
+
await sender.connect()
|
|
43
|
+
session = await sender.create_session("my-task-id")
|
|
44
|
+
|
|
45
|
+
# Send events
|
|
46
|
+
await sender.send_events(events, source_type="message")
|
|
47
|
+
|
|
48
|
+
# Heartbeat
|
|
49
|
+
await sender.heartbeat()
|
|
50
|
+
|
|
51
|
+
# Cleanup
|
|
52
|
+
await sender.close()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Entry Points
|
|
56
|
+
|
|
57
|
+
This package registers itself as:
|
|
58
|
+
- `fustor.senders:http` - New sender registry
|
|
59
|
+
- `fustor_agent.drivers.senders:fusion` - Legacy sender registry (backward compat)
|
|
60
|
+
|
|
61
|
+
## Migration from fustor-sender-fusion
|
|
62
|
+
|
|
63
|
+
The `fustor-sender-fusion` package is deprecated. To migrate:
|
|
64
|
+
|
|
65
|
+
1. Replace `from fustor_sender_fusion import FusionDriver` with `from fustor_sender_http import HTTPSender`
|
|
66
|
+
2. Update configuration to use `sender` instead of `sender` terminology
|
|
67
|
+
3. The `HTTPSender` class implements the new `Sender` interface but maintains API compatibility
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/fustor_sender_http/__init__.py
|
|
4
|
+
src/fustor_sender_http.egg-info/PKG-INFO
|
|
5
|
+
src/fustor_sender_http.egg-info/SOURCES.txt
|
|
6
|
+
src/fustor_sender_http.egg-info/dependency_links.txt
|
|
7
|
+
src/fustor_sender_http.egg-info/entry_points.txt
|
|
8
|
+
src/fustor_sender_http.egg-info/requires.txt
|
|
9
|
+
src/fustor_sender_http.egg-info/top_level.txt
|
|
10
|
+
tests/test_http_sender.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fustor_sender_http
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
3
|
+
from fustor_sender_http import HTTPSender
|
|
4
|
+
from fustor_core.event import EventBase
|
|
5
|
+
from fustor_core.exceptions import SessionObsoletedError
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
class MockEvent(EventBase):
|
|
9
|
+
def model_dump(self, mode=None):
|
|
10
|
+
return {"id": 1, "data": "test"}
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_fusion_client():
|
|
14
|
+
with patch("fustor_fusion_sdk.client.FusionClient") as MockClient:
|
|
15
|
+
client_instance = MockClient.return_value
|
|
16
|
+
client_instance.create_session = AsyncMock()
|
|
17
|
+
client_instance.push_events = AsyncMock()
|
|
18
|
+
client_instance.send_heartbeat = AsyncMock()
|
|
19
|
+
client_instance.terminate_session = AsyncMock()
|
|
20
|
+
client_instance.signal_audit_start = AsyncMock()
|
|
21
|
+
client_instance.signal_audit_end = AsyncMock()
|
|
22
|
+
yield client_instance
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def sender(mock_fusion_client):
|
|
26
|
+
credential = {"api_key": "test-key"}
|
|
27
|
+
return HTTPSender("test-sender", "http://localhost", credential)
|
|
28
|
+
|
|
29
|
+
@pytest.mark.asyncio
|
|
30
|
+
async def test_init(sender, mock_fusion_client):
|
|
31
|
+
assert sender.id == "test-sender"
|
|
32
|
+
assert sender.credential["api_key"] == "test-key"
|
|
33
|
+
# Verify client init happened (implicitly via fixture)
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_create_session(sender, mock_fusion_client):
|
|
37
|
+
mock_fusion_client.create_session.return_value = {
|
|
38
|
+
"session_id": "sess-1",
|
|
39
|
+
"role": "leader",
|
|
40
|
+
"session_timeout_seconds": 60
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
session_id, result = await sender.create_session("task-1", "snapshot", 60)
|
|
44
|
+
|
|
45
|
+
assert result["session_id"] == "sess-1"
|
|
46
|
+
assert session_id == "sess-1"
|
|
47
|
+
assert sender.session_id == "sess-1"
|
|
48
|
+
mock_fusion_client.create_session.assert_called_once_with("task-1", source_type="snapshot", session_timeout_seconds=60, client_info={})
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_create_session_failure(sender, mock_fusion_client):
|
|
52
|
+
mock_fusion_client.create_session.return_value = None
|
|
53
|
+
|
|
54
|
+
with pytest.raises(RuntimeError, match="Failed to create session"):
|
|
55
|
+
await sender.create_session("task-1")
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_send_events_success(sender, mock_fusion_client):
|
|
59
|
+
sender.session_id = "sess-1"
|
|
60
|
+
mock_fusion_client.push_events.return_value = True
|
|
61
|
+
events = [{"event_type": "INSERT", "table": "t", "rows": []}]
|
|
62
|
+
|
|
63
|
+
result = await sender._send_events_impl(events, "message", False)
|
|
64
|
+
|
|
65
|
+
assert result["success"] is True
|
|
66
|
+
mock_fusion_client.push_events.assert_called_once()
|
|
67
|
+
args, kwargs = mock_fusion_client.push_events.call_args
|
|
68
|
+
assert kwargs["session_id"] == "sess-1"
|
|
69
|
+
assert len(kwargs["events"]) == 1
|
|
70
|
+
|
|
71
|
+
@pytest.mark.asyncio
|
|
72
|
+
async def test_send_events_no_session(sender):
|
|
73
|
+
# No session_id set
|
|
74
|
+
result = await sender._send_events_impl([], "message")
|
|
75
|
+
assert result["success"] is False
|
|
76
|
+
assert result["error"] == "No session"
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_send_events_obsolete_session(sender, mock_fusion_client):
|
|
80
|
+
sender.session_id = "sess-1"
|
|
81
|
+
# Simulate 419 error
|
|
82
|
+
request = httpx.Request("POST", "http://locahost")
|
|
83
|
+
response = httpx.Response(419, request=request)
|
|
84
|
+
mock_fusion_client.push_events.side_effect = httpx.HTTPStatusError("obsolete", request=request, response=response)
|
|
85
|
+
|
|
86
|
+
with pytest.raises(SessionObsoletedError):
|
|
87
|
+
await sender._send_events_impl([{"event_type": "INSERT", "table": "t", "rows": []}])
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_heartbeat_success(sender, mock_fusion_client):
|
|
91
|
+
sender.session_id = "sess-1"
|
|
92
|
+
mock_fusion_client.send_heartbeat.return_value = {"status": "ok", "role": "follower"}
|
|
93
|
+
|
|
94
|
+
result = await sender.heartbeat()
|
|
95
|
+
|
|
96
|
+
assert result["status"] == "ok"
|
|
97
|
+
mock_fusion_client.send_heartbeat.assert_called_once_with("sess-1", can_realtime=False)
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_audit_signals(sender, mock_fusion_client):
|
|
101
|
+
mock_fusion_client.signal_audit_start.return_value = True
|
|
102
|
+
mock_fusion_client.signal_audit_end.return_value = True
|
|
103
|
+
|
|
104
|
+
assert await sender.signal_audit_start() is True
|
|
105
|
+
assert await sender.signal_audit_end() is True
|
|
106
|
+
|
|
107
|
+
mock_fusion_client.signal_audit_start.assert_called_once_with("test-sender")
|
|
108
|
+
mock_fusion_client.signal_audit_end.assert_called_once_with("test-sender")
|