webscout 8.3.4__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 (55) hide show
  1. webscout/AIutel.py +52 -1016
  2. webscout/Provider/AISEARCH/__init__.py +11 -10
  3. webscout/Provider/AISEARCH/felo_search.py +7 -3
  4. webscout/Provider/AISEARCH/scira_search.py +2 -0
  5. webscout/Provider/AISEARCH/stellar_search.py +53 -8
  6. webscout/Provider/Deepinfra.py +7 -1
  7. webscout/Provider/OPENAI/TogetherAI.py +57 -48
  8. webscout/Provider/OPENAI/TwoAI.py +94 -1
  9. webscout/Provider/OPENAI/__init__.py +0 -2
  10. webscout/Provider/OPENAI/deepinfra.py +6 -0
  11. webscout/Provider/OPENAI/scirachat.py +4 -0
  12. webscout/Provider/OPENAI/textpollinations.py +11 -7
  13. webscout/Provider/OPENAI/venice.py +1 -0
  14. webscout/Provider/Perplexitylabs.py +163 -147
  15. webscout/Provider/Qodo.py +30 -6
  16. webscout/Provider/TTI/__init__.py +1 -0
  17. webscout/Provider/TTI/together.py +7 -6
  18. webscout/Provider/TTI/venice.py +368 -0
  19. webscout/Provider/TextPollinationsAI.py +11 -7
  20. webscout/Provider/TogetherAI.py +57 -44
  21. webscout/Provider/TwoAI.py +96 -2
  22. webscout/Provider/TypliAI.py +33 -27
  23. webscout/Provider/UNFINISHED/PERPLEXED_search.py +254 -0
  24. webscout/Provider/UNFINISHED/fetch_together_models.py +6 -11
  25. webscout/Provider/Venice.py +1 -0
  26. webscout/Provider/WiseCat.py +18 -20
  27. webscout/Provider/__init__.py +0 -6
  28. webscout/Provider/scira_chat.py +4 -0
  29. webscout/Provider/toolbaz.py +5 -10
  30. webscout/Provider/typefully.py +1 -11
  31. webscout/__init__.py +3 -15
  32. webscout/auth/__init__.py +19 -4
  33. webscout/auth/api_key_manager.py +189 -189
  34. webscout/auth/auth_system.py +25 -40
  35. webscout/auth/config.py +105 -6
  36. webscout/auth/database.py +377 -22
  37. webscout/auth/models.py +185 -130
  38. webscout/auth/request_processing.py +175 -11
  39. webscout/auth/routes.py +99 -2
  40. webscout/auth/server.py +9 -2
  41. webscout/auth/simple_logger.py +236 -0
  42. webscout/sanitize.py +1074 -0
  43. webscout/version.py +1 -1
  44. {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/METADATA +9 -149
  45. {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/RECORD +49 -51
  46. webscout/Provider/OPENAI/README_AUTOPROXY.md +0 -238
  47. webscout/Provider/OPENAI/typegpt.py +0 -368
  48. webscout/Provider/OPENAI/uncovrAI.py +0 -477
  49. webscout/Provider/WritingMate.py +0 -273
  50. webscout/Provider/typegpt.py +0 -284
  51. webscout/Provider/uncovr.py +0 -333
  52. {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/WHEEL +0 -0
  53. {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/entry_points.txt +0 -0
  54. {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/licenses/LICENSE.md +0 -0
  55. {webscout-8.3.4.dist-info → webscout-8.3.5.dist-info}/top_level.txt +0 -0
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
+ }