import httpx import asyncio from typing import Dict, Any, Optional, List from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import logging from app.core.config import settings from app.integrations.external_apis.circuit_breaker import ( user_service_circuit_breaker, payment_service_circuit_breaker, communication_service_circuit_breaker ) logger = logging.getLogger(__name__) class ExternalAPIClient: def __init__(self, base_url: str, api_key: Optional[str] = None, timeout: int = 30): self.base_url = base_url.rstrip('/') self.api_key = api_key self.timeout = timeout @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10), retry=retry_if_exception_type((httpx.RequestError, httpx.HTTPStatusError)) ) async def _make_request( self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: """Make HTTP request with retry logic""" url = f"{self.base_url}{endpoint}" # Prepare headers request_headers = { "Content-Type": "application/json", "User-Agent": "MultiTenant-SaaS-Platform/1.0" } if self.api_key: request_headers["Authorization"] = f"Bearer {self.api_key}" if headers: request_headers.update(headers) async with httpx.AsyncClient(timeout=self.timeout) as client: logger.info(f"Making {method} request to {url}") response = await client.request( method=method, url=url, json=data, params=params, headers=request_headers ) # Raise exception for HTTP error status codes response.raise_for_status() return response.json() async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Make GET request""" return await self._make_request("GET", endpoint, params=params) async def post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]: """Make POST request""" return await self._make_request("POST", endpoint, data=data) async def put(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]: """Make PUT request""" return await self._make_request("PUT", endpoint, data=data) async def delete(self, endpoint: str) -> Dict[str, Any]: """Make DELETE request""" return await self._make_request("DELETE", endpoint) class UserServiceClient(ExternalAPIClient): def __init__(self): super().__init__( base_url=settings.EXTERNAL_USER_SERVICE_URL, api_key="user-service-api-key" ) self.circuit_breaker = user_service_circuit_breaker async def create_user(self, user_data: Dict[str, Any]) -> Dict[str, Any]: """Create user in external service""" def _create_user(): return asyncio.run(self.post("/users", user_data)) return self.circuit_breaker.call(_create_user) async def get_user(self, user_id: str) -> Dict[str, Any]: """Get user from external service""" def _get_user(): return asyncio.run(self.get(f"/users/{user_id}")) return self.circuit_breaker.call(_get_user) async def update_user(self, user_id: str, user_data: Dict[str, Any]) -> Dict[str, Any]: """Update user in external service""" def _update_user(): return asyncio.run(self.put(f"/users/{user_id}", user_data)) return self.circuit_breaker.call(_update_user) async def delete_user(self, user_id: str) -> Dict[str, Any]: """Delete user from external service""" def _delete_user(): return asyncio.run(self.delete(f"/users/{user_id}")) return self.circuit_breaker.call(_delete_user) async def sync_users(self, organization_id: int) -> List[Dict[str, Any]]: """Sync users from external service""" def _sync_users(): return asyncio.run(self.get(f"/organizations/{organization_id}/users")) return self.circuit_breaker.call(_sync_users) class PaymentServiceClient(ExternalAPIClient): def __init__(self): super().__init__( base_url=settings.EXTERNAL_PAYMENT_SERVICE_URL, api_key="payment-service-api-key" ) self.circuit_breaker = payment_service_circuit_breaker async def create_subscription(self, subscription_data: Dict[str, Any]) -> Dict[str, Any]: """Create subscription in payment service""" def _create_subscription(): return asyncio.run(self.post("/subscriptions", subscription_data)) return self.circuit_breaker.call(_create_subscription) async def get_subscription(self, subscription_id: str) -> Dict[str, Any]: """Get subscription from payment service""" def _get_subscription(): return asyncio.run(self.get(f"/subscriptions/{subscription_id}")) return self.circuit_breaker.call(_get_subscription) async def cancel_subscription(self, subscription_id: str) -> Dict[str, Any]: """Cancel subscription in payment service""" def _cancel_subscription(): return asyncio.run(self.delete(f"/subscriptions/{subscription_id}")) return self.circuit_breaker.call(_cancel_subscription) async def process_payment(self, payment_data: Dict[str, Any]) -> Dict[str, Any]: """Process payment""" def _process_payment(): return asyncio.run(self.post("/payments", payment_data)) return self.circuit_breaker.call(_process_payment) async def get_billing_history(self, organization_id: int) -> List[Dict[str, Any]]: """Get billing history for organization""" def _get_billing_history(): return asyncio.run(self.get(f"/organizations/{organization_id}/billing")) return self.circuit_breaker.call(_get_billing_history) class CommunicationServiceClient(ExternalAPIClient): def __init__(self): super().__init__( base_url=settings.EXTERNAL_COMMUNICATION_SERVICE_URL, api_key="communication-service-api-key" ) self.circuit_breaker = communication_service_circuit_breaker async def send_email(self, email_data: Dict[str, Any]) -> Dict[str, Any]: """Send email via communication service""" def _send_email(): return asyncio.run(self.post("/emails", email_data)) return self.circuit_breaker.call(_send_email) async def send_sms(self, sms_data: Dict[str, Any]) -> Dict[str, Any]: """Send SMS via communication service""" def _send_sms(): return asyncio.run(self.post("/sms", sms_data)) return self.circuit_breaker.call(_send_sms) async def send_notification(self, notification_data: Dict[str, Any]) -> Dict[str, Any]: """Send push notification""" def _send_notification(): return asyncio.run(self.post("/notifications", notification_data)) return self.circuit_breaker.call(_send_notification) async def get_delivery_status(self, message_id: str) -> Dict[str, Any]: """Get message delivery status""" def _get_delivery_status(): return asyncio.run(self.get(f"/messages/{message_id}/status")) return self.circuit_breaker.call(_get_delivery_status) async def get_communication_history(self, organization_id: int) -> List[Dict[str, Any]]: """Get communication history for organization""" def _get_communication_history(): return asyncio.run(self.get(f"/organizations/{organization_id}/communications")) return self.circuit_breaker.call(_get_communication_history)