"""Ribbon / Sonus PSX or EMA preset — REST API pull. Ribbon's Element Management Application (EMA) and PSX both expose CDR export APIs. The common path is ``/rest/cdrs`` with basic auth. Output format varies: CSV (EMA default) or XML (PSX default); we request CSV and feed the generic_csv adapter. """ from __future__ import annotations import base64 import urllib.parse import urllib.request from datetime import datetime, timedelta from typing import Iterable, Optional from .base import BasePreset, CredentialField, FetchedFile class RibbonPreset(BasePreset): PRESET_SLUG = "ribbon" LABEL = "Ribbon / Sonus SBC (EMA / PSX)" CDR_FORMAT = "generic_csv" TRANSPORT_METHOD = "api" DEFAULT_CRON = "0 3 * * *" CREDENTIAL_FIELDS = ( CredentialField("api_host", "EMA / PSX host", "text", help="e.g. https://ema.example.com"), CredentialField("username", "API username", "text"), CredentialField("password", "API password", "password", sensitive=True), ) FORMAT_CONFIG = { "start_time": "startTime", "caller_number": "callingNumber", "called_number": "calledNumber", "duration_sec": "callDurationSeconds", "billed_amount": "totalCharge", "call_id": "globalCallId", "trunk_group": "ingressTrunkGroup", "disposition": "releaseCause", "ts_format": "%Y-%m-%dT%H:%M:%S%z", } def _auth_header(self, cfg: dict, secrets: dict) -> str: creds = base64.b64encode( f"{cfg['username']}:{secrets['password']}".encode("utf-8") ).decode("ascii") return f"Basic {creds}" def _request(self, url: str, headers: dict) -> bytes: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=60) as resp: return resp.read() def validate(self, profile_config: dict, secrets: dict) -> tuple[bool, str]: try: base = profile_config["api_host"].rstrip("/") self._request( f"{base}/rest/v1/system/status", headers={"Authorization": self._auth_header(profile_config, secrets)}, ) return True, "EMA/PSX reachable" except Exception as exc: return False, f"Ribbon validate failed: {exc}" def fetch( self, profile_config: dict, secrets: dict, since: Optional[datetime], ) -> Iterable[FetchedFile]: base = profile_config["api_host"].rstrip("/") since = since or (datetime.utcnow() - timedelta(days=1)) end = datetime.utcnow() qs = urllib.parse.urlencode({ "startTime": since.strftime("%Y-%m-%dT%H:%M:%SZ"), "endTime": end.strftime("%Y-%m-%dT%H:%M:%SZ"), "format": "csv", }) headers = { "Authorization": self._auth_header(profile_config, secrets), "Accept": "text/csv", } content = self._request(f"{base}/rest/v1/cdrs?{qs}", headers=headers) yield FetchedFile( remote_path=f"ribbon_cdrs_{end:%Y%m%dT%H%M%SZ}.csv", mtime=end, content=content, size_bytes=len(content), )