feat: TUI restructure — console UI, PostgreSQL sink, package reorganization - #6
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Об'єднує ClickHouseSink з sinks.py та всю допоміжну логіку з clickhouse_export.py в один модуль olap_tool/sinks/clickhouse.py. Імпортує AnalyticsSink та sanitize_df з .base, ClickHouseConfig з ..config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…it__.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Старі плоскі файли замінені пакетом sinks/. Імпорт у import_xlsx_to_clickhouse.py оновлено на olap_tool.sinks.clickhouse. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Переміщено плоскі модулі в тематичні субпакети: - core/: config, cli, runner, periods, profiles, scheduler, compression, progress, utils - connection/: connection, auth, security, prompt - data/: queries, exporter Оновлено крос-пакетні імпорти та sinks/*.py (..config → ..core.config, ..utils → ..core.utils). Оновлено olap_tool/__init__.py і кореневий olap.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Замінює два окремі скрипти (import_xlsx_to_clickhouse.py, import_xlsx_to_duckdb.py) єдиним scripts/import_xlsx.py з аргументом --target ch|duck|pg. ClickHouse використовує thread-local sinks, DuckDB і PostgreSQL — один спільний sink на весь запуск. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ити olap.py - Додано textual>=0.70.0 до requirements.txt (виправлено також злиплий рядок rich+pyarrow) - TUIStream у utils.py: перехоплює stdout, очищує ANSI, пише у RichLog потокобезпечно - olap_tool/tui/: OlapApp + MainMenuScreen + OlapExportScreen + XlsxImportScreen - olap.py: без аргументів → TUI, з аргументами → CLI (sys.exit(main())) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TUIStream.flush() тепер дренує _buf замість no-op, щоб не втрачати останній рядок - PostgreSQLSink у scripts/import_xlsx.py переведено на thread-local патерн (_pg_local/_get_pg_sink/_process_pg), аналогічно ClickHouse; оновлено коментарі - _list_profiles() прив'язано до Path(__file__) замість CWD, щоб profiles/ знаходились незалежно від робочої директорії Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ля reconfigure, assert для cfg narrowing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BLOCKER: thread-local CH/PG sinks тепер викликають setup() при створенні (зберігаємо _ch_setup_df / _pg_setup_df під час ініціалізації) - asyncio.get_event_loop() → get_running_loop() в TUI screens - query_one() виноситься на головний потік, передається як аргумент - DuckDBSink._refresh_schema(): try/except з інформативним RuntimeError - _duck_value() видалено (мертвий код — вставка через Parquet, не VALUES) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Шукаємо перший непорожній файл для ініціалізації sink замість завжди брати files[0]. Без цього thread-local CH/PG sinks не отримували б setup_df і _client залишався None → тихий AttributeError у воркер-потоці. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Textual Select очікує (label, value), а не (value, label). Некоректний порядок призводив до InvalidSelectValueError при запуску. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ls, alongside connection authentication and prompting utilities.
…CSV/XLSX streaming, connection management, and progress display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… для quoting у exporter.py
… руйнує XmlReader стан
Критичні: - argv тепер передається через parse_arguments(argv) — sys.argv більше не мутується - DAX filter: умовне KEEPFILTERS/TREATAS — None не фільтрує до 0 рядків - ClickHouse _delete_period: перевірка обох ключів year_num+week_num - Shell injection: subprocess.run з list args замість shell=True - fetchmany default: arraysize=1 (DB-API 2.0) замість FieldCount Безпека: - Credentials: JSON серіалізація (колон у username) зі зворотною сумісністю - Plaintext credentials: попередження при encrypted=False - OleDbCommand.Dispose() у close() для звільнення .NET ресурсів Баги та якість: - max_year+1: дозволяє крос-річні періоди - DuckDB insert: повертає серверний count замість len(df) - sanitize_df: детекція колізій імен колонок (суфікс _1) - Sink orchestration: виділено _flush_to_sinks() helper (без дублювання) - Видалено подвійний sanitize_df у export_to_clickhouse - assert → if/raise у import_xlsx.py - Partial ZIP cleanup при помилці - XlsxStreamWriter.close() error handling - PROFILES_DIR відносно кореня проєкту - _shutdown_requested reset перед циклом - animation_running → threading.Event (thread-safe) - Raw print() → print_info/print_info_detail/print_error - ensure_dir: друкує тільки при створенні - DirectoryValidator у xlsx_import wizard - cryptography<46.0.0 upper bound Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Видалені невикористані імпорти: - runner.py: import sys - queries.py: import csv, import math - exporter.py: Tuple з typing - clickhouse.py: _safe_column_name re-export - import_xlsx.py: import os, дубльований dc_fields Видалені невикористані поля SecretsConfig: - port, http_url, timeout — ніколи не читались кодом - Оновлено .env.example (видалено OLAP_PORT, OLAP_HTTP_URL, OLAP_TIMEOUT) Видалений невикористаний параметр: - loading_spinner(estimated_time) — ніколи не використовувався в тілі функції Дедуплікація: - _show_summary() → спільний show_summary() в olap_tool/ui/__init__.py - dc_fields імпорт винесено на рівень модуля в import_xlsx.py Видалено: - scripts/__init__.py — порожній файл, scripts не є пакетом Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…_env_int format_file_size() → utils.py: - Видалено дублікат з compression.py (_format_file_size) - Замінено inline код у runner.py та queries.py - Єдина функція з підтримкою Б/КБ/МБ/ГБ ThreadLocalSinkPool + _process_file (import_xlsx.py): - 3 worker-функції (_process_ch, _process_pg, _process_shared) → 1 generic - 2 thread-local блоки (ch/pg) → 1 клас ThreadLocalSinkPool - Видалено 6 глобальних змінних (_ch_local, _ch_all_sinks, тощо) - Cleanup: sink_pool.close_all() замість 3 окремих блоків - Executor dispatch: один futures dict замість if/elif/else _env_int() → config.py: - Винесено повторюваний int parsing з try/except - Спрощено load_clickhouse_from_env, load_duckdb_from_env, load_postgres_from_env Загалом: -120 рядків Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wizard pre-populate з профілю (olap_export.py): - Після вибору профілю читає його YAML і витягує defaults - format, period_type, period_value, compress — pre-populated з профілю - Користувач може прийняти defaults (Enter) або змінити - Профіль weekly_sales з format=xlsx, period=last-weeks/4 → 7 Enter запуск ZIP summary (runner.py): - Додано print_success з шляхом та розміром ZIP після стиснення Dead TUI guard (progress.py): - Видалено hasattr(sys.stdout, "_app") — залишок від видаленого Textual TUI print_profiles_list (profiles.py): - raw print() → print_info_detail() з кольорами та timestamps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Якщо профіль містить format/period/compress — wizard не питає, а одразу використовує значення з профілю. Користувач бачить тільки summary + "Запустити?". Було: Профіль → Format? → Period? → Weeks? → Compress? → Summary → Запустити? Стало: Профіль (все визначено) → Summary → Запустити? (2 Enter) Питання задаються ТІЛЬКИ для параметрів, яких немає в профілі. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- print_header: colorama == HEADER == → rich.Rule (по центру з лініями) - print_info_detail: raw print + Fore → rich.Table (вирівняні key:value) - print_tech_error: raw print → rich.Table для помилок - print_info/warning/error/success/progress: colorama → rich markup - queries.py: видалено пряме використання Fore у success message - colorama залишено тільки для progress.py (spinner \r overwrite) Тепер весь статичний вивід (runner, queries, profiles, compression, connection) використовує rich — візуально консистентний з wizard UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clear screen: - Очищення консолі перед wizard і при поверненні в меню - "Натисніть Enter щоб повернутися в меню..." після завершення - Кожен wizard = окремий "екран", а не потік рядків Sink success output: - В sink-only mode (CH/DuckDB/PG) тепер виводиться success message з назвою sink та кількістю рядків після завантаження Dead code: - Видалено streaming_spinner() — не використовувався ніде - Видалено import Callable (використовувався тільки streaming_spinner) - scheduler.py: raw print() → print_info/print_warning Console instance: - menu.py тепер використовує спільний console з ui/__init__.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Спінер: - Не додає \n при завершенні — наступний print_* сам переходить на новий рядок - Рядок спінера повністю очищується перед наступним повідомленням Вивід: - Видалено дублюючий header "ПОЧАТОК РОБОТИ" (одного header достатньо) - Видалено print_info_detail з DAX параметрами (рік/тиждень) для кожного тижня — ця інформація вже є в header тижня - Duplicate column warning виводиться тільки один раз (перший тиждень) - Прогрес форматується в один рядок замість multi-line - Кожен тиждень починається з rich.Rule header замість print_info роздільника Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Спінер/countdown: - Зупиняємо спінер ПЕРЕД будь-яким текстовим виводом (join перед print) - Countdown очищує свій рядок при завершенні (як спінер) - Виправлено race condition: спінер thread + main thread rich Console Менше noise: - Видалено "Облікові дані успішно розшифровано" (internal) - "Спроба підключення через Pyadomd (ADOMD.NET)..." → "Підключення до OLAP сервера..." - load_profile(silent=True) у wizard — профіль не логується двічі - Duplicate column warning — спінер зупинений ДО виводу warnings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
XlsxStreamWriter.write_chunk() (exporter.py): - df.values.tolist() замість df.itertuples() + per-cell enumerate - NaN/inf обробка: vectorized df.replace() замість per-cell isinstance - Ширина колонок: vectorized df.astype(str).str.len().max() замість per-cell len(str(value)) - nan_inf_to_errors=True у Workbook options як safety net convert_dotnet_to_python (utils.py): - import System кешується у module-level _System (один раз) замість import на кожен виклик (~470K import lookups → 1) - Локальна змінна S = _System для швидшого доступу в циклі Benchmark (15K рядків × 30 колонок): Було: ~185 сек (3 хв 5 сек) Стало: ~2.9 сек Прискорення: ~60x Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…колонок) df[col_name] повертає DataFrame при дублікатах імен колонок, що ламає .str accessor. Замінено на df.iloc[:, col_idx] — працює незалежно від дублікатів. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Видалено невикористані імпорти: math, threading, convert_dotnet_to_python, progress - Видалено помилкову заміну NaN→None у float колонках (no-op, pandas конвертує назад) - NaN/Inf обробляються xlsxwriter через nan_inf_to_errors=True - CsvStreamWriter: inf→NaN (na_rep="" обробляє обидва) - Виправлено коментарі Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DuckDB: - BUG FIX: 4xx помилки (крім 429) тепер не ретраяться — raise одразу Раніше raise err ловився тим же except Exception і ретраївся 3 рази - Видалено подвійний sanitize_df в insert() — caller вже sanitize зробив - _normalize_bigint_date_cols: df.copy() один раз (було N разів у loop) - _to_str → _numeric_to_str: винесено з for-loop (одна функція замість N) ClickHouse: - DELETE в export_to_clickhouse тільки у standalone режимі (own_client) У sink режимі DELETE вже зроблено через sink.delete_period() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- failed_sinks set трекає sinks що впали (connection timeout тощо) - Впавший sink пропускається на наступних chunks (не ретраїться) - Success повідомлення показується тільки для sinks що реально завантажили дані - Було: ❌ Помилка PostgreSQLSink → ✅ Дані завантажено у PostgreSQL (хибний success) - Стало: ❌ Помилка PostgreSQLSink → (без success для PG) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Claude finished @starychenko's task in 4m 1s —— View job Огляд PR #6 — feat: TUI restructure
РезюмеPR реорганізує Вердикт: ✅ Approve з коментарями Блокерів немає. Одна IMPORTANT проблема (тиха втрата помилок у batch-скрипті), кілька NIT. Що зроблено добре
[IMPORTANT]
|
| Критерій | Статус |
|---|---|
os.getenv() тільки в config.py / security.py |
✅ |
Всі методи AnalyticsSink реалізовані |
✅ |
DELETE WHERE year_num+week_num перед INSERT |
✅ |
З'єднання закриваються у finally |
✅ |
sanitize_df не дублюється |
✅ |
| SQL без f-string інтерполяції user-контрольованих даних | ✅ (year/week — int, колонки — через sanitize_df) |
- _process_file: пробросити exceptions замість проковтування — outer handler у as_completed вже вміє виводити деталі помилок - requirements.txt: оновити коментар (import_xlsx_to_clickhouse.py → scripts/import_xlsx.py) - duckdb.py: прибрати зайвий type: ignore після _refresh_schema() - import_xlsx.py: передавати cfg_kwargs тільки для pool-based sinks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Claude finished @starychenko's task in 51s —— View job
Огляд PR #6 (оновлено після commit
|
| Зауваження | Статус |
|---|---|
[IMPORTANT] _process_file проковтував exceptions без логування |
✅ Виправлено — try/except прибрано, винятки тепер пробрасуються до outer handler у as_completed |
[NIT] requirements.txt — застарілий коментар про import_xlsx_to_clickhouse.py |
✅ Оновлено на scripts/import_xlsx.py |
[NIT] duckdb.py:180 — зайвий # type: ignore[arg-type] |
✅ Прибрано |
[NIT] cfg_kwargs передавався у DuckDB де він ігнорується |
✅ Виправлено — cfg_kwargs if sink_pool is not None else None |
Перевірка блокерів (всі відсутні ✅)
| Критерій | Статус |
|---|---|
os.getenv() тільки в config.py / security.py |
✅ |
Всі методи AnalyticsSink реалізовані |
✅ |
DELETE WHERE year_num+week_num перед INSERT |
✅ |
З'єднання закриваються у finally |
✅ |
sanitize_df не дублюється |
✅ |
| SQL без f-string інтерполяції user-контрольованих даних | ✅ |
Summary
olap_tool/реорганізовано вcore/,connection/,data/,sinks/,ui/PostgreSQLSink(psycopg2 COPY FROM STDIN), PG_* env varsscripts/import_xlsx.pyзамінює окреміimport_xlsx_to_clickhouse.pyтаimport_xlsx_to_duckdb.pysinks.py→ modularsinks/base.py,sinks/clickhouse.py,sinks/duckdb.py,sinks/postgresql.py;silentmode для batch-імпортуcolorama→richдля консольного виводуTest plan
python olap.py— відкривається інтерактивне менюpython olap.py --last-weeks 1 --format ch— CLI mode, ClickHouse sink працює без помилокis_tenderpython scripts/import_xlsx.py --target ch --dir result/ --year 2026— batch import, чистий вивід без зайвих логівpython scripts/import_xlsx.py --target pg --dir result/ --dry-run— PostgreSQL dry-runpython -c "from olap_tool.sinks import ClickHouseSink, DuckDBSink, PostgreSQLSink; print('OK')"— imports OK🤖 Generated with Claude Code