Skip to content

Commit fb4f49b

Browse files
committed
fixup! ✨(summary) add multi-tenant support and v2 tasks / API
1 parent 0e390dd commit fb4f49b

3 files changed

Lines changed: 148 additions & 10 deletions

File tree

src/summary/summary/api/route/tasks_v2.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,19 @@
77
process_audio_transcribe_v2_task,
88
summarize_v2_task,
99
)
10-
from summary.core.config import get_settings
10+
from summary.core.config import AuthorizedTenant
1111
from summary.core.models import SummarizeTaskV2Request, TranscribeTaskV2Request
12-
from summary.core.security import verify_tenant_api_key
12+
from summary.core.security import verify_tenant_api_key_v2
1313

14-
router_tasks_v2 = APIRouter(prefix="/tasks")
15-
settings = get_settings()
14+
router_tasks_v2 = APIRouter()
1615

1716

1817
@router_tasks_v2.post("/async/transcribe")
1918
async def create_transcribe_task_v2(
2019
request: TranscribeTaskV2Request,
21-
tenant_api_key: str = Depends(verify_tenant_api_key),
20+
request_tenant: AuthorizedTenant = Depends(verify_tenant_api_key_v2),
2221
):
2322
"""Create a transcription task."""
24-
request_tenant = settings.get_authorized_tenant(api_key=tenant_api_key)
2523
task = process_audio_transcribe_v2_task.apply_async(
2624
args=[{**request.model_dump(), "tenant_id": request_tenant.id}]
2725
)
@@ -32,10 +30,9 @@ async def create_transcribe_task_v2(
3230
@router_tasks_v2.post("/async/summarize")
3331
async def create_summarize_task_v2(
3432
request: SummarizeTaskV2Request,
35-
tenant_api_key: str = Depends(verify_tenant_api_key),
33+
request_tenant: AuthorizedTenant = Depends(verify_tenant_api_key_v2),
3634
):
3735
"""Create a summarization task."""
38-
request_tenant = settings.get_authorized_tenant(api_key=tenant_api_key)
3936
task = summarize_v2_task.apply_async(
4037
args=[{**request.model_dump(), "tenant_id": request_tenant.id}]
4138
)
@@ -46,11 +43,11 @@ async def create_summarize_task_v2(
4643
@router_tasks_v2.get("/async/transcribe/{task_id}")
4744
@router_tasks_v2.get("/async/summarize/{task_id}")
4845
async def get_task_status(
49-
task_id: str, tenant_api_key: str = Depends(verify_tenant_api_key)
46+
task_id: str,
47+
request_tenant: AuthorizedTenant = Depends(verify_tenant_api_key_v2),
5048
):
5149
"""Check task status by ID."""
5250
task = AsyncResult(task_id)
53-
request_tenant = settings.get_authorized_tenant(api_key=tenant_api_key)
5451
try:
5552
if (
5653
isinstance(task.args, (list, tuple))

src/summary/summary/core/security.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,14 @@ def verify_tenant_api_key(
1717
if api_key not in settings.authorized_tenant_api_keys:
1818
raise HTTPException(status_code=403, detail="Unauthorized")
1919
return api_key
20+
21+
22+
def verify_tenant_api_key_v2(
23+
settings: SettingsDeps,
24+
credentials: HTTPAuthorizationCredentials = Security(security), # noqa: B008
25+
):
26+
"""Verify the bearer api_key from the Authorization header."""
27+
api_key = credentials.credentials
28+
if api_key not in settings.authorized_tenant_api_keys:
29+
raise HTTPException(status_code=403, detail="Unauthorized")
30+
return settings.get_authorized_tenant(api_key=api_key)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Integration tests for the V2 task API endpoints."""
2+
3+
from unittest.mock import MagicMock, patch
4+
5+
6+
class TestTasksV2:
7+
"""Tests for the /tasks async V2 endpoints."""
8+
9+
@patch(
10+
"summary.api.route.tasks_v2.process_audio_transcribe_v2_task.apply_async",
11+
return_value=MagicMock(id="transcribe-task-id-abc"),
12+
)
13+
def test_create_transcribe_task_v2_returns_task_id(self, mock_apply_async, client):
14+
"""POST /async/transcribe creates a task and injects tenant_id."""
15+
response = client.post(
16+
"/api/v2/async/transcribe",
17+
headers={"Authorization": "Bearer test-api-token"},
18+
json={
19+
"remote_id": "remote-001",
20+
"remote_owner_id": "owner-123",
21+
"cloud_storage_url": "https://example.com/audio.mp3",
22+
"language": "en",
23+
"context_language": "fr",
24+
},
25+
)
26+
27+
assert response.status_code == 200
28+
assert response.json() == {
29+
"id": "transcribe-task-id-abc",
30+
"message": "Transcribe task created",
31+
}
32+
33+
args = mock_apply_async.call_args.kwargs["args"]
34+
assert args == [
35+
{
36+
"remote_id": "remote-001",
37+
"remote_owner_id": "owner-123",
38+
"cloud_storage_url": "https://example.com/audio.mp3",
39+
"language": "en",
40+
"context_language": "fr",
41+
"tenant_id": "test-tenant",
42+
}
43+
]
44+
45+
@patch(
46+
"summary.api.route.tasks_v2.summarize_v2_task.apply_async",
47+
return_value=MagicMock(id="summarize-task-id-abc"),
48+
)
49+
def test_create_summarize_task_v2_returns_task_id(self, mock_apply_async, client):
50+
"""POST /async/summarize creates a task and injects tenant_id."""
51+
response = client.post(
52+
"/api/v2/async/summarize",
53+
headers={"Authorization": "Bearer test-api-token"},
54+
json={
55+
"remote_id": "remote-002",
56+
"remote_owner_id": "owner-456",
57+
"content": "This is a long meeting transcript to summarize.",
58+
},
59+
)
60+
61+
assert response.status_code == 200
62+
assert response.json() == {
63+
"id": "summarize-task-id-abc",
64+
"message": "Summarize task created",
65+
}
66+
67+
args = mock_apply_async.call_args.kwargs["args"]
68+
assert args == [
69+
{
70+
"remote_id": "remote-002",
71+
"remote_owner_id": "owner-456",
72+
"content": "This is a long meeting transcript to summarize.",
73+
"tenant_id": "test-tenant",
74+
}
75+
]
76+
77+
@patch("summary.api.route.tasks_v2.AsyncResult")
78+
def test_get_transcribe_task_status_returns_status_for_same_tenant(
79+
self, mock_async_result, client
80+
):
81+
"""GET /async/transcribe/{id} returns status when tenant matches."""
82+
mock_async_result.return_value = MagicMock(
83+
status="PENDING",
84+
args=[{"tenant_id": "test-tenant"}],
85+
)
86+
87+
response = client.get(
88+
"/api/v2/async/transcribe/task-id-abc",
89+
headers={"Authorization": "Bearer test-api-token"},
90+
)
91+
92+
assert response.status_code == 200
93+
assert response.json() == {"id": "task-id-abc", "status": "PENDING"}
94+
95+
mock_async_result.assert_called_once_with("task-id-abc")
96+
97+
@patch("summary.api.route.tasks_v2.AsyncResult")
98+
def test_get_summarize_task_status_returns_status_for_same_tenant(
99+
self, mock_async_result, client
100+
):
101+
"""GET /async/summarize/{id} returns status when tenant matches."""
102+
mock_async_result.return_value = MagicMock(
103+
status="SUCCESS",
104+
args=[{"tenant_id": "test-tenant"}],
105+
)
106+
107+
response = client.get(
108+
"/api/v2/async/summarize/task-id-abc",
109+
headers={"Authorization": "Bearer test-api-token"},
110+
)
111+
112+
assert response.status_code == 200
113+
assert response.json() == {"id": "task-id-abc", "status": "SUCCESS"}
114+
115+
mock_async_result.assert_called_once_with("task-id-abc")
116+
117+
@patch("summary.api.route.tasks_v2.AsyncResult")
118+
def test_get_task_status_returns_404_when_args_are_missing(
119+
self, mock_async_result, client
120+
):
121+
"""GET /async/.../{id} returns 404 when task args are invalid."""
122+
mock_async_result.return_value = MagicMock(status="SUCCESS", args=None)
123+
124+
response = client.get(
125+
"/api/v2/async/transcribe/task-id-abc",
126+
headers={"Authorization": "Bearer test-api-token"},
127+
)
128+
129+
assert response.status_code == 404
130+
assert response.json() == {"detail": "Not found"}

0 commit comments

Comments
 (0)