Files
pg-stand-sync/CLAUDE.md
T
mikhail f48aa3bcbd Запуск python напрямую вместо обёртки run.ps1
Профиль Windows Terminal вызывает .venv\Scripts\python.exe с
PYTHONUTF8=1 в окружении профиля, поэтому промежуточный PowerShell
и его настройка консоли больше не нужны.
2026-08-20 06:05:36 +03:00

136 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Что это
`pg_stand_sync.py` переносит базы PostgreSQL со стенда на локальный сервер через
`pg_dump -Fc``DROP/CREATE DATABASE``pg_restore -j`. Ядро (CLI и текстовое меню) —
только stdlib Python 3.10+; TUI-пульт лежит в `pg_stand_sync_tui.py` и требует `textual`
(`requirements.txt`). Без textual и вне tty всё работает по-старому текстом.
Утилиты берутся не из PATH, а из поставки pgAdmin 4 (`pg_bin_dir` в конфиге, psql 18) —
локальный сервер при этом PG 16, поэтому дамп со стенда новее 16 может частично
не примениться.
## Команды
Запускать нужно интерпретатором из `.venv` — textual стоит там:
```bash
.venv/Scripts/python.exe pg_stand_sync.py # TUI-пульт (основной режим)
python pg_stand_sync.py --no-tui # то же текстовым меню
python pg_stand_sync.py list # базы стенда — самая быстрая проверка связи
python pg_stand_sync.py --dry-run sync DB # печатает все команды, к серверам не ходит
```
Проверка изменений — `tests_tui.py` (pytest, TUI гоняется headless через `App.run_test()`
и `Pilot`, к стенду никто не ходит), плюс `--dry-run` для сверки состава командных строк и
`list` для реального подключения:
```bash
.venv/Scripts/python.exe -m pytest tests_tui.py -q
```
⚠️ Запускать TUI в терминале агента нельзя — сессия заблокируется. Всё проверяется только
через Pilot и `--dry-run`. Pylance в строгом режиме шумит десятками «частично неизвестных
типов» — это ожидаемо, аннотации намеренно нестрогие.
Стенд — `postgresql.lan` (`postgres.lan` не резолвится), PG 16; локальный сервер — PG 15,
утилиты из pgAdmin — 18, отсюда шум `unrecognized configuration parameter` при restore
(объясняется строкой `NEWER_CLIENT_HINT`, данные не страдают).
Пароль стенда лежит в `config.json` (файл в `.gitignore`), поэтому неинтерактивные проверки
проходят молча. Если его там нет — задать `$env:STAND_PGPASSWORD`: без пароля и без tty
`getpass` подвесил бы скрипт.
## Устройство скрипта
Слои, снизу вверх, — правки почти всегда касаются одного из них:
```
conn_env(node,label,db) libpq-переменные: пароль только через PGPASSWORD, никогда в argv
└─ resolve_password() config.password → password_env → хук UI → getpass (кэш на процесс)
run() / capture() Popen + построчное чтение; убиваемы через request_cancel()
no_prompt(env) --no-password, когда пароля нет: иначе psql ждёт ввода вечно
dump / recreate_target / restore / post_sql шаги, все принимают dry_run
do_dump / do_restore / do_sync сценарии, ими пользуются оба режима
pick() + interactive() текстовый режим; CLI-ветка в main() вызывает те же do_*
tui_enabled(args) единственная точка решения о режиме
└─ pg_stand_sync_tui.run_tui() пульт на Textual, те же do_*
```
Четыре необязательных хука — всё, что ядро знает об UI. Пока они не установлены, текстовый
вывод не отличается от прежнего ни на символ:
```ini
set_sink(fn) ; fn(kind, text), kind = log | warn | out; иначе print в stdout
set_progress(fn) ; fn(event, what, elapsed), event = start | done | fail
set_password_prompt(fn) ; fn(label, node) вместо getpass; вернул None — Cancelled
request_cancel() ; terminate + taskkill /T /F дерева; это kill, а не rollback
matches(text, query) ; общий фильтр для pick() и обоих списков TUI
```
Ключевые инварианты:
- Диалог `DbOpModal` — единственный вход в операции над базой: галочка автоподнятия решает
`kind` (`dump` или `sync`), а правки полей уезжают в `OpSpec.overrides` и накладываются
копией конфига в `op_cfg()`. Файл `config.json` пульт не переписывает, значения живут
до выхода в `DashboardScreen.op_defaults`.
- `source.exclude_databases` отсеивается в `list_databases()` — единственном месте, где
список рождается, поэтому фильтр одинаков для меню, пульта и `list`. Явное имя базы в CLI
через список не проходит и потому не фильтруется.
- Имя базы — параметр, а не поле конфига. `source` описывает только сервер
(`maintenance_database` — куда подключаться, чтобы прочитать `pg_database`),
`target.database: null` означает «локально как на стенде»; переопределяется `--target-db`.
- Любая новая операция добавляется как `do_*` и подключается в оба режима — в `interactive()`
и в `main()`. Дублировать логику в ветке меню нельзя.
- `dry_run` протаскивается до `run()`, который в этом режиме только печатает команду. Новый
вызов внешней утилиты обязан идти через `run`/`capture`, иначе `--dry-run` соврёт.
- `pick()` — способ выбора из списка в текстовом режиме: текст фильтрует, число выбирает,
`multi=True` разрешает `2,5,7`, `q` поднимает `Cancelled` (в меню — возврат, не выход).
Fallback-выбор в CLI-командах (`sync` без имени базы) остаётся текстовым даже при
установленном textual: TUI посреди CLI-команды сломал бы pipe-сценарии.
- Текст `StepError``«{what} завершился с кодом {N}»` — менять нельзя: на него завязано
глушение ошибок `pg_restore`.
- Диалоги наследуются от `FormScreen` и подмешивают `FORM_NAV`: у `Screen` нет готового
`action_focus_next`, поэтому обход полей стрелками сделан своим `action_walk`. Биндинги без
`priority` — внутри `Input` стрелки вверх/вниз свободны и всплывают до экрана сами, а
влево/вправо остаются за курсором.
- Новый `Ctrl`-хоткей добавляется только через `with_ru_layout([...])`: при русской раскладке
терминал присылает `ctrl+в` вместо `ctrl+d`, и голый биндинг молча не срабатывает. Полноту
дублей проверяет тест, перебирающий `PgSyncApp.BINDINGS`.
- В TUI любой `Ctrl+…`-хоткей объявляется `priority=True`: `Input` биндит `ctrl+a`, `ctrl+d`,
`ctrl+u`, `ctrl+x`, `ctrl+c` — без приоритета действия молча не сработают в фильтре.
`Ctrl+C` в Textual 8 по умолчанию копирует текст, а не выходит.
- Отмена (`Ctrl+X`) — это kill дочерних процессов, отката нет: если `DROP DATABASE` уже
прошёл, локальная база останется пустой, и UI обязан сказать это прямым текстом.
`request_cancel()` зовётся только из потока (в цикле событий он вешает UI на секунды),
а флаг отмены снимается только там, где воркер точно не в ядре, — в `begin()` и в
`on_op_done()`. В `on_unmount()` его снимать нельзя: при выходе рабочий поток ещё жив и со
снятым флагом спокойно доработает следующий `DROP DATABASE` уже без UI.
- `target_db=None` в TUI не значит «как на стенде»: `do_sync` считает
`target_db or cfg.target.database or dbname`. Поэтому sync нескольких баз запрещён,
когда `target.database` задан, — иначе они слились бы в одну.
- Строки списков строятся через `room_for()`, который вычитает `SCROLLBAR_ROOM = 2`: сам
скроллбар в `content_size` не учтён, и строка ровно по ширине переносит хвост («1277 MB»
рвётся на «1277» и «MB»). Проверяется тестом со `scrollbar-gutter: stable`.
- Раскладка: `narrow` (<100 колонок) и `short` (<32 строк) вешаются руками в `on_resize`;
`Horizontal` со сменой `layout: vertical` не раздаёт детям ни `fr`, ни проценты по высоте,
поэтому в узком режиме правая колонка задана в строках. `.zoom-log #bottom` требует
`!important` — равная по специфичности `.short` иначе побеждает.
- `F4` разворачивает журнал; `begin()` делает это сам и помечает `_zoom_auto`, чтобы
`on_op_done` свернул обратно. Ручной `F4` снимает флаг — дальше раскладкой владеет человек.
- Счётчик «показано/всего» пишется в `border_subtitle` панели, а не списка: у `OptionList`
в CSS `border: none`, а Textual рисует подпись только в строке рамки. Ширина строк списков
и плана считается от `content_size` — фиксированные колонки переносятся и рвут вёрстку.
- `pg_restore` без `--exit-on-error` возвращает 1 на игнорируемых ошибках, поэтому его
`StepError` глотается и логируется как предупреждение — не «чинить» это молча.
- Имя файла дампа `<db>-YYYYmmdd-HHMMSS.dump`: по нему `db_from_dump_name()` восстанавливает
имя базы для `restore` без `--target-db`. Формат менять только вместе с `STAMP_RE`.
- Реконфигурация stdout/stderr в UTF-8 в начале файла нужна для русского лога в cp866-консоли
Windows; вывод дочерних процессов читается с `errors="replace"`.
`config.json` (рабочий, с паролями) в `.gitignore` — структура правится в
`config.example.json`, оба файла держать в согласии.