975934373b
list_databases() прячит базы по массиву строк: подстрока без регистра или маска со * ? [. Фильтр — единственное место рождения списка, поэтому одинаков для меню, пульта и list; явное имя в CLI не трогает.
134 lines
12 KiB
Markdown
134 lines
12 KiB
Markdown
# 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 может частично
|
||
не примениться.
|
||
|
||
## Команды
|
||
|
||
```bash
|
||
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` для реального подключения:
|
||
|
||
```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`, оба файла держать в согласии.
|