Оба диалога наследуются от FormScreen с action_walk: у Screen нет готового action_focus_next, поэтому биндинг стрелок на него молча ничего не делал. Вверх и вниз ходят по полям, влево и вправо остаются за курсором внутри поля ввода — биндинги намеренно без priority.
12 KiB
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 может частично
не примениться.
Команды
python 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 для реального подключения:
.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. Пока они не установлены, текстовый вывод не отличается от прежнего ни на символ:
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описывает только сервер (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в CSSborder: 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, оба файла держать в согласии.