webscout 8.3.3__py3-none-any.whl → 8.3.5__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 (79) hide show
  1. webscout/AIutel.py +53 -800
  2. webscout/Bard.py +2 -22
  3. webscout/Provider/AISEARCH/__init__.py +11 -10
  4. webscout/Provider/AISEARCH/felo_search.py +7 -3
  5. webscout/Provider/AISEARCH/scira_search.py +26 -11
  6. webscout/Provider/AISEARCH/stellar_search.py +53 -8
  7. webscout/Provider/Deepinfra.py +81 -57
  8. webscout/Provider/ExaChat.py +9 -5
  9. webscout/Provider/Flowith.py +1 -1
  10. webscout/Provider/FreeGemini.py +2 -2
  11. webscout/Provider/Gemini.py +3 -10
  12. webscout/Provider/GeminiProxy.py +31 -5
  13. webscout/Provider/LambdaChat.py +39 -31
  14. webscout/Provider/Netwrck.py +5 -8
  15. webscout/Provider/OLLAMA.py +8 -9
  16. webscout/Provider/OPENAI/README.md +1 -1
  17. webscout/Provider/OPENAI/TogetherAI.py +57 -48
  18. webscout/Provider/OPENAI/TwoAI.py +94 -1
  19. webscout/Provider/OPENAI/__init__.py +1 -3
  20. webscout/Provider/OPENAI/autoproxy.py +1 -1
  21. webscout/Provider/OPENAI/copilot.py +73 -26
  22. webscout/Provider/OPENAI/deepinfra.py +60 -24
  23. webscout/Provider/OPENAI/exachat.py +9 -5
  24. webscout/Provider/OPENAI/monochat.py +3 -3
  25. webscout/Provider/OPENAI/netwrck.py +4 -7
  26. webscout/Provider/OPENAI/qodo.py +630 -0
  27. webscout/Provider/OPENAI/scirachat.py +86 -49
  28. webscout/Provider/OPENAI/textpollinations.py +19 -14
  29. webscout/Provider/OPENAI/venice.py +1 -0
  30. webscout/Provider/Perplexitylabs.py +163 -147
  31. webscout/Provider/Qodo.py +478 -0
  32. webscout/Provider/TTI/__init__.py +1 -0
  33. webscout/Provider/TTI/monochat.py +3 -3
  34. webscout/Provider/TTI/together.py +7 -6
  35. webscout/Provider/TTI/venice.py +368 -0
  36. webscout/Provider/TextPollinationsAI.py +19 -14
  37. webscout/Provider/TogetherAI.py +57 -44
  38. webscout/Provider/TwoAI.py +96 -2
  39. webscout/Provider/TypliAI.py +33 -27
  40. webscout/Provider/UNFINISHED/PERPLEXED_search.py +254 -0
  41. webscout/Provider/UNFINISHED/fetch_together_models.py +6 -11
  42. webscout/Provider/Venice.py +1 -0
  43. webscout/Provider/WiseCat.py +18 -20
  44. webscout/Provider/__init__.py +4 -10
  45. webscout/Provider/copilot.py +58 -61
  46. webscout/Provider/freeaichat.py +64 -55
  47. webscout/Provider/monochat.py +275 -0
  48. webscout/Provider/scira_chat.py +115 -21
  49. webscout/Provider/toolbaz.py +5 -10
  50. webscout/Provider/typefully.py +1 -11
  51. webscout/Provider/x0gpt.py +325 -315
  52. webscout/__init__.py +4 -11
  53. webscout/auth/__init__.py +19 -4
  54. webscout/auth/api_key_manager.py +189 -189
  55. webscout/auth/auth_system.py +25 -40
  56. webscout/auth/config.py +105 -6
  57. webscout/auth/database.py +377 -22
  58. webscout/auth/models.py +185 -130
  59. webscout/auth/request_processing.py +175 -11
  60. webscout/auth/routes.py +119 -5
  61. webscout/auth/server.py +9 -2
  62. webscout/auth/simple_logger.py +236 -0
  63. webscout/sanitize.py +1074 -0
  64. webscout/version.py +1 -1
  65. {webscout-8.3.3.dist-info → webscout-8.3.5.dist-info}/METADATA +9 -150
  66. {webscout-8.3.3.dist-info → webscout-8.3.5.dist-info}/RECORD +70 -72
  67. webscout/Provider/AI21.py +0 -177
  68. webscout/Provider/HuggingFaceChat.py +0 -469
  69. webscout/Provider/OPENAI/README_AUTOPROXY.md +0 -238
  70. webscout/Provider/OPENAI/freeaichat.py +0 -363
  71. webscout/Provider/OPENAI/typegpt.py +0 -368
  72. webscout/Provider/OPENAI/uncovrAI.py +0 -477
  73. webscout/Provider/WritingMate.py +0 -273
  74. webscout/Provider/typegpt.py +0 -284
  75. webscout/Provider/uncovr.py +0 -333
  76. {webscout-8.3.3.dist-info → webscout-8.3.5.dist-info}/WHEEL +0 -0
  77. {webscout-8.3.3.dist-info → webscout-8.3.5.dist-info}/entry_points.txt +0 -0
  78. {webscout-8.3.3.dist-info → webscout-8.3.5.dist-info}/licenses/LICENSE.md +0 -0
  79. {webscout-8.3.3.dist-info → webscout-8.3.5.dist-info}/top_level.txt +0 -0
webscout/auth/config.py CHANGED
@@ -2,6 +2,7 @@
2
2
  Configuration management for the Webscout API server.
3
3
  """
4
4
 
5
+ import os
5
6
  from typing import List, Dict, Optional, Any
6
7
  from webscout.Litlogger import Logger, LogLevel, LogFormat, ConsoleHandler
7
8
  import sys
@@ -19,6 +20,42 @@ logger = Logger(
19
20
  )
20
21
 
21
22
 
23
+ def _get_supabase_url() -> Optional[str]:
24
+ """Get Supabase URL from environment variables or GitHub secrets."""
25
+ # Try environment variable first
26
+ url = os.getenv("SUPABASE_URL")
27
+ if url:
28
+ logger.info("📍 Using SUPABASE_URL from environment")
29
+ return url
30
+
31
+ # Try to get from GitHub secrets (if running in GitHub Actions)
32
+ github_url = os.getenv("GITHUB_SUPABASE_URL") # GitHub Actions secret
33
+ if github_url:
34
+ logger.info("📍 Using SUPABASE_URL from GitHub secrets")
35
+ return github_url
36
+
37
+ # Don't log error during import - only when actually needed
38
+ return None
39
+
40
+
41
+ def _get_supabase_anon_key() -> Optional[str]:
42
+ """Get Supabase anon key from environment variables or GitHub secrets."""
43
+ # Try environment variable first
44
+ key = os.getenv("SUPABASE_ANON_KEY")
45
+ if key:
46
+ logger.info("🔑 Using SUPABASE_ANON_KEY from environment")
47
+ return key
48
+
49
+ # Try to get from GitHub secrets (if running in GitHub Actions)
50
+ github_key = os.getenv("GITHUB_SUPABASE_ANON_KEY") # GitHub Actions secret
51
+ if github_key:
52
+ logger.info("🔑 Using SUPABASE_ANON_KEY from GitHub secrets")
53
+ return github_key
54
+
55
+ # Don't log error during import - only when actually needed
56
+ return None
57
+
58
+
22
59
  class ServerConfig:
23
60
  """Centralized configuration management for the API server."""
24
61
 
@@ -33,9 +70,33 @@ class ServerConfig:
33
70
  self.cors_origins: List[str] = ["*"]
34
71
  self.max_request_size: int = 10 * 1024 * 1024 # 10MB
35
72
  self.request_timeout: int = 300 # 5 minutes
36
- self.auth_required: bool = True # New: Enable/disable authentication
37
- self.rate_limit_enabled: bool = True # New: Enable/disable rate limiting
73
+ self.auth_required: bool = os.getenv("WEBSCOUT_AUTH_REQUIRED", "false").lower() == "true" # Default to no auth
74
+ self.rate_limit_enabled: bool = os.getenv("WEBSCOUT_RATE_LIMIT_ENABLED", "false").lower() == "true" # Default to no rate limit
38
75
  self.default_rate_limit: int = 60 # Default rate limit for no-auth mode
76
+ self.request_logging_enabled: bool = os.getenv("WEBSCOUT_REQUEST_LOGGING", "true").lower() == "true" # Enable request logging by default
77
+
78
+ # Database configuration - lazy initialization
79
+ self._supabase_url: Optional[str] = None
80
+ self._supabase_anon_key: Optional[str] = None
81
+ self._supabase_url_checked: bool = False
82
+ self._supabase_anon_key_checked: bool = False
83
+ self.mongodb_url: Optional[str] = os.getenv("MONGODB_URL")
84
+
85
+ @property
86
+ def supabase_url(self) -> Optional[str]:
87
+ """Get Supabase URL with lazy initialization."""
88
+ if not self._supabase_url_checked:
89
+ self._supabase_url = _get_supabase_url()
90
+ self._supabase_url_checked = True
91
+ return self._supabase_url
92
+
93
+ @property
94
+ def supabase_anon_key(self) -> Optional[str]:
95
+ """Get Supabase anon key with lazy initialization."""
96
+ if not self._supabase_anon_key_checked:
97
+ self._supabase_anon_key = _get_supabase_anon_key()
98
+ self._supabase_anon_key_checked = True
99
+ return self._supabase_anon_key
39
100
 
40
101
  def update(self, **kwargs) -> None:
41
102
  """Update configuration with provided values."""
@@ -62,9 +123,42 @@ class AppConfig:
62
123
  default_provider = "ChatGPT"
63
124
  default_tti_provider = "PollinationsAI" # Add default TTI provider
64
125
  base_url: Optional[str] = None
65
- auth_required: bool = True # New: Enable/disable authentication
66
- rate_limit_enabled: bool = True # New: Enable/disable rate limiting
126
+ auth_required: bool = os.getenv("WEBSCOUT_AUTH_REQUIRED", "false").lower() == "true" # Default to no auth
127
+ rate_limit_enabled: bool = os.getenv("WEBSCOUT_RATE_LIMIT_ENABLED", "false").lower() == "true" # Default to no rate limit
67
128
  default_rate_limit: int = 60 # Default rate limit for no-auth mode
129
+ request_logging_enabled: bool = os.getenv("WEBSCOUT_REQUEST_LOGGING", "true").lower() == "true" # Enable request logging by default
130
+
131
+ # Database configuration - lazy initialization
132
+ _supabase_url: Optional[str] = None
133
+ _supabase_anon_key: Optional[str] = None
134
+ _supabase_url_checked: bool = False
135
+ _supabase_anon_key_checked: bool = False
136
+ mongodb_url: Optional[str] = os.getenv("MONGODB_URL")
137
+
138
+ @classmethod
139
+ def get_supabase_url(cls) -> Optional[str]:
140
+ """Get Supabase URL with lazy initialization."""
141
+ if not cls._supabase_url_checked:
142
+ cls._supabase_url = _get_supabase_url()
143
+ cls._supabase_url_checked = True
144
+ return cls._supabase_url
145
+
146
+ @classmethod
147
+ def get_supabase_anon_key(cls) -> Optional[str]:
148
+ """Get Supabase anon key with lazy initialization."""
149
+ if not cls._supabase_anon_key_checked:
150
+ cls._supabase_anon_key = _get_supabase_anon_key()
151
+ cls._supabase_anon_key_checked = True
152
+ return cls._supabase_anon_key
153
+
154
+ # For backward compatibility, provide properties that call the methods
155
+ @property
156
+ def supabase_url(self) -> Optional[str]:
157
+ return self.__class__.get_supabase_url()
158
+
159
+ @property
160
+ def supabase_anon_key(self) -> Optional[str]:
161
+ return self.__class__.get_supabase_anon_key()
68
162
 
69
163
  @classmethod
70
164
  def set_config(cls, **data):
@@ -72,5 +166,10 @@ class AppConfig:
72
166
  for key, value in data.items():
73
167
  setattr(cls, key, value)
74
168
  # Sync with new config system
75
- from .server import config
76
- config.update(**data)
169
+ try:
170
+ from .server import get_config
171
+ config = get_config()
172
+ config.update(**data)
173
+ except ImportError:
174
+ # Handle case where server module is not available
175
+ pass
webscout/auth/database.py CHANGED
@@ -15,7 +15,13 @@ try:
15
15
  except ImportError:
16
16
  HAS_MOTOR = False
17
17
 
18
- from .models import User, APIKey, RateLimitEntry
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
19
25
 
20
26
  logger = logging.getLogger(__name__)
21
27
 
@@ -30,11 +36,12 @@ class JSONDatabase:
30
36
  self.users_file = self.data_dir / "users.json"
31
37
  self.api_keys_file = self.data_dir / "api_keys.json"
32
38
  self.rate_limits_file = self.data_dir / "rate_limits.json"
39
+ self.request_logs_file = self.data_dir / "request_logs.json"
33
40
 
34
41
  self._lock = threading.RLock()
35
42
 
36
43
  # Initialize files if they don't exist
37
- for file_path in [self.users_file, self.api_keys_file, self.rate_limits_file]:
44
+ for file_path in [self.users_file, self.api_keys_file, self.rate_limits_file, self.request_logs_file]:
38
45
  if not file_path.exists():
39
46
  self._write_json(file_path, [])
40
47
 
@@ -171,6 +178,22 @@ class JSONDatabase:
171
178
  # Only for JSONDatabase
172
179
  entries = self._read_json(self.rate_limits_file)
173
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]
174
197
 
175
198
 
176
199
  class MongoDatabase:
@@ -316,41 +339,338 @@ class MongoDatabase:
316
339
  async for entry_data in cursor:
317
340
  entries.append(RateLimitEntry.from_dict(entry_data))
318
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 []
319
573
 
320
574
 
321
575
  class DatabaseManager:
322
- """Database manager that handles MongoDB with JSON fallback."""
576
+ """Database manager with flexible backend support."""
323
577
 
324
578
  def __init__(self, mongo_connection_string: Optional[str] = None, data_dir: str = "data"):
325
- self.mongo_connection_string = mongo_connection_string or os.getenv("MONGODB_URL")
579
+ self.mongo_connection_string = mongo_connection_string
326
580
  self.data_dir = data_dir
581
+ self.supabase_url = self._get_supabase_url()
582
+ self.supabase_key = self._get_supabase_key()
327
583
 
584
+ # Database instances
585
+ self.supabase_db = None
328
586
  self.mongo_db = None
329
- self.json_db = JSONDatabase(data_dir)
330
- self.use_mongo = False
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
331
598
 
332
- logger.info(f"Database manager initialized with data_dir: {data_dir}")
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
333
621
 
334
622
  async def initialize(self) -> None:
335
- """Initialize database connection."""
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
336
643
  if self.mongo_connection_string:
644
+ logger.info("🔗 Attempting to connect to MongoDB...")
337
645
  try:
338
646
  self.mongo_db = MongoDatabase(self.mongo_connection_string)
339
- self.use_mongo = await self.mongo_db.connect()
340
- if self.use_mongo:
341
- logger.info("Using MongoDB as primary database")
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
342
653
  else:
343
- logger.info("MongoDB connection failed, falling back to JSON database")
654
+ logger.warning("⚠️ Failed to connect to MongoDB, falling back to JSON...")
344
655
  except Exception as e:
345
- logger.warning(f"MongoDB initialization failed: {e}, using JSON database")
346
- self.use_mongo = False
347
- else:
348
- logger.info("No MongoDB connection string provided, using JSON database")
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}")
349
667
 
350
668
  @property
351
- def db(self) -> Union[MongoDatabase, JSONDatabase]:
669
+ def db(self):
352
670
  """Get the active database instance."""
353
- return self.mongo_db if self.use_mongo else self.json_db
671
+ if not self.active_db:
672
+ raise RuntimeError("Database not initialized. Call initialize() first.")
673
+ return self.active_db
354
674
 
355
675
  async def create_user(self, user: User) -> User:
356
676
  """Create a new user."""
@@ -392,9 +712,44 @@ class DatabaseManager:
392
712
  """Update rate limit entry."""
393
713
  return await self.db.update_rate_limit_entry(entry)
394
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
+
395
723
  def get_status(self) -> Dict[str, str]:
396
724
  """Get database status."""
397
- return {
398
- "type": "MongoDB" if self.use_mongo else "JSON",
399
- "status": "connected" if (self.use_mongo and self.mongo_db._connected) or (not self.use_mongo) else "disconnected"
400
- }
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
+ }