refactor: Replace httpx with shared service clients in dashboard API

Replace direct httpx calls with shared service client architecture for
better fault tolerance, authentication, and consistency.

Changes:
- Remove httpx import and usage
- Add service client imports (inventory, production, procurement)
- Initialize service clients at module level
- Refactor all 5 dashboard endpoints to use service clients:
  * health-status: Use inventory/production/procurement clients
  * orchestration-summary: Use procurement/production clients
  * action-queue: Use procurement client
  * production-timeline: Use production client
  * insights: Use inventory client

Benefits:
- Built-in circuit breaker pattern for fault tolerance
- Automatic service authentication with JWT tokens
- Consistent error handling and retry logic
- Removes hardcoded service URLs
- Better testability and maintainability
This commit is contained in:
Claude
2025-11-07 22:02:06 +00:00
parent b732c0742b
commit e46574a12b

View File

@@ -11,13 +11,25 @@ from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from datetime import datetime from datetime import datetime
import logging import logging
import httpx
from app.core.database import get_db from app.core.database import get_db
from app.core.config import settings
from ..services.dashboard_service import DashboardService from ..services.dashboard_service import DashboardService
from shared.clients import (
get_inventory_client,
get_production_client,
ProductionServiceClient,
InventoryServiceClient
)
from shared.clients.procurement_client import ProcurementServiceClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Initialize service clients
inventory_client = get_inventory_client(settings, "orchestrator")
production_client = get_production_client(settings, "orchestrator")
procurement_client = ProcurementServiceClient(settings)
router = APIRouter(prefix="/api/v1/tenants/{tenant_id}/dashboard", tags=["dashboard"]) router = APIRouter(prefix="/api/v1/tenants/{tenant_id}/dashboard", tags=["dashboard"])
@@ -177,13 +189,12 @@ async def get_bakery_health_status(
# In a real implementation, these would be fetched from respective services # In a real implementation, these would be fetched from respective services
# For now, we'll make HTTP calls to the services # For now, we'll make HTTP calls to the services
async with httpx.AsyncClient(timeout=10.0) as client: # Get alerts - using base client for alert service
# Get alerts
try: try:
alerts_response = await client.get( alerts_data = await procurement_client.get(
f"http://alert-processor-service:8000/api/v1/tenants/{tenant_id}/alerts/summary" "/alert-processor/alerts/summary",
) tenant_id=tenant_id
alerts_data = alerts_response.json() if alerts_response.status_code == 200 else {} ) or {}
critical_alerts = alerts_data.get("critical_count", 0) critical_alerts = alerts_data.get("critical_count", 0)
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch alerts: {e}") logger.warning(f"Failed to fetch alerts: {e}")
@@ -191,11 +202,11 @@ async def get_bakery_health_status(
# Get pending PO count # Get pending PO count
try: try:
po_response = await client.get( po_data = await procurement_client.get(
f"http://procurement-service:8000/api/v1/tenants/{tenant_id}/purchase-orders", f"/purchase-orders",
tenant_id=tenant_id,
params={"status": "pending_approval", "limit": 100} params={"status": "pending_approval", "limit": 100}
) ) or {}
po_data = po_response.json() if po_response.status_code == 200 else {}
pending_approvals = len(po_data.get("items", [])) pending_approvals = len(po_data.get("items", []))
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch POs: {e}") logger.warning(f"Failed to fetch POs: {e}")
@@ -203,11 +214,11 @@ async def get_bakery_health_status(
# Get production delays # Get production delays
try: try:
prod_response = await client.get( prod_data = await production_client.get(
f"http://production-service:8000/api/v1/tenants/{tenant_id}/production-batches", "/production-batches",
tenant_id=tenant_id,
params={"status": "ON_HOLD", "limit": 100} params={"status": "ON_HOLD", "limit": 100}
) ) or {}
prod_data = prod_response.json() if prod_response.status_code == 200 else {}
production_delays = len(prod_data.get("items", [])) production_delays = len(prod_data.get("items", []))
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch production batches: {e}") logger.warning(f"Failed to fetch production batches: {e}")
@@ -215,10 +226,7 @@ async def get_bakery_health_status(
# Get inventory status # Get inventory status
try: try:
inv_response = await client.get( inv_data = await inventory_client.get_inventory_dashboard(tenant_id) or {}
f"http://inventory-service:8000/api/v1/tenants/{tenant_id}/inventory/dashboard/stock-status"
)
inv_data = inv_response.json() if inv_response.status_code == 200 else {}
out_of_stock_count = inv_data.get("out_of_stock_count", 0) out_of_stock_count = inv_data.get("out_of_stock_count", 0)
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch inventory: {e}") logger.warning(f"Failed to fetch inventory: {e}")
@@ -267,14 +275,14 @@ async def get_orchestration_summary(
# Enhance with detailed PO and batch summaries # Enhance with detailed PO and batch summaries
if summary["purchaseOrdersCreated"] > 0: if summary["purchaseOrdersCreated"] > 0:
async with httpx.AsyncClient(timeout=10.0) as client:
try: try:
po_response = await client.get( po_data = await procurement_client.get(
f"http://procurement-service:8000/api/v1/tenants/{tenant_id}/purchase-orders", "/purchase-orders",
tenant_id=tenant_id,
params={"status": "pending_approval", "limit": 10} params={"status": "pending_approval", "limit": 10}
) )
if po_response.status_code == 200: if po_data:
pos = po_response.json().get("items", []) pos = po_data.get("items", [])
summary["purchaseOrdersSummary"] = [ summary["purchaseOrdersSummary"] = [
PurchaseOrderSummary( PurchaseOrderSummary(
supplierName=po.get("supplier_name", "Unknown"), supplierName=po.get("supplier_name", "Unknown"),
@@ -287,13 +295,13 @@ async def get_orchestration_summary(
logger.warning(f"Failed to fetch PO details: {e}") logger.warning(f"Failed to fetch PO details: {e}")
if summary["productionBatchesCreated"] > 0: if summary["productionBatchesCreated"] > 0:
async with httpx.AsyncClient(timeout=10.0) as client:
try: try:
batch_response = await client.get( batch_data = await production_client.get(
f"http://production-service:8000/api/v1/tenants/{tenant_id}/production-batches/today" "/production-batches/today",
tenant_id=tenant_id
) )
if batch_response.status_code == 200: if batch_data:
batches = batch_response.json().get("batches", []) batches = batch_data.get("batches", [])
summary["productionBatchesSummary"] = [ summary["productionBatchesSummary"] = [
ProductionBatchSummary( ProductionBatchSummary(
productName=batch.get("product_name", "Unknown"), productName=batch.get("product_name", "Unknown"),
@@ -327,28 +335,29 @@ async def get_action_queue(
dashboard_service = DashboardService(db) dashboard_service = DashboardService(db)
# Fetch data from various services # Fetch data from various services
async with httpx.AsyncClient(timeout=10.0) as client:
# Get pending POs # Get pending POs
pending_pos = [] pending_pos = []
try: try:
po_response = await client.get( po_data = await procurement_client.get(
f"http://procurement-service:8000/api/v1/tenants/{tenant_id}/purchase-orders", "/purchase-orders",
tenant_id=tenant_id,
params={"status": "pending_approval", "limit": 20} params={"status": "pending_approval", "limit": 20}
) )
if po_response.status_code == 200: if po_data:
pending_pos = po_response.json().get("items", []) pending_pos = po_data.get("items", [])
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch pending POs: {e}") logger.warning(f"Failed to fetch pending POs: {e}")
# Get critical alerts # Get critical alerts
critical_alerts = [] critical_alerts = []
try: try:
alerts_response = await client.get( alerts_data = await procurement_client.get(
f"http://alert-processor-service:8000/api/v1/tenants/{tenant_id}/alerts", "/alert-processor/alerts",
tenant_id=tenant_id,
params={"severity": "critical", "resolved": False, "limit": 20} params={"severity": "critical", "resolved": False, "limit": 20}
) )
if alerts_response.status_code == 200: if alerts_data:
critical_alerts = alerts_response.json().get("alerts", []) critical_alerts = alerts_data.get("alerts", [])
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch alerts: {e}") logger.warning(f"Failed to fetch alerts: {e}")
@@ -356,11 +365,11 @@ async def get_action_queue(
onboarding_incomplete = False onboarding_incomplete = False
onboarding_steps = [] onboarding_steps = []
try: try:
onboarding_response = await client.get( onboarding_data = await procurement_client.get(
f"http://auth:8000/api/v1/tenants/{tenant_id}/onboarding-progress" "/auth/onboarding-progress",
tenant_id=tenant_id
) )
if onboarding_response.status_code == 200: if onboarding_data:
onboarding_data = onboarding_response.json()
onboarding_incomplete = not onboarding_data.get("completed", True) onboarding_incomplete = not onboarding_data.get("completed", True)
onboarding_steps = onboarding_data.get("steps", []) onboarding_steps = onboarding_data.get("steps", [])
except Exception as e: except Exception as e:
@@ -406,13 +415,13 @@ async def get_production_timeline(
# Fetch today's production batches # Fetch today's production batches
batches = [] batches = []
async with httpx.AsyncClient(timeout=10.0) as client:
try: try:
response = await client.get( batch_data = await production_client.get(
f"http://production-service:8000/api/v1/tenants/{tenant_id}/production-batches/today" "/production-batches/today",
tenant_id=tenant_id
) )
if response.status_code == 200: if batch_data:
batches = response.json().get("batches", []) batches = batch_data.get("batches", [])
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch production batches: {e}") logger.warning(f"Failed to fetch production batches: {e}")
@@ -454,26 +463,23 @@ async def get_insights(
dashboard_service = DashboardService(db) dashboard_service = DashboardService(db)
# Fetch data from various services # Fetch data from various services
async with httpx.AsyncClient(timeout=10.0) as client:
# Sustainability data # Sustainability data
sustainability_data = {} sustainability_data = {}
try: try:
response = await client.get( sustainability_data = await inventory_client.get(
f"http://inventory-service:8000/api/v1/tenants/{tenant_id}/sustainability/widget" "/sustainability/widget",
) tenant_id=tenant_id
if response.status_code == 200: ) or {}
sustainability_data = response.json()
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch sustainability data: {e}") logger.warning(f"Failed to fetch sustainability data: {e}")
# Inventory data # Inventory data
inventory_data = {} inventory_data = {}
try: try:
response = await client.get( inventory_data = await inventory_client.get(
f"http://inventory-service:8000/api/v1/tenants/{tenant_id}/inventory/dashboard/stock-status" "/inventory/dashboard/stock-status",
) tenant_id=tenant_id
if response.status_code == 200: ) or {}
inventory_data = response.json()
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch inventory data: {e}") logger.warning(f"Failed to fetch inventory data: {e}")