feat(api): Add lightweight HTTP server for API hooks
This commit is contained in:
41
api_hooks.py
Normal file
41
api_hooks.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class HookHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/status':
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8'))
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
logging.info("Hook API: " + format % args)
|
||||||
|
|
||||||
|
class HookServer:
|
||||||
|
def __init__(self, app, port=8999):
|
||||||
|
self.app = app
|
||||||
|
self.port = port
|
||||||
|
self.server = None
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if not getattr(self.app, 'test_hooks_enabled', False):
|
||||||
|
return
|
||||||
|
self.server = HTTPServer(('127.0.0.1', self.port), HookHandler)
|
||||||
|
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
logging.info(f"Hook server started on port {self.port}")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.server:
|
||||||
|
self.server.shutdown()
|
||||||
|
self.server.server_close()
|
||||||
|
if self.thread:
|
||||||
|
self.thread.join()
|
||||||
|
logging.info("Hook server stopped")
|
||||||
5
gui.py
5
gui.py
@@ -25,6 +25,7 @@ import shell_runner
|
|||||||
import session_logger
|
import session_logger
|
||||||
import project_manager
|
import project_manager
|
||||||
import theme
|
import theme
|
||||||
|
import api_hooks
|
||||||
|
|
||||||
CONFIG_PATH = Path("config.toml")
|
CONFIG_PATH = Path("config.toml")
|
||||||
PROVIDERS = ["gemini", "anthropic"]
|
PROVIDERS = ["gemini", "anthropic"]
|
||||||
@@ -396,6 +397,7 @@ class App:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = load_config()
|
self.config = load_config()
|
||||||
self.test_hooks_enabled = '--enable-test-hooks' in sys.argv or os.environ.get('SLOP_TEST_HOOKS') == '1'
|
self.test_hooks_enabled = '--enable-test-hooks' in sys.argv or os.environ.get('SLOP_TEST_HOOKS') == '1'
|
||||||
|
self.hook_server = api_hooks.HookServer(self)
|
||||||
|
|
||||||
# ---- global settings from config.toml ----
|
# ---- global settings from config.toml ----
|
||||||
ai_cfg = self.config.get("ai", {})
|
ai_cfg = self.config.get("ai", {})
|
||||||
@@ -2058,6 +2060,8 @@ class App:
|
|||||||
self._rebuild_discussion_selector()
|
self._rebuild_discussion_selector()
|
||||||
self._fetch_models(self.current_provider)
|
self._fetch_models(self.current_provider)
|
||||||
|
|
||||||
|
self.hook_server.start()
|
||||||
|
|
||||||
while dpg.is_dearpygui_running():
|
while dpg.is_dearpygui_running():
|
||||||
# Show any pending confirmation dialog on the main thread safely
|
# Show any pending confirmation dialog on the main thread safely
|
||||||
with self._pending_dialog_lock:
|
with self._pending_dialog_lock:
|
||||||
@@ -2184,6 +2188,7 @@ class App:
|
|||||||
dpg.save_init_file("dpg_layout.ini")
|
dpg.save_init_file("dpg_layout.ini")
|
||||||
session_logger.close_session()
|
session_logger.close_session()
|
||||||
ai_client.cleanup() # Destroy active API caches to stop billing
|
ai_client.cleanup() # Destroy active API caches to stop billing
|
||||||
|
self.hook_server.stop()
|
||||||
dpg.destroy_context()
|
dpg.destroy_context()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import gui
|
import gui
|
||||||
|
import api_hooks
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
def test_hooks_enabled_via_cli():
|
def test_hooks_enabled_via_cli():
|
||||||
with patch.object(sys, 'argv', ['gui.py', '--enable-test-hooks']):
|
with patch.object(sys, 'argv', ['gui.py', '--enable-test-hooks']):
|
||||||
@@ -22,3 +27,20 @@ def test_hooks_enabled_via_env():
|
|||||||
with patch.dict(os.environ, {'SLOP_TEST_HOOKS': '1'}):
|
with patch.dict(os.environ, {'SLOP_TEST_HOOKS': '1'}):
|
||||||
app = gui.App()
|
app = gui.App()
|
||||||
assert app.test_hooks_enabled is True
|
assert app.test_hooks_enabled is True
|
||||||
|
|
||||||
|
def test_ipc_server_starts_and_responds():
|
||||||
|
app_mock = gui.App()
|
||||||
|
app_mock.test_hooks_enabled = True
|
||||||
|
server = api_hooks.HookServer(app_mock, port=8999)
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
# Wait for server to start
|
||||||
|
time.sleep(0.5)
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request("http://127.0.0.1:8999/status")
|
||||||
|
with urllib.request.urlopen(req) as response:
|
||||||
|
assert response.status == 200
|
||||||
|
data = json.loads(response.read().decode())
|
||||||
|
assert data.get("status") == "ok"
|
||||||
|
finally:
|
||||||
|
server.stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user