Skip to content

Commit 32ec62e

Browse files
committed
feat: обновление backend файлов рефакторинга
- Интеграция централизованного логирования - Обновление конфигурации и типов - Обновление Dockerfile с curl для healthcheck
1 parent bad75eb commit 32ec62e

File tree

6 files changed

+114
-33
lines changed

6 files changed

+114
-33
lines changed

backend/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
66
WORKDIR /app
77

88
# System deps for WeasyPrint (cairo/pango/gdk-pixbuf) and fonts
9+
# curl для healthcheck
910
RUN apt-get update && apt-get install -y --no-install-recommends \
1011
libcairo2 \
1112
libpango-1.0-0 \
1213
libpangoft2-1.0-0 \
1314
libgdk-pixbuf-2.0-0 \
1415
shared-mime-info \
1516
fonts-liberation \
17+
curl \
1618
&& rm -rf /var/lib/apt/lists/*
1719

1820
COPY backend/requirements.txt /app/backend/requirements.txt

backend/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class Config:
5050
# Temp files cleanup (minutes)
5151
TEMP_CLEANUP_AGE_MIN: int = int(os.getenv("TEMP_CLEANUP_AGE_MIN", "30"))
5252

53+
# Logging
54+
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
55+
LOG_JSON_FORMAT: bool = _get_bool("LOG_JSON_FORMAT", "false")
56+
LOG_FILE: Optional[str] = os.getenv("LOG_FILE")
57+
5358
@classmethod
5459
def validate(cls) -> None:
5560
"""

backend/pdf_server.py

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,22 @@
2323
UploadPageResponse,
2424
TestResponse
2525
)
26-
from backend.services import AnalysisCache, perform_basic_analysis
26+
from backend.services import AnalysisCache, perform_basic_analysis, get_incremental_analysis
2727
from bleach.sanitizer import Cleaner
2828
from backend.errors import register_error_handlers, ValidationError
2929
from backend.config import Config
30+
from backend.logging_config import setup_logging, generate_correlation_id, get_correlation_id
3031
import time
3132
import hashlib
3233
from itertools import islice
3334
from collections import deque
3435

35-
# Настраиваем логирование
36-
logging.basicConfig(level=logging.DEBUG)
36+
# Настраиваем логирование с JSON форматом и correlation IDs
37+
setup_logging(
38+
level=Config.LOG_LEVEL,
39+
json_format=Config.LOG_JSON_FORMAT,
40+
log_file=Config.LOG_FILE
41+
)
3742
logger = logging.getLogger(__name__)
3843

3944
# Импортируем LLM-обработчик после загрузки Config
@@ -110,6 +115,30 @@ def _prune_bucket(bucket: deque[float], cutoff: float) -> None:
110115
while bucket and bucket[0] <= cutoff:
111116
bucket.popleft()
112117

118+
@app.before_request
119+
def _set_correlation_id() -> None:
120+
"""
121+
Устанавливает correlation ID для каждого запроса.
122+
Использует X-Correlation-ID из заголовка или генерирует новый.
123+
"""
124+
from flask import g, after_this_request
125+
126+
# Проверяем есть ли correlation ID в заголовке запроса
127+
correlation_id = request.headers.get('X-Correlation-ID')
128+
if not correlation_id:
129+
# Генерируем новый correlation ID
130+
correlation_id = generate_correlation_id()
131+
132+
# Сохраняем в Flask g для доступа в других частях приложения
133+
g.correlation_id = correlation_id
134+
135+
# Добавляем в response заголовки для клиента
136+
@after_this_request
137+
def add_correlation_id_header(response: Response) -> Response:
138+
response.headers['X-Correlation-ID'] = correlation_id
139+
return response
140+
141+
113142
@app.before_request
114143
def _security_and_rate_limit() -> Optional[Response]:
115144
"""
@@ -441,6 +470,8 @@ def upload_file() -> Response:
441470

442471
dataset_id = _make_dataset_id(temp_file_name)
443472

473+
incremental_analysis = get_incremental_analysis()
474+
444475
if file_extension.endswith('.csv'):
445476
# CSV: считаем общее число строк быстро и читаем только нужную страницу
446477
total_rows = _csv_count_rows_fast(temp_file_name)
@@ -453,20 +484,25 @@ def upload_file() -> Response:
453484
columns = []
454485
df_for_analysis = df_page # быстрый анализ по текущей странице
455486
kind = 'csv'
487+
cached_df = None # CSV не кэшируем полностью
456488
elif file_extension.endswith(('.xlsx', '.xls')):
489+
# Excel: читаем полностью и кэшируем DataFrame
457490
df = process_excel(temp_file_name)
458491
total_rows = len(df)
459492
df_page = df.iloc[(page - 1) * page_size: (page - 1) * page_size + page_size]
460493
columns = df.columns.tolist()
461-
df_for_analysis = df
494+
df_for_analysis = df_page # Анализ только текущей страницы
462495
kind = 'excel'
496+
cached_df = df # Кэшируем полный DataFrame
463497
elif file_extension.endswith('.pdf'):
498+
# PDF: читаем полностью и кэшируем DataFrame
464499
df = process_pdf(temp_file_name)
465500
total_rows = len(df)
466501
df_page = df.iloc[(page - 1) * page_size: (page - 1) * page_size + page_size]
467502
columns = df.columns.tolist()
468-
df_for_analysis = df
503+
df_for_analysis = df_page # Анализ только текущей страницы
469504
kind = 'pdf'
505+
cached_df = df # Кэшируем полный DataFrame
470506
else:
471507
raise ValidationError("Unsupported file format")
472508

@@ -484,8 +520,13 @@ def upload_file() -> Response:
484520
# Нормализуем записи (заменяем отсутствующие/NaN значения на дефолты)
485521
records = [normalize_record(record) for record in records]
486522

487-
# Выполняем базовый анализ (для CSV — по текущей странице как укороченный вариант)
488-
basic_analysis = perform_basic_analysis(df_for_analysis if not df_for_analysis.empty else df_page)
523+
# Используем инкрементальный анализ
524+
if page == 1:
525+
# Первая страница - инициализируем анализ
526+
basic_analysis = incremental_analysis.initialize_analysis(dataset_id, df_page)
527+
else:
528+
# Последующие страницы - обновляем анализ инкрементально
529+
basic_analysis = incremental_analysis.update_analysis(dataset_id, df_page)
489530

490531
# Сохраняем метаданные датасета для последующих запросов страниц
491532
_datasets[dataset_id] = {
@@ -495,6 +536,7 @@ def upload_file() -> Response:
495536
'columns': columns,
496537
'total_rows': total_rows,
497538
'created_at': time.time(),
539+
'cached_df': cached_df, # Кэшированный DataFrame для Excel/PDF
498540
}
499541

500542
# Формируем ответ
@@ -768,17 +810,31 @@ def get_upload_page() -> Response:
768810
columns = meta['columns']
769811
total_rows = int(meta['total_rows'])
770812
total_pages = (total_rows + page_size - 1) // page_size if total_rows > 0 else 1
813+
cached_df = meta.get('cached_df') # Кэшированный DataFrame для Excel/PDF
814+
815+
incremental_analysis = get_incremental_analysis()
771816

772817
if kind == 'csv':
818+
# CSV: читаем страницу напрямую из файла
773819
df_page, _ = _csv_get_page(file_path, page, page_size)
774820
if not columns:
775821
columns = df_page.columns.tolist()
776822
elif kind == 'excel':
777-
df = process_excel(file_path)
823+
# Excel: используем кэшированный DataFrame если есть, иначе читаем заново
824+
if cached_df is not None:
825+
df = cached_df
826+
else:
827+
df = process_excel(file_path)
828+
meta['cached_df'] = df # Кэшируем для следующих запросов
778829
start_idx = (page - 1) * page_size
779830
df_page = df.iloc[start_idx:start_idx + page_size]
780831
elif kind == 'pdf':
781-
df = process_pdf(file_path)
832+
# PDF: используем кэшированный DataFrame если есть, иначе читаем заново
833+
if cached_df is not None:
834+
df = cached_df
835+
else:
836+
df = process_pdf(file_path)
837+
meta['cached_df'] = df # Кэшируем для следующих запросов
782838
start_idx = (page - 1) * page_size
783839
df_page = df.iloc[start_idx:start_idx + page_size]
784840
else:
@@ -789,14 +845,18 @@ def get_upload_page() -> Response:
789845
# Нормализуем записи (используем общую функцию)
790846
records = [normalize_record(record) for record in records]
791847

848+
# Обновляем инкрементальный анализ
849+
basic_analysis = incremental_analysis.update_analysis(dataset_id, df_page)
850+
792851
return jsonify({
793852
'table_data': records,
794853
'columns': columns,
795854
'total_rows': total_rows,
796855
'current_page': page,
797856
'page_size': page_size,
798857
'total_pages': total_pages,
799-
'dataset_id': dataset_id
858+
'dataset_id': dataset_id,
859+
'basic_analysis': basic_analysis # Возвращаем обновленный анализ
800860
})
801861
except ValidationError as e:
802862
raise e
@@ -808,10 +868,13 @@ def _cleanup_old_datasets() -> None:
808868
"""
809869
Удаляет старые датасеты из _datasets и их директории.
810870
Использует TEMP_CLEANUP_AGE_MIN из конфигурации для определения возраста.
871+
Также очищает инкрементальный анализ для удаляемых датасетов.
811872
"""
812873
now = time.time()
813874
age_sec = Config.TEMP_CLEANUP_AGE_MIN * 60
814875
to_remove = []
876+
incremental_analysis = get_incremental_analysis()
877+
815878
for dataset_id, meta in _datasets.items():
816879
if now - meta.get('created_at', 0) > age_sec:
817880
to_remove.append(dataset_id)
@@ -823,6 +886,9 @@ def _cleanup_old_datasets() -> None:
823886
shutil.rmtree(temp_dir, ignore_errors=True)
824887
except Exception as e:
825888
logger.warning(f"Failed to remove temp dir {temp_dir}: {e}")
889+
# Очищаем инкрементальный анализ
890+
incremental_analysis.clear_analysis(dataset_id)
891+
826892
for dataset_id in to_remove:
827893
_datasets.pop(dataset_id, None)
828894

@@ -837,4 +903,13 @@ def _cleanup_before_request() -> None:
837903
_cleanup_old_datasets()
838904

839905
if __name__ == '__main__':
906+
# Логируем информацию о запуске
907+
logger.info("Starting VCb03 Backend API", extra={
908+
'extra_fields': {
909+
'test_mode': Config.TEST_MODE,
910+
'log_level': Config.LOG_LEVEL,
911+
'json_logging': Config.LOG_JSON_FORMAT,
912+
'api_key_set': bool(Config.API_KEY)
913+
}
914+
})
840915
app.run(host='0.0.0.0', port=5000, debug=Config.get_debug_flag())

backend/requirements.txt

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
# Для веб-сервера и переменных окружения
2-
Flask
3-
python-dotenv
4-
requests
5-
flask-cors
2+
Flask==3.0.0
3+
python-dotenv==1.0.0
4+
requests==2.31.0
5+
flask-cors==4.0.0
6+
67
# Для работы с нейросетями
7-
gigachat
8-
openai
8+
gigachat==0.1.12
9+
openai==1.12.0
10+
911
# Для генерации PDF
10-
WeasyPrint
11-
bleach
12+
WeasyPrint==60.2
13+
bleach==6.1.0
14+
1215
# Для работы с файлами данных
13-
pandas
14-
pdfplumber
15-
openpyxl # для работы с Excel
16+
pandas==2.1.4
17+
pdfplumber==0.10.3
18+
openpyxl==3.1.2 # для работы с Excel
19+
1620
# Для обработки PDF и изображений
17-
Pillow # для работы с изображениями
18-
pytesseract # для OCR
19-
python-docx # для структурированного текста
20-
# Для тестирования
21-
pytest
22-
pytest-cov
23-
# Для форматирования и проверки кода
24-
black
25-
mypy
26-
types-requests
21+
Pillow==10.2.0 # для работы с изображениями
22+
pytesseract==0.3.10 # для OCR
23+
python-docx==1.1.0 # для структурированного текста

backend/services/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
Сервисные модули для обработки данных и кэширования.
33
"""
44

5-
from .analysis_cache import AnalysisCache
5+
from .analysis_cache import AnalysisCache, get_cache
66
from .table_analysis import perform_basic_analysis
7+
from .incremental_analysis import IncrementalAnalysis, get_incremental_analysis
78

8-
__all__ = ['AnalysisCache', 'perform_basic_analysis']
9+
__all__ = ['AnalysisCache', 'get_cache', 'perform_basic_analysis', 'IncrementalAnalysis', 'get_incremental_analysis']
910

backend/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class UploadPageResponse(TypedDict):
7676
page_size: int
7777
total_pages: int
7878
dataset_id: str
79+
basic_analysis: BasicAnalysis
7980

8081

8182
class TestResponse(TypedDict):

0 commit comments

Comments
 (0)