webscout 8.3__py3-none-any.whl → 8.3.2__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.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (120) hide show
  1. webscout/AIauto.py +4 -4
  2. webscout/AIbase.py +61 -1
  3. webscout/AIutel.py +46 -53
  4. webscout/Bing_search.py +418 -0
  5. webscout/Extra/YTToolkit/ytapi/patterns.py +45 -45
  6. webscout/Extra/YTToolkit/ytapi/stream.py +1 -1
  7. webscout/Extra/YTToolkit/ytapi/video.py +10 -10
  8. webscout/Extra/autocoder/autocoder_utiles.py +1 -1
  9. webscout/Extra/gguf.py +706 -177
  10. webscout/Litlogger/formats.py +9 -0
  11. webscout/Litlogger/handlers.py +18 -0
  12. webscout/Litlogger/logger.py +43 -1
  13. webscout/Provider/AISEARCH/genspark_search.py +7 -7
  14. webscout/Provider/AISEARCH/scira_search.py +3 -2
  15. webscout/Provider/GeminiProxy.py +140 -0
  16. webscout/Provider/LambdaChat.py +7 -1
  17. webscout/Provider/MCPCore.py +78 -75
  18. webscout/Provider/OPENAI/BLACKBOXAI.py +1046 -1017
  19. webscout/Provider/OPENAI/GeminiProxy.py +328 -0
  20. webscout/Provider/OPENAI/Qwen3.py +303 -303
  21. webscout/Provider/OPENAI/README.md +5 -0
  22. webscout/Provider/OPENAI/README_AUTOPROXY.md +238 -0
  23. webscout/Provider/OPENAI/TogetherAI.py +355 -0
  24. webscout/Provider/OPENAI/__init__.py +16 -1
  25. webscout/Provider/OPENAI/autoproxy.py +332 -0
  26. webscout/Provider/OPENAI/base.py +101 -14
  27. webscout/Provider/OPENAI/chatgpt.py +15 -2
  28. webscout/Provider/OPENAI/chatgptclone.py +14 -3
  29. webscout/Provider/OPENAI/deepinfra.py +339 -328
  30. webscout/Provider/OPENAI/e2b.py +295 -74
  31. webscout/Provider/OPENAI/mcpcore.py +109 -70
  32. webscout/Provider/OPENAI/opkfc.py +18 -6
  33. webscout/Provider/OPENAI/scirachat.py +59 -50
  34. webscout/Provider/OPENAI/toolbaz.py +2 -10
  35. webscout/Provider/OPENAI/writecream.py +166 -166
  36. webscout/Provider/OPENAI/x0gpt.py +367 -367
  37. webscout/Provider/OPENAI/xenai.py +514 -0
  38. webscout/Provider/OPENAI/yep.py +389 -383
  39. webscout/Provider/STT/__init__.py +3 -0
  40. webscout/Provider/STT/base.py +281 -0
  41. webscout/Provider/STT/elevenlabs.py +265 -0
  42. webscout/Provider/TTI/__init__.py +4 -1
  43. webscout/Provider/TTI/aiarta.py +399 -365
  44. webscout/Provider/TTI/base.py +74 -2
  45. webscout/Provider/TTI/bing.py +231 -0
  46. webscout/Provider/TTI/fastflux.py +63 -30
  47. webscout/Provider/TTI/gpt1image.py +149 -0
  48. webscout/Provider/TTI/imagen.py +196 -0
  49. webscout/Provider/TTI/magicstudio.py +60 -29
  50. webscout/Provider/TTI/piclumen.py +43 -32
  51. webscout/Provider/TTI/pixelmuse.py +232 -225
  52. webscout/Provider/TTI/pollinations.py +43 -32
  53. webscout/Provider/TTI/together.py +287 -0
  54. webscout/Provider/TTI/utils.py +2 -1
  55. webscout/Provider/TTS/README.md +1 -0
  56. webscout/Provider/TTS/__init__.py +2 -1
  57. webscout/Provider/TTS/freetts.py +140 -0
  58. webscout/Provider/TTS/speechma.py +45 -39
  59. webscout/Provider/TogetherAI.py +366 -0
  60. webscout/Provider/UNFINISHED/ChutesAI.py +314 -0
  61. webscout/Provider/UNFINISHED/fetch_together_models.py +95 -0
  62. webscout/Provider/XenAI.py +324 -0
  63. webscout/Provider/__init__.py +8 -0
  64. webscout/Provider/deepseek_assistant.py +378 -0
  65. webscout/Provider/scira_chat.py +3 -2
  66. webscout/Provider/toolbaz.py +0 -1
  67. webscout/auth/__init__.py +44 -0
  68. webscout/auth/api_key_manager.py +189 -0
  69. webscout/auth/auth_system.py +100 -0
  70. webscout/auth/config.py +76 -0
  71. webscout/auth/database.py +400 -0
  72. webscout/auth/exceptions.py +67 -0
  73. webscout/auth/middleware.py +248 -0
  74. webscout/auth/models.py +130 -0
  75. webscout/auth/providers.py +257 -0
  76. webscout/auth/rate_limiter.py +254 -0
  77. webscout/auth/request_models.py +127 -0
  78. webscout/auth/request_processing.py +226 -0
  79. webscout/auth/routes.py +526 -0
  80. webscout/auth/schemas.py +103 -0
  81. webscout/auth/server.py +312 -0
  82. webscout/auth/static/favicon.svg +11 -0
  83. webscout/auth/swagger_ui.py +203 -0
  84. webscout/auth/templates/components/authentication.html +237 -0
  85. webscout/auth/templates/components/base.html +103 -0
  86. webscout/auth/templates/components/endpoints.html +750 -0
  87. webscout/auth/templates/components/examples.html +491 -0
  88. webscout/auth/templates/components/footer.html +75 -0
  89. webscout/auth/templates/components/header.html +27 -0
  90. webscout/auth/templates/components/models.html +286 -0
  91. webscout/auth/templates/components/navigation.html +70 -0
  92. webscout/auth/templates/static/api.js +455 -0
  93. webscout/auth/templates/static/icons.js +168 -0
  94. webscout/auth/templates/static/main.js +784 -0
  95. webscout/auth/templates/static/particles.js +201 -0
  96. webscout/auth/templates/static/styles.css +3353 -0
  97. webscout/auth/templates/static/ui.js +374 -0
  98. webscout/auth/templates/swagger_ui.html +170 -0
  99. webscout/client.py +49 -3
  100. webscout/litagent/Readme.md +12 -3
  101. webscout/litagent/agent.py +99 -62
  102. webscout/scout/core/scout.py +104 -26
  103. webscout/scout/element.py +139 -18
  104. webscout/swiftcli/core/cli.py +14 -3
  105. webscout/swiftcli/decorators/output.py +59 -9
  106. webscout/update_checker.py +31 -49
  107. webscout/version.py +1 -1
  108. webscout/webscout_search.py +4 -12
  109. webscout/webscout_search_async.py +3 -10
  110. webscout/yep_search.py +2 -11
  111. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/METADATA +41 -11
  112. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/RECORD +116 -68
  113. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/entry_points.txt +1 -1
  114. webscout/Provider/HF_space/__init__.py +0 -0
  115. webscout/Provider/HF_space/qwen_qwen2.py +0 -206
  116. webscout/Provider/OPENAI/api.py +0 -1035
  117. webscout/Provider/TTI/artbit.py +0 -0
  118. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/WHEEL +0 -0
  119. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/licenses/LICENSE.md +0 -0
  120. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,526 @@
1
+ """
2
+ API routes for the Webscout server.
3
+ """
4
+
5
+ import time
6
+ import uuid
7
+ import secrets
8
+ import sys
9
+ from datetime import datetime, timezone
10
+ from typing import Any
11
+
12
+ from fastapi import FastAPI, Request, Body, Query
13
+ from fastapi.responses import JSONResponse
14
+ from fastapi.exceptions import RequestValidationError
15
+ from fastapi.security import APIKeyHeader
16
+ from starlette.exceptions import HTTPException as StarletteHTTPException
17
+ from starlette.status import (
18
+ HTTP_422_UNPROCESSABLE_ENTITY,
19
+ HTTP_401_UNAUTHORIZED,
20
+ HTTP_403_FORBIDDEN,
21
+ HTTP_500_INTERNAL_SERVER_ERROR,
22
+ )
23
+
24
+ from webscout.Litlogger import Logger, LogLevel, LogFormat, ConsoleHandler
25
+ from .config import AppConfig
26
+ from .request_models import (
27
+ ChatCompletionRequest, ImageGenerationRequest, ModelListResponse,
28
+ ErrorResponse
29
+ )
30
+ from .schemas import (
31
+ APIKeyCreateRequest, APIKeyCreateResponse, APIKeyValidationResponse,
32
+ HealthCheckResponse
33
+ )
34
+ from .exceptions import APIError
35
+ from .providers import (
36
+ resolve_provider_and_model, resolve_tti_provider_and_model,
37
+ get_provider_instance, get_tti_provider_instance
38
+ )
39
+ from .request_processing import (
40
+ process_messages, prepare_provider_params,
41
+ handle_streaming_response, handle_non_streaming_response
42
+ )
43
+ from .auth_system import get_auth_components
44
+ from webscout.DWEBS import GoogleSearch
45
+ from webscout.yep_search import YepSearch
46
+ from webscout.webscout_search import WEBS
47
+
48
+ # Setup logger
49
+ logger = Logger(
50
+ name="webscout.api",
51
+ level=LogLevel.INFO,
52
+ handlers=[ConsoleHandler(stream=sys.stdout)],
53
+ fmt=LogFormat.DEFAULT
54
+ )
55
+
56
+
57
+ class Api:
58
+ """API route handler class."""
59
+
60
+ def __init__(self, app: FastAPI) -> None:
61
+ self.app = app
62
+ self.get_api_key = APIKeyHeader(name="authorization", auto_error=False)
63
+
64
+ def register_authorization(self):
65
+ """Register legacy authorization middleware."""
66
+ @self.app.middleware("http")
67
+ async def authorization(request: Request, call_next):
68
+ if AppConfig.api_key is not None:
69
+ auth_header = await self.get_api_key(request)
70
+ path = request.url.path
71
+ if path.startswith("/v1"): # Only protect /v1 routes
72
+ if auth_header is None:
73
+ return JSONResponse(
74
+ status_code=HTTP_401_UNAUTHORIZED,
75
+ content={"error": {"message": "API key required", "type": "authentication_error"}}
76
+ )
77
+ if auth_header.startswith("Bearer "):
78
+ auth_header = auth_header[7:]
79
+ if not secrets.compare_digest(AppConfig.api_key, auth_header):
80
+ return JSONResponse(
81
+ status_code=HTTP_403_FORBIDDEN,
82
+ content={"error": {"message": "Invalid API key", "type": "authentication_error"}}
83
+ )
84
+ return await call_next(request)
85
+
86
+ def register_validation_exception_handler(self):
87
+ """Register comprehensive exception handlers."""
88
+
89
+ @self.app.exception_handler(APIError)
90
+ async def api_error_handler(request: Request, exc: APIError):
91
+ """Handle custom API errors."""
92
+ logger.error(f"API Error: {exc.message} (Status: {exc.status_code})")
93
+ return exc.to_response()
94
+
95
+ @self.app.exception_handler(RequestValidationError)
96
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
97
+ errors = exc.errors()
98
+ error_messages = []
99
+ body = await request.body()
100
+ is_empty_body = not body or body.strip() in (b"", b"null", b"{}")
101
+
102
+ for error in errors:
103
+ loc = error.get("loc", [])
104
+ # Ensure loc_str is user-friendly
105
+ loc_str_parts = []
106
+ for item in loc:
107
+ if item == "body": # Skip "body" part if it's the first element of a longer path
108
+ if len(loc) > 1:
109
+ continue
110
+ loc_str_parts.append(str(item))
111
+ loc_str = " -> ".join(loc_str_parts)
112
+
113
+ msg = error.get("msg", "Validation error")
114
+
115
+ # Check if this error is for the 'content' field specifically due to multimodal input
116
+ if len(loc) >= 3 and loc[0] == 'body' and loc[1] == 'messages' and loc[-1] == 'content':
117
+ # Check if the error type suggests a string was expected but a list (or vice-versa) was given for content
118
+ if "Input should be a valid string" in msg and error.get("input_type") == "list":
119
+ error_messages.append({
120
+ "loc": loc,
121
+ "message": f"Invalid message content: {msg}. Ensure content matches the expected format (string or list of content parts). Path: {loc_str}",
122
+ "type": error.get("type", "validation_error")
123
+ })
124
+ continue # Skip default message formatting for this specific case
125
+ elif "Input should be a valid list" in msg and error.get("input_type") == "string":
126
+ error_messages.append({
127
+ "loc": loc,
128
+ "message": f"Invalid message content: {msg}. Ensure content matches the expected format (string or list of content parts). Path: {loc_str}",
129
+ "type": error.get("type", "validation_error")
130
+ })
131
+ continue
132
+
133
+ if "body" in loc:
134
+ if len(loc) > 1 and loc[1] == "messages":
135
+ error_messages.append({
136
+ "loc": loc,
137
+ "message": "The 'messages' field is required and must be a non-empty array of message objects. " + f"Error: {msg} at {loc_str}",
138
+ "type": error.get("type", "validation_error")
139
+ })
140
+ elif len(loc) > 1 and loc[1] == "model":
141
+ error_messages.append({
142
+ "loc": loc,
143
+ "message": "The 'model' field is required and must be a string. " + f"Error: {msg} at {loc_str}",
144
+ "type": error.get("type", "validation_error")
145
+ })
146
+ else:
147
+ error_messages.append({
148
+ "loc": loc,
149
+ "message": f"{msg} at {loc_str}",
150
+ "type": error.get("type", "validation_error")
151
+ })
152
+ else:
153
+ error_messages.append({
154
+ "loc": loc,
155
+ "message": f"{msg} at {loc_str}",
156
+ "type": error.get("type", "validation_error")
157
+ })
158
+
159
+ if request.url.path == "/v1/chat/completions":
160
+ example = ChatCompletionRequest.Config.schema_extra["example"]
161
+ if is_empty_body:
162
+ return JSONResponse(
163
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
164
+ content={
165
+ "error": {
166
+ "message": "Request body is required and must include 'model' and 'messages'.",
167
+ "type": "invalid_request_error",
168
+ "param": None,
169
+ "code": "body_missing"
170
+ },
171
+ "example": example
172
+ }
173
+ )
174
+ return JSONResponse(
175
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
176
+ content={"detail": error_messages, "example": example}
177
+ )
178
+ return JSONResponse(
179
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
180
+ content={"detail": error_messages}
181
+ )
182
+
183
+ @self.app.exception_handler(StarletteHTTPException)
184
+ async def http_exception_handler(request: Request, exc: StarletteHTTPException):
185
+ return JSONResponse(
186
+ status_code=exc.status_code,
187
+ content={"detail": exc.detail}
188
+ )
189
+
190
+ @self.app.exception_handler(Exception)
191
+ async def general_exception_handler(request: Request, exc: Exception):
192
+ return JSONResponse(
193
+ status_code=HTTP_500_INTERNAL_SERVER_ERROR,
194
+ content={"detail": f"Internal server error: {str(exc)}"}
195
+ )
196
+
197
+ def register_routes(self):
198
+ """Register all API routes."""
199
+ self._register_model_routes()
200
+ self._register_chat_routes()
201
+ self._register_auth_routes()
202
+ self._register_websearch_routes()
203
+
204
+ def _register_model_routes(self):
205
+ """Register model listing routes."""
206
+ @self.app.get("/v1/models", response_model=ModelListResponse, tags=["Chat Completions"])
207
+ async def list_models():
208
+ models = []
209
+ for model_name, provider_class in AppConfig.provider_map.items():
210
+ if "/" not in model_name:
211
+ continue # Skip provider names
212
+ if any(m["id"] == model_name for m in models):
213
+ continue
214
+ models.append({
215
+ "id": model_name,
216
+ "object": "model",
217
+ "created": int(time.time()),
218
+ "owned_by": 'webscout' # Set owned_by to webscout
219
+ })
220
+ # Sort models alphabetically by the part after the first '/'
221
+ models = sorted(models, key=lambda m: m["id"].split("/", 1)[1].lower())
222
+ return {
223
+ "object": "list",
224
+ "data": models
225
+ }
226
+ @self.app.get("/v1/TTI/models", response_model=ModelListResponse, tags=["Image Generation"])
227
+ async def list_tti_models():
228
+ models = []
229
+ for model_name, provider_class in AppConfig.tti_provider_map.items():
230
+ if "/" not in model_name:
231
+ continue # Skip provider names
232
+ if any(m["id"] == model_name for m in models):
233
+ continue
234
+ models.append({
235
+ "id": model_name,
236
+ "object": "model",
237
+ "created": int(time.time()),
238
+ "owned_by": 'webscout' # Set owned_by to webscout
239
+ })
240
+ # Sort models alphabetically by the part after the first '/'
241
+ models = sorted(models, key=lambda m: m["id"].split("/", 1)[1].lower())
242
+ return {
243
+ "object": "list",
244
+ "data": models
245
+ }
246
+
247
+ def _register_chat_routes(self):
248
+ """Register chat completion routes."""
249
+ @self.app.post(
250
+ "/v1/chat/completions",
251
+ response_model_exclude_none=True,
252
+ response_model_exclude_unset=True,
253
+ openapi_extra={
254
+ "requestBody": {
255
+ "content": {
256
+ "application/json": {
257
+ "schema": {
258
+ "$ref": "#/components/schemas/ChatCompletionRequest"
259
+ },
260
+ "example": ChatCompletionRequest.Config.schema_extra["example"]
261
+ }
262
+ }
263
+ }
264
+ }
265
+ )
266
+ async def chat_completions(
267
+ chat_request: ChatCompletionRequest = Body(...)
268
+ ):
269
+ """Handle chat completion requests with comprehensive error handling."""
270
+ start_time = time.time()
271
+ request_id = f"chatcmpl-{uuid.uuid4()}"
272
+
273
+ try:
274
+ logger.info(f"Processing chat completion request {request_id} for model: {chat_request.model}")
275
+
276
+ # Resolve provider and model
277
+ provider_class, model_name = resolve_provider_and_model(chat_request.model)
278
+
279
+ # Initialize provider with caching and error handling
280
+ try:
281
+ provider = get_provider_instance(provider_class)
282
+ logger.debug(f"Using provider instance: {provider_class.__name__}")
283
+ except Exception as e:
284
+ logger.error(f"Failed to initialize provider {provider_class.__name__}: {e}")
285
+ raise APIError(
286
+ f"Failed to initialize provider {provider_class.__name__}: {e}",
287
+ HTTP_500_INTERNAL_SERVER_ERROR,
288
+ "provider_error"
289
+ )
290
+
291
+ # Process and validate messages
292
+ processed_messages = process_messages(chat_request.messages)
293
+
294
+ # Prepare parameters for provider
295
+ params = prepare_provider_params(chat_request, model_name, processed_messages)
296
+
297
+ # Handle streaming vs non-streaming
298
+ if chat_request.stream:
299
+ return await handle_streaming_response(provider, params, request_id)
300
+ else:
301
+ return await handle_non_streaming_response(provider, params, request_id, start_time)
302
+
303
+ except APIError:
304
+ # Re-raise API errors as-is
305
+ raise
306
+ except Exception as e:
307
+ logger.error(f"Unexpected error in chat completion {request_id}: {e}")
308
+ raise APIError(
309
+ f"Internal server error: {str(e)}",
310
+ HTTP_500_INTERNAL_SERVER_ERROR,
311
+ "internal_error"
312
+ )
313
+
314
+
315
+ @self.app.post("/v1/images/generations", tags=["Image Generation"])
316
+ async def image_generations(
317
+ image_request: ImageGenerationRequest = Body(...)
318
+ ):
319
+ """Handle image generation requests."""
320
+ start_time = time.time()
321
+ request_id = f"img-{uuid.uuid4()}"
322
+
323
+ try:
324
+ logger.info(f"Processing image generation request {request_id} for model: {image_request.model}")
325
+
326
+ # Resolve TTI provider and model
327
+ provider_class, model_name = resolve_tti_provider_and_model(image_request.model)
328
+
329
+ # Initialize TTI provider
330
+ try:
331
+ provider = get_tti_provider_instance(provider_class)
332
+ logger.debug(f"Using TTI provider instance: {provider_class.__name__}")
333
+ except Exception as e:
334
+ logger.error(f"Failed to initialize TTI provider {provider_class.__name__}: {e}")
335
+ raise APIError(
336
+ f"Failed to initialize TTI provider {provider_class.__name__}: {e}",
337
+ HTTP_500_INTERNAL_SERVER_ERROR,
338
+ "provider_error"
339
+ )
340
+
341
+ # Prepare parameters for TTI provider
342
+ params = {
343
+ "prompt": image_request.prompt,
344
+ "model": model_name,
345
+ "n": image_request.n,
346
+ "size": image_request.size,
347
+ "response_format": image_request.response_format,
348
+ }
349
+
350
+ # Add optional parameters
351
+ optional_params = ["user", "style", "aspect_ratio", "timeout", "image_format", "seed"]
352
+ for param in optional_params:
353
+ value = getattr(image_request, param, None)
354
+ if value is not None:
355
+ params[param] = value
356
+
357
+ # Generate images
358
+ response = provider.images.create(**params)
359
+
360
+ # Standardize response format
361
+ if hasattr(response, "model_dump"):
362
+ response_data = response.model_dump(exclude_none=True)
363
+ elif hasattr(response, "dict"):
364
+ response_data = response.dict(exclude_none=True)
365
+ elif isinstance(response, dict):
366
+ response_data = response
367
+ else:
368
+ raise APIError(
369
+ "Invalid response format from TTI provider",
370
+ HTTP_500_INTERNAL_SERVER_ERROR,
371
+ "provider_error"
372
+ )
373
+
374
+ elapsed = time.time() - start_time
375
+ logger.info(f"Completed image generation request {request_id} in {elapsed:.2f}s")
376
+
377
+ return response_data
378
+ except APIError:
379
+ raise
380
+ except Exception as e:
381
+ logger.error(f"Unexpected error in image generation {request_id}: {e}")
382
+ raise APIError(
383
+ f"Internal server error: {str(e)}",
384
+ HTTP_500_INTERNAL_SERVER_ERROR,
385
+ "internal_error"
386
+ )
387
+
388
+ def _register_auth_routes(self):
389
+ """Register authentication routes."""
390
+ # Only register auth endpoints if authentication is required
391
+ if not AppConfig.auth_required:
392
+ logger.info("Auth endpoints are disabled (no-auth mode)")
393
+ return
394
+ auth_components = get_auth_components()
395
+ api_key_manager = auth_components.get("api_key_manager")
396
+
397
+ @self.app.post("/v1/auth/generate-key", response_model=APIKeyCreateResponse)
398
+ async def generate_api_key(request: APIKeyCreateRequest = Body(...)):
399
+ """Generate a new API key."""
400
+ if not api_key_manager:
401
+ raise APIError("Authentication system not initialized", HTTP_500_INTERNAL_SERVER_ERROR)
402
+
403
+ try:
404
+ api_key, user = await api_key_manager.create_api_key(
405
+ username=request.username,
406
+ telegram_id=request.telegram_id,
407
+ name=request.name,
408
+ rate_limit=request.rate_limit or 10,
409
+ expires_in_days=request.expires_in_days
410
+ )
411
+
412
+ return APIKeyCreateResponse(
413
+ api_key=api_key.key,
414
+ key_id=api_key.id,
415
+ user_id=user.id,
416
+ name=api_key.name,
417
+ created_at=api_key.created_at,
418
+ expires_at=api_key.expires_at,
419
+ rate_limit=api_key.rate_limit
420
+ )
421
+ except Exception as e:
422
+ logger.error(f"Error generating API key: {e}")
423
+ raise APIError(f"Failed to generate API key: {str(e)}", HTTP_500_INTERNAL_SERVER_ERROR)
424
+
425
+ @self.app.get("/v1/auth/validate", response_model=APIKeyValidationResponse)
426
+ async def validate_api_key(request: Request):
427
+ """Validate an API key."""
428
+ if not api_key_manager:
429
+ raise APIError("Authentication system not initialized", HTTP_500_INTERNAL_SERVER_ERROR)
430
+
431
+ auth_header = request.headers.get("authorization")
432
+ if not auth_header:
433
+ return APIKeyValidationResponse(valid=False, error="No authorization header provided")
434
+
435
+ # Extract API key
436
+ api_key = auth_header
437
+ if auth_header.startswith("Bearer "):
438
+ api_key = auth_header[7:]
439
+
440
+ try:
441
+ is_valid, api_key_obj, error_msg = await api_key_manager.validate_api_key(api_key)
442
+
443
+ if is_valid and api_key_obj:
444
+ return APIKeyValidationResponse(
445
+ valid=True,
446
+ user_id=api_key_obj.user_id,
447
+ key_id=api_key_obj.id,
448
+ rate_limit=api_key_obj.rate_limit,
449
+ usage_count=api_key_obj.usage_count,
450
+ last_used_at=api_key_obj.last_used_at
451
+ )
452
+ else:
453
+ return APIKeyValidationResponse(valid=False, error=error_msg)
454
+ except Exception as e:
455
+ logger.error(f"Error validating API key: {e}")
456
+ return APIKeyValidationResponse(valid=False, error="Internal validation error")
457
+
458
+ @self.app.get("/health", response_model=HealthCheckResponse)
459
+ async def health_check():
460
+ """Health check endpoint."""
461
+ db_status = "unknown"
462
+ db_manager = auth_components.get("db_manager")
463
+ if db_manager:
464
+ status_info = db_manager.get_status()
465
+ db_status = f"{status_info['type']} - {status_info['status']}"
466
+
467
+ return HealthCheckResponse(
468
+ status="healthy",
469
+ database=db_status,
470
+ timestamp=datetime.now(timezone.utc)
471
+ )
472
+
473
+ def _register_websearch_routes(self):
474
+ """Register web search endpoint."""
475
+
476
+ @self.app.get("/search")
477
+ async def websearch(
478
+ q: str = Query(..., description="Search query"),
479
+ engine: str = Query("google", description="Search engine: google, yep, duckduckgo"),
480
+ max_results: int = Query(10, description="Maximum number of results"),
481
+ region: str = Query("all", description="Region code (optional)"),
482
+ safesearch: str = Query("moderate", description="Safe search: on, moderate, off"),
483
+ type: str = Query("text", description="Search type: text, news, images, suggestions"),
484
+ ):
485
+ """Unified web search endpoint."""
486
+ try:
487
+ if engine == "google":
488
+ gs = GoogleSearch()
489
+ if type == "text":
490
+ results = gs.text(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
491
+ return {"engine": "google", "type": "text", "results": [r.__dict__ for r in results]}
492
+ elif type == "news":
493
+ results = gs.news(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
494
+ return {"engine": "google", "type": "news", "results": [r.__dict__ for r in results]}
495
+ elif type == "suggestions":
496
+ results = gs.suggestions(q, region=region)
497
+ return {"engine": "google", "type": "suggestions", "results": results}
498
+ else:
499
+ return {"error": "Google only supports text, news, and suggestions in this API."}
500
+ elif engine == "yep":
501
+ ys = YepSearch()
502
+ if type == "text":
503
+ results = ys.text(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
504
+ return {"engine": "yep", "type": "text", "results": results}
505
+ elif type == "images":
506
+ results = ys.images(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
507
+ return {"engine": "yep", "type": "images", "results": results}
508
+ elif type == "suggestions":
509
+ results = ys.suggestions(q, region=region)
510
+ return {"engine": "yep", "type": "suggestions", "results": results}
511
+ else:
512
+ return {"error": "Yep only supports text, images, and suggestions in this API."}
513
+ elif engine == "duckduckgo":
514
+ ws = WEBS()
515
+ if type == "text":
516
+ results = ws.text(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
517
+ return {"engine": "duckduckgo", "type": "text", "results": results}
518
+ elif type == "suggestions":
519
+ results = ws.suggestions(keywords=q, region=region)
520
+ return {"engine": "duckduckgo", "type": "suggestions", "results": results}
521
+ else:
522
+ return {"error": "DuckDuckGo only supports text and suggestions in this API."}
523
+ else:
524
+ return {"error": "Unknown engine. Use one of: google, yep, duckduckgo."}
525
+ except Exception as e:
526
+ return {"error": str(e)}
@@ -0,0 +1,103 @@
1
+ # webscout/auth/schemas.py
2
+
3
+ from datetime import datetime
4
+ from typing import Optional, Dict, Any
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class APIKeyCreateRequest(BaseModel):
9
+ """Request model for creating a new API key."""
10
+ username: str = Field(..., min_length=3, max_length=50, description="Username for the API key owner (required)")
11
+ telegram_id: str = Field(..., min_length=1, description="Telegram user ID (required)")
12
+ name: Optional[str] = Field(None, description="Optional name for the API key")
13
+ rate_limit: Optional[int] = Field(10, description="Rate limit per minute (default: 10)")
14
+ expires_in_days: Optional[int] = Field(None, description="Number of days until expiration")
15
+ metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional metadata")
16
+
17
+
18
+ class APIKeyCreateResponse(BaseModel):
19
+ """Response model for API key creation."""
20
+ api_key: str = Field(..., description="The generated API key")
21
+ key_id: str = Field(..., description="Unique identifier for the API key")
22
+ user_id: str = Field(..., description="User ID associated with the API key")
23
+ name: Optional[str] = Field(None, description="Name of the API key")
24
+ created_at: datetime = Field(..., description="Creation timestamp")
25
+ expires_at: Optional[datetime] = Field(None, description="Expiration timestamp")
26
+ rate_limit: int = Field(..., description="Rate limit per minute")
27
+
28
+ class Config:
29
+ json_encoders = {
30
+ datetime: lambda v: v.isoformat()
31
+ }
32
+
33
+
34
+ class APIKeyValidationResponse(BaseModel):
35
+ """Response model for API key validation."""
36
+ valid: bool = Field(..., description="Whether the API key is valid")
37
+ user_id: Optional[str] = Field(None, description="User ID if key is valid")
38
+ key_id: Optional[str] = Field(None, description="Key ID if key is valid")
39
+ rate_limit: Optional[int] = Field(None, description="Rate limit per minute")
40
+ usage_count: Optional[int] = Field(None, description="Total usage count")
41
+ last_used_at: Optional[datetime] = Field(None, description="Last usage timestamp")
42
+ error: Optional[str] = Field(None, description="Error message if key is invalid")
43
+
44
+ class Config:
45
+ json_encoders = {
46
+ datetime: lambda v: v.isoformat()
47
+ }
48
+
49
+
50
+ class UserCreateRequest(BaseModel):
51
+ """Request model for creating a new user."""
52
+ username: str = Field(..., min_length=3, max_length=50, description="Username for the new user")
53
+ telegram_id: str = Field(..., min_length=1, description="Telegram user ID (required)")
54
+ metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional metadata")
55
+
56
+
57
+ class UserResponse(BaseModel):
58
+ """Response model for user information."""
59
+ id: str = Field(..., description="User ID")
60
+ username: str = Field(..., description="Username")
61
+ telegram_id: str = Field(..., description="Telegram user ID")
62
+ created_at: datetime = Field(..., description="Creation timestamp")
63
+ updated_at: datetime = Field(..., description="Last update timestamp")
64
+ is_active: bool = Field(..., description="Whether the user is active")
65
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
66
+
67
+ class Config:
68
+ json_encoders = {
69
+ datetime: lambda v: v.isoformat()
70
+ }
71
+
72
+
73
+ class RateLimitStatus(BaseModel):
74
+ """Response model for rate limit status."""
75
+ allowed: bool = Field(..., description="Whether the request is allowed")
76
+ limit: int = Field(..., description="Rate limit per minute")
77
+ remaining: int = Field(..., description="Remaining requests in current window")
78
+ reset_at: datetime = Field(..., description="When the rate limit resets")
79
+ retry_after: Optional[int] = Field(None, description="Seconds to wait before retry")
80
+
81
+ class Config:
82
+ json_encoders = {
83
+ datetime: lambda v: v.isoformat()
84
+ }
85
+
86
+
87
+ class ErrorResponse(BaseModel):
88
+ """Standard error response model."""
89
+ error: str = Field(..., description="Error message")
90
+ code: str = Field(..., description="Error code")
91
+ details: Optional[Dict[str, Any]] = Field(None, description="Additional error details")
92
+
93
+
94
+ class HealthCheckResponse(BaseModel):
95
+ """Health check response model."""
96
+ status: str = Field(..., description="Service status")
97
+ database: str = Field(..., description="Database status")
98
+ timestamp: datetime = Field(..., description="Check timestamp")
99
+
100
+ class Config:
101
+ json_encoders = {
102
+ datetime: lambda v: v.isoformat()
103
+ }