Mark-Lasfar
commited on
Commit
·
7f03ffe
1
Parent(s):
361e98a
Fix ChunkedIteratorResult in SQLAlchemyUserDatabase and toggleBtn null error
Browse files- api/auth.py +7 -26
- api/database.py +3 -3
- api/endpoints.py +41 -7
- api/models.py +2 -0
- api/user_db.py +51 -0
- utils/web_search.py +3 -8
api/auth.py
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
|
|
| 1 |
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
| 2 |
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
|
| 4 |
from fastapi_users import FastAPIUsers
|
| 5 |
from fastapi_users.authentication import CookieTransport, JWTStrategy, AuthenticationBackend
|
| 6 |
-
from fastapi_users.
|
| 7 |
from httpx_oauth.clients.google import GoogleOAuth2
|
| 8 |
from httpx_oauth.clients.github import GitHubOAuth2
|
| 9 |
-
from fastapi_users.router.oauth import get_oauth_router
|
| 10 |
-
from api.database import User, OAuthAccount, get_user_db
|
| 11 |
-
from api.models import UserRead, UserCreate, UserUpdate
|
| 12 |
from fastapi_users.manager import BaseUserManager, IntegerIDMixin
|
| 13 |
from fastapi import Depends, Request, FastAPI
|
| 14 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 15 |
from sqlalchemy import select
|
| 16 |
from fastapi_users.models import UP
|
| 17 |
-
from typing import Optional, Dict
|
| 18 |
import os
|
| 19 |
import logging
|
| 20 |
import secrets
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# إعداد اللوقينج
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
|
@@ -55,27 +57,6 @@ GITHUB_REDIRECT_URL = os.getenv("GITHUB_REDIRECT_URL", "https://mgzon-mgzon-app.
|
|
| 55 |
google_oauth_client = GoogleOAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
|
| 56 |
github_oauth_client = GitHubOAuth2(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)
|
| 57 |
|
| 58 |
-
# قاعدة بيانات المستخدم
|
| 59 |
-
class CustomSQLAlchemyUserDatabase(SQLAlchemyUserDatabase):
|
| 60 |
-
def parse_id(self, value: Any) -> int:
|
| 61 |
-
"""تحويل الـ ID من string إلى int لتوافق JWTStrategy"""
|
| 62 |
-
logger.debug(f"Parsing ID: {value} (type: {type(value)})")
|
| 63 |
-
return int(value) if isinstance(value, str) else value
|
| 64 |
-
|
| 65 |
-
async def get_by_email(self, email: str) -> Optional[User]:
|
| 66 |
-
logger.info(f"Checking for user with email: {email}")
|
| 67 |
-
statement = select(self.user_table).where(self.user_table.email == email)
|
| 68 |
-
result = await self.session.execute(statement)
|
| 69 |
-
return result.scalar_one_or_none()
|
| 70 |
-
|
| 71 |
-
async def create(self, create_dict: Dict[str, Any]) -> User:
|
| 72 |
-
logger.info(f"Creating user with email: {create_dict.get('email')}")
|
| 73 |
-
user = self.user_table(**create_dict)
|
| 74 |
-
self.session.add(user)
|
| 75 |
-
await self.session.commit()
|
| 76 |
-
await self.session.refresh(user)
|
| 77 |
-
return user
|
| 78 |
-
|
| 79 |
# مدير المستخدمين
|
| 80 |
class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
|
| 81 |
reset_password_token_secret = SECRET
|
|
|
|
| 1 |
+
# api/auth.py
|
| 2 |
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
| 3 |
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
|
| 5 |
from fastapi_users import FastAPIUsers
|
| 6 |
from fastapi_users.authentication import CookieTransport, JWTStrategy, AuthenticationBackend
|
| 7 |
+
from fastapi_users.router.oauth import get_oauth_router
|
| 8 |
from httpx_oauth.clients.google import GoogleOAuth2
|
| 9 |
from httpx_oauth.clients.github import GitHubOAuth2
|
|
|
|
|
|
|
|
|
|
| 10 |
from fastapi_users.manager import BaseUserManager, IntegerIDMixin
|
| 11 |
from fastapi import Depends, Request, FastAPI
|
| 12 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 13 |
from sqlalchemy import select
|
| 14 |
from fastapi_users.models import UP
|
| 15 |
+
from typing import Optional, Dict
|
| 16 |
import os
|
| 17 |
import logging
|
| 18 |
import secrets
|
| 19 |
|
| 20 |
+
from api.user_db import CustomSQLAlchemyUserDatabase, get_user_db # استيراد من user_db.py
|
| 21 |
+
from api.database import User, OAuthAccount
|
| 22 |
+
from api.models import UserRead, UserCreate, UserUpdate
|
| 23 |
+
|
| 24 |
# إعداد اللوقينج
|
| 25 |
logger = logging.getLogger(__name__)
|
| 26 |
|
|
|
|
| 57 |
google_oauth_client = GoogleOAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
|
| 58 |
github_oauth_client = GitHubOAuth2(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
# مدير المستخدمين
|
| 61 |
class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
|
| 62 |
reset_password_token_secret = SECRET
|
api/database.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
| 2 |
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
|
|
@@ -10,10 +11,9 @@ from sqlalchemy import Column, String, Integer, ForeignKey, DateTime, Boolean, T
|
|
| 10 |
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
| 11 |
from sqlalchemy.ext.declarative import declarative_base
|
| 12 |
from sqlalchemy.orm import relationship
|
| 13 |
-
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
|
| 14 |
from fastapi import Depends
|
| 15 |
import aiosqlite
|
| 16 |
-
from api.
|
| 17 |
|
| 18 |
# إعداد اللوج
|
| 19 |
logger = logging.getLogger(__name__)
|
|
@@ -58,7 +58,7 @@ class OAuthAccount(Base):
|
|
| 58 |
|
| 59 |
user = relationship("User", back_populates="oauth_accounts", lazy="selectin")
|
| 60 |
|
| 61 |
-
class User(
|
| 62 |
__tablename__ = "user"
|
| 63 |
id = Column(Integer, primary_key=True, index=True)
|
| 64 |
email = Column(String, unique=True, index=True, nullable=False)
|
|
|
|
| 1 |
+
# api/database.py
|
| 2 |
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
| 3 |
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
|
|
|
|
| 11 |
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
| 12 |
from sqlalchemy.ext.declarative import declarative_base
|
| 13 |
from sqlalchemy.orm import relationship
|
|
|
|
| 14 |
from fastapi import Depends
|
| 15 |
import aiosqlite
|
| 16 |
+
from api.user_db import CustomSQLAlchemyUserDatabase, get_user_db # استيراد من user_db.py
|
| 17 |
|
| 18 |
# إعداد اللوج
|
| 19 |
logger = logging.getLogger(__name__)
|
|
|
|
| 58 |
|
| 59 |
user = relationship("User", back_populates="oauth_accounts", lazy="selectin")
|
| 60 |
|
| 61 |
+
class User(Base): # إزالة SQLAlchemyBaseUserTable لأن fastapi-users بيستخدم CustomSQLAlchemyUserDatabase
|
| 62 |
__tablename__ = "user"
|
| 63 |
id = Column(Integer, primary_key=True, index=True)
|
| 64 |
email = Column(String, unique=True, index=True, nullable=False)
|
api/endpoints.py
CHANGED
|
@@ -141,6 +141,7 @@ async def performance_stats():
|
|
| 141 |
"uptime": os.popen("uptime").read().strip()
|
| 142 |
}
|
| 143 |
|
|
|
|
| 144 |
@router.post("/api/chat")
|
| 145 |
async def chat_endpoint(
|
| 146 |
request: Request,
|
|
@@ -178,10 +179,15 @@ async def chat_endpoint(
|
|
| 178 |
preferred_model = user.preferred_model if user else None
|
| 179 |
model_name, api_endpoint = select_model(req.message, input_type="text", preferred_model=preferred_model)
|
| 180 |
|
|
|
|
| 181 |
is_available, api_key, selected_endpoint = check_model_availability(model_name, HF_TOKEN)
|
| 182 |
if not is_available:
|
| 183 |
-
logger.
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
system_prompt = enhance_system_prompt(req.system_prompt, req.message, user)
|
| 187 |
|
|
@@ -209,12 +215,12 @@ async def chat_endpoint(
|
|
| 209 |
logger.warning(f"Unexpected non-bytes chunk in audio stream: {chunk}")
|
| 210 |
if not audio_chunks:
|
| 211 |
logger.error("No audio data generated.")
|
| 212 |
-
raise HTTPException(status_code=
|
| 213 |
audio_data = b"".join(audio_chunks)
|
| 214 |
return StreamingResponse(io.BytesIO(audio_data), media_type="audio/wav")
|
| 215 |
except Exception as e:
|
| 216 |
logger.error(f"Audio generation failed: {e}")
|
| 217 |
-
raise HTTPException(status_code=
|
| 218 |
|
| 219 |
response_chunks = []
|
| 220 |
try:
|
|
@@ -225,8 +231,37 @@ async def chat_endpoint(
|
|
| 225 |
logger.warning(f"Unexpected non-string chunk in text stream: {chunk}")
|
| 226 |
response = "".join(response_chunks)
|
| 227 |
if not response.strip():
|
| 228 |
-
logger.
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
logger.info(f"Chat response: {response[:100]}...")
|
| 231 |
except Exception as e:
|
| 232 |
logger.error(f"Chat generation failed: {e}")
|
|
@@ -246,7 +281,6 @@ async def chat_endpoint(
|
|
| 246 |
}
|
| 247 |
|
| 248 |
return {"response": response}
|
| 249 |
-
|
| 250 |
@router.post("/api/audio-transcription")
|
| 251 |
async def audio_transcription_endpoint(
|
| 252 |
request: Request,
|
|
|
|
| 141 |
"uptime": os.popen("uptime").read().strip()
|
| 142 |
}
|
| 143 |
|
| 144 |
+
|
| 145 |
@router.post("/api/chat")
|
| 146 |
async def chat_endpoint(
|
| 147 |
request: Request,
|
|
|
|
| 179 |
preferred_model = user.preferred_model if user else None
|
| 180 |
model_name, api_endpoint = select_model(req.message, input_type="text", preferred_model=preferred_model)
|
| 181 |
|
| 182 |
+
# جرب النموذج الأساسي
|
| 183 |
is_available, api_key, selected_endpoint = check_model_availability(model_name, HF_TOKEN)
|
| 184 |
if not is_available:
|
| 185 |
+
logger.warning(f"Model {model_name} is not available at {api_endpoint}, trying fallback model.")
|
| 186 |
+
model_name = SECONDARY_MODEL_NAME # جرب النموذج البديل
|
| 187 |
+
is_available, api_key, selected_endpoint = check_model_availability(model_name, HF_TOKEN)
|
| 188 |
+
if not is_available:
|
| 189 |
+
logger.error(f"Fallback model {model_name} is not available at {selected_endpoint}")
|
| 190 |
+
raise HTTPException(status_code=503, detail=f"No available models. Tried {MODEL_NAME} and {SECONDARY_MODEL_NAME}.")
|
| 191 |
|
| 192 |
system_prompt = enhance_system_prompt(req.system_prompt, req.message, user)
|
| 193 |
|
|
|
|
| 215 |
logger.warning(f"Unexpected non-bytes chunk in audio stream: {chunk}")
|
| 216 |
if not audio_chunks:
|
| 217 |
logger.error("No audio data generated.")
|
| 218 |
+
raise HTTPException(status_code=502, detail="No audio data generated. Model may be unavailable.")
|
| 219 |
audio_data = b"".join(audio_chunks)
|
| 220 |
return StreamingResponse(io.BytesIO(audio_data), media_type="audio/wav")
|
| 221 |
except Exception as e:
|
| 222 |
logger.error(f"Audio generation failed: {e}")
|
| 223 |
+
raise HTTPException(status_code=502, detail=f"Audio generation failed: {str(e)}")
|
| 224 |
|
| 225 |
response_chunks = []
|
| 226 |
try:
|
|
|
|
| 231 |
logger.warning(f"Unexpected non-string chunk in text stream: {chunk}")
|
| 232 |
response = "".join(response_chunks)
|
| 233 |
if not response.strip():
|
| 234 |
+
logger.warning(f"Empty response from {model_name}. Trying fallback model {SECONDARY_MODEL_NAME}.")
|
| 235 |
+
# جرب النموذج البديل
|
| 236 |
+
model_name = SECONDARY_MODEL_NAME
|
| 237 |
+
is_available, api_key, selected_endpoint = check_model_availability(model_name, HF_TOKEN)
|
| 238 |
+
if not is_available:
|
| 239 |
+
logger.error(f"Fallback model {model_name} is not available at {selected_endpoint}")
|
| 240 |
+
raise HTTPException(status_code=503, detail=f"No available models. Tried {MODEL_NAME} and {SECONDARY_MODEL_NAME}.")
|
| 241 |
+
|
| 242 |
+
stream = request_generation(
|
| 243 |
+
api_key=api_key,
|
| 244 |
+
api_base=selected_endpoint,
|
| 245 |
+
message=req.message,
|
| 246 |
+
system_prompt=system_prompt,
|
| 247 |
+
model_name=model_name,
|
| 248 |
+
chat_history=req.history,
|
| 249 |
+
temperature=req.temperature,
|
| 250 |
+
max_new_tokens=req.max_new_tokens or 2048,
|
| 251 |
+
deep_search=req.enable_browsing,
|
| 252 |
+
input_type="text",
|
| 253 |
+
output_format=req.output_format
|
| 254 |
+
)
|
| 255 |
+
response_chunks = []
|
| 256 |
+
for chunk in stream:
|
| 257 |
+
if isinstance(chunk, str):
|
| 258 |
+
response_chunks.append(chunk)
|
| 259 |
+
else:
|
| 260 |
+
logger.warning(f"Unexpected non-string chunk in text stream: {chunk}")
|
| 261 |
+
response = "".join(response_chunks)
|
| 262 |
+
if not response.strip():
|
| 263 |
+
logger.error(f"Empty response from fallback model {model_name}.")
|
| 264 |
+
raise HTTPException(status_code=502, detail=f"Empty response from both {MODEL_NAME} and {SECONDARY_MODEL_NAME}.")
|
| 265 |
logger.info(f"Chat response: {response[:100]}...")
|
| 266 |
except Exception as e:
|
| 267 |
logger.error(f"Chat generation failed: {e}")
|
|
|
|
| 281 |
}
|
| 282 |
|
| 283 |
return {"response": response}
|
|
|
|
| 284 |
@router.post("/api/audio-transcription")
|
| 285 |
async def audio_transcription_endpoint(
|
| 286 |
request: Request,
|
api/models.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from pydantic import BaseModel, Field
|
| 2 |
from typing import List, Optional
|
| 3 |
from fastapi_users import schemas
|
|
@@ -34,6 +35,7 @@ class UserCreate(schemas.BaseUserCreate):
|
|
| 34 |
|
| 35 |
model_config = {"from_attributes": True}
|
| 36 |
|
|
|
|
| 37 |
class UserUpdate(BaseModel):
|
| 38 |
display_name: Optional[str] = None
|
| 39 |
preferred_model: Optional[str] = None
|
|
|
|
| 1 |
+
# api/models.py
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
from typing import List, Optional
|
| 4 |
from fastapi_users import schemas
|
|
|
|
| 35 |
|
| 36 |
model_config = {"from_attributes": True}
|
| 37 |
|
| 38 |
+
# Pydantic schema for updating user settings
|
| 39 |
class UserUpdate(BaseModel):
|
| 40 |
display_name: Optional[str] = None
|
| 41 |
preferred_model: Optional[str] = None
|
api/user_db.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# api/user_db.py
|
| 2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Any, AsyncGenerator, Dict, Optional
|
| 7 |
+
|
| 8 |
+
from fastapi import Depends
|
| 9 |
+
from fastapi_users.db import SQLAlchemyUserDatabase
|
| 10 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 11 |
+
from sqlalchemy import select
|
| 12 |
+
|
| 13 |
+
from api.database import User, OAuthAccount # استيراد جداولك
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
class CustomSQLAlchemyUserDatabase(SQLAlchemyUserDatabase[User, int]):
|
| 18 |
+
"""
|
| 19 |
+
قاعدة بيانات مخصَّصة لمكتبة fastapi‑users.
|
| 20 |
+
تضيف طريقة parse_id التي تُحوِّل الـ ID من str → int.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def parse_id(self, value: Any) -> int:
|
| 24 |
+
logger.debug(f"Parsing user id: {value} (type={type(value)})")
|
| 25 |
+
# إذا كان الـ ID نصًا (من JWT) → حوّله إلى int
|
| 26 |
+
return int(value) if isinstance(value, str) else value
|
| 27 |
+
|
| 28 |
+
# ---------- وظائف مساعدة ----------
|
| 29 |
+
async def get_by_email(self, email: str) -> Optional[User]:
|
| 30 |
+
logger.info(f"Looking for user with email: {email}")
|
| 31 |
+
stmt = select(self.user_table).where(self.user_table.email == email)
|
| 32 |
+
result = await self.session.execute(stmt)
|
| 33 |
+
return result.scalar_one_or_none()
|
| 34 |
+
|
| 35 |
+
async def create(self, create_dict: Dict[str, Any]) -> User:
|
| 36 |
+
logger.info(f"Creating new user: {create_dict.get('email')}")
|
| 37 |
+
user = self.user_table(**create_dict)
|
| 38 |
+
self.session.add(user)
|
| 39 |
+
await self.session.commit()
|
| 40 |
+
await self.session.refresh(user)
|
| 41 |
+
return user
|
| 42 |
+
|
| 43 |
+
# ---------- Dependency يُستَخدم في باقي المشروع ----------
|
| 44 |
+
async def get_user_db(
|
| 45 |
+
session: AsyncSession = Depends(lambda: None) # سيتم استبداله في database.py
|
| 46 |
+
) -> AsyncGenerator[CustomSQLAlchemyUserDatabase, None]:
|
| 47 |
+
"""
|
| 48 |
+
يُستَخدم كـ Depends في جميع المسارات التي تحتاج إلى قاعدة بيانات المستخدم.
|
| 49 |
+
سيتم تمرير الـ AsyncSession الفعلي من `api/database.py`.
|
| 50 |
+
"""
|
| 51 |
+
yield CustomSQLAlchemyUserDatabase(session, User, OAuthAccount)
|
utils/web_search.py
CHANGED
|
@@ -2,7 +2,6 @@ import os
|
|
| 2 |
import requests
|
| 3 |
from bs4 import BeautifulSoup
|
| 4 |
import logging
|
| 5 |
-
import time
|
| 6 |
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
|
@@ -12,11 +11,8 @@ def web_search(query: str) -> str:
|
|
| 12 |
google_cse_id = os.getenv("GOOGLE_CSE_ID")
|
| 13 |
if not google_api_key or not google_cse_id:
|
| 14 |
return "Web search requires GOOGLE_API_KEY and GOOGLE_CSE_ID to be set."
|
| 15 |
-
url = f"https://www.googleapis.com/customsearch/v1?key={google_api_key}&cx={google_cse_id}&q={query}"
|
| 16 |
-
|
| 17 |
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
| 18 |
-
}
|
| 19 |
-
response = requests.get(url, headers=headers, timeout=10)
|
| 20 |
response.raise_for_status()
|
| 21 |
results = response.json().get("items", [])
|
| 22 |
if not results:
|
|
@@ -27,8 +23,7 @@ def web_search(query: str) -> str:
|
|
| 27 |
snippet = item.get("snippet", "")
|
| 28 |
link = item.get("link", "")
|
| 29 |
try:
|
| 30 |
-
|
| 31 |
-
page_response = requests.get(link, headers=headers, timeout=10)
|
| 32 |
page_response.raise_for_status()
|
| 33 |
soup = BeautifulSoup(page_response.text, "html.parser")
|
| 34 |
paragraphs = soup.find_all("p")
|
|
|
|
| 2 |
import requests
|
| 3 |
from bs4 import BeautifulSoup
|
| 4 |
import logging
|
|
|
|
| 5 |
|
| 6 |
logger = logging.getLogger(__name__)
|
| 7 |
|
|
|
|
| 11 |
google_cse_id = os.getenv("GOOGLE_CSE_ID")
|
| 12 |
if not google_api_key or not google_cse_id:
|
| 13 |
return "Web search requires GOOGLE_API_KEY and GOOGLE_CSE_ID to be set."
|
| 14 |
+
url = f"https://www.googleapis.com/customsearch/v1?key={google_api_key}&cx={google_cse_id}&q={query}+site:https://hager-zon.vercel.app/"
|
| 15 |
+
response = requests.get(url, timeout=10)
|
|
|
|
|
|
|
|
|
|
| 16 |
response.raise_for_status()
|
| 17 |
results = response.json().get("items", [])
|
| 18 |
if not results:
|
|
|
|
| 23 |
snippet = item.get("snippet", "")
|
| 24 |
link = item.get("link", "")
|
| 25 |
try:
|
| 26 |
+
page_response = requests.get(link, timeout=5)
|
|
|
|
| 27 |
page_response.raise_for_status()
|
| 28 |
soup = BeautifulSoup(page_response.text, "html.parser")
|
| 29 |
paragraphs = soup.find_all("p")
|