redesign fully scaffolded and web login works
This commit is contained in:
275
client-py/main.py
Normal file
275
client-py/main.py
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DexOrder User Container Main Entry Point
|
||||
|
||||
Brings together:
|
||||
- Config and secrets loading from k8s mounted YAML files
|
||||
- ZeroMQ event publisher for user events
|
||||
- MCP server with minimal "hello world" resource
|
||||
- Lifecycle management integration
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
from dexorder import EventPublisher, start_lifecycle_manager, get_lifecycle_manager
|
||||
from dexorder.events import EventType, UserEvent, DeliverySpec
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Configuration
|
||||
# =============================================================================
|
||||
|
||||
class Config:
|
||||
"""Application configuration loaded from config.yaml and secrets.yaml"""
|
||||
|
||||
def __init__(self):
|
||||
# User ID (required)
|
||||
self.user_id: str = os.getenv("USER_ID", "")
|
||||
if not self.user_id:
|
||||
raise ValueError("USER_ID environment variable required")
|
||||
|
||||
# Config and secrets paths (k8s mounted)
|
||||
self.config_path = Path(os.getenv("CONFIG_PATH", "/app/config/config.yaml"))
|
||||
self.secrets_path = Path(os.getenv("SECRETS_PATH", "/app/config/secrets.yaml"))
|
||||
|
||||
# ZMQ ports for event system
|
||||
self.zmq_xpub_port: int = int(os.getenv("ZMQ_XPUB_PORT", "5570"))
|
||||
self.zmq_gateway_endpoint: str = os.getenv(
|
||||
"ZMQ_GATEWAY_ENDPOINT",
|
||||
"tcp://gateway:5571"
|
||||
)
|
||||
|
||||
# MCP server settings
|
||||
self.mcp_server_name: str = os.getenv("MCP_SERVER_NAME", "dexorder-user")
|
||||
|
||||
# Lifecycle settings
|
||||
self.idle_timeout_minutes: int = int(os.getenv("IDLE_TIMEOUT_MINUTES", "15"))
|
||||
self.enable_idle_shutdown: bool = os.getenv("ENABLE_IDLE_SHUTDOWN", "true").lower() == "true"
|
||||
|
||||
# Loaded from files
|
||||
self.config_data: dict = {}
|
||||
self.secrets_data: dict = {}
|
||||
|
||||
def load(self) -> None:
|
||||
"""Load configuration and secrets from YAML files"""
|
||||
# Load config.yaml if exists
|
||||
if self.config_path.exists():
|
||||
with open(self.config_path) as f:
|
||||
self.config_data = yaml.safe_load(f) or {}
|
||||
logging.info(f"Loaded config from {self.config_path}")
|
||||
else:
|
||||
logging.warning(f"Config file not found: {self.config_path}")
|
||||
|
||||
# Load secrets.yaml if exists
|
||||
if self.secrets_path.exists():
|
||||
with open(self.secrets_path) as f:
|
||||
self.secrets_data = yaml.safe_load(f) or {}
|
||||
logging.info(f"Loaded secrets from {self.secrets_path}")
|
||||
else:
|
||||
logging.warning(f"Secrets file not found: {self.secrets_path}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MCP Server Setup
|
||||
# =============================================================================
|
||||
|
||||
def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server:
|
||||
"""Create MCP server with minimal hello world resource"""
|
||||
|
||||
server = Server(config.mcp_server_name)
|
||||
|
||||
@server.list_resources()
|
||||
async def list_resources():
|
||||
"""List available resources"""
|
||||
return [
|
||||
{
|
||||
"uri": f"dexorder://user/{config.user_id}/hello",
|
||||
"name": "Hello World",
|
||||
"description": "A simple hello world resource",
|
||||
"mimeType": "text/plain",
|
||||
}
|
||||
]
|
||||
|
||||
@server.read_resource()
|
||||
async def read_resource(uri: str):
|
||||
"""Read a resource by URI"""
|
||||
if uri == f"dexorder://user/{config.user_id}/hello":
|
||||
# Publish an event when resource is accessed
|
||||
await event_publisher.publish(UserEvent(
|
||||
event_type=EventType.STRATEGY_LOG,
|
||||
payload={
|
||||
"message": "Hello world resource accessed",
|
||||
"uri": uri,
|
||||
},
|
||||
delivery=DeliverySpec.informational(),
|
||||
))
|
||||
|
||||
return {
|
||||
"uri": uri,
|
||||
"mimeType": "text/plain",
|
||||
"text": f"Hello from DexOrder user container!\nUser ID: {config.user_id}\n",
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unknown resource: {uri}")
|
||||
|
||||
logging.info(f"MCP server '{config.mcp_server_name}' created")
|
||||
return server
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main Application
|
||||
# =============================================================================
|
||||
|
||||
class UserContainer:
|
||||
"""Main user container application"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = Config()
|
||||
self.event_publisher: Optional[EventPublisher] = None
|
||||
self.mcp_server: Optional[Server] = None
|
||||
self.running = False
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start all subsystems"""
|
||||
logging.info("Starting user container")
|
||||
|
||||
# Load configuration
|
||||
self.config.load()
|
||||
|
||||
# Start lifecycle manager
|
||||
await start_lifecycle_manager(
|
||||
user_id=self.config.user_id,
|
||||
idle_timeout_minutes=self.config.idle_timeout_minutes,
|
||||
enable_idle_shutdown=self.config.enable_idle_shutdown,
|
||||
)
|
||||
logging.info("Lifecycle manager started")
|
||||
|
||||
# Start event publisher
|
||||
self.event_publisher = EventPublisher(
|
||||
user_id=self.config.user_id,
|
||||
xpub_port=self.config.zmq_xpub_port,
|
||||
gateway_router_endpoint=self.config.zmq_gateway_endpoint,
|
||||
)
|
||||
await self.event_publisher.start()
|
||||
logging.info("Event publisher started")
|
||||
|
||||
# Publish CONTAINER_STARTING event
|
||||
await self.event_publisher.publish(UserEvent(
|
||||
event_type=EventType.CONTAINER_STARTING,
|
||||
payload={
|
||||
"user_id": self.config.user_id,
|
||||
"timestamp": None, # Will be auto-filled
|
||||
},
|
||||
delivery=DeliverySpec.active_or_telegram(),
|
||||
))
|
||||
|
||||
# Create MCP server
|
||||
self.mcp_server = create_mcp_server(self.config, self.event_publisher)
|
||||
|
||||
# Publish CONTAINER_READY event
|
||||
await self.event_publisher.publish(UserEvent(
|
||||
event_type=EventType.CONTAINER_READY,
|
||||
payload={
|
||||
"user_id": self.config.user_id,
|
||||
},
|
||||
delivery=DeliverySpec.active_or_telegram(),
|
||||
))
|
||||
|
||||
self.running = True
|
||||
logging.info("User container ready")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop all subsystems"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
logging.info("Stopping user container")
|
||||
self.running = False
|
||||
|
||||
# Publish CONTAINER_SHUTTING_DOWN event
|
||||
if self.event_publisher:
|
||||
await self.event_publisher.publish(UserEvent(
|
||||
event_type=EventType.CONTAINER_SHUTTING_DOWN,
|
||||
payload={
|
||||
"user_id": self.config.user_id,
|
||||
},
|
||||
delivery=DeliverySpec.active_or_telegram(),
|
||||
))
|
||||
|
||||
# Stop subsystems
|
||||
if self.event_publisher:
|
||||
await self.event_publisher.stop()
|
||||
logging.info("Event publisher stopped")
|
||||
|
||||
lifecycle = get_lifecycle_manager()
|
||||
if lifecycle:
|
||||
await lifecycle.stop()
|
||||
logging.info("Lifecycle manager stopped")
|
||||
|
||||
logging.info("User container stopped")
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Run the MCP server via stdio"""
|
||||
await self.start()
|
||||
|
||||
try:
|
||||
# Run MCP server on stdio
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await self.mcp_server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
self.mcp_server.create_initialization_options()
|
||||
)
|
||||
finally:
|
||||
await self.stop()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Entry Point
|
||||
# =============================================================================
|
||||
|
||||
async def main():
|
||||
"""Main entry point"""
|
||||
# Setup logging
|
||||
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, log_level),
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
stream=sys.stderr, # MCP uses stdout for protocol
|
||||
)
|
||||
|
||||
# Create and run container
|
||||
container = UserContainer()
|
||||
|
||||
# Handle shutdown signals
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def handle_signal(sig):
|
||||
logging.info(f"Received signal {sig}, shutting down...")
|
||||
asyncio.create_task(container.stop())
|
||||
loop.stop()
|
||||
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
loop.add_signal_handler(sig, lambda s=sig: handle_signal(s))
|
||||
|
||||
try:
|
||||
await container.run()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Keyboard interrupt received")
|
||||
except Exception as e:
|
||||
logging.error(f"Fatal error: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user