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
2727from bleach .sanitizer import Cleaner
2828from backend .errors import register_error_handlers , ValidationError
2929from backend .config import Config
30+ from backend .logging_config import setup_logging , generate_correlation_id , get_correlation_id
3031import time
3132import hashlib
3233from itertools import islice
3334from 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+ )
3742logger = 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
114143def _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
839905if __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 ())
0 commit comments