webscout 2025.10.14.1__py3-none-any.whl → 2025.10.16__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 (70) hide show
  1. webscout/Extra/YTToolkit/README.md +1 -1
  2. webscout/Extra/tempmail/README.md +3 -3
  3. webscout/Provider/OPENAI/README.md +1 -1
  4. webscout/Provider/TTI/bing.py +4 -4
  5. webscout/__init__.py +1 -1
  6. webscout/cli.py +0 -147
  7. webscout/client.py +4 -5
  8. webscout/litprinter/__init__.py +0 -42
  9. webscout/scout/README.md +59 -8
  10. webscout/scout/core/scout.py +62 -0
  11. webscout/scout/element.py +251 -45
  12. webscout/search/__init__.py +5 -8
  13. webscout/search/bing_main.py +42 -0
  14. webscout/search/engines/bing/__init__.py +1 -0
  15. webscout/search/engines/bing/base.py +33 -0
  16. webscout/search/engines/bing/images.py +108 -0
  17. webscout/search/engines/bing/news.py +91 -0
  18. webscout/search/engines/bing/suggestions.py +34 -0
  19. webscout/search/engines/bing/text.py +106 -0
  20. webscout/search/engines/duckduckgo/maps.py +13 -0
  21. webscout/search/engines/yahoo/__init__.py +41 -0
  22. webscout/search/engines/yahoo/answers.py +16 -0
  23. webscout/search/engines/yahoo/base.py +34 -0
  24. webscout/search/engines/yahoo/images.py +324 -0
  25. webscout/search/engines/yahoo/maps.py +16 -0
  26. webscout/search/engines/yahoo/news.py +258 -0
  27. webscout/search/engines/yahoo/suggestions.py +140 -0
  28. webscout/search/engines/yahoo/text.py +273 -0
  29. webscout/search/engines/yahoo/translate.py +16 -0
  30. webscout/search/engines/yahoo/videos.py +302 -0
  31. webscout/search/engines/yahoo/weather.py +220 -0
  32. webscout/search/http_client.py +1 -1
  33. webscout/search/yahoo_main.py +54 -0
  34. webscout/{auth → server}/__init__.py +2 -23
  35. webscout/server/config.py +84 -0
  36. webscout/{auth → server}/request_processing.py +3 -28
  37. webscout/{auth → server}/routes.py +14 -170
  38. webscout/server/schemas.py +23 -0
  39. webscout/{auth → server}/server.py +11 -43
  40. webscout/server/simple_logger.py +84 -0
  41. webscout/version.py +1 -1
  42. webscout/version.py.bak +1 -1
  43. webscout/zeroart/README.md +17 -9
  44. webscout/zeroart/__init__.py +78 -6
  45. webscout/zeroart/effects.py +51 -1
  46. webscout/zeroart/fonts.py +559 -1
  47. {webscout-2025.10.14.1.dist-info → webscout-2025.10.16.dist-info}/METADATA +15 -332
  48. {webscout-2025.10.14.1.dist-info → webscout-2025.10.16.dist-info}/RECORD +55 -48
  49. {webscout-2025.10.14.1.dist-info → webscout-2025.10.16.dist-info}/entry_points.txt +1 -1
  50. webscout/Bing_search.py +0 -417
  51. webscout/DWEBS.py +0 -529
  52. webscout/auth/api_key_manager.py +0 -189
  53. webscout/auth/auth_system.py +0 -85
  54. webscout/auth/config.py +0 -175
  55. webscout/auth/database.py +0 -755
  56. webscout/auth/middleware.py +0 -248
  57. webscout/auth/models.py +0 -185
  58. webscout/auth/rate_limiter.py +0 -254
  59. webscout/auth/schemas.py +0 -103
  60. webscout/auth/simple_logger.py +0 -236
  61. webscout/search/engines/bing.py +0 -84
  62. webscout/search/engines/bing_news.py +0 -52
  63. webscout/search/engines/yahoo.py +0 -65
  64. webscout/search/engines/yahoo_news.py +0 -64
  65. /webscout/{auth → server}/exceptions.py +0 -0
  66. /webscout/{auth → server}/providers.py +0 -0
  67. /webscout/{auth → server}/request_models.py +0 -0
  68. {webscout-2025.10.14.1.dist-info → webscout-2025.10.16.dist-info}/WHEEL +0 -0
  69. {webscout-2025.10.14.1.dist-info → webscout-2025.10.16.dist-info}/licenses/LICENSE.md +0 -0
  70. {webscout-2025.10.14.1.dist-info → webscout-2025.10.16.dist-info}/top_level.txt +0 -0
@@ -1,248 +0,0 @@
1
- # webscout/auth/middleware.py
2
-
3
- import secrets
4
- from datetime import datetime
5
- from typing import Optional, Tuple
6
- import logging
7
-
8
- from fastapi import Request, HTTPException, status
9
- from fastapi.security import APIKeyHeader
10
- from fastapi.responses import JSONResponse
11
-
12
- from .api_key_manager import APIKeyManager
13
- from .rate_limiter import RateLimiter
14
- from .models import APIKey
15
- from .schemas import ErrorResponse, RateLimitStatus
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class AuthMiddleware:
21
- """Authentication and rate limiting middleware."""
22
-
23
- def __init__(self, api_key_manager: APIKeyManager, rate_limiter: RateLimiter, auth_required: bool = True, rate_limit_enabled: bool = True):
24
- self.api_key_manager = api_key_manager
25
- self.rate_limiter = rate_limiter
26
- self.api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
27
- self.auth_required = auth_required
28
- self.rate_limit_enabled = rate_limit_enabled
29
-
30
- # Paths that don't require authentication
31
- self.public_paths = {
32
- "/",
33
- "/docs",
34
- "/redoc",
35
- "/openapi.json",
36
- "/health",
37
- "/v1/auth/generate-key" # API key generation endpoint
38
- }
39
-
40
- # Paths that require authentication (when auth is enabled)
41
- self.protected_path_prefixes = [
42
- "/v1/chat/completions",
43
- "/v1/images/generations",
44
- "/v1/models",
45
- "/v1/TTI/models"
46
- ]
47
-
48
- def is_protected_path(self, path: str) -> bool:
49
- """Check if a path requires authentication."""
50
- if not self.auth_required:
51
- return False # No authentication required in no-auth mode
52
-
53
- if path in self.public_paths:
54
- return False
55
-
56
- return any(path.startswith(prefix) for prefix in self.protected_path_prefixes)
57
-
58
- def extract_api_key(self, authorization_header: Optional[str]) -> Optional[str]:
59
- """Extract API key from Authorization header."""
60
- if not authorization_header:
61
- return None
62
-
63
- # Support both "Bearer <key>" and direct key formats
64
- if authorization_header.startswith("Bearer "):
65
- return authorization_header[7:]
66
- elif authorization_header.startswith("ws_"):
67
- return authorization_header
68
- else:
69
- return None
70
-
71
- async def authenticate_request(self, request: Request) -> Tuple[bool, Optional[APIKey], Optional[dict]]:
72
- """
73
- Authenticate a request.
74
-
75
- Returns:
76
- Tuple of (is_authenticated, api_key_object, error_response)
77
- """
78
- path = request.url.path
79
-
80
- # Check if path requires authentication
81
- if not self.is_protected_path(path):
82
- return True, None, None
83
-
84
- # Get authorization header
85
- auth_header = request.headers.get("authorization")
86
- api_key = self.extract_api_key(auth_header)
87
-
88
- if not api_key:
89
- error_response = {
90
- "error": "API key required",
91
- "code": "missing_api_key",
92
- "details": {
93
- "message": "Please provide a valid API key in the Authorization header",
94
- "format": "Authorization: Bearer <your-api-key>"
95
- }
96
- }
97
- return False, None, error_response
98
-
99
- # Validate API key
100
- is_valid, api_key_obj, error_msg = await self.api_key_manager.validate_api_key(api_key)
101
-
102
- if not is_valid:
103
- error_response = {
104
- "error": error_msg or "Invalid API key",
105
- "code": "invalid_api_key",
106
- "details": {
107
- "message": "The provided API key is invalid, expired, or inactive"
108
- }
109
- }
110
- return False, None, error_response
111
-
112
- return True, api_key_obj, None
113
-
114
- async def check_rate_limit(self, api_key: Optional[APIKey], client_ip: str = "unknown") -> Tuple[bool, dict]:
115
- """
116
- Check rate limit for an API key or IP address.
117
-
118
- Returns:
119
- Tuple of (is_allowed, rate_limit_info)
120
- """
121
- if not self.rate_limit_enabled:
122
- # Rate limiting disabled
123
- return True, {
124
- "allowed": True,
125
- "limit": 999999,
126
- "remaining": 999999,
127
- "reset_at": None,
128
- "retry_after": None
129
- }
130
-
131
- if api_key:
132
- return await self.rate_limiter.check_rate_limit(api_key)
133
- else:
134
- # No-auth mode: use IP-based rate limiting
135
- return await self.rate_limiter.check_ip_rate_limit(client_ip)
136
-
137
- async def process_request(self, request: Request) -> Tuple[bool, Optional[dict], Optional[dict]]:
138
- """
139
- Process a request through authentication and rate limiting.
140
-
141
- Returns:
142
- Tuple of (is_allowed, error_response, rate_limit_headers)
143
- """
144
- # Authenticate request
145
- is_authenticated, api_key_obj, auth_error = await self.authenticate_request(request)
146
-
147
- if not is_authenticated:
148
- return False, auth_error, None
149
-
150
- # If no API key (public endpoint or no-auth mode), handle rate limiting
151
- if not api_key_obj:
152
- if not self.auth_required:
153
- # No-auth mode: still check rate limiting by IP
154
- client_ip = request.client.host if request.client else "unknown"
155
- is_allowed, rate_limit_info = await self.check_rate_limit(None, client_ip)
156
-
157
- if not is_allowed:
158
- error_response = {
159
- "error": "Rate limit exceeded",
160
- "code": "rate_limit_exceeded",
161
- "details": {
162
- "message": f"Rate limit of {rate_limit_info['limit']} requests per minute exceeded",
163
- "retry_after": rate_limit_info["retry_after"],
164
- "reset_at": rate_limit_info["reset_at"].isoformat() if rate_limit_info["reset_at"] else None
165
- }
166
- }
167
- rate_limit_headers = {
168
- "X-RateLimit-Limit": str(rate_limit_info["limit"]),
169
- "X-RateLimit-Remaining": str(rate_limit_info["remaining"]),
170
- "Retry-After": str(rate_limit_info["retry_after"])
171
- }
172
- if rate_limit_info["reset_at"]:
173
- rate_limit_headers["X-RateLimit-Reset"] = rate_limit_info["reset_at"].isoformat()
174
- return False, error_response, rate_limit_headers
175
-
176
- # Store rate limit info for no-auth mode
177
- request.state.rate_limit_info = rate_limit_info
178
- rate_limit_headers = {
179
- "X-RateLimit-Limit": str(rate_limit_info["limit"]),
180
- "X-RateLimit-Remaining": str(rate_limit_info["remaining"])
181
- }
182
- if rate_limit_info["reset_at"]:
183
- rate_limit_headers["X-RateLimit-Reset"] = rate_limit_info["reset_at"].isoformat()
184
- return True, None, rate_limit_headers
185
- else:
186
- return True, None, None
187
-
188
- # Check rate limit for authenticated requests
189
- client_ip = request.client.host if request.client else "unknown"
190
- is_allowed, rate_limit_info = await self.check_rate_limit(api_key_obj, client_ip)
191
-
192
- # Prepare rate limit headers
193
- rate_limit_headers = {
194
- "X-RateLimit-Limit": str(rate_limit_info["limit"]),
195
- "X-RateLimit-Remaining": str(rate_limit_info["remaining"]),
196
- "X-RateLimit-Reset": rate_limit_info["reset_at"].isoformat()
197
- }
198
-
199
- if not is_allowed:
200
- error_response = {
201
- "error": "Rate limit exceeded",
202
- "code": "rate_limit_exceeded",
203
- "details": {
204
- "message": f"Rate limit of {rate_limit_info['limit']} requests per minute exceeded",
205
- "retry_after": rate_limit_info["retry_after"],
206
- "reset_at": rate_limit_info["reset_at"].isoformat()
207
- }
208
- }
209
- rate_limit_headers["Retry-After"] = str(rate_limit_info["retry_after"])
210
- return False, error_response, rate_limit_headers
211
-
212
- # Store API key info in request state for use in endpoints
213
- request.state.api_key = api_key_obj
214
- request.state.rate_limit_info = rate_limit_info
215
-
216
- return True, None, rate_limit_headers
217
-
218
- def create_error_response(self, error_data: dict, status_code: int = 401, headers: Optional[dict] = None) -> JSONResponse:
219
- """Create a standardized error response."""
220
- response = JSONResponse(
221
- status_code=status_code,
222
- content=error_data
223
- )
224
-
225
- if headers:
226
- for key, value in headers.items():
227
- response.headers[key] = value
228
-
229
- return response
230
-
231
- async def __call__(self, request: Request, call_next):
232
- """Middleware callable for FastAPI."""
233
- # Process request
234
- is_allowed, error_response, rate_limit_headers = await self.process_request(request)
235
-
236
- if not is_allowed:
237
- status_code = 429 if error_response.get("code") == "rate_limit_exceeded" else 401
238
- return self.create_error_response(error_response, status_code, rate_limit_headers)
239
-
240
- # Continue with request
241
- response = await call_next(request)
242
-
243
- # Add rate limit headers to response
244
- if rate_limit_headers:
245
- for key, value in rate_limit_headers.items():
246
- response.headers[key] = value
247
-
248
- return response
webscout/auth/models.py DELETED
@@ -1,185 +0,0 @@
1
- # webscout/auth/models.py
2
-
3
- from datetime import datetime, timezone
4
- from typing import Optional, Dict, Any, List
5
- from dataclasses import dataclass, field
6
- import uuid
7
- import json
8
-
9
-
10
- @dataclass
11
- class User:
12
- """User model for authentication system."""
13
- id: str = field(default_factory=lambda: str(uuid.uuid4()))
14
- username: str = ""
15
- telegram_id: int = field(default_factory=lambda: 0) # Required Telegram ID as number only
16
-
17
- def validate_telegram_id(self) -> None:
18
- """Ensure telegram_id is an integer."""
19
- if not isinstance(self.telegram_id, int):
20
- raise ValueError("telegram_id must be an integer.")
21
- created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
22
- updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
23
- is_active: bool = True
24
- metadata: Dict[str, Any] = field(default_factory=dict)
25
-
26
- def to_dict(self) -> Dict[str, Any]:
27
- """Convert user to dictionary for storage."""
28
- return {
29
- "id": self.id,
30
- "username": self.username,
31
- "telegram_id": self.telegram_id,
32
- "created_at": self.created_at.isoformat(),
33
- "updated_at": self.updated_at.isoformat(),
34
- "is_active": self.is_active,
35
- "metadata": self.metadata
36
- }
37
-
38
- @classmethod
39
- def from_dict(cls, data: Dict[str, Any]) -> "User":
40
- """Create user from dictionary."""
41
- return cls(
42
- id=data.get("id", str(uuid.uuid4())),
43
- username=data.get("username", ""),
44
- telegram_id=int(data.get("telegram_id", 0)),
45
- created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
46
- updated_at=datetime.fromisoformat(data.get("updated_at", datetime.now(timezone.utc).isoformat())),
47
- is_active=data.get("is_active", True),
48
- metadata=data.get("metadata", {})
49
- )
50
-
51
-
52
- @dataclass
53
- class APIKey:
54
- """API Key model for authentication system."""
55
- id: str = field(default_factory=lambda: str(uuid.uuid4()))
56
- key: str = ""
57
- user_id: str = ""
58
- name: Optional[str] = None
59
- created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
60
- last_used_at: Optional[datetime] = None
61
- expires_at: Optional[datetime] = None
62
- is_active: bool = True
63
- rate_limit: int = 10 # requests per minute
64
- usage_count: int = 0
65
- metadata: Dict[str, Any] = field(default_factory=dict)
66
-
67
- def to_dict(self) -> Dict[str, Any]:
68
- """Convert API key to dictionary for storage."""
69
- return {
70
- "id": self.id,
71
- "key": self.key,
72
- "user_id": self.user_id,
73
- "name": self.name,
74
- "created_at": self.created_at.isoformat(),
75
- "last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
76
- "expires_at": self.expires_at.isoformat() if self.expires_at else None,
77
- "is_active": self.is_active,
78
- "rate_limit": self.rate_limit,
79
- "usage_count": self.usage_count,
80
- "metadata": self.metadata
81
- }
82
-
83
- @classmethod
84
- def from_dict(cls, data: Dict[str, Any]) -> "APIKey":
85
- """Create API key from dictionary."""
86
- return cls(
87
- id=data.get("id", str(uuid.uuid4())),
88
- key=data.get("key", ""),
89
- user_id=data.get("user_id", ""),
90
- name=data.get("name"),
91
- created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
92
- last_used_at=datetime.fromisoformat(data["last_used_at"]) if data.get("last_used_at") else None,
93
- expires_at=datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None,
94
- is_active=data.get("is_active", True),
95
- rate_limit=data.get("rate_limit", 10),
96
- usage_count=data.get("usage_count", 0),
97
- metadata=data.get("metadata", {})
98
- )
99
-
100
- def is_expired(self) -> bool:
101
- """Check if API key is expired."""
102
- if not self.expires_at:
103
- return False
104
- return datetime.now(timezone.utc) > self.expires_at
105
-
106
- def is_valid(self) -> bool:
107
- """Check if API key is valid (active and not expired)."""
108
- return self.is_active and not self.is_expired()
109
-
110
-
111
- @dataclass
112
- class RateLimitEntry:
113
- """Rate limit tracking entry."""
114
- api_key_id: str
115
- requests: List[datetime] = field(default_factory=list)
116
-
117
- def to_dict(self) -> Dict[str, Any]:
118
- """Convert to dictionary for storage."""
119
- return {
120
- "api_key_id": self.api_key_id,
121
- "requests": [req.isoformat() for req in self.requests]
122
- }
123
-
124
- @classmethod
125
- def from_dict(cls, data: Dict[str, Any]) -> "RateLimitEntry":
126
- """Create from dictionary."""
127
- return cls(
128
- api_key_id=data.get("api_key_id", ""),
129
- requests=[datetime.fromisoformat(req) for req in data.get("requests", [])]
130
- )
131
-
132
-
133
- @dataclass
134
- class RequestLog:
135
- """Request log entry for API usage tracking."""
136
- id: str = field(default_factory=lambda: str(uuid.uuid4()))
137
- request_id: str = ""
138
- ip_address: str = ""
139
- model_used: str = ""
140
- question: str = ""
141
- answer: str = ""
142
- user_id: Optional[str] = None
143
- api_key_id: Optional[str] = None
144
- created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
145
- response_time_ms: Optional[int] = None
146
- status_code: int = 200
147
- error_message: Optional[str] = None
148
- metadata: Dict[str, Any] = field(default_factory=dict)
149
-
150
- def to_dict(self) -> Dict[str, Any]:
151
- """Convert request log to dictionary for storage."""
152
- return {
153
- "id": self.id,
154
- "request_id": self.request_id,
155
- "ip_address": self.ip_address,
156
- "model_used": self.model_used,
157
- "question": self.question,
158
- "answer": self.answer,
159
- "user_id": self.user_id,
160
- "api_key_id": self.api_key_id,
161
- "created_at": self.created_at.isoformat(),
162
- "response_time_ms": self.response_time_ms,
163
- "status_code": self.status_code,
164
- "error_message": self.error_message,
165
- "metadata": self.metadata
166
- }
167
-
168
- @classmethod
169
- def from_dict(cls, data: Dict[str, Any]) -> "RequestLog":
170
- """Create request log from dictionary."""
171
- return cls(
172
- id=data.get("id", str(uuid.uuid4())),
173
- request_id=data.get("request_id", ""),
174
- ip_address=data.get("ip_address", ""),
175
- model_used=data.get("model_used", ""),
176
- question=data.get("question", ""),
177
- answer=data.get("answer", ""),
178
- user_id=data.get("user_id"),
179
- api_key_id=data.get("api_key_id"),
180
- created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
181
- response_time_ms=data.get("response_time_ms"),
182
- status_code=data.get("status_code", 200),
183
- error_message=data.get("error_message"),
184
- metadata=data.get("metadata", {})
185
- )
@@ -1,254 +0,0 @@
1
- # webscout/auth/rate_limiter.py
2
-
3
- import asyncio
4
- from datetime import datetime, timezone, timedelta
5
- from typing import Optional, Tuple
6
- import logging
7
-
8
- from .models import APIKey, RateLimitEntry
9
- from .database import DatabaseManager
10
- from .config import AppConfig
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- class RateLimiter:
16
- """Rate limiter for API requests."""
17
-
18
- def __init__(self, database_manager: DatabaseManager):
19
- self.db = database_manager
20
- self.default_rate_limit = 10 # requests per minute
21
- self.window_size = 60 # 1 minute in seconds
22
-
23
- async def check_rate_limit(self, api_key: APIKey) -> Tuple[bool, dict]:
24
- """
25
- Check if a request is allowed under the rate limit.
26
-
27
- Returns:
28
- Tuple of (is_allowed, rate_limit_info)
29
- """
30
- now = datetime.now(timezone.utc)
31
- window_start = now - timedelta(seconds=self.window_size)
32
-
33
- try:
34
- # Get or create rate limit entry
35
- entry = await self.db.get_rate_limit_entry(api_key.id)
36
-
37
- if not entry:
38
- entry = RateLimitEntry(api_key_id=api_key.id, requests=[])
39
-
40
- # Clean old requests outside the window
41
- entry.requests = [req for req in entry.requests if req > window_start]
42
-
43
- # Check if limit is exceeded
44
- current_count = len(entry.requests)
45
- limit = api_key.rate_limit or self.default_rate_limit
46
-
47
- if current_count >= limit:
48
- # Rate limit exceeded
49
- oldest_request = min(entry.requests) if entry.requests else now
50
- reset_at = oldest_request + timedelta(seconds=self.window_size)
51
- retry_after = int((reset_at - now).total_seconds())
52
-
53
- rate_limit_info = {
54
- "allowed": False,
55
- "limit": limit,
56
- "remaining": 0,
57
- "reset_at": reset_at,
58
- "retry_after": max(retry_after, 1)
59
- }
60
-
61
- logger.warning(f"Rate limit exceeded for API key {api_key.id}: {current_count}/{limit}")
62
- return False, rate_limit_info
63
-
64
- # Add current request
65
- entry.requests.append(now)
66
-
67
- # Update database
68
- await self.db.update_rate_limit_entry(entry)
69
-
70
- # Calculate next reset time
71
- if entry.requests:
72
- oldest_request = min(entry.requests)
73
- reset_at = oldest_request + timedelta(seconds=self.window_size)
74
- else:
75
- reset_at = now + timedelta(seconds=self.window_size)
76
-
77
- rate_limit_info = {
78
- "allowed": True,
79
- "limit": limit,
80
- "remaining": limit - len(entry.requests),
81
- "reset_at": reset_at,
82
- "retry_after": None
83
- }
84
-
85
- return True, rate_limit_info
86
-
87
- except Exception as e:
88
- logger.error(f"Error checking rate limit for API key {api_key.id}: {e}")
89
- # In case of error, allow the request but log the issue
90
- rate_limit_info = {
91
- "allowed": True,
92
- "limit": api_key.rate_limit or self.default_rate_limit,
93
- "remaining": api_key.rate_limit or self.default_rate_limit,
94
- "reset_at": now + timedelta(seconds=self.window_size),
95
- "retry_after": None
96
- }
97
- return True, rate_limit_info
98
-
99
- async def reset_rate_limit(self, api_key_id: str) -> bool:
100
- """Reset rate limit for an API key (admin function)."""
101
- try:
102
- entry = RateLimitEntry(api_key_id=api_key_id, requests=[])
103
- await self.db.update_rate_limit_entry(entry)
104
- logger.info(f"Reset rate limit for API key {api_key_id}")
105
- return True
106
- except Exception as e:
107
- logger.error(f"Error resetting rate limit for API key {api_key_id}: {e}")
108
- return False
109
-
110
- async def get_rate_limit_status(self, api_key: APIKey) -> dict:
111
- """Get current rate limit status without making a request."""
112
- now = datetime.now(timezone.utc)
113
- window_start = now - timedelta(seconds=self.window_size)
114
-
115
- try:
116
- entry = await self.db.get_rate_limit_entry(api_key.id)
117
-
118
- if not entry:
119
- entry = RateLimitEntry(api_key_id=api_key.id, requests=[])
120
-
121
- # Clean old requests
122
- entry.requests = [req for req in entry.requests if req > window_start]
123
-
124
- limit = api_key.rate_limit or self.default_rate_limit
125
- current_count = len(entry.requests)
126
-
127
- # Calculate reset time
128
- if entry.requests:
129
- oldest_request = min(entry.requests)
130
- reset_at = oldest_request + timedelta(seconds=self.window_size)
131
- else:
132
- reset_at = now + timedelta(seconds=self.window_size)
133
-
134
- return {
135
- "limit": limit,
136
- "remaining": max(0, limit - current_count),
137
- "reset_at": reset_at,
138
- "window_size": self.window_size
139
- }
140
-
141
- except Exception as e:
142
- logger.error(f"Error getting rate limit status for API key {api_key.id}: {e}")
143
- return {
144
- "limit": api_key.rate_limit or self.default_rate_limit,
145
- "remaining": api_key.rate_limit or self.default_rate_limit,
146
- "reset_at": now + timedelta(seconds=self.window_size),
147
- "window_size": self.window_size
148
- }
149
-
150
- async def cleanup_old_entries(self) -> int:
151
- """Clean up old rate limit entries (maintenance function)."""
152
- # Remove requests older than the window_size for all rate limit entries
153
- try:
154
- # Try to get all rate limit entries from the database
155
- if hasattr(self.db, 'get_all_rate_limit_entries'):
156
- entries = await self.db.get_all_rate_limit_entries()
157
- else:
158
- logger.warning("Database does not support get_all_rate_limit_entries; cleanup skipped.")
159
- return 0
160
-
161
- now = datetime.now(timezone.utc)
162
- window_start = now - timedelta(seconds=self.window_size)
163
- cleaned = 0
164
-
165
- for entry in entries:
166
- old_count = len(entry.requests)
167
- entry.requests = [req for req in entry.requests if req > window_start]
168
- if len(entry.requests) < old_count:
169
- await self.db.update_rate_limit_entry(entry)
170
- cleaned += 1
171
- logger.info(f"Cleaned up {cleaned} old rate limit entries.")
172
- return cleaned
173
- except Exception as e:
174
- logger.error(f"Error cleaning up old rate limit entries: {e}")
175
- return 0
176
-
177
- async def check_ip_rate_limit(self, client_ip: str) -> Tuple[bool, dict]:
178
- """
179
- Check rate limit for an IP address (used in no-auth mode).
180
-
181
- Returns:
182
- Tuple of (is_allowed, rate_limit_info)
183
- """
184
- now = datetime.now(timezone.utc)
185
- window_start = now - timedelta(seconds=self.window_size)
186
-
187
- # Create a pseudo API key for IP-based rate limiting
188
- ip_key_id = f"ip_{client_ip}"
189
- limit = AppConfig.default_rate_limit
190
-
191
- try:
192
- # Get or create rate limit entry for IP
193
- entry = await self.db.get_rate_limit_entry(ip_key_id)
194
-
195
- if not entry:
196
- entry = RateLimitEntry(api_key_id=ip_key_id, requests=[])
197
-
198
- # Clean old requests outside the window
199
- entry.requests = [req for req in entry.requests if req > window_start]
200
-
201
- # Check if limit is exceeded
202
- current_count = len(entry.requests)
203
-
204
- if current_count >= limit:
205
- # Rate limit exceeded
206
- oldest_request = min(entry.requests) if entry.requests else now
207
- reset_at = oldest_request + timedelta(seconds=self.window_size)
208
- retry_after = int((reset_at - now).total_seconds())
209
-
210
- rate_limit_info = {
211
- "allowed": False,
212
- "limit": limit,
213
- "remaining": 0,
214
- "reset_at": reset_at,
215
- "retry_after": max(retry_after, 1)
216
- }
217
-
218
- logger.warning(f"Rate limit exceeded for IP {client_ip}: {current_count}/{limit}")
219
- return False, rate_limit_info
220
-
221
- # Add current request
222
- entry.requests.append(now)
223
-
224
- # Update database
225
- await self.db.update_rate_limit_entry(entry)
226
-
227
- # Calculate next reset time
228
- if entry.requests:
229
- oldest_request = min(entry.requests)
230
- reset_at = oldest_request + timedelta(seconds=self.window_size)
231
- else:
232
- reset_at = now + timedelta(seconds=self.window_size)
233
-
234
- rate_limit_info = {
235
- "allowed": True,
236
- "limit": limit,
237
- "remaining": limit - len(entry.requests),
238
- "reset_at": reset_at,
239
- "retry_after": None
240
- }
241
-
242
- return True, rate_limit_info
243
-
244
- except Exception as e:
245
- logger.error(f"Error checking rate limit for IP {client_ip}: {e}")
246
- # In case of error, allow the request but log the issue
247
- rate_limit_info = {
248
- "allowed": True,
249
- "limit": limit,
250
- "remaining": limit,
251
- "reset_at": now + timedelta(seconds=self.window_size),
252
- "retry_after": None
253
- }
254
- return True, rate_limit_info