diff --git a/.idea/misc.xml b/.idea/misc.xml
index 3c99962..ced1df3 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,7 @@
-
-
+
+
+
\ No newline at end of file
diff --git a/.idea/new-planet-backend.iml b/.idea/new-planet-backend.iml
index d6ebd48..14d3ec3 100644
--- a/.idea/new-planet-backend.iml
+++ b/.idea/new-planet-backend.iml
@@ -2,8 +2,11 @@
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/new-planet-backend/.env b/new-planet-backend/.env
index e817043..1ed5ccf 100644
--- a/new-planet-backend/.env
+++ b/new-planet-backend/.env
@@ -1,21 +1,27 @@
-# Database
+
+GIGACHAT_CLIENT_ID=019966f4-1c5c-7382-9006-b84419fbe5d1
+GIGACHAT_CLIENT_SECRET=MDE5OTY2ZjQtMWM1Yy03MzgyLTkwMDYtYjg0NDE5ZmJlNWQxOjJjODBmOWE2LWU4YWMtNDE4YS1iOGVkLWE4NTE0YzVkNDAwNw==
+GIGACHAT_AUTH_URL=https://ngw.devices.sberbank.ru:9443/api/v2/oauth
+GIGACHAT_BASE_URL=https://gigachat.devices.sberbank.ru/api/v1
+GIGACHAT_MODEL_CHAT=GigaChat-2-Lite
+GIGACHAT_MODEL_SCHEDULE=GigaChat-2-Pro
+
+# Security
+SECRET_KEY=3db8542397edddbd6162ad823157e36f8d47232aa646725d4799266229ba7aa4
+
+# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=newplanet
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
-DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/newplanet
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
-REDIS_URL=redis://localhost:6379
-# Security
-SECRET_KEY=jwt-secret-key
-
-# Storage (MinIO/S3)
+# Storage
STORAGE_ENDPOINT=localhost:9000
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin
@@ -23,6 +29,3 @@ STORAGE_BUCKET=new-planet-images
STORAGE_USE_SSL=false
STORAGE_REGION=us-east-1
-# GigaChat API
-GIGACHAT_CLIENT_ID=gigachat-client-id
-GIGACHAT_CLIENT_SECRET="gigachat-token-here"
diff --git a/new-planet-backend/README.md b/new-planet-backend/README.md
index 7313f07..1e62c21 100644
--- a/new-planet-backend/README.md
+++ b/new-planet-backend/README.md
@@ -23,12 +23,13 @@ Backend API для мобильного приложения **Новая Пла
### Установка
1. Клонируйте репозиторий
-2. Установите зависимости:
+2. Установить окружение ("python -m venv venv")
+3. Установите зависимости:
```bash
pip install -r requirements.txt
```
-3. Настройте `.env`:
+4. Настройте `.env`:
```bash
cp .env.example .env
#В целом вам нужно поменять GIGACHAT API секцию, JWT Secret key сгенерить, просто в поисковике генератор на 256 байт сделаете JWT
@@ -36,7 +37,7 @@ cp .env.example .env
# Отредактируйте .env с вашими настройками
```
-4. Запустите инфраструктуру (Docker):
+5. Запустите инфраструктуру (Docker):
```bash
docker-compose -f docker/docker-compose.yml up -d
# или используйте вариант ниже,но лучше вариант выше для избежания непредвиденного
@@ -44,12 +45,12 @@ docker-compose -f docker/docker-compose.yml up -d
docker-compose up
```
-5. Примените миграции:
+6. Примените миграции:
```bash
alembic upgrade head
```
-6. Запустите сервер:
+7. Запустите сервер:
```bash
uvicorn app.main:app --reload
# если не запустилось, проверяйте есть ли .venv(установлено ли окружение для питона), также попробуйте в венве uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
diff --git a/new-planet-backend/app/api/deps.py b/new-planet-backend/app/api/deps.py
index bd73c8d..e924143 100644
--- a/new-planet-backend/app/api/deps.py
+++ b/new-planet-backend/app/api/deps.py
@@ -7,7 +7,7 @@ from app.crud import user as crud_user
from app.services.auth_service import auth_service
from app.models.user import User
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth/login")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(
diff --git a/new-planet-backend/app/api/v1/ai.py b/new-planet-backend/app/api/v1/ai.py
index 4025fe6..72e4d0b 100644
--- a/new-planet-backend/app/api/v1/ai.py
+++ b/new-planet-backend/app/api/v1/ai.py
@@ -3,9 +3,27 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.api.deps import get_current_active_user
from app.models.user import User
-from app.schemas.ai import ChatRequest, ChatResponse, ScheduleGenerateRequest, ScheduleGenerateResponse
+from app.models.ai_conversation import AIConversation
+from app.schemas.ai import (
+ ChatRequest,
+ ChatResponse,
+ ScheduleGenerateRequest,
+ ScheduleGenerateResponse,
+ ConversationHistory,
+ ConversationListItem,
+ ScheduleUpdateRequest,
+ ScheduleUpdateResponse,
+ RecommendationRequest,
+ RecommendationResponse,
+)
from app.services.chat_service import chat_service
from app.services.schedule_generator import schedule_generator
+from app.services.cache_service import cache_service
+from app.services.gigachat_service import gigachat_service
+from app.crud import schedule as crud_schedule, task as crud_task
+from app.schemas.task import TaskCreate
+from app.schemas.schedule import ScheduleUpdate
+from app.core.config import settings
router = APIRouter()
@@ -30,7 +48,6 @@ async def chat_with_ai(
detail=f"Chat error: {str(e)}"
)
-
@router.post("/schedule/generate", response_model=ScheduleGenerateResponse)
async def generate_schedule_ai(
request: ScheduleGenerateRequest,
@@ -58,3 +75,4 @@ async def generate_schedule_ai(
detail=f"Failed to generate schedule: {str(e)}"
)
+
diff --git a/new-planet-backend/app/core/config.py b/new-planet-backend/app/core/config.py
index 4a71bd6..7bf8087 100644
--- a/new-planet-backend/app/core/config.py
+++ b/new-planet-backend/app/core/config.py
@@ -10,15 +10,15 @@ class Settings(BaseSettings):
DEBUG: bool = False
# Security
- SECRET_KEY: str
+ SECRET_KEY: str = "3db8542397edddbd6162ad823157e36f8d47232aa646725d4799266229ba7aa4"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
# Database
- POSTGRES_USER: str
- POSTGRES_PASSWORD: str
- POSTGRES_DB: str
+ POSTGRES_USER: str = "postgres"
+ POSTGRES_PASSWORD: str = "postgres"
+ POSTGRES_DB: str = "newplanet"
POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432
DATABASE_URL: Optional[str] = None
@@ -43,15 +43,15 @@ class Settings(BaseSettings):
# Storage (MinIO/S3)
STORAGE_ENDPOINT: str = "localhost:9000"
- STORAGE_ACCESS_KEY: str
- STORAGE_SECRET_KEY: str
+ STORAGE_ACCESS_KEY: str = "minioadmin"
+ STORAGE_SECRET_KEY: str = "minioadmin"
STORAGE_BUCKET: str = "new-planet-images"
STORAGE_USE_SSL: bool = False
STORAGE_REGION: str = "us-east-1"
# GigaChat
- GIGACHAT_CLIENT_ID: str
- GIGACHAT_CLIENT_SECRET: str
+ GIGACHAT_CLIENT_ID: str = "019966f4-1c5c-7382-9006-b84419fbe5d1"
+ GIGACHAT_CLIENT_SECRET: str = "MDE5OTY2ZjQtMWM1Yy03MzgyLTkwMDYtYjg0NDE5ZmJlNWQxOjJjODBmOWE2LWU4YWMtNDE4YS1iOGVkLWE4NTE0YzVkNDAwNw=="
GIGACHAT_AUTH_URL: str = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"
GIGACHAT_BASE_URL: str = "https://gigachat.devices.sberbank.ru/api/v1"
GIGACHAT_MODEL_CHAT: str = "GigaChat-2-Lite"
diff --git a/new-planet-backend/app/core/security.py b/new-planet-backend/app/core/security.py
index a4eb2c3..d24198e 100644
--- a/new-planet-backend/app/core/security.py
+++ b/new-planet-backend/app/core/security.py
@@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from jose import JWTError, jwt
import bcrypt
-from app.core.config import settings
+from config import settings
def verify_password(plain_password: str, hashed_password: str) -> bool:
diff --git a/new-planet-backend/app/db/base.py b/new-planet-backend/app/db/base.py
index 0435fb7..4a3d0e7 100644
--- a/new-planet-backend/app/db/base.py
+++ b/new-planet-backend/app/db/base.py
@@ -1,4 +1,4 @@
-from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, DateTime, func, String
import uuid
diff --git a/new-planet-backend/app/db/init_db.py b/new-planet-backend/app/db/init_db.py
index b973e2c..e802165 100644
--- a/new-planet-backend/app/db/init_db.py
+++ b/new-planet-backend/app/db/init_db.py
@@ -1,5 +1,5 @@
-from app.db.base import Base
-from app.db.session import engine
+from base import Base
+from session import engine
from app.models import user, schedule, task, reward, ai_conversation
diff --git a/new-planet-backend/app/db/session.py b/new-planet-backend/app/db/session.py
index 04c4b72..e4632e5 100644
--- a/new-planet-backend/app/db/session.py
+++ b/new-planet-backend/app/db/session.py
@@ -3,8 +3,7 @@ from app.core.config import settings
engine = create_async_engine(
settings.database_url,
- echo=settings.DEBUG,
- future=True
+ echo=settings.DEBUG
)
AsyncSessionLocal = async_sessionmaker(
diff --git a/new-planet-backend/app/main.py b/new-planet-backend/app/main.py
index 80e524f..8404054 100644
--- a/new-planet-backend/app/main.py
+++ b/new-planet-backend/app/main.py
@@ -1,3 +1,18 @@
+import sys
+from pathlib import Path
+
+# Добавляем корневую директорию проекта в PYTHONPATH при прямом запуске
+# Это нужно, чтобы Python мог найти модуль 'app'
+# Проверяем, запускается ли файл напрямую, проверяя имя скрипта
+if sys.argv and len(sys.argv) > 0:
+ script_path = Path(sys.argv[0]).resolve()
+ current_file = Path(__file__).resolve()
+ # Если скрипт запускается напрямую (не через модуль)
+ if script_path == current_file or script_path.name == current_file.name:
+ project_root = current_file.parent.parent # new-planet-backend
+ if str(project_root) not in sys.path:
+ sys.path.insert(0, str(project_root))
+
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
@@ -73,7 +88,7 @@ if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
- host="0.0.0.0",
+ host="127.0.0.1",
port=8000,
reload=settings.DEBUG
)
diff --git a/new-planet-backend/app/schemas/ai.py b/new-planet-backend/app/schemas/ai.py
index 3f17ed9..4c5e9e3 100644
--- a/new-planet-backend/app/schemas/ai.py
+++ b/new-planet-backend/app/schemas/ai.py
@@ -38,3 +38,41 @@ class ConversationHistory(BaseModel):
class Config:
from_attributes = True
+
+class ConversationListItem(BaseModel):
+ """Элемент списка разговоров"""
+ conversation_id: str
+ last_message: Optional[str] = None
+ created_at: datetime
+ updated_at: datetime
+ message_count: int = 0
+
+ class Config:
+ from_attributes = True
+
+
+class ScheduleUpdateRequest(BaseModel):
+ """Запрос на обновление расписания через ИИ"""
+ user_request: str = Field(..., min_length=1, max_length=1000, description="Описание желаемых изменений")
+
+
+class ScheduleUpdateResponse(BaseModel):
+ """Ответ после обновления расписания"""
+ schedule_id: str
+ title: str
+ tasks: List[Dict[str, Any]]
+ tokens_used: Optional[int] = None
+
+
+class RecommendationRequest(BaseModel):
+ """Запрос на получение рекомендаций"""
+ preferences: List[str] = Field(default_factory=list, description="Предпочтения пользователя")
+ category: Optional[str] = Field(None, description="Категория заданий")
+ completed_tasks: Optional[List[str]] = Field(default_factory=list, description="Уже выполненные задания")
+ top_k: int = Field(5, ge=1, le=20, description="Количество рекомендаций")
+
+
+class RecommendationResponse(BaseModel):
+ """Ответ с рекомендациями"""
+ recommendations: List[Dict[str, Any]]
+ total: int
\ No newline at end of file
diff --git a/new-planet-backend/app/services/gigachat_service.py b/new-planet-backend/app/services/gigachat_service.py
index cd3ed52..e7e73b8 100644
--- a/new-planet-backend/app/services/gigachat_service.py
+++ b/new-planet-backend/app/services/gigachat_service.py
@@ -1,11 +1,17 @@
+import os
+
import aiohttp
-import ssl
import base64
import uuid
import time
+from urllib.parse import urlencode
from typing import Optional, List, Dict, Any
+
+from dotenv import load_dotenv
+
from app.core.config import settings
+load_dotenv()
class GigaChatService:
def __init__(self):
@@ -19,8 +25,19 @@ class GigaChatService:
if time.time() < (self.token_expires_at - 60):
return self.access_token
- credentials = f"{settings.GIGACHAT_CLIENT_ID}:{settings.GIGACHAT_CLIENT_SECRET}"
- encoded_credentials = base64.b64encode(credentials.encode()).decode()
+ # Проверяем наличие credentials
+ client_id = os.getenv("GIGACHAT_CLIENT_ID")
+ client_secret = os.getenv("GIGACHAT_CLIENT_SECRET")
+
+ if not client_id or not client_secret:
+ raise Exception(
+ "GigaChat credentials not configured. "
+ "Please set GIGACHAT_CLIENT_ID and GIGACHAT_CLIENT_SECRET in .env file"
+ )
+
+ # Формируем credentials и кодируем в Base64 с явным указанием UTF-8
+ credentials = f"{client_id}:{client_secret}".strip().encode('utf-8') # Обрезаем лишние символы
+ encoded_credentials = base64.b64encode(credentials).decode('utf-8')
headers = {
"Authorization": f"Basic {encoded_credentials}",
@@ -29,31 +46,61 @@ class GigaChatService:
"RqUID": str(uuid.uuid4())
}
- data = {"scope": "GIGACHAT_API_PERS"}
+ # Правильно кодируем данные формы (как в рабочем примере)
+ form_data = {
+ "grant_type": "client_credentials",
+ "scope": "GIGACHAT_API_PERS"
+ }
- # Создаем SSL контекст без проверки сертификата (только для разработки!)
- ssl_context = ssl.create_default_context()
- ssl_context.check_hostname = False
- ssl_context.verify_mode = ssl.CERT_NONE
-
- connector = aiohttp.TCPConnector(ssl=ssl_context)
+ # Отключаем проверку SSL (только для разработки!)
+ # Используем ssl=False для полного отключения проверки сертификата
+ connector = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post(
- settings.GIGACHAT_AUTH_URL,
+ os.getenv("GIGACHAT_BASE_URL"),
headers=headers,
- data=data
+ data=form_data
) as response:
if response.status != 200:
- raise Exception(f"Failed to get token: {response.status}")
+ # Получаем детали ошибки из ответа
+ try:
+ error_body = await response.text()
+ # Пытаемся распарсить как JSON, если не получается - возвращаем текст
+ try:
+ error_json = await response.json()
+ error_detail = error_json.get("error_description") or error_json.get("error") or str(error_json)
+ except:
+ error_detail = error_body
+ except:
+ error_detail = "No error details available"
+
+ raise Exception(
+ f"Failed to get token: HTTP {response.status}. "
+ f"Error details: {error_detail}. "
+ f"Check your GIGACHAT_CLIENT_ID and GIGACHAT_CLIENT_SECRET_2 in .env file"
+ )
result = await response.json()
self.access_token = result.get("access_token")
- expires_in = result.get("expires_at", 1800)
- # expires_at может быть timestamp или количество секунд
- if expires_in > 1000000000: # Это timestamp
- self.token_expires_at = expires_in
- else: # Это количество секунд
+ if not self.access_token:
+ raise Exception(f"Token not found in response: {result}")
+
+ # Обрабатываем время истечения токена (может быть expires_at или expires_in)
+ expires_at = result.get("expires_at")
+ expires_in = result.get("expires_in")
+
+ if expires_at:
+ # expires_at может быть timestamp или количество секунд
+ if expires_at > 1000000000: # Это timestamp
+ self.token_expires_at = expires_at
+ else: # Это количество секунд
+ self.token_expires_at = time.time() + expires_at
+ elif expires_in:
+ # expires_in - это всегда количество секунд до истечения
self.token_expires_at = time.time() + expires_in
+ else:
+ # По умолчанию 30 минут (1800 секунд)
+ self.token_expires_at = time.time() + 1800
return self.access_token
@@ -65,7 +112,7 @@ class GigaChatService:
) -> Dict[str, Any]:
"""Отправить сообщение в GigaChat"""
token = await self._get_token()
- model = model or settings.GIGACHAT_MODEL_CHAT
+ model = model or settings.GIGACHAT_MODEL_CHAT or "GigaChat"
messages = context or []
messages.append({"role": "user", "content": message})
@@ -82,12 +129,9 @@ class GigaChatService:
"max_tokens": 2000
}
- # Создаем SSL контекст без проверки сертификата (только для разработки!)
- ssl_context = ssl.create_default_context()
- ssl_context.check_hostname = False
- ssl_context.verify_mode = ssl.CERT_NONE
-
- connector = aiohttp.TCPConnector(ssl=ssl_context)
+ # Отключаем проверку SSL (только для разработки!)
+ # Используем ssl=False для полного отключения проверки сертификата
+ connector = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post(
f"{settings.GIGACHAT_BASE_URL}/chat/completions",
diff --git a/new-planet-backend/docker/docker-compose.yml b/new-planet-backend/docker/docker-compose.yml
index ff323b9..c75e92b 100644
--- a/new-planet-backend/docker/docker-compose.yml
+++ b/new-planet-backend/docker/docker-compose.yml
@@ -17,6 +17,8 @@ services:
interval: 10s
timeout: 5s
retries: 5
+ networks:
+ - new-planet-network
redis:
image: redis:7-alpine
@@ -30,6 +32,8 @@ services:
interval: 10s
timeout: 5s
retries: 5
+ networks:
+ - new-planet-network
minio:
image: minio/minio:latest
@@ -48,9 +52,15 @@ services:
interval: 30s
timeout: 20s
retries: 3
+ networks:
+ - new-planet-network
volumes:
postgres_data:
redis_data:
minio_data:
+networks:
+ new-planet-network:
+ driver: bridge
+
diff --git a/new-planet-backend/logs/app.log b/new-planet-backend/logs/app.log
index d1f111d..fb61425 100644
--- a/new-planet-backend/logs/app.log
+++ b/new-planet-backend/logs/app.log
@@ -1341,3 +1341,242 @@ WHERE users.id = $1::UUID]
2025-12-18 13:49:33 - root - INFO - Shutting down...
2025-12-18 13:49:38 - root - INFO - Starting up...
2025-12-18 13:51:42 - root - INFO - Shutting down...
+2025-12-18 18:44:49 - root - INFO - Starting up...
+2025-12-18 19:39:26 - root - INFO - Starting up...
+2025-12-18 19:40:16 - app.middleware.error_handler - ERROR - Unhandled exception: Error 111 connecting to localhost:6379. Connection refused.
+ + Exception Group Traceback (most recent call last):
+ | File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 79, in collapse_excgroups
+ | yield
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 192, in __call__
+ | async with anyio.create_task_group() as task_group:
+ | File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 783, in __aexit__
+ | raise BaseExceptionGroup(
+ | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+ +-+---------------- 1 ----------------
+ | Traceback (most recent call last):
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
+ | await self.app(scope, receive, _send)
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
+ | with recv_stream, send_stream, collapse_excgroups():
+ | File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
+ | self.gen.throw(typ, value, traceback)
+ | File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
+ | raise exc
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
+ | response = await self.dispatch_func(request, call_next)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
+ | current_requests = await cache_service.get(cache_key)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/app/app/services/cache_service.py", line 28, in get
+ | return await self.redis_client.get(key)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
+ | conn = self.connection or await pool.get_connection()
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
+ | await self.ensure_connection(connection)
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
+ | await connection.connect()
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
+ | await self.connect_check_health(check_health=True)
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
+ | raise ConnectionError(self._error_message(e))
+ | redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+ +------------------------------------
+
+During handling of the above exception, another exception occurred:
+
+Traceback (most recent call last):
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
+ await self.app(scope, receive, _send)
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
+ with recv_stream, send_stream, collapse_excgroups():
+ File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
+ self.gen.throw(typ, value, traceback)
+ File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
+ raise exc
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
+ response = await self.dispatch_func(request, call_next)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
+ current_requests = await cache_service.get(cache_key)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/app/app/services/cache_service.py", line 28, in get
+ return await self.redis_client.get(key)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
+ conn = self.connection or await pool.get_connection()
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
+ await self.ensure_connection(connection)
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
+ await connection.connect()
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
+ await self.connect_check_health(check_health=True)
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
+ raise ConnectionError(self._error_message(e))
+redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+2025-12-18 19:40:16 - app.middleware.error_handler - ERROR - Unhandled exception: Error 111 connecting to localhost:6379. Connection refused.
+ + Exception Group Traceback (most recent call last):
+ | File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 79, in collapse_excgroups
+ | yield
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 192, in __call__
+ | async with anyio.create_task_group() as task_group:
+ | File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 783, in __aexit__
+ | raise BaseExceptionGroup(
+ | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+ +-+---------------- 1 ----------------
+ | Traceback (most recent call last):
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
+ | await self.app(scope, receive, _send)
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
+ | with recv_stream, send_stream, collapse_excgroups():
+ | File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
+ | self.gen.throw(typ, value, traceback)
+ | File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
+ | raise exc
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
+ | response = await self.dispatch_func(request, call_next)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
+ | current_requests = await cache_service.get(cache_key)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/app/app/services/cache_service.py", line 28, in get
+ | return await self.redis_client.get(key)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
+ | conn = self.connection or await pool.get_connection()
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
+ | await self.ensure_connection(connection)
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
+ | await connection.connect()
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
+ | await self.connect_check_health(check_health=True)
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
+ | raise ConnectionError(self._error_message(e))
+ | redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+ +------------------------------------
+
+During handling of the above exception, another exception occurred:
+
+Traceback (most recent call last):
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
+ await self.app(scope, receive, _send)
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
+ with recv_stream, send_stream, collapse_excgroups():
+ File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
+ self.gen.throw(typ, value, traceback)
+ File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
+ raise exc
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
+ response = await self.dispatch_func(request, call_next)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
+ current_requests = await cache_service.get(cache_key)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/app/app/services/cache_service.py", line 28, in get
+ return await self.redis_client.get(key)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
+ conn = self.connection or await pool.get_connection()
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
+ await self.ensure_connection(connection)
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
+ await connection.connect()
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
+ await self.connect_check_health(check_health=True)
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
+ raise ConnectionError(self._error_message(e))
+redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+2025-12-18 19:40:16 - app.middleware.error_handler - ERROR - Unhandled exception: Error 111 connecting to localhost:6379. Connection refused.
+ + Exception Group Traceback (most recent call last):
+ | File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 79, in collapse_excgroups
+ | yield
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 192, in __call__
+ | async with anyio.create_task_group() as task_group:
+ | File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 783, in __aexit__
+ | raise BaseExceptionGroup(
+ | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+ +-+---------------- 1 ----------------
+ | Traceback (most recent call last):
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
+ | await self.app(scope, receive, _send)
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
+ | with recv_stream, send_stream, collapse_excgroups():
+ | File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
+ | self.gen.throw(typ, value, traceback)
+ | File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
+ | raise exc
+ | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
+ | response = await self.dispatch_func(request, call_next)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
+ | current_requests = await cache_service.get(cache_key)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/app/app/services/cache_service.py", line 28, in get
+ | return await self.redis_client.get(key)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
+ | conn = self.connection or await pool.get_connection()
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
+ | await self.ensure_connection(connection)
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
+ | await connection.connect()
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
+ | await self.connect_check_health(check_health=True)
+ | File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
+ | raise ConnectionError(self._error_message(e))
+ | redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+ +------------------------------------
+
+During handling of the above exception, another exception occurred:
+
+Traceback (most recent call last):
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
+ await self.app(scope, receive, _send)
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
+ with recv_stream, send_stream, collapse_excgroups():
+ File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
+ self.gen.throw(typ, value, traceback)
+ File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
+ raise exc
+ File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
+ response = await self.dispatch_func(request, call_next)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
+ current_requests = await cache_service.get(cache_key)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/app/app/services/cache_service.py", line 28, in get
+ return await self.redis_client.get(key)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
+ conn = self.connection or await pool.get_connection()
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
+ await self.ensure_connection(connection)
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
+ await connection.connect()
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
+ await self.connect_check_health(check_health=True)
+ File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
+ raise ConnectionError(self._error_message(e))
+redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+2025-12-18 22:55:17 - root - INFO - Starting up...
+2025-12-18 22:55:17 - root - INFO - Starting up...
+2025-12-18 22:56:56 - root - INFO - Starting up...
+2025-12-18 22:56:56 - root - INFO - Shutting down...
+2025-12-18 22:59:02 - root - INFO - Starting up...
+2025-12-18 22:59:02 - root - INFO - Shutting down...
+2025-12-18 22:59:53 - root - INFO - Starting up...
+2025-12-18 22:59:53 - root - INFO - Starting up...
+2025-12-18 22:59:53 - root - INFO - Shutting down...
+2025-12-18 22:59:53 - root - INFO - Shutting down...
+2025-12-18 20:00:09 - root - INFO - Shutting down...
+2025-12-18 23:00:15 - root - INFO - Starting up...
+2025-12-18 23:00:15 - root - INFO - Starting up...
+2025-12-18 23:00:15 - root - INFO - Shutting down...
+2025-12-18 23:00:15 - root - INFO - Shutting down...