Skip to content

feat: TUI restructure — console UI, PostgreSQL sink, package reorganization - #6

Merged
starychenko merged 47 commits into
mainfrom
feature/tui-restructure
Mar 16, 2026
Merged

feat: TUI restructure — console UI, PostgreSQL sink, package reorganization#6
starychenko merged 47 commits into
mainfrom
feature/tui-restructure

Conversation

@starychenko

Copy link
Copy Markdown
Owner

Summary

  • Console UI — інтерактивне меню (InquirerPy + rich) при запуску без аргументів: wizard експорту з OLAP та wizard імпорту XLSX
  • Package restructureolap_tool/ реорганізовано в core/, connection/, data/, sinks/, ui/
  • PostgreSQL sink — новий PostgreSQLSink (psycopg2 COPY FROM STDIN), PG_* env vars
  • Unified import scriptscripts/import_xlsx.py замінює окремі import_xlsx_to_clickhouse.py та import_xlsx_to_duckdb.py
  • Sinks refactorsinks.py → modular sinks/base.py, sinks/clickhouse.py, sinks/duckdb.py, sinks/postgresql.py; silent mode для batch-імпорту
  • Bug fixes — non-Nullable String handling в CH, XLSX vectorized write ~60x faster, sink error tracking, 4xx retry, NaN/inf, duplicate columns
  • Utils migrationcoloramarich для консольного виводу
  • README — актуалізовано під поточний функціонал

Test plan

  • python olap.py — відкривається інтерактивне меню
  • python olap.py --last-weeks 1 --format ch — CLI mode, ClickHouse sink працює без помилок is_tender
  • python 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-run
  • python -c "from olap_tool.sinks import ClickHouseSink, DuckDBSink, PostgreSQLSink; print('OK')" — imports OK

🤖 Generated with Claude Code

starychenko and others added 30 commits March 10, 2026 11:07
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>
Критичні:
- 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>
starychenko and others added 16 commits March 16, 2026 09:24
…_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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@claude

claude Bot commented Mar 16, 2026

Copy link
Copy Markdown

Claude finished @starychenko's task in 4m 1s —— View job


Огляд PR #6 — feat: TUI restructure

  • Читання ключових файлів
  • Перевірка архітектури (AppConfig, AnalyticsSink contract)
  • Перевірка resource management
  • Перевірка коректності та data integrity
  • Перевірка конвенцій
  • Thread safety
  • Формування висновку

Резюме

PR реорганізує olap_tool/ на субпакети (core/, connection/, data/, sinks/, ui/), додає PostgreSQLSink через COPY FROM STDIN, замінює два окремі скрипти на scripts/import_xlsx.py, та реалізує інтерактивне консольне меню (InquirerPy + rich).

Вердикт: ✅ Approve з коментарями

Блокерів немає. Одна IMPORTANT проблема (тиха втрата помилок у batch-скрипті), кілька NIT.


Що зроблено добре

  • AnalyticsSink контракт повністю реалізований в усіх трьох sinks (setup, delete_period, insert, close). Ідемпотентність дотримана скрізь — DELETE перед INSERT у кожному sink.
  • Thread safety правильна: ClickHouse та PostgreSQL використовують ThreadLocalSinkPool (один sink на потік), DuckDB — один shared sink (requests.Session є thread-safe). _schema_lock у PG/DuckDB sinks захищає спільний кеш схеми.

[IMPORTANT] scripts/import_xlsx.py — тихе проковтування помилок

Рядки 169–170 та 188–190:

# Рядок 169-170: читання файлу
except Exception:
    return 0, False, time.monotonic() - t0   # виключення проковтується без логування

# Рядки 188-190: операції з sink
except Exception:
    return 0, False, time.monotonic() - t0   # аналогічно

Проблема в тому, що _process_file повертає (0, False, ...) замість того, щоб пробросити виключення. Зовнішній except Exception as e у циклі as_completed (рядок 402) ніколи не спрацює для помилок читання або sink — вони маскуються як тихий ❌. При batch-імпорті сотень файлів це унеможливлює діагностику: видно що N файлів впало, але не чому.

Мінімальний фікс — логувати помилку перед поверненням:

except Exception as exc:
    # logging enough to diagnose
    return 0, False, time.monotonic() - t0

Або пробросити виключення і дати outer handler у as_completed вивести деталі (він уже є на рядках 402–407 і вміє друкувати {e}).

Fix this →


NIT

requirements.txt:22 — коментар посилається на видалений скрипт:

openpyxl>=3.0.0  # Для читання Excel-файлів (import_xlsx_to_clickhouse.py)

→ замінити на (scripts/import_xlsx.py).

olap_tool/sinks/duckdb.py:180:

schema = dict(self._schema)  # type: ignore[arg-type]

_refresh_schema() щойно викликана (рядок 178) і завжди встановлює self._schema у dict, тому # type: ignore зайвий — self._schema гарантовано не None.

scripts/import_xlsx.py:394cfg_kwargs передається у _process_file навіть для DuckDB (де він ігнорується). Незначна плутанина в сигнатурі.


Перевірка блокерів (всі відсутні ✅)

Критерій Статус
os.getenv() тільки в config.py / security.py
Всі методи AnalyticsSink реалізовані
DELETE WHERE year_num+week_num перед INSERT
З'єднання закриваються у finally
sanitize_df не дублюється
SQL без f-string інтерполяції user-контрольованих даних ✅ (year/weekint, колонки — через 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

claude Bot commented Mar 16, 2026

Copy link
Copy Markdown

Claude finished @starychenko's task in 51s —— View job


  • Читання ключових файлів
  • Перевірка архітектури (AppConfig, AnalyticsSink contract)
  • Перевірка resource management
  • Перевірка коректності та data integrity
  • Перевірка конвенцій
  • Thread safety
  • Формування висновку

Огляд PR #6 (оновлено після commit 9d0ed19)

Всі зауваження з попереднього рев'ю виправлені в останньому коміті.

Вердикт: ✅ Approve


Що виправлено

Зауваження Статус
[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-контрольованих даних

@starychenko
starychenko merged commit 92b0233 into main Mar 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant