Add GET /tools/environment endpoint that fetches weather, forecast, sun times, and air quality data from user's volatile Qdrant collection. - Add qdrant-client dependency - Create QdrantReadClient wrapper for read-only queries - Add environment schemas and service in tools domain - Parse weather, forecast, sun times, and air quality from Qdrant payloads - Support user-specific collections via preferred_username from OIDC - Add comprehensive service tests (13 tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
218 lines
7.8 KiB
Python
218 lines
7.8 KiB
Python
"""
|
|
Tools Controller
|
|
|
|
Provides utility tool endpoints including:
|
|
- DNS lookups
|
|
- System stats
|
|
- Environment data (weather, forecast, sun times)
|
|
"""
|
|
from typing import Dict, Optional
|
|
from fastapi import APIRouter, HTTPException, status, Depends
|
|
|
|
from src.shared.base import BaseController
|
|
from src.shared.logging import get_logger
|
|
from src.domains.tools.dns.schemas import DNSLookupRequest, DNSLookupResponse
|
|
from src.domains.tools.dns.service import DNSService
|
|
from src.domains.tools.dns.exceptions import DNSQueryError
|
|
from src.domains.tools.system.schemas import SystemStatsResponse
|
|
from src.domains.tools.system.service import SystemStatsService
|
|
from src.domains.tools.environment.schemas import EnvironmentResponse
|
|
from src.domains.tools.environment.service import EnvironmentService
|
|
from src.domains.auth.oidc import get_optional_user
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class ToolsController(BaseController):
|
|
"""
|
|
Controller for utility tools
|
|
|
|
Provides endpoints for:
|
|
- DNS lookups
|
|
- System stats
|
|
- Environment data (weather, forecast, sun times)
|
|
"""
|
|
|
|
def __init__(self):
|
|
super().__init__(prefix="/tools", tags=["Tools"])
|
|
self.dns_service = DNSService()
|
|
self.system_stats_service = SystemStatsService()
|
|
self.environment_service = EnvironmentService()
|
|
|
|
def create_router(self) -> APIRouter:
|
|
"""Create and configure the router"""
|
|
router = APIRouter(prefix=self.prefix, tags=self.tags)
|
|
|
|
@router.post(
|
|
"/dns/lookup",
|
|
response_model=DNSLookupResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Perform DNS lookup",
|
|
description="""
|
|
Perform DNS lookups for various record types.
|
|
|
|
Uses dnspython for reliable DNS queries with support for multiple record types
|
|
and custom nameservers. Perfect for troubleshooting DNS issues and checking
|
|
domain configurations.
|
|
|
|
**Supported Record Types:**
|
|
- A: IPv4 address records
|
|
- AAAA: IPv6 address records
|
|
- MX: Mail exchange records
|
|
- TXT: Text records (SPF, DKIM, etc.)
|
|
- CNAME: Canonical name records
|
|
- NS: Nameserver records
|
|
- SOA: Start of authority records
|
|
- PTR: Pointer records (reverse DNS)
|
|
- CAA: Certification authority authorization
|
|
- SRV: Service records
|
|
|
|
**Features:**
|
|
- Custom nameserver support (e.g., 8.8.8.8, 1.1.1.1)
|
|
- Query time measurement
|
|
- Detailed error messages
|
|
|
|
**Rate Limiting:** None (internal network use only)
|
|
"""
|
|
)
|
|
async def dns_lookup(request: DNSLookupRequest) -> DNSLookupResponse:
|
|
"""
|
|
Perform DNS lookup for a domain
|
|
|
|
Args:
|
|
request: DNS lookup request with domain, record type, and optional nameserver
|
|
|
|
Returns:
|
|
DNS lookup results with records and metadata
|
|
|
|
Raises:
|
|
HTTPException: 400 for invalid queries, 500 for processing errors
|
|
"""
|
|
try:
|
|
logger.info(f"Received DNS lookup request for: {request.domain} ({request.record_type})")
|
|
result = await self.dns_service.lookup(request)
|
|
return result
|
|
|
|
except DNSQueryError as e:
|
|
logger.warning(f"DNS query error: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"DNS query failed: {str(e)}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error during DNS lookup: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="An unexpected error occurred during DNS lookup"
|
|
)
|
|
|
|
@router.get(
|
|
"/system/stats",
|
|
response_model=SystemStatsResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Get host system statistics",
|
|
description="""
|
|
Get real-time host system resource statistics.
|
|
|
|
Returns CPU, memory, disk, network, and GPU/VRAM usage for the host machine
|
|
(not Docker container metrics).
|
|
|
|
**Metrics Returned:**
|
|
- **CPU:** Usage percentage, core count, load averages
|
|
- **Memory:** Usage percentage, total/used/available bytes
|
|
- **Disk:** Usage percentage, total/used/free bytes (root partition)
|
|
- **Network:** Total bytes sent/received
|
|
- **GPU:** VRAM usage (if NVIDIA GPU available via nvidia-smi)
|
|
|
|
**Use Cases:**
|
|
- Dashboard system monitoring widgets
|
|
- Health checks and alerting
|
|
- Capacity planning
|
|
"""
|
|
)
|
|
async def get_system_stats() -> SystemStatsResponse:
|
|
"""
|
|
Get current host system statistics
|
|
|
|
Returns:
|
|
System statistics including CPU, memory, disk, network, and GPU
|
|
|
|
Raises:
|
|
HTTPException: 500 for processing errors
|
|
"""
|
|
try:
|
|
logger.info("Fetching system stats")
|
|
result = await self.system_stats_service.get_stats()
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get system stats: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to collect system stats: {str(e)}"
|
|
)
|
|
|
|
@router.get(
|
|
"/environment",
|
|
response_model=EnvironmentResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Get environment data",
|
|
description="""
|
|
Get current environment data including weather, forecast, and sun times.
|
|
|
|
Fetches data from the Qdrant volatile collection for the authenticated user.
|
|
Falls back to 'default' user if not authenticated.
|
|
|
|
**Data Returned:**
|
|
- **Weather:** Current temperature, conditions, humidity, wind
|
|
- **Forecast:** Multi-day weather outlook
|
|
- **Sun Times:** Sunrise, sunset, daylight duration
|
|
- **Air Quality:** AQI and pollutant levels (if available)
|
|
|
|
**Data Source:** Qdrant volatile_{user} collection
|
|
|
|
**Use Cases:**
|
|
- Dashboard environment widgets
|
|
- Home automation context
|
|
- Weather-based automations
|
|
"""
|
|
)
|
|
async def get_environment(
|
|
user: Optional[Dict] = Depends(get_optional_user),
|
|
) -> EnvironmentResponse:
|
|
"""
|
|
Get current environment data
|
|
|
|
Args:
|
|
user: Optional authenticated user from OIDC
|
|
|
|
Returns:
|
|
Environment data including weather, forecast, sun times
|
|
|
|
Raises:
|
|
HTTPException: 500 for processing errors
|
|
"""
|
|
try:
|
|
# Get user identifier from OIDC claims, fallback to 'default'
|
|
user_id = "default"
|
|
if user:
|
|
user_id = user.get("preferred_username") or user.get("sub", "default")
|
|
|
|
logger.info(f"Fetching environment data for user: {user_id}")
|
|
result = await self.environment_service.get_current(user_id)
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get environment data: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to fetch environment data: {str(e)}"
|
|
)
|
|
|
|
return router
|
|
|
|
|
|
# Create controller instance
|
|
tools_controller = ToolsController()
|