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.

Files changed (63) hide show
  1. webscout/Extra/YTToolkit/README.md +1 -1
  2. webscout/Extra/tempmail/README.md +3 -3
  3. webscout/Provider/ClaudeOnline.py +350 -0
  4. webscout/Provider/OPENAI/README.md +1 -1
  5. webscout/Provider/TTI/bing.py +4 -4
  6. webscout/Provider/TTI/claudeonline.py +315 -0
  7. webscout/__init__.py +1 -1
  8. webscout/client.py +4 -5
  9. webscout/litprinter/__init__.py +0 -42
  10. webscout/scout/README.md +59 -8
  11. webscout/scout/core/scout.py +62 -0
  12. webscout/scout/element.py +251 -45
  13. webscout/search/__init__.py +3 -4
  14. webscout/search/engines/bing/images.py +5 -2
  15. webscout/search/engines/bing/news.py +6 -4
  16. webscout/search/engines/bing/text.py +5 -2
  17. webscout/search/engines/yahoo/__init__.py +41 -0
  18. webscout/search/engines/yahoo/answers.py +16 -0
  19. webscout/search/engines/yahoo/base.py +34 -0
  20. webscout/search/engines/yahoo/images.py +324 -0
  21. webscout/search/engines/yahoo/maps.py +16 -0
  22. webscout/search/engines/yahoo/news.py +258 -0
  23. webscout/search/engines/yahoo/suggestions.py +140 -0
  24. webscout/search/engines/yahoo/text.py +273 -0
  25. webscout/search/engines/yahoo/translate.py +16 -0
  26. webscout/search/engines/yahoo/videos.py +302 -0
  27. webscout/search/engines/yahoo/weather.py +220 -0
  28. webscout/search/http_client.py +1 -1
  29. webscout/search/yahoo_main.py +54 -0
  30. webscout/{auth → server}/__init__.py +2 -23
  31. webscout/server/config.py +84 -0
  32. webscout/{auth → server}/request_processing.py +3 -28
  33. webscout/{auth → server}/routes.py +6 -148
  34. webscout/server/schemas.py +23 -0
  35. webscout/{auth → server}/server.py +11 -43
  36. webscout/server/simple_logger.py +84 -0
  37. webscout/version.py +1 -1
  38. webscout/version.py.bak +1 -1
  39. webscout/zeroart/README.md +17 -9
  40. webscout/zeroart/__init__.py +78 -6
  41. webscout/zeroart/effects.py +51 -1
  42. webscout/zeroart/fonts.py +559 -1
  43. {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/METADATA +11 -54
  44. {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/RECORD +51 -46
  45. {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/entry_points.txt +1 -1
  46. webscout/Extra/weather.md +0 -281
  47. webscout/auth/api_key_manager.py +0 -189
  48. webscout/auth/auth_system.py +0 -85
  49. webscout/auth/config.py +0 -175
  50. webscout/auth/database.py +0 -755
  51. webscout/auth/middleware.py +0 -248
  52. webscout/auth/models.py +0 -185
  53. webscout/auth/rate_limiter.py +0 -254
  54. webscout/auth/schemas.py +0 -103
  55. webscout/auth/simple_logger.py +0 -236
  56. webscout/search/engines/yahoo.py +0 -65
  57. webscout/search/engines/yahoo_news.py +0 -64
  58. /webscout/{auth → server}/exceptions.py +0 -0
  59. /webscout/{auth → server}/providers.py +0 -0
  60. /webscout/{auth → server}/request_models.py +0 -0
  61. {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/WHEEL +0 -0
  62. {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/licenses/LICENSE.md +0 -0
  63. {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
- }