feat: Add Python SDK with async/await support and comprehensive Telecom Platform API integration

- Add TelecomSDK class with async context manager and aiohttp ClientSession with 30s timeout, 3 retries
- Add subscriber management: get_subscriber, list_subscribers with pagination/status filter, create_subscriber, update_subscriber, suspend_subscriber, activate_subscriber, terminate_subscriber
- Add usage tracking: get_usage_stats with date range, list_usage_events with filtering, get_real_time_usage
This commit is contained in:
nutcas3
2026-04-17 17:49:05 +03:00
parent 63506fdc24
commit 7aab9a058b
6 changed files with 930 additions and 0 deletions
+422
View File
@@ -0,0 +1,422 @@
import asyncio
import json
import logging
from typing import Optional, Dict, Any, List, Union
from datetime import datetime
import aiohttp
import websockets
from websockets.exceptions import ConnectionClosed
from .types import (
Subscriber,
UsageStats,
PaymentTransaction,
SystemStats,
HealthStatus,
WebSocketMessage,
UsageEvent,
RatingPlan,
RealTimeUsage,
PaginatedResponse,
)
from .exceptions import (
TelecomError,
AuthenticationError,
APIError,
NetworkError,
ValidationError,
RateLimitError,
ServerError,
)
class TelecomSDK:
def __init__(
self,
api_url: str = "http://localhost:8000",
api_key: Optional[str] = None,
timeout: int = 30,
max_retries: int = 3,
retry_delay: float = 1.0,
enable_logging: bool = False,
):
"""
Initialize the Telecom SDK client.
Args:
api_url: Base URL for the API server
api_key: API key for authentication
timeout: Request timeout in seconds
max_retries: Maximum number of retry attempts
retry_delay: Delay between retries in seconds
enable_logging: Enable debug logging
"""
self.api_url = api_url.rstrip('/')
self.api_key = api_key
self.timeout = aiohttp.ClientTimeout(total=timeout)
self.max_retries = max_retries
self.retry_delay = retry_delay
self.enable_logging = enable_logging
if enable_logging:
logging.basicConfig(level=logging.DEBUG)
self.logger = logging.getLogger(__name__)
else:
self.logger = logging.getLogger(__name__)
self.logger.disabled = True
self._session: Optional[aiohttp.ClientSession] = None
self._websocket: Optional[websockets.WebSocketServerProtocol] = None
async def __aenter__(self):
"""Async context manager entry."""
await self._ensure_session()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit."""
await self.close()
async def _ensure_session(self):
"""Ensure aiohttp session is created."""
if self._session is None or self._session.closed:
headers = self._get_headers()
self._session = aiohttp.ClientSession(
headers=headers,
timeout=self.timeout,
)
def _get_headers(self) -> Dict[str, str]:
"""Get default headers for API requests."""
headers = {
"Content-Type": "application/json",
"User-Agent": f"Telecom-Python-SDK/1.0.0",
}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
return headers
async def _make_request(
self,
method: str,
endpoint: str,
data: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""
Make an HTTP request with retry logic.
Args:
method: HTTP method
endpoint: API endpoint
data: Request body data
params: Query parameters
Returns:
Response data as dictionary
Raises:
TelecomError: On API errors
"""
await self._ensure_session()
url = f"{self.api_url}{endpoint}"
for attempt in range(self.max_retries + 1):
try:
self.logger.debug(f"Making {method} request to {url} (attempt {attempt + 1})")
async with self._session.request(
method=method,
url=url,
json=data,
params=params,
) as response:
await self._handle_response_errors(response)
return await response.json()
except aiohttp.ClientError as e:
if attempt == self.max_retries:
raise NetworkError(f"Network error after {self.max_retries} retries: {e}")
self.logger.warning(f"Request failed (attempt {attempt + 1}), retrying in {self.retry_delay}s: {e}")
await asyncio.sleep(self.retry_delay * (2 ** attempt))
async def _handle_response_errors(self, response: aiohttp.ClientResponse):
"""Handle HTTP response errors."""
if response.status == 401:
raise AuthenticationError("Authentication failed")
elif response.status == 429:
raise RateLimitError("Rate limit exceeded")
elif 400 <= response.status < 500:
error_data = await response.json().get("error", "Bad request")
raise APIError(f"API error: {error_data}", response.status)
elif response.status >= 500:
raise ServerError(f"Server error: {response.status}", response.status)
# Subscriber Management
async def get_subscriber(self, subscriber_id: int) -> Subscriber:
"""Get subscriber by ID."""
response = await self._make_request("GET", f"/v1/subscribers/{subscriber_id}")
return Subscriber(**response)
async def list_subscribers(
self,
page: int = 1,
page_size: int = 50,
status: Optional[str] = None,
) -> PaginatedResponse:
"""List subscribers with pagination."""
params = {"page": page, "page_size": page_size}
if status:
params["status"] = status
response = await self._make_request("GET", "/v1/subscribers", params=params)
return PaginatedResponse(**response)
async def create_subscriber(
self,
imsi: str,
msisdn: str,
first_name: str,
last_name: str,
email: str,
plan_id: int,
organization_id: Optional[str] = None,
) -> Subscriber:
"""Create a new subscriber."""
data = {
"imsi": imsi,
"msisdn": msisdn,
"first_name": first_name,
"last_name": last_name,
"email": email,
"plan_id": plan_id,
}
if organization_id:
data["organization_id"] = organization_id
response = await self._make_request("POST", "/v1/subscribers", data=data)
return Subscriber(**response)
async def update_subscriber(
self,
subscriber_id: int,
**kwargs,
) -> Subscriber:
"""Update subscriber details."""
response = await self._make_request(
"PUT",
f"/v1/subscribers/{subscriber_id}",
data=kwargs,
)
return Subscriber(**response)
async def suspend_subscriber(self, subscriber_id: int) -> Subscriber:
"""Suspend a subscriber."""
response = await self._make_request("POST", f"/v1/subscribers/{subscriber_id}/suspend")
return Subscriber(**response)
async def activate_subscriber(self, subscriber_id: int) -> Subscriber:
"""Activate a suspended subscriber."""
response = await self._make_request("POST", f"/v1/subscribers/{subscriber_id}/activate")
return Subscriber(**response)
async def terminate_subscriber(self, subscriber_id: int) -> bool:
"""Terminate a subscriber."""
response = await self._make_request("DELETE", f"/v1/subscribers/{subscriber_id}")
return response.get("success", False)
# Usage Management
async def get_usage_stats(
self,
subscriber_id: int,
start_date: datetime,
end_date: datetime,
) -> UsageStats:
"""Get usage statistics for a subscriber."""
params = {
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
}
response = await self._make_request(
"GET",
f"/v1/subscribers/{subscriber_id}/usage",
params=params,
)
return UsageStats(**response)
async def list_usage_events(
self,
subscriber_id: Optional[int] = None,
usage_type: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
page: int = 1,
page_size: int = 50,
) -> PaginatedResponse:
"""List usage events with filtering."""
params = {"page": page, "page_size": page_size}
if subscriber_id:
params["subscriber_id"] = subscriber_id
if usage_type:
params["usage_type"] = usage_type
if start_date:
params["start_date"] = start_date.isoformat()
if end_date:
params["end_date"] = end_date.isoformat()
response = await self._make_request("GET", "/v1/usage/events", params=params)
return PaginatedResponse(**response)
async def get_real_time_usage(self, subscriber_id: int) -> RealTimeUsage:
"""Get real-time usage for a subscriber."""
response = await self._make_request("GET", f"/v1/subscribers/{subscriber_id}/realtime")
return RealTimeUsage(**response)
# Payment Management
async def create_payment_transaction(
self,
subscriber_id: int,
amount: float,
currency: str = "USD",
gateway: str = "stripe",
metadata: Optional[Dict[str, Any]] = None,
) -> PaymentTransaction:
"""Create a payment transaction."""
data = {
"subscriber_id": subscriber_id,
"amount": amount,
"currency": currency,
"gateway": gateway,
}
if metadata:
data["metadata"] = metadata
response = await self._make_request("POST", "/v1/payments/transactions", data=data)
return PaymentTransaction(**response)
async def get_payment_transaction(self, transaction_id: str) -> PaymentTransaction:
"""Get payment transaction by ID."""
response = await self._make_request("GET", f"/v1/payments/transactions/{transaction_id}")
return PaymentTransaction(**response)
async def list_payment_transactions(
self,
subscriber_id: Optional[int] = None,
status: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> PaginatedResponse:
"""List payment transactions with filtering."""
params = {"page": page, "page_size": page_size}
if subscriber_id:
params["subscriber_id"] = subscriber_id
if status:
params["status"] = status
response = await self._make_request("GET", "/v1/payments/transactions", params=params)
return PaginatedResponse(**response)
# Rating Plans
async def list_rating_plans(self) -> List[RatingPlan]:
"""List all available rating plans."""
response = await self._make_request("GET", "/v1/rating-plans")
return [RatingPlan(**plan) for plan in response]
async def get_rating_plan(self, plan_id: str) -> RatingPlan:
"""Get rating plan by ID."""
response = await self._make_request("GET", f"/v1/rating-plans/{plan_id}")
return RatingPlan(**response)
# System Management
async def get_system_stats(self) -> SystemStats:
"""Get system statistics."""
response = await self._make_request("GET", "/v1/system/stats")
return SystemStats(**response)
async def get_health_status(self) -> HealthStatus:
"""Get system health status."""
response = await self._make_request("GET", "/v1/health")
return HealthStatus(**response)
# WebSocket Support
async def connect_websocket(self, message_handler: callable) -> None:
"""
Connect to WebSocket for real-time updates.
Args:
message_handler: Async function to handle WebSocket messages
"""
ws_url = self.api_url.replace("http://", "ws://") + "/ws"
try:
self._websocket = await websockets.connect(ws_url)
self.logger.info("WebSocket connected")
async for message in self._websocket:
try:
data = json.loads(message)
ws_message = WebSocketMessage(**data)
await message_handler(ws_message)
except json.JSONDecodeError as e:
self.logger.error(f"Failed to decode WebSocket message: {e}")
except Exception as e:
self.logger.error(f"Error handling WebSocket message: {e}")
except ConnectionClosed:
self.logger.warning("WebSocket connection closed")
except Exception as e:
self.logger.error(f"WebSocket error: {e}")
raise NetworkError(f"WebSocket connection failed: {e}")
async def disconnect_websocket(self) -> None:
"""Disconnect from WebSocket."""
if self._websocket:
await self._websocket.close()
self._websocket = None
self.logger.info("WebSocket disconnected")
# GraphQL Support
async def execute_graphql_query(
self,
query: str,
variables: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Execute a GraphQL query.
Args:
query: GraphQL query string
variables: Optional variables for the query
Returns:
GraphQL response data
"""
data = {"query": query}
if variables:
data["variables"] = variables
response = await self._make_request("POST", "/graphql", data=data)
return response
# Utility Methods
async def close(self) -> None:
"""Close the SDK client and cleanup resources."""
if self._websocket:
await self.disconnect_websocket()
if self._session and not self._session.closed:
await self._session.close()
self.logger.info("Telecom SDK client closed")