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
97
scripts/workers/cdr_presets/sip_navigator.py
Normal file
97
scripts/workers/cdr_presets/sip_navigator.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""SIP Navigator (Cataleya Orchid One) preset.
|
||||
|
||||
Orchid One publishes a REST CDR API in newer deployments, but older ones
|
||||
only expose the web CDR-export page. The preset tries the API first
|
||||
(requires ``api_key``); if the admin doesn't have API access, it falls
|
||||
back to the Playwright web-scrape flow.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from .base import BasePreset, CredentialField, FetchedFile
|
||||
from ._scrape_base import ScrapePreset
|
||||
|
||||
|
||||
class SIPNavigatorPreset(ScrapePreset):
|
||||
PRESET_SLUG = "sip_navigator"
|
||||
LABEL = "SIP Navigator (Cataleya Orchid One)"
|
||||
CDR_FORMAT = "generic_csv"
|
||||
TRANSPORT_METHOD = "api" # preferred; scrape fallback if api_key absent
|
||||
DEFAULT_CRON = "0 3 * * *"
|
||||
|
||||
CREDENTIAL_FIELDS = (
|
||||
CredentialField("admin_url", "Orchid One admin URL", "text",
|
||||
help="e.g. https://orchid.example.com"),
|
||||
CredentialField("username", "Admin username", "text"),
|
||||
CredentialField("password", "Admin password", "password", sensitive=True),
|
||||
CredentialField("api_key", "Orchid One API key", "password",
|
||||
required=False, sensitive=True,
|
||||
help="Leave blank if the deployment doesn't expose the REST API."),
|
||||
)
|
||||
|
||||
FORMAT_CONFIG = {
|
||||
"start_time": "setup_time",
|
||||
"caller_number": "calling_party",
|
||||
"called_number": "called_party",
|
||||
"duration_sec": "answer_duration",
|
||||
"billed_amount": "charge_amount",
|
||||
"call_id": "global_call_id",
|
||||
"trunk_group": "ingress_peering",
|
||||
"disposition": "release_reason",
|
||||
"ts_format": "%Y-%m-%dT%H:%M:%S",
|
||||
}
|
||||
|
||||
# ── API path (preferred) ────────────────────────────────────────
|
||||
|
||||
def validate(self, profile_config: dict, secrets: dict) -> tuple[bool, str]:
|
||||
if secrets.get("api_key"):
|
||||
try:
|
||||
base = profile_config["admin_url"].rstrip("/")
|
||||
req = urllib.request.Request(
|
||||
f"{base}/api/v1/system/status",
|
||||
headers={"Authorization": f"Bearer {secrets['api_key']}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return True, f"API reachable (HTTP {resp.status})"
|
||||
except Exception as exc:
|
||||
return False, f"Orchid API validate failed: {exc}"
|
||||
# No API key — fall back to ScrapePreset.validate() (HEAD the admin URL)
|
||||
return super().validate(profile_config, secrets)
|
||||
|
||||
def fetch(
|
||||
self,
|
||||
profile_config: dict,
|
||||
secrets: dict,
|
||||
since: Optional[datetime],
|
||||
) -> Iterable[FetchedFile]:
|
||||
if secrets.get("api_key"):
|
||||
return self._fetch_api(profile_config, secrets, since)
|
||||
# No API key → scrape (scaffolded, raises until recon finalized)
|
||||
return super().fetch(profile_config, secrets, since)
|
||||
|
||||
def _fetch_api(
|
||||
self,
|
||||
cfg: dict,
|
||||
secrets: dict,
|
||||
since: Optional[datetime],
|
||||
) -> Iterable[FetchedFile]:
|
||||
base = cfg["admin_url"].rstrip("/")
|
||||
since = since or (datetime.utcnow() - timedelta(days=1))
|
||||
end = datetime.utcnow()
|
||||
req = urllib.request.Request(
|
||||
f"{base}/api/v1/cdrs?start={since:%Y-%m-%dT%H:%M:%SZ}"
|
||||
f"&end={end:%Y-%m-%dT%H:%M:%SZ}&format=csv",
|
||||
headers={"Authorization": f"Bearer {secrets['api_key']}",
|
||||
"Accept": "text/csv"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
content = resp.read()
|
||||
yield FetchedFile(
|
||||
remote_path=f"sip_navigator_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