webscout 8.3.1__py3-none-any.whl → 8.3.3__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/AIutel.py +180 -78
- webscout/Bing_search.py +417 -0
- webscout/Extra/gguf.py +706 -177
- webscout/Provider/AISEARCH/__init__.py +1 -0
- webscout/Provider/AISEARCH/genspark_search.py +7 -7
- webscout/Provider/AISEARCH/stellar_search.py +132 -0
- webscout/Provider/ExaChat.py +84 -58
- webscout/Provider/GeminiProxy.py +140 -0
- webscout/Provider/HeckAI.py +85 -80
- webscout/Provider/Jadve.py +56 -50
- webscout/Provider/MCPCore.py +78 -75
- webscout/Provider/MiniMax.py +207 -0
- webscout/Provider/Nemotron.py +41 -13
- webscout/Provider/Netwrck.py +34 -51
- webscout/Provider/OPENAI/BLACKBOXAI.py +0 -4
- webscout/Provider/OPENAI/GeminiProxy.py +328 -0
- webscout/Provider/OPENAI/MiniMax.py +298 -0
- webscout/Provider/OPENAI/README.md +32 -29
- webscout/Provider/OPENAI/README_AUTOPROXY.md +238 -0
- webscout/Provider/OPENAI/TogetherAI.py +4 -17
- webscout/Provider/OPENAI/__init__.py +17 -1
- webscout/Provider/OPENAI/autoproxy.py +1067 -39
- webscout/Provider/OPENAI/base.py +17 -76
- webscout/Provider/OPENAI/deepinfra.py +42 -108
- webscout/Provider/OPENAI/e2b.py +0 -1
- webscout/Provider/OPENAI/flowith.py +179 -166
- webscout/Provider/OPENAI/friendli.py +233 -0
- webscout/Provider/OPENAI/mcpcore.py +109 -70
- webscout/Provider/OPENAI/monochat.py +329 -0
- webscout/Provider/OPENAI/pydantic_imports.py +1 -172
- webscout/Provider/OPENAI/scirachat.py +59 -51
- webscout/Provider/OPENAI/toolbaz.py +3 -9
- webscout/Provider/OPENAI/typegpt.py +1 -1
- webscout/Provider/OPENAI/utils.py +19 -42
- webscout/Provider/OPENAI/x0gpt.py +14 -2
- webscout/Provider/OPENAI/xenai.py +514 -0
- webscout/Provider/OPENAI/yep.py +8 -2
- webscout/Provider/OpenGPT.py +54 -32
- webscout/Provider/PI.py +58 -84
- webscout/Provider/StandardInput.py +32 -13
- webscout/Provider/TTI/README.md +9 -9
- webscout/Provider/TTI/__init__.py +3 -1
- webscout/Provider/TTI/aiarta.py +92 -78
- webscout/Provider/TTI/bing.py +231 -0
- webscout/Provider/TTI/infip.py +212 -0
- webscout/Provider/TTI/monochat.py +220 -0
- webscout/Provider/TTS/speechma.py +45 -39
- webscout/Provider/TeachAnything.py +11 -3
- webscout/Provider/TextPollinationsAI.py +78 -70
- webscout/Provider/TogetherAI.py +350 -0
- webscout/Provider/Venice.py +37 -46
- webscout/Provider/VercelAI.py +27 -24
- webscout/Provider/WiseCat.py +35 -35
- webscout/Provider/WrDoChat.py +22 -26
- webscout/Provider/WritingMate.py +26 -22
- webscout/Provider/XenAI.py +324 -0
- webscout/Provider/__init__.py +10 -5
- webscout/Provider/deepseek_assistant.py +378 -0
- webscout/Provider/granite.py +48 -57
- webscout/Provider/koala.py +51 -39
- webscout/Provider/learnfastai.py +49 -64
- webscout/Provider/llmchat.py +79 -93
- webscout/Provider/llmchatco.py +63 -78
- webscout/Provider/multichat.py +51 -40
- webscout/Provider/oivscode.py +1 -1
- webscout/Provider/scira_chat.py +159 -96
- webscout/Provider/scnet.py +13 -13
- webscout/Provider/searchchat.py +13 -13
- webscout/Provider/sonus.py +12 -11
- webscout/Provider/toolbaz.py +25 -8
- webscout/Provider/turboseek.py +41 -42
- webscout/Provider/typefully.py +27 -12
- webscout/Provider/typegpt.py +41 -46
- webscout/Provider/uncovr.py +55 -90
- webscout/Provider/x0gpt.py +33 -17
- webscout/Provider/yep.py +79 -96
- webscout/auth/__init__.py +55 -0
- webscout/auth/api_key_manager.py +189 -0
- webscout/auth/auth_system.py +100 -0
- webscout/auth/config.py +76 -0
- webscout/auth/database.py +400 -0
- webscout/auth/exceptions.py +67 -0
- webscout/auth/middleware.py +248 -0
- webscout/auth/models.py +130 -0
- webscout/auth/providers.py +279 -0
- webscout/auth/rate_limiter.py +254 -0
- webscout/auth/request_models.py +127 -0
- webscout/auth/request_processing.py +226 -0
- webscout/auth/routes.py +550 -0
- webscout/auth/schemas.py +103 -0
- webscout/auth/server.py +367 -0
- webscout/client.py +121 -70
- webscout/litagent/Readme.md +68 -55
- webscout/litagent/agent.py +99 -9
- webscout/scout/core/scout.py +104 -26
- webscout/scout/element.py +139 -18
- webscout/swiftcli/core/cli.py +14 -3
- webscout/swiftcli/decorators/output.py +59 -9
- webscout/update_checker.py +31 -49
- webscout/version.py +1 -1
- webscout/webscout_search.py +4 -12
- webscout/webscout_search_async.py +3 -10
- webscout/yep_search.py +2 -11
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/METADATA +141 -99
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/RECORD +109 -83
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/entry_points.txt +1 -1
- webscout/Provider/HF_space/__init__.py +0 -0
- webscout/Provider/HF_space/qwen_qwen2.py +0 -206
- webscout/Provider/OPENAI/api.py +0 -1320
- webscout/Provider/TTI/fastflux.py +0 -233
- webscout/Provider/Writecream.py +0 -246
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/WHEEL +0 -0
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,400 @@
|
|
|
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
|
+
from .models import User, APIKey, RateLimitEntry
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JSONDatabase:
|
|
24
|
+
"""JSON file-based database fallback."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, data_dir: str = "data"):
|
|
27
|
+
self.data_dir = Path(data_dir)
|
|
28
|
+
self.data_dir.mkdir(exist_ok=True)
|
|
29
|
+
|
|
30
|
+
self.users_file = self.data_dir / "users.json"
|
|
31
|
+
self.api_keys_file = self.data_dir / "api_keys.json"
|
|
32
|
+
self.rate_limits_file = self.data_dir / "rate_limits.json"
|
|
33
|
+
|
|
34
|
+
self._lock = threading.RLock()
|
|
35
|
+
|
|
36
|
+
# Initialize files if they don't exist
|
|
37
|
+
for file_path in [self.users_file, self.api_keys_file, self.rate_limits_file]:
|
|
38
|
+
if not file_path.exists():
|
|
39
|
+
self._write_json(file_path, [])
|
|
40
|
+
|
|
41
|
+
def _read_json(self, file_path: Path) -> List[Dict[str, Any]]:
|
|
42
|
+
"""Read JSON file safely."""
|
|
43
|
+
try:
|
|
44
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
45
|
+
return json.load(f)
|
|
46
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
def _write_json(self, file_path: Path, data: List[Dict[str, Any]]) -> None:
|
|
50
|
+
"""Write JSON file safely."""
|
|
51
|
+
with self._lock:
|
|
52
|
+
# Write to temporary file first, then rename for atomicity
|
|
53
|
+
temp_file = file_path.with_suffix('.tmp')
|
|
54
|
+
try:
|
|
55
|
+
with open(temp_file, 'w', encoding='utf-8') as f:
|
|
56
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
57
|
+
temp_file.replace(file_path)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
if temp_file.exists():
|
|
60
|
+
temp_file.unlink()
|
|
61
|
+
raise e
|
|
62
|
+
|
|
63
|
+
async def create_user(self, user: User) -> User:
|
|
64
|
+
"""Create a new user."""
|
|
65
|
+
users = self._read_json(self.users_file)
|
|
66
|
+
|
|
67
|
+
# Check if user already exists
|
|
68
|
+
for existing_user in users:
|
|
69
|
+
if existing_user.get("username") == user.username:
|
|
70
|
+
raise ValueError(f"User with username '{user.username}' already exists")
|
|
71
|
+
|
|
72
|
+
users.append(user.to_dict())
|
|
73
|
+
self._write_json(self.users_file, users)
|
|
74
|
+
return user
|
|
75
|
+
|
|
76
|
+
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
77
|
+
"""Get user by ID."""
|
|
78
|
+
users = self._read_json(self.users_file)
|
|
79
|
+
for user_data in users:
|
|
80
|
+
if user_data.get("id") == user_id:
|
|
81
|
+
return User.from_dict(user_data)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
async def get_user_by_username(self, username: str) -> Optional[User]:
|
|
85
|
+
"""Get user by username."""
|
|
86
|
+
users = self._read_json(self.users_file)
|
|
87
|
+
for user_data in users:
|
|
88
|
+
if user_data.get("username") == username:
|
|
89
|
+
return User.from_dict(user_data)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
|
93
|
+
"""Get user by Telegram ID."""
|
|
94
|
+
users = self._read_json(self.users_file)
|
|
95
|
+
for user_data in users:
|
|
96
|
+
if user_data.get("telegram_id") == telegram_id:
|
|
97
|
+
return User.from_dict(user_data)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
async def create_api_key(self, api_key: APIKey) -> APIKey:
|
|
101
|
+
"""Create a new API key."""
|
|
102
|
+
api_keys = self._read_json(self.api_keys_file)
|
|
103
|
+
|
|
104
|
+
# Check if key already exists
|
|
105
|
+
for existing_key in api_keys:
|
|
106
|
+
if existing_key.get("key") == api_key.key:
|
|
107
|
+
raise ValueError("API key already exists")
|
|
108
|
+
|
|
109
|
+
api_keys.append(api_key.to_dict())
|
|
110
|
+
self._write_json(self.api_keys_file, api_keys)
|
|
111
|
+
return api_key
|
|
112
|
+
|
|
113
|
+
async def get_api_key(self, key: str) -> Optional[APIKey]:
|
|
114
|
+
"""Get API key by key value."""
|
|
115
|
+
api_keys = self._read_json(self.api_keys_file)
|
|
116
|
+
for key_data in api_keys:
|
|
117
|
+
if key_data.get("key") == key:
|
|
118
|
+
return APIKey.from_dict(key_data)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
async def update_api_key(self, api_key: APIKey) -> APIKey:
|
|
122
|
+
"""Update an existing API key."""
|
|
123
|
+
api_keys = self._read_json(self.api_keys_file)
|
|
124
|
+
|
|
125
|
+
for i, key_data in enumerate(api_keys):
|
|
126
|
+
if key_data.get("id") == api_key.id:
|
|
127
|
+
api_keys[i] = api_key.to_dict()
|
|
128
|
+
self._write_json(self.api_keys_file, api_keys)
|
|
129
|
+
return api_key
|
|
130
|
+
|
|
131
|
+
raise ValueError(f"API key with ID '{api_key.id}' not found")
|
|
132
|
+
|
|
133
|
+
async def get_api_keys_by_user(self, user_id: str) -> List[APIKey]:
|
|
134
|
+
"""Get all API keys for a user."""
|
|
135
|
+
api_keys = self._read_json(self.api_keys_file)
|
|
136
|
+
user_keys = []
|
|
137
|
+
|
|
138
|
+
for key_data in api_keys:
|
|
139
|
+
if key_data.get("user_id") == user_id:
|
|
140
|
+
user_keys.append(APIKey.from_dict(key_data))
|
|
141
|
+
|
|
142
|
+
return user_keys
|
|
143
|
+
|
|
144
|
+
async def get_rate_limit_entry(self, api_key_id: str) -> Optional[RateLimitEntry]:
|
|
145
|
+
"""Get rate limit entry for API key."""
|
|
146
|
+
rate_limits = self._read_json(self.rate_limits_file)
|
|
147
|
+
|
|
148
|
+
for entry_data in rate_limits:
|
|
149
|
+
if entry_data.get("api_key_id") == api_key_id:
|
|
150
|
+
return RateLimitEntry.from_dict(entry_data)
|
|
151
|
+
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
async def update_rate_limit_entry(self, entry: RateLimitEntry) -> RateLimitEntry:
|
|
155
|
+
"""Update rate limit entry."""
|
|
156
|
+
rate_limits = self._read_json(self.rate_limits_file)
|
|
157
|
+
|
|
158
|
+
for i, entry_data in enumerate(rate_limits):
|
|
159
|
+
if entry_data.get("api_key_id") == entry.api_key_id:
|
|
160
|
+
rate_limits[i] = entry.to_dict()
|
|
161
|
+
self._write_json(self.rate_limits_file, rate_limits)
|
|
162
|
+
return entry
|
|
163
|
+
|
|
164
|
+
# Create new entry if not found
|
|
165
|
+
rate_limits.append(entry.to_dict())
|
|
166
|
+
self._write_json(self.rate_limits_file, rate_limits)
|
|
167
|
+
return entry
|
|
168
|
+
|
|
169
|
+
async def get_all_rate_limit_entries(self) -> list:
|
|
170
|
+
"""Return all rate limit entries (for maintenance/cleanup)."""
|
|
171
|
+
# Only for JSONDatabase
|
|
172
|
+
entries = self._read_json(self.rate_limits_file)
|
|
173
|
+
return [RateLimitEntry.from_dict(e) for e in entries]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class MongoDatabase:
|
|
177
|
+
"""MongoDB database implementation."""
|
|
178
|
+
|
|
179
|
+
def __init__(self, connection_string: str = "mongodb://localhost:27017", database_name: str = "webscout"):
|
|
180
|
+
self.connection_string = connection_string
|
|
181
|
+
self.database_name = database_name
|
|
182
|
+
self.client = None
|
|
183
|
+
self.db = None
|
|
184
|
+
self._connected = False
|
|
185
|
+
|
|
186
|
+
async def connect(self) -> bool:
|
|
187
|
+
"""Connect to MongoDB."""
|
|
188
|
+
if not HAS_MOTOR:
|
|
189
|
+
logger.warning("motor package not available, cannot connect to MongoDB")
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
self.client = motor.motor_asyncio.AsyncIOMotorClient(self.connection_string)
|
|
194
|
+
self.db = self.client[self.database_name]
|
|
195
|
+
|
|
196
|
+
# Test connection
|
|
197
|
+
await self.client.admin.command('ping')
|
|
198
|
+
self._connected = True
|
|
199
|
+
logger.info("Connected to MongoDB successfully")
|
|
200
|
+
return True
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.warning(f"Failed to connect to MongoDB: {e}")
|
|
203
|
+
self._connected = False
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
async def create_user(self, user: User) -> User:
|
|
207
|
+
"""Create a new user."""
|
|
208
|
+
if not self._connected:
|
|
209
|
+
raise RuntimeError("Database not connected")
|
|
210
|
+
|
|
211
|
+
# Check if user exists
|
|
212
|
+
existing = await self.db.users.find_one({"username": user.username})
|
|
213
|
+
if existing:
|
|
214
|
+
raise ValueError(f"User with username '{user.username}' already exists")
|
|
215
|
+
|
|
216
|
+
await self.db.users.insert_one(user.to_dict())
|
|
217
|
+
return user
|
|
218
|
+
|
|
219
|
+
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
220
|
+
"""Get user by ID."""
|
|
221
|
+
if not self._connected:
|
|
222
|
+
raise RuntimeError("Database not connected")
|
|
223
|
+
|
|
224
|
+
user_data = await self.db.users.find_one({"id": user_id})
|
|
225
|
+
return User.from_dict(user_data) if user_data else None
|
|
226
|
+
|
|
227
|
+
async def get_user_by_username(self, username: str) -> Optional[User]:
|
|
228
|
+
"""Get user by username."""
|
|
229
|
+
if not self._connected:
|
|
230
|
+
raise RuntimeError("Database not connected")
|
|
231
|
+
|
|
232
|
+
user_data = await self.db.users.find_one({"username": username})
|
|
233
|
+
return User.from_dict(user_data) if user_data else None
|
|
234
|
+
|
|
235
|
+
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
|
236
|
+
"""Get user by Telegram ID."""
|
|
237
|
+
if not self._connected:
|
|
238
|
+
raise RuntimeError("Database not connected")
|
|
239
|
+
|
|
240
|
+
user_data = await self.db.users.find_one({"telegram_id": telegram_id})
|
|
241
|
+
return User.from_dict(user_data) if user_data else None
|
|
242
|
+
|
|
243
|
+
async def create_api_key(self, api_key: APIKey) -> APIKey:
|
|
244
|
+
"""Create a new API key."""
|
|
245
|
+
if not self._connected:
|
|
246
|
+
raise RuntimeError("Database not connected")
|
|
247
|
+
|
|
248
|
+
# Check if key exists
|
|
249
|
+
existing = await self.db.api_keys.find_one({"key": api_key.key})
|
|
250
|
+
if existing:
|
|
251
|
+
raise ValueError("API key already exists")
|
|
252
|
+
|
|
253
|
+
await self.db.api_keys.insert_one(api_key.to_dict())
|
|
254
|
+
return api_key
|
|
255
|
+
|
|
256
|
+
async def get_api_key(self, key: str) -> Optional[APIKey]:
|
|
257
|
+
"""Get API key by key value."""
|
|
258
|
+
if not self._connected:
|
|
259
|
+
raise RuntimeError("Database not connected")
|
|
260
|
+
|
|
261
|
+
key_data = await self.db.api_keys.find_one({"key": key})
|
|
262
|
+
return APIKey.from_dict(key_data) if key_data else None
|
|
263
|
+
|
|
264
|
+
async def update_api_key(self, api_key: APIKey) -> APIKey:
|
|
265
|
+
"""Update an existing API key."""
|
|
266
|
+
if not self._connected:
|
|
267
|
+
raise RuntimeError("Database not connected")
|
|
268
|
+
|
|
269
|
+
result = await self.db.api_keys.update_one(
|
|
270
|
+
{"id": api_key.id},
|
|
271
|
+
{"$set": api_key.to_dict()}
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if result.matched_count == 0:
|
|
275
|
+
raise ValueError(f"API key with ID '{api_key.id}' not found")
|
|
276
|
+
|
|
277
|
+
return api_key
|
|
278
|
+
|
|
279
|
+
async def get_api_keys_by_user(self, user_id: str) -> List[APIKey]:
|
|
280
|
+
"""Get all API keys for a user."""
|
|
281
|
+
if not self._connected:
|
|
282
|
+
raise RuntimeError("Database not connected")
|
|
283
|
+
|
|
284
|
+
cursor = self.db.api_keys.find({"user_id": user_id})
|
|
285
|
+
keys = []
|
|
286
|
+
async for key_data in cursor:
|
|
287
|
+
keys.append(APIKey.from_dict(key_data))
|
|
288
|
+
return keys
|
|
289
|
+
|
|
290
|
+
async def get_rate_limit_entry(self, api_key_id: str) -> Optional[RateLimitEntry]:
|
|
291
|
+
"""Get rate limit entry for API key."""
|
|
292
|
+
if not self._connected:
|
|
293
|
+
raise RuntimeError("Database not connected")
|
|
294
|
+
|
|
295
|
+
entry_data = await self.db.rate_limits.find_one({"api_key_id": api_key_id})
|
|
296
|
+
return RateLimitEntry.from_dict(entry_data) if entry_data else None
|
|
297
|
+
|
|
298
|
+
async def update_rate_limit_entry(self, entry: RateLimitEntry) -> RateLimitEntry:
|
|
299
|
+
"""Update rate limit entry."""
|
|
300
|
+
if not self._connected:
|
|
301
|
+
raise RuntimeError("Database not connected")
|
|
302
|
+
|
|
303
|
+
await self.db.rate_limits.update_one(
|
|
304
|
+
{"api_key_id": entry.api_key_id},
|
|
305
|
+
{"$set": entry.to_dict()},
|
|
306
|
+
upsert=True
|
|
307
|
+
)
|
|
308
|
+
return entry
|
|
309
|
+
|
|
310
|
+
async def get_all_rate_limit_entries(self) -> list:
|
|
311
|
+
"""Return all rate limit entries (for maintenance/cleanup) from MongoDB."""
|
|
312
|
+
if not self._connected:
|
|
313
|
+
raise RuntimeError("Database not connected")
|
|
314
|
+
entries = []
|
|
315
|
+
cursor = self.db.rate_limits.find({})
|
|
316
|
+
async for entry_data in cursor:
|
|
317
|
+
entries.append(RateLimitEntry.from_dict(entry_data))
|
|
318
|
+
return entries
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class DatabaseManager:
|
|
322
|
+
"""Database manager that handles MongoDB with JSON fallback."""
|
|
323
|
+
|
|
324
|
+
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")
|
|
326
|
+
self.data_dir = data_dir
|
|
327
|
+
|
|
328
|
+
self.mongo_db = None
|
|
329
|
+
self.json_db = JSONDatabase(data_dir)
|
|
330
|
+
self.use_mongo = False
|
|
331
|
+
|
|
332
|
+
logger.info(f"Database manager initialized with data_dir: {data_dir}")
|
|
333
|
+
|
|
334
|
+
async def initialize(self) -> None:
|
|
335
|
+
"""Initialize database connection."""
|
|
336
|
+
if self.mongo_connection_string:
|
|
337
|
+
try:
|
|
338
|
+
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")
|
|
342
|
+
else:
|
|
343
|
+
logger.info("MongoDB connection failed, falling back to JSON database")
|
|
344
|
+
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")
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def db(self) -> Union[MongoDatabase, JSONDatabase]:
|
|
352
|
+
"""Get the active database instance."""
|
|
353
|
+
return self.mongo_db if self.use_mongo else self.json_db
|
|
354
|
+
|
|
355
|
+
async def create_user(self, user: User) -> User:
|
|
356
|
+
"""Create a new user."""
|
|
357
|
+
return await self.db.create_user(user)
|
|
358
|
+
|
|
359
|
+
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
360
|
+
"""Get user by ID."""
|
|
361
|
+
return await self.db.get_user_by_id(user_id)
|
|
362
|
+
|
|
363
|
+
async def get_user_by_username(self, username: str) -> Optional[User]:
|
|
364
|
+
"""Get user by username."""
|
|
365
|
+
return await self.db.get_user_by_username(username)
|
|
366
|
+
|
|
367
|
+
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
|
368
|
+
"""Get user by Telegram ID."""
|
|
369
|
+
return await self.db.get_user_by_telegram_id(telegram_id)
|
|
370
|
+
|
|
371
|
+
async def create_api_key(self, api_key: APIKey) -> APIKey:
|
|
372
|
+
"""Create a new API key."""
|
|
373
|
+
return await self.db.create_api_key(api_key)
|
|
374
|
+
|
|
375
|
+
async def get_api_key(self, key: str) -> Optional[APIKey]:
|
|
376
|
+
"""Get API key by key value."""
|
|
377
|
+
return await self.db.get_api_key(key)
|
|
378
|
+
|
|
379
|
+
async def update_api_key(self, api_key: APIKey) -> APIKey:
|
|
380
|
+
"""Update an existing API key."""
|
|
381
|
+
return await self.db.update_api_key(api_key)
|
|
382
|
+
|
|
383
|
+
async def get_api_keys_by_user(self, user_id: str) -> List[APIKey]:
|
|
384
|
+
"""Get all API keys for a user."""
|
|
385
|
+
return await self.db.get_api_keys_by_user(user_id)
|
|
386
|
+
|
|
387
|
+
async def get_rate_limit_entry(self, api_key_id: str) -> Optional[RateLimitEntry]:
|
|
388
|
+
"""Get rate limit entry for API key."""
|
|
389
|
+
return await self.db.get_rate_limit_entry(api_key_id)
|
|
390
|
+
|
|
391
|
+
async def update_rate_limit_entry(self, entry: RateLimitEntry) -> RateLimitEntry:
|
|
392
|
+
"""Update rate limit entry."""
|
|
393
|
+
return await self.db.update_rate_limit_entry(entry)
|
|
394
|
+
|
|
395
|
+
def get_status(self) -> Dict[str, str]:
|
|
396
|
+
"""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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for the Webscout API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
|
|
10
|
+
from .request_models import ErrorDetail, ErrorResponse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def clean_text(text):
|
|
14
|
+
"""Clean text by removing null bytes and control characters except newlines and tabs."""
|
|
15
|
+
if not isinstance(text, str):
|
|
16
|
+
return text
|
|
17
|
+
|
|
18
|
+
# Remove null bytes
|
|
19
|
+
text = text.replace('\x00', '')
|
|
20
|
+
|
|
21
|
+
# Keep newlines, tabs, and other printable characters, remove other control chars
|
|
22
|
+
# This regex matches control characters except \n, \r, \t
|
|
23
|
+
return re.sub(r'[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class APIError(Exception):
|
|
27
|
+
"""Custom exception for API errors."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, status_code: int = HTTP_500_INTERNAL_SERVER_ERROR,
|
|
30
|
+
error_type: str = "server_error", param: Optional[str] = None,
|
|
31
|
+
code: Optional[str] = None):
|
|
32
|
+
self.message = message
|
|
33
|
+
self.status_code = status_code
|
|
34
|
+
self.error_type = error_type
|
|
35
|
+
self.param = param
|
|
36
|
+
self.code = code
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
|
|
39
|
+
def to_response(self) -> JSONResponse:
|
|
40
|
+
"""Convert to FastAPI JSONResponse."""
|
|
41
|
+
error_detail = ErrorDetail(
|
|
42
|
+
message=self.message,
|
|
43
|
+
type=self.error_type,
|
|
44
|
+
param=self.param,
|
|
45
|
+
code=self.code
|
|
46
|
+
)
|
|
47
|
+
error_response = ErrorResponse(error=error_detail)
|
|
48
|
+
return JSONResponse(
|
|
49
|
+
status_code=self.status_code,
|
|
50
|
+
content=error_response.model_dump(exclude_none=True)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def format_exception(e) -> str:
|
|
55
|
+
"""Format exception for JSON response."""
|
|
56
|
+
if isinstance(e, str):
|
|
57
|
+
message = e
|
|
58
|
+
else:
|
|
59
|
+
message = f"{e.__class__.__name__}: {str(e)}"
|
|
60
|
+
return json.dumps({
|
|
61
|
+
"error": {
|
|
62
|
+
"message": message,
|
|
63
|
+
"type": "server_error",
|
|
64
|
+
"param": None,
|
|
65
|
+
"code": "internal_server_error"
|
|
66
|
+
}
|
|
67
|
+
})
|