Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
90
scripts/workers/cdr_presets/ribbon.py
Normal file
90
scripts/workers/cdr_presets/ribbon.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""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),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue