Spaces:
Sleeping
Sleeping
#!/usr/bin/env python3 | |
""" | |
ResearchMate Development Server | |
A complete Python-based development environment for ResearchMate | |
""" | |
import os | |
import sys | |
import subprocess | |
import threading | |
import time | |
import logging | |
from pathlib import Path | |
from typing import Dict, List, Optional | |
import signal | |
import webbrowser | |
from watchdog.observers import Observer | |
from watchdog.events import FileSystemEventHandler | |
import platform | |
import uvicorn | |
import socket | |
# Add the project root to Python path | |
sys.path.append(str(Path(__file__).parent.parent.parent)) | |
# Import the main app from main.py | |
from ResearchMate.app import app | |
# Setup logging | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.FileHandler(Path(__file__).parent.parent.parent / 'logs' / 'development.log'), | |
logging.StreamHandler() | |
] | |
) | |
logger = logging.getLogger(__name__) | |
class FileChangeHandler(FileSystemEventHandler): | |
"""Handle file changes for auto-reload""" | |
def __init__(self, callback): | |
self.callback = callback | |
self.last_modified = {} | |
def on_modified(self, event): | |
if event.is_directory: | |
return | |
# Only watch Python files | |
if not event.src_path.endswith('.py'): | |
return | |
# Debounce rapid changes | |
current_time = time.time() | |
if event.src_path in self.last_modified: | |
if current_time - self.last_modified[event.src_path] < 1: | |
return | |
self.last_modified[event.src_path] = current_time | |
logger.info(f"File changed: {event.src_path}") | |
self.callback() | |
class ResearchMateDevServer: | |
"""Development server for ResearchMate""" | |
def __init__(self, project_root: Optional[Path] = None): | |
self.project_root = project_root or Path(__file__).parent.parent.parent | |
self.venv_path = self.project_root / "venv" | |
self.server_thread = None | |
self.observer = None | |
self.is_running = False | |
self.is_windows = platform.system() == "Windows" | |
# Store server config for restarts | |
self.server_host = "127.0.0.1" | |
self.server_port = 8000 | |
def print_banner(self): | |
"""Print development server banner""" | |
banner = """ | |
ResearchMate Development Server | |
================================= | |
AI Research Assistant - Development Mode | |
Auto-reload enabled for Python files | |
""" | |
print(banner) | |
logger.info("Starting ResearchMate development server") | |
def get_venv_python(self) -> Path: | |
"""Get path to Python executable in virtual environment""" | |
# If we're already in a virtual environment (including Conda), use the current Python executable | |
if sys.prefix != sys.base_prefix or 'CONDA_DEFAULT_ENV' in os.environ: | |
return Path(sys.executable) | |
# Otherwise, construct the path to the venv Python executable | |
if self.is_windows: | |
return self.venv_path / "Scripts" / "python.exe" | |
else: | |
return self.venv_path / "bin" / "python" | |
def check_virtual_environment(self) -> bool: | |
"""Check if virtual environment exists""" | |
# Since we're importing directly, just check if we can import the modules | |
try: | |
import ResearchMate.app as app | |
logger.info("Successfully imported main application") | |
return True | |
except ImportError as e: | |
logger.error(f"Failed to import main application: {e}") | |
logger.error("Make sure you're in the correct environment with all dependencies installed") | |
return False | |
def check_port_available(self, host: str, port: int) -> bool: | |
"""Check if a port is available""" | |
try: | |
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: | |
sock.bind((host, port)) | |
return True | |
except OSError: | |
return False | |
def find_available_port(self, host: str, start_port: int = 8000, max_attempts: int = 10) -> Optional[int]: | |
"""Find an available port starting from start_port""" | |
for port in range(start_port, start_port + max_attempts): | |
if self.check_port_available(host, port): | |
return port | |
return None | |
def start_server_process(self, host: str = "127.0.0.1", port: int = 8000): | |
"""Start the server using uvicorn directly with the imported app""" | |
try: | |
# Check if the requested port is available | |
if not self.check_port_available(host, port): | |
logger.warning(f"Port {port} is already in use on {host}") | |
available_port = self.find_available_port(host, port) | |
if available_port: | |
logger.info(f"Using available port {available_port} instead") | |
port = available_port | |
self.server_port = port # Update stored port | |
else: | |
logger.error(f"No available ports found starting from {port}") | |
return False | |
logger.info(f"Starting server on {host}:{port}") | |
# Run uvicorn with the imported app in a separate thread | |
def run_server(): | |
uvicorn.run( | |
app, | |
host=host, | |
port=port, | |
reload=False, # We handle reload ourselves with file watcher | |
log_level="info" | |
) | |
# Start server in background thread | |
self.server_thread = threading.Thread(target=run_server, daemon=True) | |
self.server_thread.start() | |
# Wait a moment for server to start | |
time.sleep(2) | |
logger.info("Server process started successfully") | |
return True | |
except Exception as e: | |
logger.error(f"Failed to start server: {e}") | |
import traceback | |
logger.error(f"Traceback: {traceback.format_exc()}") | |
return False | |
def stop_server_process(self): | |
"""Stop the server process""" | |
if self.server_thread: | |
logger.info("Stopping server...") | |
# Note: For development, we'll let the thread finish naturally | |
# In a real implementation, you might want to implement graceful shutdown | |
self.server_thread = None | |
def restart_server(self): | |
"""Restart the server""" | |
logger.info("File change detected - restarting server...") | |
logger.info("Note: For full restart, please stop and start the dev server manually") | |
# Note: Auto-restart is complex with embedded uvicorn | |
# For now, just log the change. User can manually restart. | |
def setup_file_watcher(self): | |
"""Setup file watcher for auto-reload""" | |
try: | |
self.observer = Observer() | |
# Watch source files | |
watch_paths = [ | |
self.project_root / "src", | |
self.project_root / "main.py" | |
] | |
handler = FileChangeHandler(self.restart_server) | |
for path in watch_paths: | |
if path.exists(): | |
if path.is_file(): | |
self.observer.schedule(handler, str(path.parent), recursive=False) | |
else: | |
self.observer.schedule(handler, str(path), recursive=True) | |
self.observer.start() | |
logger.info("File watcher started") | |
except Exception as e: | |
logger.error(f"Failed to setup file watcher: {e}") | |
def stop_file_watcher(self): | |
"""Stop file watcher""" | |
if self.observer: | |
self.observer.stop() | |
self.observer.join() | |
self.observer = None | |
def open_browser(self, url: str): | |
"""Open browser after server starts""" | |
def open_after_delay(): | |
time.sleep(3) # Wait for server to start | |
try: | |
webbrowser.open(url) | |
logger.info(f"Opened browser at {url}") | |
except Exception as e: | |
logger.warning(f"Could not open browser: {e}") | |
thread = threading.Thread(target=open_after_delay) | |
thread.daemon = True | |
thread.start() | |
def run_tests(self): | |
"""Run project tests""" | |
try: | |
logger.info("Running tests...") | |
logger.info("No tests configured - skipping test run") | |
except Exception as e: | |
logger.error(f"Failed to run tests: {e}") | |
def check_code_quality(self): | |
"""Check code quality with linting""" | |
try: | |
logger.info("Checking code quality...") | |
python_path = self.get_venv_python() | |
# Run flake8 if available | |
try: | |
result = subprocess.run([ | |
str(python_path), "-m", "flake8", | |
"src/", "main.py", "--max-line-length=88" | |
], cwd=self.project_root, capture_output=True, text=True) | |
if result.returncode == 0: | |
logger.info("Code quality checks passed") | |
else: | |
logger.warning("Code quality issues found:") | |
print(result.stdout) | |
except FileNotFoundError: | |
logger.info("flake8 not installed, skipping code quality check") | |
except Exception as e: | |
logger.error(f"Failed to check code quality: {e}") | |
def start(self, host: str = "127.0.0.1", port: int = 8000, open_browser: bool = True): | |
"""Start the development server""" | |
self.print_banner() | |
if not self.check_virtual_environment(): | |
return False | |
# Setup signal handlers | |
def signal_handler(signum, frame): | |
logger.info("Received interrupt signal") | |
self.stop() | |
sys.exit(0) | |
signal.signal(signal.SIGINT, signal_handler) | |
signal.signal(signal.SIGTERM, signal_handler) | |
try: | |
self.is_running = True | |
# Store server config for restarts | |
self.server_host = host | |
self.server_port = port | |
# Start server | |
if not self.start_server_process(host, port): | |
return False | |
# Use the actual port (might have changed if original was busy) | |
actual_port = self.server_port | |
# Setup file watcher | |
self.setup_file_watcher() | |
# Open browser | |
if open_browser: | |
self.open_browser(f"http://{host}:{actual_port}") | |
logger.info("Development server started successfully!") | |
logger.info(f"Web Interface: http://{host}:{actual_port}") | |
logger.info(f"API Documentation: http://{host}:{actual_port}/docs") | |
logger.info("File watcher enabled (manual restart required for changes)") | |
logger.info("Use Ctrl+C to stop") | |
# Keep the main thread alive | |
while self.is_running: | |
time.sleep(1) | |
except KeyboardInterrupt: | |
logger.info("Server stopped by user") | |
except Exception as e: | |
logger.error(f"Development server error: {e}") | |
finally: | |
self.stop() | |
def stop(self): | |
"""Stop the development server""" | |
self.is_running = False | |
self.stop_file_watcher() | |
self.stop_server_process() | |
logger.info("Development server stopped") | |
def main(): | |
"""Main development server function""" | |
import argparse | |
parser = argparse.ArgumentParser(description="ResearchMate Development Server") | |
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to") | |
parser.add_argument("--port", type=int, default=8000, help="Port to bind to") | |
parser.add_argument("--no-browser", action="store_true", help="Don't open browser") | |
parser.add_argument("--test", action="store_true", help="Run tests only") | |
parser.add_argument("--lint", action="store_true", help="Check code quality only") | |
args = parser.parse_args() | |
dev_server = ResearchMateDevServer() | |
if args.test: | |
dev_server.run_tests() | |
elif args.lint: | |
dev_server.check_code_quality() | |
else: | |
dev_server.start( | |
host=args.host, | |
port=args.port, | |
open_browser=not args.no_browser | |
) | |
if __name__ == "__main__": | |
main() | |