webscout 2025.10.15__py3-none-any.whl → 2025.10.17__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.
- webscout/Extra/YTToolkit/README.md +1 -1
- webscout/Extra/tempmail/README.md +3 -3
- webscout/Provider/ClaudeOnline.py +350 -0
- webscout/Provider/OPENAI/README.md +1 -1
- webscout/Provider/TTI/bing.py +4 -4
- webscout/Provider/TTI/claudeonline.py +315 -0
- webscout/__init__.py +1 -1
- webscout/client.py +4 -5
- webscout/litprinter/__init__.py +0 -42
- webscout/scout/README.md +59 -8
- webscout/scout/core/scout.py +62 -0
- webscout/scout/element.py +251 -45
- webscout/search/__init__.py +3 -4
- webscout/search/engines/bing/images.py +5 -2
- webscout/search/engines/bing/news.py +6 -4
- webscout/search/engines/bing/text.py +5 -2
- webscout/search/engines/yahoo/__init__.py +41 -0
- webscout/search/engines/yahoo/answers.py +16 -0
- webscout/search/engines/yahoo/base.py +34 -0
- webscout/search/engines/yahoo/images.py +324 -0
- webscout/search/engines/yahoo/maps.py +16 -0
- webscout/search/engines/yahoo/news.py +258 -0
- webscout/search/engines/yahoo/suggestions.py +140 -0
- webscout/search/engines/yahoo/text.py +273 -0
- webscout/search/engines/yahoo/translate.py +16 -0
- webscout/search/engines/yahoo/videos.py +302 -0
- webscout/search/engines/yahoo/weather.py +220 -0
- webscout/search/http_client.py +1 -1
- webscout/search/yahoo_main.py +54 -0
- webscout/{auth → server}/__init__.py +2 -23
- webscout/server/config.py +84 -0
- webscout/{auth → server}/request_processing.py +3 -28
- webscout/{auth → server}/routes.py +6 -148
- webscout/server/schemas.py +23 -0
- webscout/{auth → server}/server.py +11 -43
- webscout/server/simple_logger.py +84 -0
- webscout/version.py +1 -1
- webscout/version.py.bak +1 -1
- webscout/zeroart/README.md +17 -9
- webscout/zeroart/__init__.py +78 -6
- webscout/zeroart/effects.py +51 -1
- webscout/zeroart/fonts.py +559 -1
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/METADATA +11 -54
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/RECORD +51 -46
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/entry_points.txt +1 -1
- webscout/Extra/weather.md +0 -281
- webscout/auth/api_key_manager.py +0 -189
- webscout/auth/auth_system.py +0 -85
- webscout/auth/config.py +0 -175
- webscout/auth/database.py +0 -755
- webscout/auth/middleware.py +0 -248
- webscout/auth/models.py +0 -185
- webscout/auth/rate_limiter.py +0 -254
- webscout/auth/schemas.py +0 -103
- webscout/auth/simple_logger.py +0 -236
- webscout/search/engines/yahoo.py +0 -65
- webscout/search/engines/yahoo_news.py +0 -64
- /webscout/{auth → server}/exceptions.py +0 -0
- /webscout/{auth → server}/providers.py +0 -0
- /webscout/{auth → server}/request_models.py +0 -0
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/WHEEL +0 -0
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/top_level.txt +0 -0
webscout/auth/database.py
DELETED
|
@@ -1,755 +0,0 @@
|
|
|
1
|
-
# webscout/auth/database.py
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import asyncio
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
|
-
from typing import Optional, List, Dict, Any, Union
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
import threading
|
|
10
|
-
import logging
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
import motor.motor_asyncio
|
|
14
|
-
HAS_MOTOR = True
|
|
15
|
-
except ImportError:
|
|
16
|
-
HAS_MOTOR = False
|
|
17
|
-
|
|
18
|
-
try:
|
|
19
|
-
from supabase import create_client, Client #type: ignore
|
|
20
|
-
HAS_SUPABASE = True
|
|
21
|
-
except ImportError:
|
|
22
|
-
HAS_SUPABASE = False
|
|
23
|
-
|
|
24
|
-
from .models import User, APIKey, RateLimitEntry, RequestLog
|
|
25
|
-
|
|
26
|
-
logger = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class JSONDatabase:
|
|
30
|
-
"""JSON file-based database fallback."""
|
|
31
|
-
|
|
32
|
-
def __init__(self, data_dir: str = "data"):
|
|
33
|
-
self.data_dir = Path(data_dir)
|
|
34
|
-
self.data_dir.mkdir(exist_ok=True)
|
|
35
|
-
|
|
36
|
-
self.users_file = self.data_dir / "users.json"
|
|
37
|
-
self.api_keys_file = self.data_dir / "api_keys.json"
|
|
38
|
-
self.rate_limits_file = self.data_dir / "rate_limits.json"
|
|
39
|
-
self.request_logs_file = self.data_dir / "request_logs.json"
|
|
40
|
-
|
|
41
|
-
self._lock = threading.RLock()
|
|
42
|
-
|
|
43
|
-
# Initialize files if they don't exist
|
|
44
|
-
for file_path in [self.users_file, self.api_keys_file, self.rate_limits_file, self.request_logs_file]:
|
|
45
|
-
if not file_path.exists():
|
|
46
|
-
self._write_json(file_path, [])
|
|
47
|
-
|
|
48
|
-
def _read_json(self, file_path: Path) -> List[Dict[str, Any]]:
|
|
49
|
-
"""Read JSON file safely."""
|
|
50
|
-
try:
|
|
51
|
-
with open(file_path, 'r', encoding='utf-8') as f:
|
|
52
|
-
return json.load(f)
|
|
53
|
-
except (FileNotFoundError, json.JSONDecodeError):
|
|
54
|
-
return []
|
|
55
|
-
|
|
56
|
-
def _write_json(self, file_path: Path, data: List[Dict[str, Any]]) -> None:
|
|
57
|
-
"""Write JSON file safely."""
|
|
58
|
-
with self._lock:
|
|
59
|
-
# Write to temporary file first, then rename for atomicity
|
|
60
|
-
temp_file = file_path.with_suffix('.tmp')
|
|
61
|
-
try:
|
|
62
|
-
with open(temp_file, 'w', encoding='utf-8') as f:
|
|
63
|
-
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
64
|
-
temp_file.replace(file_path)
|
|
65
|
-
except Exception as e:
|
|
66
|
-
if temp_file.exists():
|
|
67
|
-
temp_file.unlink()
|
|
68
|
-
raise e
|
|
69
|
-
|
|
70
|
-
async def create_user(self, user: User) -> User:
|
|
71
|
-
"""Create a new user."""
|
|
72
|
-
users = self._read_json(self.users_file)
|
|
73
|
-
|
|
74
|
-
# Check if user already exists
|
|
75
|
-
for existing_user in users:
|
|
76
|
-
if existing_user.get("username") == user.username:
|
|
77
|
-
raise ValueError(f"User with username '{user.username}' already exists")
|
|
78
|
-
|
|
79
|
-
users.append(user.to_dict())
|
|
80
|
-
self._write_json(self.users_file, users)
|
|
81
|
-
return user
|
|
82
|
-
|
|
83
|
-
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
84
|
-
"""Get user by ID."""
|
|
85
|
-
users = self._read_json(self.users_file)
|
|
86
|
-
for user_data in users:
|
|
87
|
-
if user_data.get("id") == user_id:
|
|
88
|
-
return User.from_dict(user_data)
|
|
89
|
-
return None
|
|
90
|
-
|
|
91
|
-
async def get_user_by_username(self, username: str) -> Optional[User]:
|
|
92
|
-
"""Get user by username."""
|
|
93
|
-
users = self._read_json(self.users_file)
|
|
94
|
-
for user_data in users:
|
|
95
|
-
if user_data.get("username") == username:
|
|
96
|
-
return User.from_dict(user_data)
|
|
97
|
-
return None
|
|
98
|
-
|
|
99
|
-
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
|
100
|
-
"""Get user by Telegram ID."""
|
|
101
|
-
users = self._read_json(self.users_file)
|
|
102
|
-
for user_data in users:
|
|
103
|
-
if user_data.get("telegram_id") == telegram_id:
|
|
104
|
-
return User.from_dict(user_data)
|
|
105
|
-
return None
|
|
106
|
-
|
|
107
|
-
async def create_api_key(self, api_key: APIKey) -> APIKey:
|
|
108
|
-
"""Create a new API key."""
|
|
109
|
-
api_keys = self._read_json(self.api_keys_file)
|
|
110
|
-
|
|
111
|
-
# Check if key already exists
|
|
112
|
-
for existing_key in api_keys:
|
|
113
|
-
if existing_key.get("key") == api_key.key:
|
|
114
|
-
raise ValueError("API key already exists")
|
|
115
|
-
|
|
116
|
-
api_keys.append(api_key.to_dict())
|
|
117
|
-
self._write_json(self.api_keys_file, api_keys)
|
|
118
|
-
return api_key
|
|
119
|
-
|
|
120
|
-
async def get_api_key(self, key: str) -> Optional[APIKey]:
|
|
121
|
-
"""Get API key by key value."""
|
|
122
|
-
api_keys = self._read_json(self.api_keys_file)
|
|
123
|
-
for key_data in api_keys:
|
|
124
|
-
if key_data.get("key") == key:
|
|
125
|
-
return APIKey.from_dict(key_data)
|
|
126
|
-
return None
|
|
127
|
-
|
|
128
|
-
async def update_api_key(self, api_key: APIKey) -> APIKey:
|
|
129
|
-
"""Update an existing API key."""
|
|
130
|
-
api_keys = self._read_json(self.api_keys_file)
|
|
131
|
-
|
|
132
|
-
for i, key_data in enumerate(api_keys):
|
|
133
|
-
if key_data.get("id") == api_key.id:
|
|
134
|
-
api_keys[i] = api_key.to_dict()
|
|
135
|
-
self._write_json(self.api_keys_file, api_keys)
|
|
136
|
-
return api_key
|
|
137
|
-
|
|
138
|
-
raise ValueError(f"API key with ID '{api_key.id}' not found")
|
|
139
|
-
|
|
140
|
-
async def get_api_keys_by_user(self, user_id: str) -> List[APIKey]:
|
|
141
|
-
"""Get all API keys for a user."""
|
|
142
|
-
api_keys = self._read_json(self.api_keys_file)
|
|
143
|
-
user_keys = []
|
|
144
|
-
|
|
145
|
-
for key_data in api_keys:
|
|
146
|
-
if key_data.get("user_id") == user_id:
|
|
147
|
-
user_keys.append(APIKey.from_dict(key_data))
|
|
148
|
-
|
|
149
|
-
return user_keys
|
|
150
|
-
|
|
151
|
-
async def get_rate_limit_entry(self, api_key_id: str) -> Optional[RateLimitEntry]:
|
|
152
|
-
"""Get rate limit entry for API key."""
|
|
153
|
-
rate_limits = self._read_json(self.rate_limits_file)
|
|
154
|
-
|
|
155
|
-
for entry_data in rate_limits:
|
|
156
|
-
if entry_data.get("api_key_id") == api_key_id:
|
|
157
|
-
return RateLimitEntry.from_dict(entry_data)
|
|
158
|
-
|
|
159
|
-
return None
|
|
160
|
-
|
|
161
|
-
async def update_rate_limit_entry(self, entry: RateLimitEntry) -> RateLimitEntry:
|
|
162
|
-
"""Update rate limit entry."""
|
|
163
|
-
rate_limits = self._read_json(self.rate_limits_file)
|
|
164
|
-
|
|
165
|
-
for i, entry_data in enumerate(rate_limits):
|
|
166
|
-
if entry_data.get("api_key_id") == entry.api_key_id:
|
|
167
|
-
rate_limits[i] = entry.to_dict()
|
|
168
|
-
self._write_json(self.rate_limits_file, rate_limits)
|
|
169
|
-
return entry
|
|
170
|
-
|
|
171
|
-
# Create new entry if not found
|
|
172
|
-
rate_limits.append(entry.to_dict())
|
|
173
|
-
self._write_json(self.rate_limits_file, rate_limits)
|
|
174
|
-
return entry
|
|
175
|
-
|
|
176
|
-
async def get_all_rate_limit_entries(self) -> list:
|
|
177
|
-
"""Return all rate limit entries (for maintenance/cleanup)."""
|
|
178
|
-
# Only for JSONDatabase
|
|
179
|
-
entries = self._read_json(self.rate_limits_file)
|
|
180
|
-
return [RateLimitEntry.from_dict(e) for e in entries]
|
|
181
|
-
|
|
182
|
-
async def create_request_log(self, request_log: RequestLog) -> RequestLog:
|
|
183
|
-
"""Create a new request log entry."""
|
|
184
|
-
request_logs = self._read_json(self.request_logs_file)
|
|
185
|
-
request_logs.append(request_log.to_dict())
|
|
186
|
-
self._write_json(self.request_logs_file, request_logs)
|
|
187
|
-
return request_log
|
|
188
|
-
|
|
189
|
-
async def get_request_logs(self, limit: int = 100, offset: int = 0) -> List[RequestLog]:
|
|
190
|
-
"""Get request logs with pagination."""
|
|
191
|
-
request_logs = self._read_json(self.request_logs_file)
|
|
192
|
-
# Sort by created_at descending (newest first)
|
|
193
|
-
request_logs.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
|
194
|
-
# Apply pagination
|
|
195
|
-
paginated_logs = request_logs[offset:offset + limit]
|
|
196
|
-
return [RequestLog.from_dict(log_data) for log_data in paginated_logs]
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
class MongoDatabase:
|
|
200
|
-
"""MongoDB database implementation."""
|
|
201
|
-
|
|
202
|
-
def __init__(self, connection_string: str = "mongodb://localhost:27017", database_name: str = "webscout"):
|
|
203
|
-
self.connection_string = connection_string
|
|
204
|
-
self.database_name = database_name
|
|
205
|
-
self.client = None
|
|
206
|
-
self.db = None
|
|
207
|
-
self._connected = False
|
|
208
|
-
|
|
209
|
-
async def connect(self) -> bool:
|
|
210
|
-
"""Connect to MongoDB."""
|
|
211
|
-
if not HAS_MOTOR:
|
|
212
|
-
logger.warning("motor package not available, cannot connect to MongoDB")
|
|
213
|
-
return False
|
|
214
|
-
|
|
215
|
-
try:
|
|
216
|
-
self.client = motor.motor_asyncio.AsyncIOMotorClient(self.connection_string)
|
|
217
|
-
self.db = self.client[self.database_name]
|
|
218
|
-
|
|
219
|
-
# Test connection
|
|
220
|
-
await self.client.admin.command('ping')
|
|
221
|
-
self._connected = True
|
|
222
|
-
logger.info("Connected to MongoDB successfully")
|
|
223
|
-
return True
|
|
224
|
-
except Exception as e:
|
|
225
|
-
logger.warning(f"Failed to connect to MongoDB: {e}")
|
|
226
|
-
self._connected = False
|
|
227
|
-
return False
|
|
228
|
-
|
|
229
|
-
async def create_user(self, user: User) -> User:
|
|
230
|
-
"""Create a new user."""
|
|
231
|
-
if not self._connected:
|
|
232
|
-
raise RuntimeError("Database not connected")
|
|
233
|
-
|
|
234
|
-
# Check if user exists
|
|
235
|
-
existing = await self.db.users.find_one({"username": user.username})
|
|
236
|
-
if existing:
|
|
237
|
-
raise ValueError(f"User with username '{user.username}' already exists")
|
|
238
|
-
|
|
239
|
-
await self.db.users.insert_one(user.to_dict())
|
|
240
|
-
return user
|
|
241
|
-
|
|
242
|
-
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
243
|
-
"""Get user by ID."""
|
|
244
|
-
if not self._connected:
|
|
245
|
-
raise RuntimeError("Database not connected")
|
|
246
|
-
|
|
247
|
-
user_data = await self.db.users.find_one({"id": user_id})
|
|
248
|
-
return User.from_dict(user_data) if user_data else None
|
|
249
|
-
|
|
250
|
-
async def get_user_by_username(self, username: str) -> Optional[User]:
|
|
251
|
-
"""Get user by username."""
|
|
252
|
-
if not self._connected:
|
|
253
|
-
raise RuntimeError("Database not connected")
|
|
254
|
-
|
|
255
|
-
user_data = await self.db.users.find_one({"username": username})
|
|
256
|
-
return User.from_dict(user_data) if user_data else None
|
|
257
|
-
|
|
258
|
-
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
|
259
|
-
"""Get user by Telegram ID."""
|
|
260
|
-
if not self._connected:
|
|
261
|
-
raise RuntimeError("Database not connected")
|
|
262
|
-
|
|
263
|
-
user_data = await self.db.users.find_one({"telegram_id": telegram_id})
|
|
264
|
-
return User.from_dict(user_data) if user_data else None
|
|
265
|
-
|
|
266
|
-
async def create_api_key(self, api_key: APIKey) -> APIKey:
|
|
267
|
-
"""Create a new API key."""
|
|
268
|
-
if not self._connected:
|
|
269
|
-
raise RuntimeError("Database not connected")
|
|
270
|
-
|
|
271
|
-
# Check if key exists
|
|
272
|
-
existing = await self.db.api_keys.find_one({"key": api_key.key})
|
|
273
|
-
if existing:
|
|
274
|
-
raise ValueError("API key already exists")
|
|
275
|
-
|
|
276
|
-
await self.db.api_keys.insert_one(api_key.to_dict())
|
|
277
|
-
return api_key
|
|
278
|
-
|
|
279
|
-
async def get_api_key(self, key: str) -> Optional[APIKey]:
|
|
280
|
-
"""Get API key by key value."""
|
|
281
|
-
if not self._connected:
|
|
282
|
-
raise RuntimeError("Database not connected")
|
|
283
|
-
|
|
284
|
-
key_data = await self.db.api_keys.find_one({"key": key})
|
|
285
|
-
return APIKey.from_dict(key_data) if key_data else None
|
|
286
|
-
|
|
287
|
-
async def update_api_key(self, api_key: APIKey) -> APIKey:
|
|
288
|
-
"""Update an existing API key."""
|
|
289
|
-
if not self._connected:
|
|
290
|
-
raise RuntimeError("Database not connected")
|
|
291
|
-
|
|
292
|
-
result = await self.db.api_keys.update_one(
|
|
293
|
-
{"id": api_key.id},
|
|
294
|
-
{"$set": api_key.to_dict()}
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
if result.matched_count == 0:
|
|
298
|
-
raise ValueError(f"API key with ID '{api_key.id}' not found")
|
|
299
|
-
|
|
300
|
-
return api_key
|
|
301
|
-
|
|
302
|
-
async def get_api_keys_by_user(self, user_id: str) -> List[APIKey]:
|
|
303
|
-
"""Get all API keys for a user."""
|
|
304
|
-
if not self._connected:
|
|
305
|
-
raise RuntimeError("Database not connected")
|
|
306
|
-
|
|
307
|
-
cursor = self.db.api_keys.find({"user_id": user_id})
|
|
308
|
-
keys = []
|
|
309
|
-
async for key_data in cursor:
|
|
310
|
-
keys.append(APIKey.from_dict(key_data))
|
|
311
|
-
return keys
|
|
312
|
-
|
|
313
|
-
async def get_rate_limit_entry(self, api_key_id: str) -> Optional[RateLimitEntry]:
|
|
314
|
-
"""Get rate limit entry for API key."""
|
|
315
|
-
if not self._connected:
|
|
316
|
-
raise RuntimeError("Database not connected")
|
|
317
|
-
|
|
318
|
-
entry_data = await self.db.rate_limits.find_one({"api_key_id": api_key_id})
|
|
319
|
-
return RateLimitEntry.from_dict(entry_data) if entry_data else None
|
|
320
|
-
|
|
321
|
-
async def update_rate_limit_entry(self, entry: RateLimitEntry) -> RateLimitEntry:
|
|
322
|
-
"""Update rate limit entry."""
|
|
323
|
-
if not self._connected:
|
|
324
|
-
raise RuntimeError("Database not connected")
|
|
325
|
-
|
|
326
|
-
await self.db.rate_limits.update_one(
|
|
327
|
-
{"api_key_id": entry.api_key_id},
|
|
328
|
-
{"$set": entry.to_dict()},
|
|
329
|
-
upsert=True
|
|
330
|
-
)
|
|
331
|
-
return entry
|
|
332
|
-
|
|
333
|
-
async def get_all_rate_limit_entries(self) -> list:
|
|
334
|
-
"""Return all rate limit entries (for maintenance/cleanup) from MongoDB."""
|
|
335
|
-
if not self._connected:
|
|
336
|
-
raise RuntimeError("Database not connected")
|
|
337
|
-
entries = []
|
|
338
|
-
cursor = self.db.rate_limits.find({})
|
|
339
|
-
async for entry_data in cursor:
|
|
340
|
-
entries.append(RateLimitEntry.from_dict(entry_data))
|
|
341
|
-
return entries
|
|
342
|
-
|
|
343
|
-
async def create_request_log(self, request_log: RequestLog) -> RequestLog:
|
|
344
|
-
"""Create a new request log entry."""
|
|
345
|
-
if not self._connected:
|
|
346
|
-
raise RuntimeError("Database not connected")
|
|
347
|
-
|
|
348
|
-
await self.db.request_logs.insert_one(request_log.to_dict())
|
|
349
|
-
return request_log
|
|
350
|
-
|
|
351
|
-
async def get_request_logs(self, limit: int = 100, offset: int = 0) -> List[RequestLog]:
|
|
352
|
-
"""Get request logs with pagination."""
|
|
353
|
-
if not self._connected:
|
|
354
|
-
raise RuntimeError("Database not connected")
|
|
355
|
-
|
|
356
|
-
cursor = self.db.request_logs.find({}).sort("created_at", -1).skip(offset).limit(limit)
|
|
357
|
-
logs = []
|
|
358
|
-
async for log_data in cursor:
|
|
359
|
-
logs.append(RequestLog.from_dict(log_data))
|
|
360
|
-
return logs
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
class SupabaseDatabase:
|
|
364
|
-
"""Supabase database implementation."""
|
|
365
|
-
|
|
366
|
-
def __init__(self, supabase_url: str, supabase_key: str):
|
|
367
|
-
self.supabase_url = supabase_url
|
|
368
|
-
self.supabase_key = supabase_key
|
|
369
|
-
self.client: Optional[Client] = None
|
|
370
|
-
self._connected = False
|
|
371
|
-
|
|
372
|
-
async def connect(self) -> bool:
|
|
373
|
-
"""Connect to Supabase."""
|
|
374
|
-
if not HAS_SUPABASE:
|
|
375
|
-
logger.warning("supabase package not available, cannot connect to Supabase")
|
|
376
|
-
return False
|
|
377
|
-
|
|
378
|
-
try:
|
|
379
|
-
self.client = create_client(self.supabase_url, self.supabase_key)
|
|
380
|
-
# Test connection by trying to access a table
|
|
381
|
-
self.client.table('users').select('id').limit(1).execute()
|
|
382
|
-
self._connected = True
|
|
383
|
-
logger.info("Connected to Supabase successfully")
|
|
384
|
-
return True
|
|
385
|
-
except Exception as e:
|
|
386
|
-
logger.warning(f"Failed to connect to Supabase: {e}")
|
|
387
|
-
self._connected = False
|
|
388
|
-
return False
|
|
389
|
-
|
|
390
|
-
async def create_user(self, user: User) -> User:
|
|
391
|
-
"""Create a new user."""
|
|
392
|
-
if not self._connected:
|
|
393
|
-
raise RuntimeError("Database not connected")
|
|
394
|
-
|
|
395
|
-
try:
|
|
396
|
-
result = self.client.table('users').insert(user.to_dict()).execute()
|
|
397
|
-
if result.data:
|
|
398
|
-
return user
|
|
399
|
-
else:
|
|
400
|
-
raise ValueError("Failed to create user")
|
|
401
|
-
except Exception as e:
|
|
402
|
-
if "duplicate key" in str(e).lower() or "already exists" in str(e).lower():
|
|
403
|
-
raise ValueError(f"User with username '{user.username}' already exists")
|
|
404
|
-
raise e
|
|
405
|
-
|
|
406
|
-
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
407
|
-
"""Get user by ID."""
|
|
408
|
-
if not self._connected:
|
|
409
|
-
raise RuntimeError("Database not connected")
|
|
410
|
-
|
|
411
|
-
try:
|
|
412
|
-
result = self.client.table('users').select('*').eq('id', user_id).execute()
|
|
413
|
-
if result.data:
|
|
414
|
-
return User.from_dict(result.data[0])
|
|
415
|
-
return None
|
|
416
|
-
except Exception as e:
|
|
417
|
-
logger.error(f"Error getting user by ID: {e}")
|
|
418
|
-
return None
|
|
419
|
-
|
|
420
|
-
async def get_user_by_username(self, username: str) -> Optional[User]:
|
|
421
|
-
"""Get user by username."""
|
|
422
|
-
if not self._connected:
|
|
423
|
-
raise RuntimeError("Database not connected")
|
|
424
|
-
|
|
425
|
-
try:
|
|
426
|
-
result = self.client.table('users').select('*').eq('username', username).execute()
|
|
427
|
-
if result.data:
|
|
428
|
-
return User.from_dict(result.data[0])
|
|
429
|
-
return None
|
|
430
|
-
except Exception as e:
|
|
431
|
-
logger.error(f"Error getting user by username: {e}")
|
|
432
|
-
return None
|
|
433
|
-
|
|
434
|
-
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
|
435
|
-
"""Get user by Telegram ID."""
|
|
436
|
-
if not self._connected:
|
|
437
|
-
raise RuntimeError("Database not connected")
|
|
438
|
-
|
|
439
|
-
try:
|
|
440
|
-
result = self.client.table('users').select('*').eq('telegram_id', int(telegram_id)).execute()
|
|
441
|
-
if result.data:
|
|
442
|
-
return User.from_dict(result.data[0])
|
|
443
|
-
return None
|
|
444
|
-
except Exception as e:
|
|
445
|
-
logger.error(f"Error getting user by telegram_id: {e}")
|
|
446
|
-
return None
|
|
447
|
-
|
|
448
|
-
async def create_api_key(self, api_key: APIKey) -> APIKey:
|
|
449
|
-
"""Create a new API key."""
|
|
450
|
-
if not self._connected:
|
|
451
|
-
raise RuntimeError("Database not connected")
|
|
452
|
-
|
|
453
|
-
try:
|
|
454
|
-
result = self.client.table('api_keys').insert(api_key.to_dict()).execute()
|
|
455
|
-
if result.data:
|
|
456
|
-
return api_key
|
|
457
|
-
else:
|
|
458
|
-
raise ValueError("Failed to create API key")
|
|
459
|
-
except Exception as e:
|
|
460
|
-
if "duplicate key" in str(e).lower() or "already exists" in str(e).lower():
|
|
461
|
-
raise ValueError("API key already exists")
|
|
462
|
-
raise e
|
|
463
|
-
|
|
464
|
-
async def get_api_key(self, key: str) -> Optional[APIKey]:
|
|
465
|
-
"""Get API key by key value."""
|
|
466
|
-
if not self._connected:
|
|
467
|
-
raise RuntimeError("Database not connected")
|
|
468
|
-
|
|
469
|
-
try:
|
|
470
|
-
result = self.client.table('api_keys').select('*').eq('key', key).execute()
|
|
471
|
-
if result.data:
|
|
472
|
-
return APIKey.from_dict(result.data[0])
|
|
473
|
-
return None
|
|
474
|
-
except Exception as e:
|
|
475
|
-
logger.error(f"Error getting API key: {e}")
|
|
476
|
-
return None
|
|
477
|
-
|
|
478
|
-
async def update_api_key(self, api_key: APIKey) -> APIKey:
|
|
479
|
-
"""Update an existing API key."""
|
|
480
|
-
if not self._connected:
|
|
481
|
-
raise RuntimeError("Database not connected")
|
|
482
|
-
|
|
483
|
-
try:
|
|
484
|
-
result = self.client.table('api_keys').update(api_key.to_dict()).eq('id', api_key.id).execute()
|
|
485
|
-
if result.data:
|
|
486
|
-
return api_key
|
|
487
|
-
else:
|
|
488
|
-
raise ValueError(f"API key with ID '{api_key.id}' not found")
|
|
489
|
-
except Exception as e:
|
|
490
|
-
logger.error(f"Error updating API key: {e}")
|
|
491
|
-
raise e
|
|
492
|
-
|
|
493
|
-
async def get_api_keys_by_user(self, user_id: str) -> List[APIKey]:
|
|
494
|
-
"""Get all API keys for a user."""
|
|
495
|
-
if not self._connected:
|
|
496
|
-
raise RuntimeError("Database not connected")
|
|
497
|
-
|
|
498
|
-
try:
|
|
499
|
-
result = self.client.table('api_keys').select('*').eq('user_id', user_id).execute()
|
|
500
|
-
return [APIKey.from_dict(key_data) for key_data in result.data]
|
|
501
|
-
except Exception as e:
|
|
502
|
-
logger.error(f"Error getting API keys by user: {e}")
|
|
503
|
-
return []
|
|
504
|
-
|
|
505
|
-
async def get_rate_limit_entry(self, api_key_id: str) -> Optional[RateLimitEntry]:
|
|
506
|
-
"""Get rate limit entry for API key."""
|
|
507
|
-
if not self._connected:
|
|
508
|
-
raise RuntimeError("Database not connected")
|
|
509
|
-
|
|
510
|
-
try:
|
|
511
|
-
result = self.client.table('rate_limits').select('*').eq('api_key_id', api_key_id).execute()
|
|
512
|
-
if result.data:
|
|
513
|
-
return RateLimitEntry.from_dict(result.data[0])
|
|
514
|
-
return None
|
|
515
|
-
except Exception as e:
|
|
516
|
-
logger.error(f"Error getting rate limit entry: {e}")
|
|
517
|
-
return None
|
|
518
|
-
|
|
519
|
-
async def update_rate_limit_entry(self, entry: RateLimitEntry) -> RateLimitEntry:
|
|
520
|
-
"""Update rate limit entry."""
|
|
521
|
-
if not self._connected:
|
|
522
|
-
raise RuntimeError("Database not connected")
|
|
523
|
-
|
|
524
|
-
try:
|
|
525
|
-
# Try to update first
|
|
526
|
-
result = self.client.table('rate_limits').update(entry.to_dict()).eq('api_key_id', entry.api_key_id).execute()
|
|
527
|
-
if not result.data:
|
|
528
|
-
# If no rows were updated, insert new entry
|
|
529
|
-
result = self.client.table('rate_limits').insert(entry.to_dict()).execute()
|
|
530
|
-
return entry
|
|
531
|
-
except Exception as e:
|
|
532
|
-
logger.error(f"Error updating rate limit entry: {e}")
|
|
533
|
-
raise e
|
|
534
|
-
|
|
535
|
-
async def get_all_rate_limit_entries(self) -> list:
|
|
536
|
-
"""Return all rate limit entries (for maintenance/cleanup) from Supabase."""
|
|
537
|
-
if not self._connected:
|
|
538
|
-
raise RuntimeError("Database not connected")
|
|
539
|
-
|
|
540
|
-
try:
|
|
541
|
-
result = self.client.table('rate_limits').select('*').execute()
|
|
542
|
-
return [RateLimitEntry.from_dict(entry_data) for entry_data in result.data]
|
|
543
|
-
except Exception as e:
|
|
544
|
-
logger.error(f"Error getting all rate limit entries: {e}")
|
|
545
|
-
return []
|
|
546
|
-
|
|
547
|
-
async def create_request_log(self, request_log: RequestLog) -> RequestLog:
|
|
548
|
-
"""Create a new request log entry."""
|
|
549
|
-
if not self._connected:
|
|
550
|
-
raise RuntimeError("Database not connected")
|
|
551
|
-
|
|
552
|
-
try:
|
|
553
|
-
result = self.client.table('request_logs').insert(request_log.to_dict()).execute()
|
|
554
|
-
if result.data:
|
|
555
|
-
return request_log
|
|
556
|
-
else:
|
|
557
|
-
raise ValueError("Failed to create request log")
|
|
558
|
-
except Exception as e:
|
|
559
|
-
logger.error(f"Error creating request log: {e}")
|
|
560
|
-
raise e
|
|
561
|
-
|
|
562
|
-
async def get_request_logs(self, limit: int = 100, offset: int = 0) -> List[RequestLog]:
|
|
563
|
-
"""Get request logs with pagination."""
|
|
564
|
-
if not self._connected:
|
|
565
|
-
raise RuntimeError("Database not connected")
|
|
566
|
-
|
|
567
|
-
try:
|
|
568
|
-
result = self.client.table('request_logs').select('*').order('created_at', desc=True).range(offset, offset + limit - 1).execute()
|
|
569
|
-
return [RequestLog.from_dict(log_data) for log_data in result.data]
|
|
570
|
-
except Exception as e:
|
|
571
|
-
logger.error(f"Error getting request logs: {e}")
|
|
572
|
-
return []
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
class DatabaseManager:
|
|
576
|
-
"""Database manager with flexible backend support."""
|
|
577
|
-
|
|
578
|
-
def __init__(self, mongo_connection_string: Optional[str] = None, data_dir: str = "data"):
|
|
579
|
-
self.mongo_connection_string = mongo_connection_string
|
|
580
|
-
self.data_dir = data_dir
|
|
581
|
-
self.supabase_url = self._get_supabase_url()
|
|
582
|
-
self.supabase_key = self._get_supabase_key()
|
|
583
|
-
|
|
584
|
-
# Database instances
|
|
585
|
-
self.supabase_db = None
|
|
586
|
-
self.mongo_db = None
|
|
587
|
-
self.json_db = None
|
|
588
|
-
self.active_db = None
|
|
589
|
-
|
|
590
|
-
logger.info("🔗 Database manager initialized with flexible backend support")
|
|
591
|
-
|
|
592
|
-
def _get_supabase_url(self) -> Optional[str]:
|
|
593
|
-
"""Get Supabase URL from environment variables or GitHub secrets."""
|
|
594
|
-
# Try environment variable first
|
|
595
|
-
url = os.getenv("SUPABASE_URL")
|
|
596
|
-
if url:
|
|
597
|
-
return url
|
|
598
|
-
|
|
599
|
-
# Try to get from GitHub secrets (if running in GitHub Actions)
|
|
600
|
-
github_url = os.getenv("GITHUB_SUPABASE_URL") # GitHub Actions secret
|
|
601
|
-
if github_url:
|
|
602
|
-
return github_url
|
|
603
|
-
|
|
604
|
-
# Don't log error during initialization - only when actually needed
|
|
605
|
-
return None
|
|
606
|
-
|
|
607
|
-
def _get_supabase_key(self) -> Optional[str]:
|
|
608
|
-
"""Get Supabase key from environment variables or GitHub secrets."""
|
|
609
|
-
# Try environment variable first
|
|
610
|
-
key = os.getenv("SUPABASE_ANON_KEY")
|
|
611
|
-
if key:
|
|
612
|
-
return key
|
|
613
|
-
|
|
614
|
-
# Try to get from GitHub secrets (if running in GitHub Actions)
|
|
615
|
-
github_key = os.getenv("GITHUB_SUPABASE_ANON_KEY") # GitHub Actions secret
|
|
616
|
-
if github_key:
|
|
617
|
-
return github_key
|
|
618
|
-
|
|
619
|
-
# Don't log error during initialization - only when actually needed
|
|
620
|
-
return None
|
|
621
|
-
|
|
622
|
-
async def initialize(self) -> None:
|
|
623
|
-
"""Initialize database connection with fallback support."""
|
|
624
|
-
# Try Supabase first if credentials are available
|
|
625
|
-
if self.supabase_url and self.supabase_key:
|
|
626
|
-
logger.info("🔗 Attempting to connect to Supabase...")
|
|
627
|
-
try:
|
|
628
|
-
self.supabase_db = SupabaseDatabase(self.supabase_url, self.supabase_key)
|
|
629
|
-
connected = await self.supabase_db.connect()
|
|
630
|
-
|
|
631
|
-
if connected:
|
|
632
|
-
self.active_db = self.supabase_db
|
|
633
|
-
logger.info("✅ Successfully connected to Supabase database")
|
|
634
|
-
return
|
|
635
|
-
else:
|
|
636
|
-
logger.warning("⚠️ Failed to connect to Supabase, trying fallbacks...")
|
|
637
|
-
except Exception as e:
|
|
638
|
-
logger.warning(f"⚠️ Supabase connection error: {e}, trying fallbacks...")
|
|
639
|
-
else:
|
|
640
|
-
logger.info("ℹ️ Supabase credentials not found, using fallback databases")
|
|
641
|
-
|
|
642
|
-
# Try MongoDB if connection string is provided
|
|
643
|
-
if self.mongo_connection_string:
|
|
644
|
-
logger.info("🔗 Attempting to connect to MongoDB...")
|
|
645
|
-
try:
|
|
646
|
-
self.mongo_db = MongoDatabase(self.mongo_connection_string)
|
|
647
|
-
connected = await self.mongo_db.connect()
|
|
648
|
-
|
|
649
|
-
if connected:
|
|
650
|
-
self.active_db = self.mongo_db
|
|
651
|
-
logger.info("✅ Successfully connected to MongoDB database")
|
|
652
|
-
return
|
|
653
|
-
else:
|
|
654
|
-
logger.warning("⚠️ Failed to connect to MongoDB, falling back to JSON...")
|
|
655
|
-
except Exception as e:
|
|
656
|
-
logger.warning(f"⚠️ MongoDB connection error: {e}, falling back to JSON...")
|
|
657
|
-
|
|
658
|
-
# Fall back to JSON database
|
|
659
|
-
logger.info("📁 Using JSON file database as fallback")
|
|
660
|
-
try:
|
|
661
|
-
self.json_db = JSONDatabase(self.data_dir)
|
|
662
|
-
self.active_db = self.json_db
|
|
663
|
-
logger.info("✅ Successfully initialized JSON database")
|
|
664
|
-
except Exception as e:
|
|
665
|
-
logger.error(f"❌ Failed to initialize JSON database: {e}")
|
|
666
|
-
raise RuntimeError(f"Failed to initialize any database backend: {e}")
|
|
667
|
-
|
|
668
|
-
@property
|
|
669
|
-
def db(self):
|
|
670
|
-
"""Get the active database instance."""
|
|
671
|
-
if not self.active_db:
|
|
672
|
-
raise RuntimeError("Database not initialized. Call initialize() first.")
|
|
673
|
-
return self.active_db
|
|
674
|
-
|
|
675
|
-
async def create_user(self, user: User) -> User:
|
|
676
|
-
"""Create a new user."""
|
|
677
|
-
return await self.db.create_user(user)
|
|
678
|
-
|
|
679
|
-
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
680
|
-
"""Get user by ID."""
|
|
681
|
-
return await self.db.get_user_by_id(user_id)
|
|
682
|
-
|
|
683
|
-
async def get_user_by_username(self, username: str) -> Optional[User]:
|
|
684
|
-
"""Get user by username."""
|
|
685
|
-
return await self.db.get_user_by_username(username)
|
|
686
|
-
|
|
687
|
-
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
|
688
|
-
"""Get user by Telegram ID."""
|
|
689
|
-
return await self.db.get_user_by_telegram_id(telegram_id)
|
|
690
|
-
|
|
691
|
-
async def create_api_key(self, api_key: APIKey) -> APIKey:
|
|
692
|
-
"""Create a new API key."""
|
|
693
|
-
return await self.db.create_api_key(api_key)
|
|
694
|
-
|
|
695
|
-
async def get_api_key(self, key: str) -> Optional[APIKey]:
|
|
696
|
-
"""Get API key by key value."""
|
|
697
|
-
return await self.db.get_api_key(key)
|
|
698
|
-
|
|
699
|
-
async def update_api_key(self, api_key: APIKey) -> APIKey:
|
|
700
|
-
"""Update an existing API key."""
|
|
701
|
-
return await self.db.update_api_key(api_key)
|
|
702
|
-
|
|
703
|
-
async def get_api_keys_by_user(self, user_id: str) -> List[APIKey]:
|
|
704
|
-
"""Get all API keys for a user."""
|
|
705
|
-
return await self.db.get_api_keys_by_user(user_id)
|
|
706
|
-
|
|
707
|
-
async def get_rate_limit_entry(self, api_key_id: str) -> Optional[RateLimitEntry]:
|
|
708
|
-
"""Get rate limit entry for API key."""
|
|
709
|
-
return await self.db.get_rate_limit_entry(api_key_id)
|
|
710
|
-
|
|
711
|
-
async def update_rate_limit_entry(self, entry: RateLimitEntry) -> RateLimitEntry:
|
|
712
|
-
"""Update rate limit entry."""
|
|
713
|
-
return await self.db.update_rate_limit_entry(entry)
|
|
714
|
-
|
|
715
|
-
async def create_request_log(self, request_log: RequestLog) -> RequestLog:
|
|
716
|
-
"""Create a new request log entry."""
|
|
717
|
-
return await self.db.create_request_log(request_log)
|
|
718
|
-
|
|
719
|
-
async def get_request_logs(self, limit: int = 100, offset: int = 0) -> List[RequestLog]:
|
|
720
|
-
"""Get request logs with pagination."""
|
|
721
|
-
return await self.db.get_request_logs(limit, offset)
|
|
722
|
-
|
|
723
|
-
def get_status(self) -> Dict[str, str]:
|
|
724
|
-
"""Get database status."""
|
|
725
|
-
if not self.active_db:
|
|
726
|
-
return {
|
|
727
|
-
"type": "None",
|
|
728
|
-
"status": "not_initialized",
|
|
729
|
-
"message": "Database not initialized"
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if isinstance(self.active_db, SupabaseDatabase):
|
|
733
|
-
return {
|
|
734
|
-
"type": "Supabase",
|
|
735
|
-
"status": "connected" if self.active_db._connected else "disconnected",
|
|
736
|
-
"message": "Using Supabase database"
|
|
737
|
-
}
|
|
738
|
-
elif isinstance(self.active_db, MongoDatabase):
|
|
739
|
-
return {
|
|
740
|
-
"type": "MongoDB",
|
|
741
|
-
"status": "connected" if self.active_db._connected else "disconnected",
|
|
742
|
-
"message": "Using MongoDB database"
|
|
743
|
-
}
|
|
744
|
-
elif isinstance(self.active_db, JSONDatabase):
|
|
745
|
-
return {
|
|
746
|
-
"type": "JSON",
|
|
747
|
-
"status": "connected",
|
|
748
|
-
"message": "Using JSON file database"
|
|
749
|
-
}
|
|
750
|
-
else:
|
|
751
|
-
return {
|
|
752
|
-
"type": "Unknown",
|
|
753
|
-
"status": "unknown",
|
|
754
|
-
"message": "Unknown database type"
|
|
755
|
-
}
|