sinas 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sinas/__init__.py +20 -0
- sinas/auth.py +100 -0
- sinas/chats.py +130 -0
- sinas/client.py +198 -0
- sinas/exceptions.py +41 -0
- sinas/executions.py +81 -0
- sinas/integrations/__init__.py +20 -0
- sinas/integrations/fastapi.py +288 -0
- sinas/integrations/routers.py +324 -0
- sinas/py.typed +0 -0
- sinas/state.py +164 -0
- sinas/webhooks.py +57 -0
- sinas-0.1.0.dist-info/METADATA +502 -0
- sinas-0.1.0.dist-info/RECORD +16 -0
- sinas-0.1.0.dist-info/WHEEL +4 -0
- sinas-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""FastAPI integration for SINAS authentication and authorization.
|
|
2
|
+
|
|
3
|
+
This module provides zero-boilerplate SINAS auth for FastAPI applications.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
```python
|
|
7
|
+
from fastapi import FastAPI, Depends
|
|
8
|
+
from sinas.integrations.fastapi import SinasAuth
|
|
9
|
+
|
|
10
|
+
app = FastAPI()
|
|
11
|
+
sinas = SinasAuth(base_url="http://localhost:51245")
|
|
12
|
+
|
|
13
|
+
# Auto-authenticated endpoints
|
|
14
|
+
@app.get("/chats")
|
|
15
|
+
async def get_chats(client: SinasClient = Depends(sinas)):
|
|
16
|
+
return client.chats.list()
|
|
17
|
+
|
|
18
|
+
# Permission-protected endpoints
|
|
19
|
+
@app.delete("/users/{user_id}")
|
|
20
|
+
async def delete_user(
|
|
21
|
+
user_id: str,
|
|
22
|
+
client: SinasClient = Depends(sinas.require("sinas.users.delete:all"))
|
|
23
|
+
):
|
|
24
|
+
return client.users.delete(user_id)
|
|
25
|
+
|
|
26
|
+
# Include auto-generated auth routes
|
|
27
|
+
app.include_router(sinas.router, prefix="/auth")
|
|
28
|
+
```
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
|
35
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
36
|
+
from pydantic import BaseModel, EmailStr
|
|
37
|
+
except ImportError:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"FastAPI integration requires fastapi and pydantic. "
|
|
40
|
+
"Install with: pip install 'sinas[fastapi]'"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from sinas.client import SinasClient
|
|
45
|
+
|
|
46
|
+
from sinas import SinasAPIError, SinasAuthError, SinasClient
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LoginRequest(BaseModel):
|
|
50
|
+
"""Login request body."""
|
|
51
|
+
|
|
52
|
+
email: EmailStr
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class VerifyOTPRequest(BaseModel):
|
|
56
|
+
"""OTP verification request body."""
|
|
57
|
+
|
|
58
|
+
session_id: str
|
|
59
|
+
otp_code: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SinasAuth:
|
|
63
|
+
"""FastAPI dependency for SINAS authentication and authorization.
|
|
64
|
+
|
|
65
|
+
This class provides:
|
|
66
|
+
- Automatic token extraction from Authorization header
|
|
67
|
+
- Per-request SinasClient instantiation with user token
|
|
68
|
+
- Permission checking decorators
|
|
69
|
+
- Auto-generated auth endpoints (login, verify-otp, me)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
base_url: str,
|
|
75
|
+
token_url: str = "/auth/token",
|
|
76
|
+
auto_error: bool = True,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Initialize SINAS FastAPI auth.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
base_url: Base URL for SINAS API.
|
|
82
|
+
token_url: URL for token endpoint (for OpenAPI docs).
|
|
83
|
+
auto_error: Whether to automatically raise HTTPException on auth errors.
|
|
84
|
+
"""
|
|
85
|
+
self.base_url = base_url
|
|
86
|
+
self.auto_error = auto_error
|
|
87
|
+
self._security = HTTPBearer(auto_error=auto_error)
|
|
88
|
+
|
|
89
|
+
# Create router with auto-generated auth endpoints
|
|
90
|
+
self.router = self._create_auth_router()
|
|
91
|
+
|
|
92
|
+
async def __call__(
|
|
93
|
+
self,
|
|
94
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)),
|
|
95
|
+
) -> "SinasClient":
|
|
96
|
+
"""Extract token and return authenticated SinasClient.
|
|
97
|
+
|
|
98
|
+
This is the main dependency - use it with Depends(sinas_auth).
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
credentials: HTTP Bearer credentials from Authorization header.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Authenticated SinasClient instance.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
HTTPException: If authentication fails (when auto_error=True).
|
|
108
|
+
"""
|
|
109
|
+
if not credentials:
|
|
110
|
+
if self.auto_error:
|
|
111
|
+
raise HTTPException(
|
|
112
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
113
|
+
detail="Missing authentication credentials",
|
|
114
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
115
|
+
)
|
|
116
|
+
# Return unauthenticated client
|
|
117
|
+
return SinasClient(base_url=self.base_url)
|
|
118
|
+
|
|
119
|
+
token = credentials.credentials
|
|
120
|
+
|
|
121
|
+
# Create client with user's token
|
|
122
|
+
client = SinasClient(base_url=self.base_url, token=token)
|
|
123
|
+
|
|
124
|
+
# Optionally validate token by fetching user info
|
|
125
|
+
# This adds one extra request but ensures the token is valid
|
|
126
|
+
try:
|
|
127
|
+
await self._validate_token(client)
|
|
128
|
+
except SinasAuthError:
|
|
129
|
+
if self.auto_error:
|
|
130
|
+
raise HTTPException(
|
|
131
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
132
|
+
detail="Invalid or expired token",
|
|
133
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return client
|
|
137
|
+
|
|
138
|
+
async def _validate_token(self, client: "SinasClient") -> None:
|
|
139
|
+
"""Validate token by calling /auth/me endpoint.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
client: SINAS client with token.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
SinasAuthError: If token is invalid.
|
|
146
|
+
"""
|
|
147
|
+
# This will raise SinasAuthError if token is invalid
|
|
148
|
+
client.auth.get_me()
|
|
149
|
+
|
|
150
|
+
def require(self, *permissions: str) -> Callable:
|
|
151
|
+
"""Create a dependency that requires specific permissions.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
*permissions: One or more permission strings (e.g., "sinas.chats.read:own").
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
FastAPI dependency function that checks permissions.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
```python
|
|
161
|
+
@app.delete("/chats/{chat_id}")
|
|
162
|
+
async def delete_chat(
|
|
163
|
+
chat_id: str,
|
|
164
|
+
client: SinasClient = Depends(sinas.require("sinas.chats.delete:own"))
|
|
165
|
+
):
|
|
166
|
+
client.chats.delete(chat_id)
|
|
167
|
+
```
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
async def permission_checker(
|
|
171
|
+
client: "SinasClient" = Depends(self),
|
|
172
|
+
) -> "SinasClient":
|
|
173
|
+
"""Check if user has required permissions."""
|
|
174
|
+
try:
|
|
175
|
+
user = client.auth.get_me()
|
|
176
|
+
user_permissions = set(user.get("permissions", []))
|
|
177
|
+
|
|
178
|
+
# Check if user has any of the required permissions
|
|
179
|
+
has_permission = False
|
|
180
|
+
for perm in permissions:
|
|
181
|
+
if self._check_permission(perm, user_permissions):
|
|
182
|
+
has_permission = True
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
if not has_permission:
|
|
186
|
+
raise HTTPException(
|
|
187
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
188
|
+
detail=f"Missing required permission: {' OR '.join(permissions)}",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return client
|
|
192
|
+
except SinasAuthError:
|
|
193
|
+
raise HTTPException(
|
|
194
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
195
|
+
detail="Authentication required",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return permission_checker
|
|
199
|
+
|
|
200
|
+
def _check_permission(self, required: str, user_permissions: set) -> bool:
|
|
201
|
+
"""Check if user has a specific permission.
|
|
202
|
+
|
|
203
|
+
Supports wildcards like "sinas.chats.*" or "sinas.*:all".
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
required: Required permission string.
|
|
207
|
+
user_permissions: Set of user's permissions.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if user has the permission.
|
|
211
|
+
"""
|
|
212
|
+
# Direct match
|
|
213
|
+
if required in user_permissions:
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
# Check for wildcard matches
|
|
217
|
+
# e.g., user has "sinas.*" and needs "sinas.chats.read:own"
|
|
218
|
+
for user_perm in user_permissions:
|
|
219
|
+
if "*" in user_perm:
|
|
220
|
+
# Simple wildcard matching
|
|
221
|
+
parts = user_perm.split(".")
|
|
222
|
+
required_parts = required.split(".")
|
|
223
|
+
|
|
224
|
+
match = True
|
|
225
|
+
for i, part in enumerate(parts):
|
|
226
|
+
if i >= len(required_parts):
|
|
227
|
+
match = False
|
|
228
|
+
break
|
|
229
|
+
if part == "*":
|
|
230
|
+
continue
|
|
231
|
+
if part != required_parts[i]:
|
|
232
|
+
match = False
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
if match:
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
def _create_auth_router(self) -> APIRouter:
|
|
241
|
+
"""Create auto-generated authentication routes.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
FastAPI router with /login, /verify-otp, and /me endpoints.
|
|
245
|
+
"""
|
|
246
|
+
router = APIRouter(tags=["authentication"])
|
|
247
|
+
|
|
248
|
+
# Unauthenticated client for login/verify
|
|
249
|
+
base_client = SinasClient(base_url=self.base_url)
|
|
250
|
+
|
|
251
|
+
@router.post("/login")
|
|
252
|
+
async def login(request: LoginRequest) -> Dict[str, Any]:
|
|
253
|
+
"""Initiate login by sending OTP to email."""
|
|
254
|
+
try:
|
|
255
|
+
return base_client.auth.login(request.email)
|
|
256
|
+
except SinasAPIError as e:
|
|
257
|
+
raise HTTPException(
|
|
258
|
+
status_code=e.status_code or status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
259
|
+
detail=str(e),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
@router.post("/verify-otp")
|
|
263
|
+
async def verify_otp(request: VerifyOTPRequest) -> Dict[str, Any]:
|
|
264
|
+
"""Verify OTP and return access token."""
|
|
265
|
+
try:
|
|
266
|
+
return base_client.auth.verify_otp(request.session_id, request.otp_code)
|
|
267
|
+
except SinasAPIError as e:
|
|
268
|
+
raise HTTPException(
|
|
269
|
+
status_code=e.status_code or status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
270
|
+
detail=str(e),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
@router.get("/me")
|
|
274
|
+
async def get_current_user(client: "SinasClient" = Depends(self)) -> Dict[str, Any]:
|
|
275
|
+
"""Get current authenticated user info."""
|
|
276
|
+
try:
|
|
277
|
+
return client.auth.get_me()
|
|
278
|
+
except SinasAuthError:
|
|
279
|
+
raise HTTPException(
|
|
280
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
281
|
+
detail="Authentication required",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return router
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# Convenience alias
|
|
288
|
+
SinasFastAPI = SinasAuth
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""FastAPI routers for SINAS Runtime API endpoints.
|
|
2
|
+
|
|
3
|
+
This module provides ready-to-mount FastAPI routers that proxy requests to SINAS Runtime API.
|
|
4
|
+
Each router handles authentication automatically and forwards requests to the backend.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from fastapi import APIRouter, Depends, Request
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
except ImportError:
|
|
13
|
+
raise ImportError(
|
|
14
|
+
"FastAPI integration requires fastapi. "
|
|
15
|
+
"Install with: pip install 'sinas[fastapi]'"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from sinas import SinasClient
|
|
19
|
+
from sinas.integrations.fastapi import SinasAuth
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_state_router(base_url: str, prefix: str = "") -> APIRouter:
|
|
23
|
+
"""Create a FastAPI router for state management endpoints.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
base_url: SINAS API base URL.
|
|
27
|
+
prefix: Optional prefix for routes (e.g., "/states").
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
FastAPI router with state endpoints.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
```python
|
|
34
|
+
app.include_router(create_state_router("http://localhost:51245"), prefix="/runtime/states")
|
|
35
|
+
```
|
|
36
|
+
"""
|
|
37
|
+
router = APIRouter(tags=["states"])
|
|
38
|
+
auth = SinasAuth(base_url=base_url)
|
|
39
|
+
|
|
40
|
+
@router.post(prefix or "")
|
|
41
|
+
async def create_state(
|
|
42
|
+
request: Request,
|
|
43
|
+
client: SinasClient = Depends(auth)
|
|
44
|
+
) -> Dict[str, Any]:
|
|
45
|
+
"""Create a new state entry."""
|
|
46
|
+
body = await request.json()
|
|
47
|
+
return client.state.set(**body)
|
|
48
|
+
|
|
49
|
+
@router.get(prefix or "")
|
|
50
|
+
async def list_states(
|
|
51
|
+
namespace: Optional[str] = None,
|
|
52
|
+
visibility: Optional[str] = None,
|
|
53
|
+
assistant_id: Optional[str] = None,
|
|
54
|
+
tags: Optional[str] = None,
|
|
55
|
+
search: Optional[str] = None,
|
|
56
|
+
skip: int = 0,
|
|
57
|
+
limit: int = 100,
|
|
58
|
+
client: SinasClient = Depends(auth)
|
|
59
|
+
) -> List[Dict[str, Any]]:
|
|
60
|
+
"""List state entries."""
|
|
61
|
+
return client.state.list(
|
|
62
|
+
namespace=namespace,
|
|
63
|
+
visibility=visibility,
|
|
64
|
+
assistant_id=assistant_id,
|
|
65
|
+
tags=tags,
|
|
66
|
+
search=search,
|
|
67
|
+
skip=skip,
|
|
68
|
+
limit=limit
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@router.get(f"{prefix}/{{state_id}}")
|
|
72
|
+
async def get_state(
|
|
73
|
+
state_id: str,
|
|
74
|
+
client: SinasClient = Depends(auth)
|
|
75
|
+
) -> Dict[str, Any]:
|
|
76
|
+
"""Get a specific state entry."""
|
|
77
|
+
return client.state.get(state_id)
|
|
78
|
+
|
|
79
|
+
@router.put(f"{prefix}/{{state_id}}")
|
|
80
|
+
async def update_state(
|
|
81
|
+
state_id: str,
|
|
82
|
+
request: Request,
|
|
83
|
+
client: SinasClient = Depends(auth)
|
|
84
|
+
) -> Dict[str, Any]:
|
|
85
|
+
"""Update a state entry."""
|
|
86
|
+
body = await request.json()
|
|
87
|
+
return client.state.update(state_id, **body)
|
|
88
|
+
|
|
89
|
+
@router.delete(f"{prefix}/{{state_id}}")
|
|
90
|
+
async def delete_state(
|
|
91
|
+
state_id: str,
|
|
92
|
+
client: SinasClient = Depends(auth)
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
"""Delete a state entry."""
|
|
95
|
+
return client.state.delete(state_id)
|
|
96
|
+
|
|
97
|
+
return router
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def create_chat_router(base_url: str, prefix: str = "") -> APIRouter:
|
|
101
|
+
"""Create a FastAPI router for chat endpoints.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
base_url: SINAS API base URL.
|
|
105
|
+
prefix: Optional prefix for routes (e.g., "/chats").
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
FastAPI router with chat endpoints.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
```python
|
|
112
|
+
app.include_router(create_chat_router("http://localhost:51245"), prefix="/runtime/chats")
|
|
113
|
+
```
|
|
114
|
+
"""
|
|
115
|
+
router = APIRouter(tags=["chats"])
|
|
116
|
+
auth = SinasAuth(base_url=base_url)
|
|
117
|
+
|
|
118
|
+
@router.post("/agents/{namespace}/{agent_name}/chats")
|
|
119
|
+
async def create_chat(
|
|
120
|
+
namespace: str,
|
|
121
|
+
agent_name: str,
|
|
122
|
+
request: Request,
|
|
123
|
+
client: SinasClient = Depends(auth)
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
"""Create a new chat with an agent."""
|
|
126
|
+
body = await request.json()
|
|
127
|
+
return client.chats.create(namespace, agent_name, **body)
|
|
128
|
+
|
|
129
|
+
@router.post(f"{prefix}/{{chat_id}}/messages")
|
|
130
|
+
async def send_message(
|
|
131
|
+
chat_id: str,
|
|
132
|
+
request: Request,
|
|
133
|
+
client: SinasClient = Depends(auth)
|
|
134
|
+
) -> Dict[str, Any]:
|
|
135
|
+
"""Send a message to a chat."""
|
|
136
|
+
body = await request.json()
|
|
137
|
+
return client.chats.send(chat_id, body["content"])
|
|
138
|
+
|
|
139
|
+
@router.post(f"{prefix}/{{chat_id}}/messages/stream")
|
|
140
|
+
async def stream_message(
|
|
141
|
+
chat_id: str,
|
|
142
|
+
request: Request,
|
|
143
|
+
client: SinasClient = Depends(auth)
|
|
144
|
+
):
|
|
145
|
+
"""Stream a message to a chat."""
|
|
146
|
+
body = await request.json()
|
|
147
|
+
|
|
148
|
+
async def event_generator():
|
|
149
|
+
for chunk in client.chats.stream(chat_id, body["content"]):
|
|
150
|
+
yield f"data: {chunk}\n\n"
|
|
151
|
+
|
|
152
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
153
|
+
|
|
154
|
+
@router.get(prefix or "")
|
|
155
|
+
async def list_chats(
|
|
156
|
+
client: SinasClient = Depends(auth)
|
|
157
|
+
) -> List[Dict[str, Any]]:
|
|
158
|
+
"""List all chats for the current user."""
|
|
159
|
+
return client.chats.list()
|
|
160
|
+
|
|
161
|
+
@router.get(f"{prefix}/{{chat_id}}")
|
|
162
|
+
async def get_chat(
|
|
163
|
+
chat_id: str,
|
|
164
|
+
client: SinasClient = Depends(auth)
|
|
165
|
+
) -> Dict[str, Any]:
|
|
166
|
+
"""Get a chat with all messages."""
|
|
167
|
+
return client.chats.get(chat_id)
|
|
168
|
+
|
|
169
|
+
@router.put(f"{prefix}/{{chat_id}}")
|
|
170
|
+
async def update_chat(
|
|
171
|
+
chat_id: str,
|
|
172
|
+
request: Request,
|
|
173
|
+
client: SinasClient = Depends(auth)
|
|
174
|
+
) -> Dict[str, Any]:
|
|
175
|
+
"""Update a chat."""
|
|
176
|
+
body = await request.json()
|
|
177
|
+
return client.chats.update(chat_id, **body)
|
|
178
|
+
|
|
179
|
+
@router.delete(f"{prefix}/{{chat_id}}")
|
|
180
|
+
async def delete_chat(
|
|
181
|
+
chat_id: str,
|
|
182
|
+
client: SinasClient = Depends(auth)
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Delete a chat."""
|
|
185
|
+
client.chats.delete(chat_id)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
return router
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def create_webhook_router(base_url: str, prefix: str = "") -> APIRouter:
|
|
192
|
+
"""Create a FastAPI router for webhook execution endpoints.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
base_url: SINAS API base URL.
|
|
196
|
+
prefix: Optional prefix for routes (e.g., "/webhooks").
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
FastAPI router with webhook endpoints.
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
```python
|
|
203
|
+
app.include_router(create_webhook_router("http://localhost:51245"), prefix="/runtime/webhooks")
|
|
204
|
+
```
|
|
205
|
+
"""
|
|
206
|
+
router = APIRouter(tags=["webhooks"])
|
|
207
|
+
auth = SinasAuth(base_url=base_url)
|
|
208
|
+
|
|
209
|
+
@router.api_route(
|
|
210
|
+
f"{prefix}/{{path:path}}",
|
|
211
|
+
methods=["GET", "POST", "PUT", "DELETE", "PATCH"]
|
|
212
|
+
)
|
|
213
|
+
async def execute_webhook(
|
|
214
|
+
path: str,
|
|
215
|
+
request: Request,
|
|
216
|
+
client: SinasClient = Depends(auth)
|
|
217
|
+
) -> Dict[str, Any]:
|
|
218
|
+
"""Execute a webhook."""
|
|
219
|
+
body = None
|
|
220
|
+
if request.method in ["POST", "PUT", "PATCH"]:
|
|
221
|
+
body = await request.json() if request.headers.get("content-type") == "application/json" else None
|
|
222
|
+
|
|
223
|
+
return client.webhooks.run(
|
|
224
|
+
path=path,
|
|
225
|
+
method=request.method,
|
|
226
|
+
body=body,
|
|
227
|
+
query=dict(request.query_params)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return router
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def create_executions_router(base_url: str, prefix: str = "") -> APIRouter:
|
|
234
|
+
"""Create a FastAPI router for execution endpoints.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
base_url: SINAS API base URL.
|
|
238
|
+
prefix: Optional prefix for routes (e.g., "/executions").
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
FastAPI router with execution endpoints.
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
```python
|
|
245
|
+
app.include_router(create_executions_router("http://localhost:51245"), prefix="/runtime/executions")
|
|
246
|
+
```
|
|
247
|
+
"""
|
|
248
|
+
router = APIRouter(tags=["executions"])
|
|
249
|
+
auth = SinasAuth(base_url=base_url)
|
|
250
|
+
|
|
251
|
+
@router.get(prefix or "")
|
|
252
|
+
async def list_executions(
|
|
253
|
+
function_name: Optional[str] = None,
|
|
254
|
+
status: Optional[str] = None,
|
|
255
|
+
skip: int = 0,
|
|
256
|
+
limit: int = 100,
|
|
257
|
+
client: SinasClient = Depends(auth)
|
|
258
|
+
) -> List[Dict[str, Any]]:
|
|
259
|
+
"""List executions."""
|
|
260
|
+
return client.executions.list(
|
|
261
|
+
function_name=function_name,
|
|
262
|
+
status=status,
|
|
263
|
+
skip=skip,
|
|
264
|
+
limit=limit
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
@router.get(f"{prefix}/{{execution_id}}")
|
|
268
|
+
async def get_execution(
|
|
269
|
+
execution_id: str,
|
|
270
|
+
client: SinasClient = Depends(auth)
|
|
271
|
+
) -> Dict[str, Any]:
|
|
272
|
+
"""Get execution details."""
|
|
273
|
+
return client.executions.get(execution_id)
|
|
274
|
+
|
|
275
|
+
@router.get(f"{prefix}/{{execution_id}}/steps")
|
|
276
|
+
async def get_execution_steps(
|
|
277
|
+
execution_id: str,
|
|
278
|
+
client: SinasClient = Depends(auth)
|
|
279
|
+
) -> List[Dict[str, Any]]:
|
|
280
|
+
"""Get execution steps."""
|
|
281
|
+
return client.executions.get_steps(execution_id)
|
|
282
|
+
|
|
283
|
+
@router.post(f"{prefix}/{{execution_id}}/continue")
|
|
284
|
+
async def continue_execution(
|
|
285
|
+
execution_id: str,
|
|
286
|
+
request: Request,
|
|
287
|
+
client: SinasClient = Depends(auth)
|
|
288
|
+
) -> Dict[str, Any]:
|
|
289
|
+
"""Continue a paused execution."""
|
|
290
|
+
body = await request.json()
|
|
291
|
+
return client.executions.continue_execution(execution_id, body["input"])
|
|
292
|
+
|
|
293
|
+
return router
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def create_runtime_router(base_url: str, include_auth: bool = False) -> APIRouter:
|
|
297
|
+
"""Create a complete runtime router with all endpoints.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
base_url: SINAS API base URL.
|
|
301
|
+
include_auth: Whether to include auth endpoints (login, verify-otp, etc.).
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
FastAPI router with all runtime endpoints.
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
```python
|
|
308
|
+
app.include_router(create_runtime_router("http://localhost:51245"), prefix="/runtime")
|
|
309
|
+
```
|
|
310
|
+
"""
|
|
311
|
+
router = APIRouter()
|
|
312
|
+
|
|
313
|
+
# Include individual routers
|
|
314
|
+
router.include_router(create_state_router(base_url), prefix="/states")
|
|
315
|
+
router.include_router(create_chat_router(base_url), prefix="/chats")
|
|
316
|
+
router.include_router(create_webhook_router(base_url), prefix="/webhooks")
|
|
317
|
+
router.include_router(create_executions_router(base_url), prefix="/executions")
|
|
318
|
+
|
|
319
|
+
# Optionally include auth endpoints
|
|
320
|
+
if include_auth:
|
|
321
|
+
auth = SinasAuth(base_url=base_url)
|
|
322
|
+
router.include_router(auth.router, prefix="/auth")
|
|
323
|
+
|
|
324
|
+
return router
|
sinas/py.typed
ADDED
|
File without changes
|