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.
@@ -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