webscout 8.3.1__py3-none-any.whl → 8.3.3__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 (114) hide show
  1. webscout/AIutel.py +180 -78
  2. webscout/Bing_search.py +417 -0
  3. webscout/Extra/gguf.py +706 -177
  4. webscout/Provider/AISEARCH/__init__.py +1 -0
  5. webscout/Provider/AISEARCH/genspark_search.py +7 -7
  6. webscout/Provider/AISEARCH/stellar_search.py +132 -0
  7. webscout/Provider/ExaChat.py +84 -58
  8. webscout/Provider/GeminiProxy.py +140 -0
  9. webscout/Provider/HeckAI.py +85 -80
  10. webscout/Provider/Jadve.py +56 -50
  11. webscout/Provider/MCPCore.py +78 -75
  12. webscout/Provider/MiniMax.py +207 -0
  13. webscout/Provider/Nemotron.py +41 -13
  14. webscout/Provider/Netwrck.py +34 -51
  15. webscout/Provider/OPENAI/BLACKBOXAI.py +0 -4
  16. webscout/Provider/OPENAI/GeminiProxy.py +328 -0
  17. webscout/Provider/OPENAI/MiniMax.py +298 -0
  18. webscout/Provider/OPENAI/README.md +32 -29
  19. webscout/Provider/OPENAI/README_AUTOPROXY.md +238 -0
  20. webscout/Provider/OPENAI/TogetherAI.py +4 -17
  21. webscout/Provider/OPENAI/__init__.py +17 -1
  22. webscout/Provider/OPENAI/autoproxy.py +1067 -39
  23. webscout/Provider/OPENAI/base.py +17 -76
  24. webscout/Provider/OPENAI/deepinfra.py +42 -108
  25. webscout/Provider/OPENAI/e2b.py +0 -1
  26. webscout/Provider/OPENAI/flowith.py +179 -166
  27. webscout/Provider/OPENAI/friendli.py +233 -0
  28. webscout/Provider/OPENAI/mcpcore.py +109 -70
  29. webscout/Provider/OPENAI/monochat.py +329 -0
  30. webscout/Provider/OPENAI/pydantic_imports.py +1 -172
  31. webscout/Provider/OPENAI/scirachat.py +59 -51
  32. webscout/Provider/OPENAI/toolbaz.py +3 -9
  33. webscout/Provider/OPENAI/typegpt.py +1 -1
  34. webscout/Provider/OPENAI/utils.py +19 -42
  35. webscout/Provider/OPENAI/x0gpt.py +14 -2
  36. webscout/Provider/OPENAI/xenai.py +514 -0
  37. webscout/Provider/OPENAI/yep.py +8 -2
  38. webscout/Provider/OpenGPT.py +54 -32
  39. webscout/Provider/PI.py +58 -84
  40. webscout/Provider/StandardInput.py +32 -13
  41. webscout/Provider/TTI/README.md +9 -9
  42. webscout/Provider/TTI/__init__.py +3 -1
  43. webscout/Provider/TTI/aiarta.py +92 -78
  44. webscout/Provider/TTI/bing.py +231 -0
  45. webscout/Provider/TTI/infip.py +212 -0
  46. webscout/Provider/TTI/monochat.py +220 -0
  47. webscout/Provider/TTS/speechma.py +45 -39
  48. webscout/Provider/TeachAnything.py +11 -3
  49. webscout/Provider/TextPollinationsAI.py +78 -70
  50. webscout/Provider/TogetherAI.py +350 -0
  51. webscout/Provider/Venice.py +37 -46
  52. webscout/Provider/VercelAI.py +27 -24
  53. webscout/Provider/WiseCat.py +35 -35
  54. webscout/Provider/WrDoChat.py +22 -26
  55. webscout/Provider/WritingMate.py +26 -22
  56. webscout/Provider/XenAI.py +324 -0
  57. webscout/Provider/__init__.py +10 -5
  58. webscout/Provider/deepseek_assistant.py +378 -0
  59. webscout/Provider/granite.py +48 -57
  60. webscout/Provider/koala.py +51 -39
  61. webscout/Provider/learnfastai.py +49 -64
  62. webscout/Provider/llmchat.py +79 -93
  63. webscout/Provider/llmchatco.py +63 -78
  64. webscout/Provider/multichat.py +51 -40
  65. webscout/Provider/oivscode.py +1 -1
  66. webscout/Provider/scira_chat.py +159 -96
  67. webscout/Provider/scnet.py +13 -13
  68. webscout/Provider/searchchat.py +13 -13
  69. webscout/Provider/sonus.py +12 -11
  70. webscout/Provider/toolbaz.py +25 -8
  71. webscout/Provider/turboseek.py +41 -42
  72. webscout/Provider/typefully.py +27 -12
  73. webscout/Provider/typegpt.py +41 -46
  74. webscout/Provider/uncovr.py +55 -90
  75. webscout/Provider/x0gpt.py +33 -17
  76. webscout/Provider/yep.py +79 -96
  77. webscout/auth/__init__.py +55 -0
  78. webscout/auth/api_key_manager.py +189 -0
  79. webscout/auth/auth_system.py +100 -0
  80. webscout/auth/config.py +76 -0
  81. webscout/auth/database.py +400 -0
  82. webscout/auth/exceptions.py +67 -0
  83. webscout/auth/middleware.py +248 -0
  84. webscout/auth/models.py +130 -0
  85. webscout/auth/providers.py +279 -0
  86. webscout/auth/rate_limiter.py +254 -0
  87. webscout/auth/request_models.py +127 -0
  88. webscout/auth/request_processing.py +226 -0
  89. webscout/auth/routes.py +550 -0
  90. webscout/auth/schemas.py +103 -0
  91. webscout/auth/server.py +367 -0
  92. webscout/client.py +121 -70
  93. webscout/litagent/Readme.md +68 -55
  94. webscout/litagent/agent.py +99 -9
  95. webscout/scout/core/scout.py +104 -26
  96. webscout/scout/element.py +139 -18
  97. webscout/swiftcli/core/cli.py +14 -3
  98. webscout/swiftcli/decorators/output.py +59 -9
  99. webscout/update_checker.py +31 -49
  100. webscout/version.py +1 -1
  101. webscout/webscout_search.py +4 -12
  102. webscout/webscout_search_async.py +3 -10
  103. webscout/yep_search.py +2 -11
  104. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/METADATA +141 -99
  105. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/RECORD +109 -83
  106. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/entry_points.txt +1 -1
  107. webscout/Provider/HF_space/__init__.py +0 -0
  108. webscout/Provider/HF_space/qwen_qwen2.py +0 -206
  109. webscout/Provider/OPENAI/api.py +0 -1320
  110. webscout/Provider/TTI/fastflux.py +0 -233
  111. webscout/Provider/Writecream.py +0 -246
  112. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/WHEEL +0 -0
  113. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/licenses/LICENSE.md +0 -0
  114. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,550 @@
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
+ from fastapi.exceptions import RequestValidationError
89
+ from starlette.exceptions import HTTPException as StarletteHTTPException
90
+ from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, HTTP_500_INTERNAL_SERVER_ERROR
91
+ from .exceptions import APIError
92
+
93
+ github_footer = "If you believe this is a bug, please pull an issue at https://github.com/OEvortex/Webscout."
94
+
95
+ @self.app.exception_handler(APIError)
96
+ async def api_error_handler(request, exc: APIError):
97
+ logger.error(f"API Error: {exc.message} (Status: {exc.status_code})")
98
+ # Patch: add footer to error content before creating JSONResponse
99
+ error_response = exc.to_response()
100
+ # If the response is a JSONResponse, patch its content dict before returning
101
+ if hasattr(error_response, 'body') and hasattr(error_response, 'media_type'):
102
+ # Try to decode the body to dict and add footer if possible
103
+ try:
104
+ import json
105
+ content_dict = json.loads(error_response.body.decode())
106
+ if "error" in content_dict:
107
+ content_dict["error"]["footer"] = github_footer
108
+ return JSONResponse(status_code=error_response.status_code, content=content_dict)
109
+ except Exception:
110
+ pass
111
+ return error_response
112
+
113
+ @self.app.exception_handler(RequestValidationError)
114
+ async def validation_exception_handler(request, exc: RequestValidationError):
115
+ errors = exc.errors()
116
+ error_messages = []
117
+ body = await request.body()
118
+ is_empty_body = not body or body.strip() in (b"", b"null", b"{}")
119
+ for error in errors:
120
+ loc = error.get("loc", [])
121
+ loc_str = " -> ".join(str(item) for item in loc)
122
+ msg = error.get("msg", "Validation error")
123
+ error_messages.append({
124
+ "loc": loc,
125
+ "message": f"{msg} at {loc_str}",
126
+ "type": error.get("type", "validation_error")
127
+ })
128
+ content = {
129
+ "error": {
130
+ "message": "Request validation error.",
131
+ "details": error_messages,
132
+ "type": "validation_error",
133
+ "footer": github_footer
134
+ }
135
+ }
136
+ return JSONResponse(status_code=HTTP_422_UNPROCESSABLE_ENTITY, content=content)
137
+
138
+ @self.app.exception_handler(StarletteHTTPException)
139
+ async def http_exception_handler(request, exc: StarletteHTTPException):
140
+ content = {
141
+ "error": {
142
+ "message": exc.detail or "HTTP error occurred.",
143
+ "type": "http_error",
144
+ "footer": github_footer
145
+ }
146
+ }
147
+ return JSONResponse(status_code=exc.status_code, content=content)
148
+
149
+ @self.app.exception_handler(Exception)
150
+ async def general_exception_handler(request, exc: Exception):
151
+ logger.error(f"Unhandled server error: {exc}")
152
+ content = {
153
+ "error": {
154
+ "message": f"Internal server error: {str(exc)}",
155
+ "type": "server_error",
156
+ "footer": github_footer
157
+ }
158
+ }
159
+ return JSONResponse(status_code=HTTP_500_INTERNAL_SERVER_ERROR, content=content)
160
+
161
+ def register_routes(self):
162
+ """Register all API routes."""
163
+ self._register_model_routes()
164
+ self._register_chat_routes()
165
+ self._register_auth_routes()
166
+ self._register_websearch_routes()
167
+
168
+ def _register_model_routes(self):
169
+ """Register model listing routes."""
170
+ @self.app.get(
171
+ "/v1/models",
172
+ response_model=ModelListResponse,
173
+ tags=["Chat Completions"],
174
+ description="List all available chat completion models."
175
+ )
176
+ async def list_models():
177
+ models = []
178
+ for model_name, provider_class in AppConfig.provider_map.items():
179
+ if "/" not in model_name:
180
+ continue # Skip provider names
181
+ if any(m["id"] == model_name for m in models):
182
+ continue
183
+ models.append({
184
+ "id": model_name,
185
+ "object": "model",
186
+ "created": int(time.time()),
187
+ "owned_by": 'webscout' # Set owned_by to webscout
188
+ })
189
+ # Sort models alphabetically by the part after the first '/'
190
+ models = sorted(models, key=lambda m: m["id"].split("/", 1)[1].lower())
191
+ return {
192
+ "object": "list",
193
+ "data": models
194
+ }
195
+ @self.app.get(
196
+ "/v1/TTI/models",
197
+ response_model=ModelListResponse,
198
+ tags=["Image Generation"],
199
+ description="List all available text-to-image (TTI) models."
200
+ )
201
+ async def list_tti_models():
202
+ models = []
203
+ for model_name, provider_class in AppConfig.tti_provider_map.items():
204
+ if "/" not in model_name:
205
+ continue # Skip provider names
206
+ if any(m["id"] == model_name for m in models):
207
+ continue
208
+ models.append({
209
+ "id": model_name,
210
+ "object": "model",
211
+ "created": int(time.time()),
212
+ "owned_by": 'webscout' # Set owned_by to webscout
213
+ })
214
+ # Sort models alphabetically by the part after the first '/'
215
+ models = sorted(models, key=lambda m: m["id"].split("/", 1)[1].lower())
216
+ return {
217
+ "object": "list",
218
+ "data": models
219
+ }
220
+
221
+ def _register_chat_routes(self):
222
+ """Register chat completion routes."""
223
+ @self.app.post(
224
+ "/v1/chat/completions",
225
+ response_model_exclude_none=True,
226
+ response_model_exclude_unset=True,
227
+ tags=["Chat Completions"],
228
+ description="Generate chat completions using the specified model.",
229
+ openapi_extra={
230
+ "requestBody": {
231
+ "content": {
232
+ "application/json": {
233
+ "schema": {
234
+ "$ref": "#/components/schemas/ChatCompletionRequest"
235
+ },
236
+ "example": ChatCompletionRequest.Config.schema_extra["example"]
237
+ }
238
+ }
239
+ }
240
+ }
241
+ )
242
+ async def chat_completions(
243
+ chat_request: ChatCompletionRequest = Body(...)
244
+ ):
245
+ """Handle chat completion requests with comprehensive error handling."""
246
+ start_time = time.time()
247
+ request_id = f"chatcmpl-{uuid.uuid4()}"
248
+
249
+ try:
250
+ logger.info(f"Processing chat completion request {request_id} for model: {chat_request.model}")
251
+
252
+ # Resolve provider and model
253
+ provider_class, model_name = resolve_provider_and_model(chat_request.model)
254
+
255
+ # Initialize provider with caching and error handling
256
+ try:
257
+ provider = get_provider_instance(provider_class)
258
+ logger.debug(f"Using provider instance: {provider_class.__name__}")
259
+ except Exception as e:
260
+ logger.error(f"Failed to initialize provider {provider_class.__name__}: {e}")
261
+ raise APIError(
262
+ f"Failed to initialize provider {provider_class.__name__}: {e}",
263
+ HTTP_500_INTERNAL_SERVER_ERROR,
264
+ "provider_error"
265
+ )
266
+
267
+ # Process and validate messages
268
+ processed_messages = process_messages(chat_request.messages)
269
+
270
+ # Prepare parameters for provider
271
+ params = prepare_provider_params(chat_request, model_name, processed_messages)
272
+
273
+ # Handle streaming vs non-streaming
274
+ if chat_request.stream:
275
+ return await handle_streaming_response(provider, params, request_id)
276
+ else:
277
+ return await handle_non_streaming_response(provider, params, request_id, start_time)
278
+
279
+ except APIError:
280
+ # Re-raise API errors as-is
281
+ raise
282
+ except Exception as e:
283
+ logger.error(f"Unexpected error in chat completion {request_id}: {e}")
284
+ raise APIError(
285
+ f"Internal server error: {str(e)}",
286
+ HTTP_500_INTERNAL_SERVER_ERROR,
287
+ "internal_error"
288
+ )
289
+
290
+
291
+ @self.app.post(
292
+ "/v1/images/generations",
293
+ tags=["Image Generation"],
294
+ description="Generate images from text prompts using the specified TTI model."
295
+ )
296
+ async def image_generations(
297
+ image_request: ImageGenerationRequest = Body(...)
298
+ ):
299
+ """Handle image generation requests."""
300
+ start_time = time.time()
301
+ request_id = f"img-{uuid.uuid4()}"
302
+
303
+ try:
304
+ logger.info(f"Processing image generation request {request_id} for model: {image_request.model}")
305
+
306
+ # Resolve TTI provider and model
307
+ provider_class, model_name = resolve_tti_provider_and_model(image_request.model)
308
+
309
+ # Initialize TTI provider
310
+ try:
311
+ provider = get_tti_provider_instance(provider_class)
312
+ logger.debug(f"Using TTI provider instance: {provider_class.__name__}")
313
+ except APIError as e:
314
+ # Add helpful footer for provider errors
315
+ return JSONResponse(
316
+ status_code=e.status_code,
317
+ content={
318
+ "error": {
319
+ "message": e.message,
320
+ "type": e.error_type,
321
+ "footer": "If you believe this is a bug, please pull an issue at https://github.com/OEvortex/Webscout."
322
+ }
323
+ }
324
+ )
325
+ except Exception as e:
326
+ logger.error(f"Failed to initialize TTI provider {provider_class.__name__}: {e}")
327
+ raise APIError(
328
+ f"Failed to initialize TTI provider {provider_class.__name__}: {e}",
329
+ HTTP_500_INTERNAL_SERVER_ERROR,
330
+ "provider_error"
331
+ )
332
+
333
+ # Prepare parameters for TTI provider
334
+ params = {
335
+ "prompt": image_request.prompt,
336
+ "model": model_name,
337
+ "n": image_request.n,
338
+ "size": image_request.size,
339
+ "response_format": image_request.response_format,
340
+ }
341
+
342
+ # Add optional parameters
343
+ optional_params = ["user", "style", "aspect_ratio", "timeout", "image_format", "seed"]
344
+ for param in optional_params:
345
+ value = getattr(image_request, param, None)
346
+ if value is not None:
347
+ params[param] = value
348
+
349
+ # Generate images
350
+ response = provider.images.create(**params)
351
+
352
+ # Standardize response format
353
+ if hasattr(response, "model_dump"):
354
+ response_data = response.model_dump(exclude_none=True)
355
+ elif hasattr(response, "dict"):
356
+ response_data = response.dict(exclude_none=True)
357
+ elif isinstance(response, dict):
358
+ response_data = response
359
+ else:
360
+ raise APIError(
361
+ "Invalid response format from TTI provider",
362
+ HTTP_500_INTERNAL_SERVER_ERROR,
363
+ "provider_error"
364
+ )
365
+
366
+ elapsed = time.time() - start_time
367
+ logger.info(f"Completed image generation request {request_id} in {elapsed:.2f}s")
368
+
369
+ return response_data
370
+ except APIError:
371
+ raise
372
+ except Exception as e:
373
+ logger.error(f"Unexpected error in image generation {request_id}: {e}")
374
+ raise APIError(
375
+ f"Internal server error: {str(e)}",
376
+ HTTP_500_INTERNAL_SERVER_ERROR,
377
+ "internal_error"
378
+ )
379
+
380
+ def _register_auth_routes(self):
381
+ """Register authentication routes."""
382
+ # Only register auth endpoints if authentication is required
383
+ if not AppConfig.auth_required:
384
+ logger.info("Auth endpoints are disabled (no-auth mode)")
385
+ return
386
+ auth_components = get_auth_components()
387
+ api_key_manager = auth_components.get("api_key_manager")
388
+
389
+ @self.app.post(
390
+ "/v1/auth/generate-key",
391
+ response_model=APIKeyCreateResponse,
392
+ tags=["Authentication"],
393
+ description="Generate a new API key for a user."
394
+ )
395
+ async def generate_api_key(request: APIKeyCreateRequest = Body(...)):
396
+ """Generate a new API key."""
397
+ if not api_key_manager:
398
+ raise APIError("Authentication system not initialized", HTTP_500_INTERNAL_SERVER_ERROR)
399
+
400
+ try:
401
+ api_key, user = await api_key_manager.create_api_key(
402
+ username=request.username,
403
+ telegram_id=request.telegram_id,
404
+ name=request.name,
405
+ rate_limit=request.rate_limit or 10,
406
+ expires_in_days=request.expires_in_days
407
+ )
408
+
409
+ return APIKeyCreateResponse(
410
+ api_key=api_key.key,
411
+ key_id=api_key.id,
412
+ user_id=user.id,
413
+ name=api_key.name,
414
+ created_at=api_key.created_at,
415
+ expires_at=api_key.expires_at,
416
+ rate_limit=api_key.rate_limit
417
+ )
418
+ except Exception as e:
419
+ logger.error(f"Error generating API key: {e}")
420
+ raise APIError(f"Failed to generate API key: {str(e)}", HTTP_500_INTERNAL_SERVER_ERROR)
421
+
422
+ @self.app.get(
423
+ "/v1/auth/validate",
424
+ response_model=APIKeyValidationResponse,
425
+ tags=["Authentication"],
426
+ description="Validate an API key and return its status."
427
+ )
428
+ async def validate_api_key(request: Request):
429
+ """Validate an API key."""
430
+ if not api_key_manager:
431
+ raise APIError("Authentication system not initialized", HTTP_500_INTERNAL_SERVER_ERROR)
432
+
433
+ auth_header = request.headers.get("authorization")
434
+ if not auth_header:
435
+ return APIKeyValidationResponse(valid=False, error="No authorization header provided")
436
+
437
+ # Extract API key
438
+ api_key = auth_header
439
+ if auth_header.startswith("Bearer "):
440
+ api_key = auth_header[7:]
441
+
442
+ try:
443
+ is_valid, api_key_obj, error_msg = await api_key_manager.validate_api_key(api_key)
444
+
445
+ if is_valid and api_key_obj:
446
+ return APIKeyValidationResponse(
447
+ valid=True,
448
+ user_id=api_key_obj.user_id,
449
+ key_id=api_key_obj.id,
450
+ rate_limit=api_key_obj.rate_limit,
451
+ usage_count=api_key_obj.usage_count,
452
+ last_used_at=api_key_obj.last_used_at
453
+ )
454
+ else:
455
+ return APIKeyValidationResponse(valid=False, error=error_msg)
456
+ except Exception as e:
457
+ logger.error(f"Error validating API key: {e}")
458
+ return APIKeyValidationResponse(valid=False, error="Internal validation error")
459
+
460
+ @self.app.get(
461
+ "/health",
462
+ response_model=HealthCheckResponse,
463
+ tags=["Health"],
464
+ description="Health check endpoint for the API and database."
465
+ )
466
+ async def health_check():
467
+ """Health check endpoint."""
468
+ db_status = "unknown"
469
+ db_manager = auth_components.get("db_manager")
470
+ if db_manager:
471
+ status_info = db_manager.get_status()
472
+ db_status = f"{status_info['type']} - {status_info['status']}"
473
+
474
+ return HealthCheckResponse(
475
+ status="healthy",
476
+ database=db_status,
477
+ timestamp=datetime.now(timezone.utc)
478
+ )
479
+
480
+ def _register_websearch_routes(self):
481
+ """Register web search endpoint."""
482
+
483
+ @self.app.get(
484
+ "/search",
485
+ tags=["Web search"],
486
+ description="Unified web search endpoint supporting Google, Yep, and DuckDuckGo with text, news, images, and suggestions search types."
487
+ )
488
+ async def websearch(
489
+ q: str = Query(..., description="Search query"),
490
+ engine: str = Query("google", description="Search engine: google, yep, duckduckgo"),
491
+ max_results: int = Query(10, description="Maximum number of results"),
492
+ region: str = Query("all", description="Region code (optional)"),
493
+ safesearch: str = Query("moderate", description="Safe search: on, moderate, off"),
494
+ type: str = Query("text", description="Search type: text, news, images, suggestions"),
495
+ ):
496
+ """Unified web search endpoint."""
497
+ github_footer = "If you believe this is a bug, please pull an issue at https://github.com/OEvortex/Webscout."
498
+ try:
499
+ if engine == "google":
500
+ gs = GoogleSearch()
501
+ if type == "text":
502
+ results = gs.text(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
503
+ return {"engine": "google", "type": "text", "results": [r.__dict__ for r in results]}
504
+ elif type == "news":
505
+ results = gs.news(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
506
+ return {"engine": "google", "type": "news", "results": [r.__dict__ for r in results]}
507
+ elif type == "suggestions":
508
+ results = gs.suggestions(q, region=region)
509
+ return {"engine": "google", "type": "suggestions", "results": results}
510
+ else:
511
+ return {"error": "Google only supports text, news, and suggestions in this API.", "footer": github_footer}
512
+ elif engine == "yep":
513
+ ys = YepSearch()
514
+ if type == "text":
515
+ results = ys.text(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
516
+ return {"engine": "yep", "type": "text", "results": results}
517
+ elif type == "images":
518
+ results = ys.images(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
519
+ return {"engine": "yep", "type": "images", "results": results}
520
+ elif type == "suggestions":
521
+ results = ys.suggestions(q, region=region)
522
+ return {"engine": "yep", "type": "suggestions", "results": results}
523
+ else:
524
+ return {"error": "Yep only supports text, images, and suggestions in this API.", "footer": github_footer}
525
+ elif engine == "duckduckgo":
526
+ ws = WEBS()
527
+ if type == "text":
528
+ results = ws.text(keywords=q, region=region, safesearch=safesearch, max_results=max_results)
529
+ return {"engine": "duckduckgo", "type": "text", "results": results}
530
+ elif type == "suggestions":
531
+ results = ws.suggestions(keywords=q, region=region)
532
+ return {"engine": "duckduckgo", "type": "suggestions", "results": results}
533
+ else:
534
+ return {"error": "DuckDuckGo only supports text and suggestions in this API.", "footer": github_footer}
535
+ else:
536
+ return {"error": "Unknown engine. Use one of: google, yep, duckduckgo.", "footer": github_footer}
537
+ except Exception as e:
538
+ # Special handling for rate limit errors
539
+ msg = str(e)
540
+ if "429" in msg or "rate limit" in msg.lower():
541
+ return {
542
+ "error": "You have hit the search rate limit. Please try again later.",
543
+ "details": msg,
544
+ "code": 429,
545
+ "footer": github_footer
546
+ }
547
+ return {
548
+ "error": f"Search request failed: {msg}",
549
+ "footer": github_footer
550
+ }
@@ -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
+ }