feat: add raw cdr ingestion endpoint

This commit is contained in:
2026-09-20 13:18:55 +03:30
parent 729872faa3
commit 3bac56906a
5 changed files with 214 additions and 0 deletions

View File

@@ -5,11 +5,14 @@ description = "High-throughput telecom CDR ingestion and analytics pipeline"
requires-python = ">=3.11"
dependencies = [
"phonenumbers>=8.13",
"fastapi>=0.115,<1.0",
"uvicorn[standard]>=0.30,<1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"httpx>=0.27,<1.0",
]
[build-system]

0
src/core/api/__init__.py Normal file
View File

22
src/core/api/main.py Normal file
View File

@@ -0,0 +1,22 @@
from fastapi import FastAPI
from core.api.routes import router
app = FastAPI(
title="Telecom CDR Ingestion API",
version="0.1.0",
)
app.include_router(
router,
prefix="/api/v1",
)
@app.get("/health")
async def health() -> dict[str, str]:
return {
"status": "ok",
}

58
src/core/api/routes.py Normal file
View File

@@ -0,0 +1,58 @@
import json
from fastapi import APIRouter, Request, status
from fastapi.responses import JSONResponse
from core.domain.errors import CDRProcessingError
from core.domain.validation import parse_raw_cdr
router = APIRouter()
@router.post(
"/cdr/raw",
status_code=status.HTTP_202_ACCEPTED,
)
async def ingest_raw_cdr(request: Request) -> JSONResponse:
try:
payload = await request.json()
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"error": "invalid_json",
"detail": str(exc),
},
)
if not isinstance(payload, dict):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"error": "invalid_payload",
"detail": "JSON payload must be an object.",
},
)
try:
raw_cdr = parse_raw_cdr(payload)
except CDRProcessingError as exc:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"error": exc.reason.value,
"detail": exc.detail,
"failed_field": exc.failed_field,
},
)
return JSONResponse(
status_code=status.HTTP_202_ACCEPTED,
content={
"status": "accepted",
"raw_id": raw_cdr.raw_id,
},
)

View File

@@ -0,0 +1,131 @@
from fastapi.testclient import TestClient
from core.api.main import app
client = TestClient(app)
def valid_payload() -> dict:
return {
"raw_id": "raw-test-001",
"src_ip": "10.240.12.1",
"device_id": "310410998877665",
"caller_num": "0019875551234",
"svc": "4G_DATA",
"up_bytes": 1048576,
"down_bytes": 4194304,
"rat": 6,
"lac": "4021",
"ci": "8812",
"epoch_start": 1789900000,
"epoch_end": 1789900300,
}
def test_health_endpoint() -> None:
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {
"status": "ok",
}
def test_valid_cdr_returns_202() -> None:
payload = valid_payload()
response = client.post(
"/api/v1/cdr/raw",
json=payload,
)
assert response.status_code == 202
assert response.json() == {
"status": "accepted",
"raw_id": "raw-test-001",
}
def test_missing_required_field_returns_400() -> None:
payload = valid_payload()
del payload["caller_num"]
response = client.post(
"/api/v1/cdr/raw",
json=payload,
)
assert response.status_code == 400
body = response.json()
assert body["error"] == "missing_required_field"
assert body["failed_field"] == "caller_num"
def test_invalid_rat_returns_400() -> None:
payload = valid_payload()
payload["rat"] = 99
response = client.post(
"/api/v1/cdr/raw",
json=payload,
)
assert response.status_code == 400
body = response.json()
assert body["error"] == "invalid_rat_value"
assert body["failed_field"] == "rat"
def test_invalid_epoch_returns_400() -> None:
payload = valid_payload()
payload["epoch_start"] = 200
payload["epoch_end"] = 100
response = client.post(
"/api/v1/cdr/raw",
json=payload,
)
assert response.status_code == 400
body = response.json()
assert body["error"] == "invalid_epoch"
def test_malformed_json_returns_400() -> None:
response = client.post(
"/api/v1/cdr/raw",
content='{"raw_id": invalid}',
headers={
"Content-Type": "application/json",
},
)
assert response.status_code == 400
body = response.json()
assert body["error"] == "invalid_json"
def test_non_object_json_returns_400() -> None:
response = client.post(
"/api/v1/cdr/raw",
json=[
"not",
"a",
"cdr",
],
)
assert response.status_code == 400
body = response.json()
assert body["error"] == "invalid_payload"