s-clikit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- s_clikit-0.1.0/.gitignore +10 -0
- s_clikit-0.1.0/AGENTS.md +38 -0
- s_clikit-0.1.0/BACKLOG.md +110 -0
- s_clikit-0.1.0/CHANGELOG.md +97 -0
- s_clikit-0.1.0/LICENSE +21 -0
- s_clikit-0.1.0/PKG-INFO +169 -0
- s_clikit-0.1.0/PUBLISH.md +112 -0
- s_clikit-0.1.0/README.md +143 -0
- s_clikit-0.1.0/clikit/__init__.py +79 -0
- s_clikit-0.1.0/clikit/command_kit.py +297 -0
- s_clikit-0.1.0/clikit/config.py +456 -0
- s_clikit-0.1.0/clikit/errors.py +38 -0
- s_clikit-0.1.0/clikit/output.py +135 -0
- s_clikit-0.1.0/clikit/paths.py +27 -0
- s_clikit-0.1.0/clikit/retry.py +33 -0
- s_clikit-0.1.0/clikit/scaffold.py +236 -0
- s_clikit-0.1.0/clikit/session.py +14 -0
- s_clikit-0.1.0/clikit/transport.py +37 -0
- s_clikit-0.1.0/pyproject.toml +63 -0
- s_clikit-0.1.0/tests/__init__.py +0 -0
- s_clikit-0.1.0/tests/test_command_kit.py +373 -0
- s_clikit-0.1.0/tests/test_config.py +253 -0
- s_clikit-0.1.0/tests/test_config_adapters.py +248 -0
- s_clikit-0.1.0/tests/test_config_interpolation.py +120 -0
- s_clikit-0.1.0/tests/test_config_layers.py +240 -0
- s_clikit-0.1.0/tests/test_config_mcp.py +159 -0
- s_clikit-0.1.0/tests/test_config_schema.py +57 -0
- s_clikit-0.1.0/tests/test_config_schema_command.py +52 -0
- s_clikit-0.1.0/tests/test_errors.py +87 -0
- s_clikit-0.1.0/tests/test_output.py +262 -0
- s_clikit-0.1.0/tests/test_paths.py +195 -0
- s_clikit-0.1.0/tests/test_retry.py +114 -0
- s_clikit-0.1.0/tests/test_scaffold.py +117 -0
- s_clikit-0.1.0/tests/test_session.py +307 -0
- s_clikit-0.1.0/tests/test_transport.py +418 -0
- s_clikit-0.1.0/uv.lock +825 -0
s_clikit-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# AGENTS.md — clikit
|
|
2
|
+
|
|
3
|
+
> Контекст для AI-ассистентов (Claude Code, ChatGPT, Cursor и т.п.), работающих
|
|
4
|
+
> над этим проектом.
|
|
5
|
+
|
|
6
|
+
## Что это
|
|
7
|
+
|
|
8
|
+
CLI-методика поверх typer: command_kit, 4-слойный config, SecretStore, scaffold; цель — реестр подключённых адаптеров проекта + команды onboard/health/list поверх них
|
|
9
|
+
|
|
10
|
+
## Atlas
|
|
11
|
+
|
|
12
|
+
Проект зарегистрирован в Atlas-БД (NP-005). Карточка:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
atlas projects get clikit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Любые изменения метаданных (приоритет, статус, теги) — через atlas CLI:
|
|
19
|
+
|
|
20
|
+
- `atlas projects update clikit --priority P0` — поменять приоритет
|
|
21
|
+
- `atlas add-tags clikit -t domain:<slug>` — добавить тег
|
|
22
|
+
- `atlas projects move clikit --to-type <type>` — конвертировать тип
|
|
23
|
+
|
|
24
|
+
## Тип / Статус (на момент создания)
|
|
25
|
+
|
|
26
|
+
- type=`shared-infrastructure`, status=`experiment`, priority=`P0`
|
|
27
|
+
|
|
28
|
+
## Правила работы
|
|
29
|
+
|
|
30
|
+
- Все исходные тексты, документы, код проекта — в этом репо.
|
|
31
|
+
- Чувствительные данные (`.env`, токены, ключи) — игнорируются `.gitignore`.
|
|
32
|
+
- AI-ассистенту разрешено: читать, генерировать, редактировать в этом репо.
|
|
33
|
+
|
|
34
|
+
## Канонические команды
|
|
35
|
+
|
|
36
|
+
- `atlas projects get clikit` — карточка проекта
|
|
37
|
+
- `atlas pm-tasks list --project clikit` — задачи проекта (когда W7
|
|
38
|
+
волна будет реализована)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# BACKLOG — CLIkit
|
|
2
|
+
|
|
3
|
+
Бэклог расширений ядра. Приоритезирован по **тому, чего реально не хватило первому
|
|
4
|
+
потребителю** — `downloader`-CLI (скачивание ассетов из сервиса с публичным
|
|
5
|
+
weblink-API), который вынужден был переизобрести загрузочный слой руками. Каждый
|
|
6
|
+
пункт — кандидат вынести из доменного навыка в переиспользуемое ядро, чтобы
|
|
7
|
+
следующий «качающий» CLI собирался так же за ~10 строк.
|
|
8
|
+
|
|
9
|
+
Статус ядра v0.1.0: транспорт/retry, config/слои, session/keyring, output, errors,
|
|
10
|
+
command-kit, paths, scaffold — **есть**. Загрузка файлов и устойчивость к обрыву —
|
|
11
|
+
**нет** (первый потребитель написал свой resumable-загрузчик + манифест прогресса).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## P1 — Resumable HTTP-Range загрузка + checkpoint/manifest
|
|
16
|
+
|
|
17
|
+
**Зачем.** Потребитель сам реализовал докачку (`.part`-файл, `Range: bytes=N-`,
|
|
18
|
+
обработка 200/206/416, ретраи с пере-резолвом хоста) и манифест прогресса
|
|
19
|
+
(завершённые единицы пропускаются при рестарте, ошибка одного файла не валит
|
|
20
|
+
батч). Это чистый инфраструктурный код без доменной специфики — должен быть в ядре.
|
|
21
|
+
|
|
22
|
+
**Что дать в API (черновик).**
|
|
23
|
+
- `clikit.download.download_file(client, url, dest, *, on_progress, retries)` —
|
|
24
|
+
resumable GET через `httpx`: `.part` + Range, корректная обработка
|
|
25
|
+
`200`/`206`/`416`, валидация полноты по `Content-Range`, ретраи с backoff.
|
|
26
|
+
- `clikit.download.Manifest(dir)` — checkpoint-стор завершённых единиц:
|
|
27
|
+
`is_done(key)` / `mark(key, result)` / `summary()`. Атомарная запись через
|
|
28
|
+
существующий `paths.atomic_write_text`.
|
|
29
|
+
- Переиспользовать `RetryPolicy`/`DEFAULT_RETRY` для пауз между попытками.
|
|
30
|
+
|
|
31
|
+
**Заметки реализации.** `trust_env=False` по умолчанию (урок: токены ссылок бывают
|
|
32
|
+
привязаны к IP — прокси ломает путь). Хост можно пере-резолвить коллбэком
|
|
33
|
+
(signed-token протухает) — параметризовать `url_provider`.
|
|
34
|
+
|
|
35
|
+
## P2 — Параллельный сегмент/чанк-загрузчик
|
|
36
|
+
|
|
37
|
+
**Зачем.** Самый ценный трюк потребителя: качать сегменты (напр. `.ts` HLS) в
|
|
38
|
+
`ThreadPoolExecutor` → склейка → постобработка, что дало **×14** к скорости на
|
|
39
|
+
стриминговом источнике (одиночный поток ~0.5 МБ/с, плато ~6.8 МБ/с на ~48 потоках).
|
|
40
|
+
Параллельный «скачать список URL → собрать» — общий примитив (HLS-сегменты,
|
|
41
|
+
многочастные файлы, батч-ассеты).
|
|
42
|
+
|
|
43
|
+
**Что дать в API.**
|
|
44
|
+
- `clikit.download.fetch_segments(client, urls, dest_dir, *, jobs, retries) -> list[Path]`
|
|
45
|
+
— параллельная загрузка с пер-сегментными ретраями, детерминированный порядок
|
|
46
|
+
имён (`{i:06d}`), `all-or-nothing`.
|
|
47
|
+
- Опц. `concat(parts, dest)` — побайтная склейка (сырьём, как TS-сегменты).
|
|
48
|
+
- Настраиваемые `httpx.Limits(max_connections=jobs+N)`.
|
|
49
|
+
|
|
50
|
+
**Граница.** Постобработка (напр. ffmpeg-конвертация) — **доменная**, в ядро НЕ
|
|
51
|
+
тащим. Ядро отдаёт собранный файл, конвертацию делает навык.
|
|
52
|
+
|
|
53
|
+
## P3 — Опциональная база SQLite-стора (`clikit[store]`)
|
|
54
|
+
|
|
55
|
+
**Зачем.** Потребитель завёл свой sqlite-стор (сущности/теги/ссылки + `kv` +
|
|
56
|
+
сессионные куки). Часто повторяющийся паттерн: «локальный каталог сущностей +
|
|
57
|
+
key-value + сессионные куки». Минимальный helper снимет бойлерплейт
|
|
58
|
+
`connect`/`executescript`/`row_factory`.
|
|
59
|
+
|
|
60
|
+
**Что дать в API (узко, без ORM).**
|
|
61
|
+
- `clikit.store.KVStore(path)` — типизированный key-value на sqlite (`get`/`set`/
|
|
62
|
+
`get_json`/`set_json`), путь по умолчанию из `AppPaths(brand).cache_dir`.
|
|
63
|
+
- Опц. `Table`-helper для простого upsert/search по dict-схеме — **без** попытки
|
|
64
|
+
стать ORM (доменные таблицы остаются в навыке).
|
|
65
|
+
|
|
66
|
+
**Граница.** Доменная схема и бизнес-логика — в навыке. В ядро — только generic
|
|
67
|
+
KV + cookie-jar persistence.
|
|
68
|
+
|
|
69
|
+
## P4 — browser-warm хелпер (`clikit[browser]`, playwright)
|
|
70
|
+
|
|
71
|
+
**Зачем.** Паттерн «логин/прогрев в реальном браузере (playwright, `channel=chrome`)
|
|
72
|
+
→ вытащить cookies/токен → дальше httpx напрямую» повторяется в разных навыках
|
|
73
|
+
(прогрев медиа для транскода, логин за антиботом, операции через REST после
|
|
74
|
+
браузерного логина). Достоин общего хелпера.
|
|
75
|
+
|
|
76
|
+
**Что дать в API (за extra, чтобы playwright не тянулся в каждый CLI).**
|
|
77
|
+
- `clikit.browser.warm_context(url, *, visible, channel="chrome") -> (cookies, ...)`
|
|
78
|
+
— поднять persistent context, дойти до целевого URL, отдать cookies/storage.
|
|
79
|
+
- `clikit.browser.cookies_to_httpx(cookies)` — мост в `httpx.Client`/заголовок
|
|
80
|
+
`Cookie`.
|
|
81
|
+
- Лениво импортировать playwright; без extra — понятная ошибка «поставь
|
|
82
|
+
clikit[browser]».
|
|
83
|
+
|
|
84
|
+
**Граница.** Хелпер тонкий; тяжёлую логику обхода антибота оставляем
|
|
85
|
+
специализированным навыкам.
|
|
86
|
+
|
|
87
|
+
## P5 — Scaffold «CLI за 10 строк» (расширение)
|
|
88
|
+
|
|
89
|
+
`clikit.scaffold` уже генерит скелет (`rest-token`/`none`). Расширения:
|
|
90
|
+
- Транспорт-шаблон `downloader` — CLI вокруг P1/P2 (источник → листинг → resumable
|
|
91
|
+
download + manifest), чтобы «качающий» навык скаффолдился сразу.
|
|
92
|
+
- Шаблон с `AppConfig`-подклассом + `make_schema_command` из коробки.
|
|
93
|
+
- `clikit new <name> --transport downloader` в CLI-обёртке.
|
|
94
|
+
|
|
95
|
+
## P6 — Migration-guide для существующих CLI-навыков
|
|
96
|
+
|
|
97
|
+
Документ-методичка «как мигрировать самописный CLI на CLIkit» (что заменить:
|
|
98
|
+
retry→transport+stamina, свои куки→session/config, `--json`→output, `~/.<app>`→
|
|
99
|
+
paths, Click→command-kit). Обобщить чек-лист «оставить доменным vs вынести в ядро»
|
|
100
|
+
как переиспользуемый шаблон для навыков со своим транспортом/выводом поверх REST.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Принцип границы (ядро vs домен)
|
|
105
|
+
|
|
106
|
+
Выносим в CLIkit — **generic, без знания конкретного сервиса**: транспорт, retry,
|
|
107
|
+
resumable/parallel загрузка, config-слои, секреты, вывод, пути, scaffold.
|
|
108
|
+
Оставляем в навыке — **доменное**: парсинг конкретного сайта/API, бизнес-схема
|
|
109
|
+
стора, постобработка (конвертация), специфика антибота.
|
|
110
|
+
Тест на вынос: «понадобится ли это второму, не связанному с первым, CLI?».
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Все значимые изменения CLIkit. Формат — по [Keep a Changelog](https://keepachangelog.com/ru/),
|
|
4
|
+
версионирование — [SemVer](https://semver.org/lang/ru/). Релизы помечаются git-тегами
|
|
5
|
+
`vX.Y.Z` (см. `PUBLISH.md`); dev ведётся в ветках.
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-06-18
|
|
8
|
+
|
|
9
|
+
Первый публикуемый релиз. **Ядро стандарт-библиотеки построения CLI**, извлечённое
|
|
10
|
+
из skills-hub CLI как переиспользуемый слой. Новый CLI-навык собирается за ~10 строк,
|
|
11
|
+
а внутренности построены на зрелых внешних библиотеках — публичный API при этом
|
|
12
|
+
остаётся стабильным контрактом (расширяется, не ломается).
|
|
13
|
+
|
|
14
|
+
### Что внутри (публичный API — `clikit.__all__`)
|
|
15
|
+
|
|
16
|
+
**Транспорт (`clikit.transport`)**
|
|
17
|
+
- `HttpClient` — асинхронный REST-клиент поверх `httpx.AsyncClient`: ретраи по
|
|
18
|
+
`RetryPolicy`, auto-refresh токена при 401 (вне бюджета ретраев), `trust_env=False`
|
|
19
|
+
по умолчанию (CLI ходит к backend напрямую, минуя системный прокси), внедряемый
|
|
20
|
+
`http_client` для тестов.
|
|
21
|
+
- `ApiError` — ошибка не-2xx с разобранным FastAPI-конвертом `{"detail": ...}`
|
|
22
|
+
(dict → `code`/`message`/`details`; str → `message`; list → 422-валидация);
|
|
23
|
+
машинные `status_code`/`code`/`message`/`details` доступны программно.
|
|
24
|
+
- `RefreshCallback` — тип async-коллбэка обновления токена.
|
|
25
|
+
|
|
26
|
+
**Политика повторов (`clikit.retry`)**
|
|
27
|
+
- `RetryPolicy` — декларативная политика: ретраебельные статусы (429/5xx),
|
|
28
|
+
transport-исключения, backoff-таблица + jitter. Применение переведено на
|
|
29
|
+
[stamina](https://github.com/hynek/stamina) (экспонента + jitter + инструментация),
|
|
30
|
+
сама политика остаётся декларативным конфигом.
|
|
31
|
+
- `DEFAULT_RETRY` — типовая REST-политика (429/5xx + transport-ошибки, 3 шага).
|
|
32
|
+
|
|
33
|
+
**Конфигурация (`clikit.config`)**
|
|
34
|
+
- `AppConfig` — база на `pydantic_settings.BaseSettings` (типизация + валидация
|
|
35
|
+
fail-fast + чтение из окружения). Подклассы добавляют typed-поля наследованием.
|
|
36
|
+
- Слои значений (низ→верх приоритета, **per-key мерж**):
|
|
37
|
+
`defaults < global(config_dir) < project(.<brand>.toml|.<brand>/config.toml,
|
|
38
|
+
найден вверх до .git) < local(*.local.toml) < ENV({BRAND}_<FIELD>) < init-kwargs`.
|
|
39
|
+
- `discover_project_config(brand, start)` — подъём от cwd до маркера `.git`.
|
|
40
|
+
- `interpolate_env` — резолв `${VAR}` / `${VAR:-default}` при load (секрет не в git);
|
|
41
|
+
`$(...)` и голый `$VAR` намеренно не подставляются (поверхность атаки).
|
|
42
|
+
- `load_dotenv_file` — best-effort `.env`-loader (env > .env, не перезаписывает).
|
|
43
|
+
- `McpServer` — единая форма с E6-манифестом навыка (`transport`/`command`/`args`/
|
|
44
|
+
`env`/`url`/`headers`/`enabled`); `AppConfig.mcp: dict[str, McpServer]`.
|
|
45
|
+
- `AppConfig.json_schema()` — JSON Schema модели для `$schema` в редакторе.
|
|
46
|
+
|
|
47
|
+
**Хранилище секретов (`clikit.session`)**
|
|
48
|
+
- `SecretStore(brand, profile)` — пара токенов (access/refresh): keyring с прозрачным
|
|
49
|
+
file-fallback (`tokens.toml`, chmod 600). Fallback закрывает Windows Credential
|
|
50
|
+
Manager limit (~2560 байт → длинный JWT даёт `CredWrite WinError 1783`).
|
|
51
|
+
Порядок чтения: env → keyring → файл.
|
|
52
|
+
|
|
53
|
+
**Вывод (`clikit.output`)**
|
|
54
|
+
- `init_output_mode` / `is_json` / `console` / `emit_data` / `emit_message` /
|
|
55
|
+
`emit_error` — унифицированный контракт text|json. **Дефолт — json** (для
|
|
56
|
+
AI-агентов и скриптинга); человек переключается `--text`/`--plain`, env
|
|
57
|
+
`{BRAND}_OUTPUT=text` или `output_format` в конфиге. Данные в stdout (JSON Lines),
|
|
58
|
+
сообщения/ошибки в stderr.
|
|
59
|
+
|
|
60
|
+
**Карта путей (`clikit.paths`)**
|
|
61
|
+
- `AppPaths(brand)` — нативные пути состояния по ОС через
|
|
62
|
+
[platformdirs](https://github.com/tox-dev/platformdirs) (`config_dir`/`cache_dir`/
|
|
63
|
+
`sessions_dir`/`locks_dir`); ENV-override `{BRAND}_HOME`.
|
|
64
|
+
- `atomic_write_text` (tmp + `os.replace`), `chmod_600` (POSIX best-effort),
|
|
65
|
+
`slugify` (детерминированный slug `[a-z0-9._-]`).
|
|
66
|
+
|
|
67
|
+
**Ошибки (`clikit.errors`)**
|
|
68
|
+
- `CliError` (база: `code`/`message`/`exit_code`) + доменные подклассы
|
|
69
|
+
`AuthRequired`, `SessionExpired`, `RateLimited` (с `retry_after`),
|
|
70
|
+
`NotImplementedYet`, `ValidationError`.
|
|
71
|
+
|
|
72
|
+
**Command-kit (`clikit.command_kit`)**
|
|
73
|
+
- `build_root_app(brand, version, help, status_data, whoami_data)` — root
|
|
74
|
+
`typer.Typer` с глобальным callback (`--json`/`--text`/`--profile`/`--version`)
|
|
75
|
+
и базовыми командами `version` + опц. `status`/`whoami`.
|
|
76
|
+
- `command` / `async_command` — декораторы с единым перехватом ошибок:
|
|
77
|
+
`CliError`/`ApiError` → `emit_error` + `typer.Exit`; `typer.Exit`/`Abort`
|
|
78
|
+
пропускаются насквозь; в async — `RuntimeError` → короткий `RUNTIME`-emit.
|
|
79
|
+
- `gated(app, permission, has_permission)` — условная регистрация команды по праву.
|
|
80
|
+
- `make_schema_command(MyConfig)` — команда экспорта JSON Schema конфига.
|
|
81
|
+
|
|
82
|
+
**Scaffold (`clikit.scaffold`)**
|
|
83
|
+
- `scaffold_cli(name, transport, target_dir)` — генерация скелета нового CLI
|
|
84
|
+
(`<pkg>/cli.py` + `pyproject.toml` + `README.md`), «CLI за ~10 строк».
|
|
85
|
+
Транспорты: `rest-token` (пример с `HttpClient`) | `none`.
|
|
86
|
+
|
|
87
|
+
### Зрелые зависимости
|
|
88
|
+
|
|
89
|
+
`typer`, `httpx`, `rich`, `tomli-w`, `keyring`, `stamina`, `platformdirs`,
|
|
90
|
+
`pydantic-settings`. Опционально: `socks` (`httpx[socks]`).
|
|
91
|
+
|
|
92
|
+
### Покрытие
|
|
93
|
+
|
|
94
|
+
154+ теста (pytest + respx), ruff-линт. Запуск:
|
|
95
|
+
`.venv/Scripts/python -m pytest -q` / `.venv/Scripts/python -m ruff check clikit`.
|
|
96
|
+
|
|
97
|
+
[0.1.0]: https://gitlab.com/cemon345rus/clikit/-/tags/v0.1.0
|
s_clikit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
s_clikit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: s-clikit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Стандартная библиотека построения CLI: transport+retry, config+профили, session(keyring+fallback), output-контракт, errors, command-kit, локальные сессии.
|
|
5
|
+
Author: Dmitry
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: httpx>=0.28
|
|
10
|
+
Requires-Dist: keyring>=25.0
|
|
11
|
+
Requires-Dist: platformdirs>=4.0
|
|
12
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
13
|
+
Requires-Dist: rich>=13.9
|
|
14
|
+
Requires-Dist: s-librarykit
|
|
15
|
+
Requires-Dist: stamina>=24.3
|
|
16
|
+
Requires-Dist: tomli-w>=1.0
|
|
17
|
+
Requires-Dist: typer>=0.15
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
21
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
23
|
+
Provides-Extra: socks
|
|
24
|
+
Requires-Dist: httpx[socks]>=0.28; extra == 'socks'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# clikit
|
|
28
|
+
|
|
29
|
+
Стандартная библиотека построения CLI: transport+retry, config+профили,
|
|
30
|
+
session (keyring+file-fallback), output-контракт, errors, command-kit,
|
|
31
|
+
локальные сессии (карта путей). Извлечена из skills-hub CLI как переиспользуемое
|
|
32
|
+
ядро. Новый CLI собирается за ~10 строк.
|
|
33
|
+
|
|
34
|
+
## Зрелые зависимости (E1.1)
|
|
35
|
+
|
|
36
|
+
Внутренности построены на проверенных библиотеках (публичный API при этом
|
|
37
|
+
сохранён — контракт не ломается, только расширяется):
|
|
38
|
+
|
|
39
|
+
- **[stamina](https://github.com/hynek/stamina)** — движок повторов транспорта
|
|
40
|
+
(экспоненциальный backoff + jitter + лимиты, инструментация). `RetryPolicy` /
|
|
41
|
+
`DEFAULT_RETRY` остаются декларативным конфигом, их применение переведено на
|
|
42
|
+
stamina.
|
|
43
|
+
- **[platformdirs](https://github.com/tox-dev/platformdirs)** — нативные пути по
|
|
44
|
+
ОС для состояния CLI (`%APPDATA%` win / `~/Library` mac / `~/.config` linux).
|
|
45
|
+
`AppPaths(brand)`: `config_dir`→`user_config_dir`, `cache_dir`→`user_cache_dir`,
|
|
46
|
+
`sessions`/`locks`→`user_data_dir`. ENV-override `{BRAND}_HOME` перебивает.
|
|
47
|
+
- **[pydantic-settings](https://github.com/pydantic/pydantic-settings)** —
|
|
48
|
+
`AppConfig` на `BaseSettings`: типизация + валидация (fail-fast) + чтение из
|
|
49
|
+
окружения. Приоритет источников: init-kwargs > env `{BRAND}_<FIELD>` > слои
|
|
50
|
+
файлов. Подклассы добавляют typed-поля обычным наследованием.
|
|
51
|
+
|
|
52
|
+
## Слои конфига: global + project + local (E1.3)
|
|
53
|
+
|
|
54
|
+
`AppConfig.load(brand)` собирает значения по слоям (низ→верх приоритета),
|
|
55
|
+
**per-key мерж** (верхний слой переопределяет ТОЛЬКО заданные ключи, не
|
|
56
|
+
заменяет объект целиком):
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
defaults(модель) < global(config_dir/config.toml)
|
|
60
|
+
< project(.<brand>.toml | .<brand>/config.toml — найден ВВЕРХ от cwd до .git)
|
|
61
|
+
< local(*.local.toml рядом с project, gitignored)
|
|
62
|
+
< ENV({BRAND}_<FIELD>) < init-kwargs
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- **project-discovery**: подъём от cwd вверх до маркера `.git` (не выходя за
|
|
66
|
+
границу проекта); ищется `.<brand>.toml` в корне ИЛИ `.<brand>/config.toml`.
|
|
67
|
+
Это даёт «конфиг рядом с проектом» — библиотека ложится в каждый репозиторий.
|
|
68
|
+
- **local-оверрайд**: `.<brand>.local.toml` (или `.<brand>/config.local.toml`) —
|
|
69
|
+
приватные правки поверх командного project-конфига (добавлен в `.gitignore`).
|
|
70
|
+
- Профили (`{BRAND}_PROFILE` → `profiles/<p>/`) сохранены на уровне global.
|
|
71
|
+
- `load(path=...)` читает ЕДИНИЧНЫЙ файл (без discovery/слоёв) — обратная
|
|
72
|
+
совместимость.
|
|
73
|
+
- `discover_project_config(brand, start=cwd)` доступен публично.
|
|
74
|
+
|
|
75
|
+
### env-интерполяция секретов (E1.3)
|
|
76
|
+
|
|
77
|
+
Строковые значения резолвятся из окружения при `load` (секрет не лежит в git):
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
api_token = "${ACME_TOKEN}" # из env ACME_TOKEN
|
|
81
|
+
base_url = "${ACME_HOST:-http://localhost:8000}" # с дефолтом (bash ${VAR:-def})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Поддержаны `${VAR}` и `${VAR:-default}`. Подстановка команд `$(...)` **не**
|
|
85
|
+
поддержана (поверхность атаки); голый `$VAR` без скобок не подставляется.
|
|
86
|
+
Заряжать env можно `.env`-loader'ом (`load_dotenv_file`) или keyring (`SecretStore`).
|
|
87
|
+
|
|
88
|
+
### MCP-секция (E1.3)
|
|
89
|
+
|
|
90
|
+
`AppConfig.mcp: dict[str, McpServer]` — единая форма с E6-манифестом навыка
|
|
91
|
+
(`transport`/`command`/`args`/`env`/`url`/`headers`/`enabled`), чтобы install
|
|
92
|
+
сервера навыка был прямым mapping. Имена серверов — через дефис (не `_`).
|
|
93
|
+
|
|
94
|
+
```toml
|
|
95
|
+
[mcp.skills-hub]
|
|
96
|
+
transport = "stdio" # stdio | sse | http
|
|
97
|
+
command = "skills-hub-mcp"
|
|
98
|
+
args = ["--stdio"]
|
|
99
|
+
env = { TOKEN = "${SH_TOKEN}" }
|
|
100
|
+
|
|
101
|
+
[mcp.remote-api]
|
|
102
|
+
transport = "http"
|
|
103
|
+
url = "https://mcp.example.com"
|
|
104
|
+
headers = { Authorization = "Bearer ${MCP_TOKEN}" }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `$schema` для редактора (E1.3)
|
|
108
|
+
|
|
109
|
+
`AppConfig.json_schema()` (или подкласса) отдаёт JSON Schema модели
|
|
110
|
+
(`model_json_schema`). Команда для CLI — `make_schema_command(MyConfig)`:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from clikit import make_schema_command, AppConfig
|
|
114
|
+
app.command(name="schema")(make_schema_command(MyConfig)) # `acme schema` печатает схему
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Сгенерированную схему публикуют как файл и подключают в редакторе. TOML остаётся
|
|
118
|
+
форматом конфига; для VS Code/JetBrains схему ассоциируют с файлом конфига через
|
|
119
|
+
настройки редактора (Taplo/`tombi` для TOML) либо через `"$schema"`, если перейти
|
|
120
|
+
на JSONC.
|
|
121
|
+
|
|
122
|
+
## `--json` по умолчанию (E1.1)
|
|
123
|
+
|
|
124
|
+
Дефолтный режим вывода — **json** (для AI-агентов и скриптинга): без флагов
|
|
125
|
+
команда отдаёт машинный JSON Lines в stdout. Человек переключается в
|
|
126
|
+
человеко-читаемый режим:
|
|
127
|
+
|
|
128
|
+
- флагом `--text` / `--plain` (форсит text);
|
|
129
|
+
- env `{BRAND}_OUTPUT=text`;
|
|
130
|
+
- в настройках `config.toml` → `output_format = "text"`.
|
|
131
|
+
|
|
132
|
+
`--json` форсит json (перебивает `--text`, если переданы оба). Полный приоритет
|
|
133
|
+
`init_output_mode`: `json_flag=True` > явный text-сигнал (`text_flag=True` или
|
|
134
|
+
`json_flag=False`) > env `{BRAND}_OUTPUT` > `config_format` > `json` (дефолт).
|
|
135
|
+
|
|
136
|
+
## Быстрый старт
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from clikit import build_root_app, command, HttpClient, AppConfig, emit_data
|
|
140
|
+
|
|
141
|
+
app = build_root_app(brand="acme", help="ACME CLI") # +--json/--text/--profile/--version
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
@command
|
|
145
|
+
def items():
|
|
146
|
+
cfg = AppConfig.load("acme")
|
|
147
|
+
client = HttpClient(cfg.base_url, access_token=...)
|
|
148
|
+
emit_data(..., text_renderer=...)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Скаффолд нового CLI: `clikit new <name>` (см. `clikit.scaffold`).
|
|
152
|
+
|
|
153
|
+
## Публичный API
|
|
154
|
+
|
|
155
|
+
`clikit.__all__`: `CliError`, `AuthRequired`, `SessionExpired`, `RateLimited`,
|
|
156
|
+
`NotImplementedYet`, `ValidationError`, `RetryPolicy`, `DEFAULT_RETRY`,
|
|
157
|
+
`init_output_mode`, `is_json`, `console`, `emit_data`, `emit_message`,
|
|
158
|
+
`emit_error`, `HttpClient`, `ApiError`, `RefreshCallback`, `AppConfig`,
|
|
159
|
+
`McpServer`, `load_dotenv_file`, `interpolate_env`, `discover_project_config`,
|
|
160
|
+
`SecretStore`, `AppPaths`, `atomic_write_text`, `chmod_600`, `slugify`,
|
|
161
|
+
`build_root_app`, `command`, `async_command`, `gated`, `make_schema_command`,
|
|
162
|
+
`scaffold_cli`.
|
|
163
|
+
|
|
164
|
+
## Разработка
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
.venv/Scripts/python -m pytest -q # тесты
|
|
168
|
+
.venv/Scripts/python -m ruff check clikit # линт
|
|
169
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# PUBLISH — публикация CLIkit и потребление в CLI-навыках
|
|
2
|
+
|
|
3
|
+
CLIkit публикуется как **git-зависимость** (без PyPI): любой CLI-навык тянет её
|
|
4
|
+
напрямую из GitLab по тегу. Версионирование — git-тегами `vX.Y.Z`; разработка —
|
|
5
|
+
в ветках. Релиз = коммит на `main` + аннотированный тег + push.
|
|
6
|
+
|
|
7
|
+
> Дисциплина (см. глобальный CLAUDE.md): **git первый, тег второй, потребление
|
|
8
|
+
> третьим.** Никакой публикации из незакоммиченного дерева. Перед тегом — чистый
|
|
9
|
+
> `git status` и зелёные тесты/линт.
|
|
10
|
+
|
|
11
|
+
## 0. Предполётная проверка
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd <path-to-clikit> # корень репозитория clikit
|
|
15
|
+
.venv/Scripts/python -m pytest -q # все тесты зелёные
|
|
16
|
+
.venv/Scripts/python -m ruff check clikit # линт чист
|
|
17
|
+
git status --short # рабочее дерево чистое
|
|
18
|
+
git log -1 --oneline # HEAD на нужном коммите
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Версия в `pyproject.toml` (`project.version`) и `clikit/__init__.py`
|
|
22
|
+
(`__version__`) должны совпадать с тегом (`0.1.0` ↔ `v0.1.0`). Запись в
|
|
23
|
+
`CHANGELOG.md` под этой версией присутствует.
|
|
24
|
+
|
|
25
|
+
## 1. Привязать origin (один раз)
|
|
26
|
+
|
|
27
|
+
`<PAT>` — GitLab Personal Access Token со scope `write_repository` (или
|
|
28
|
+
`api`). Токен в URL = «git-over-https» аутентификация для push без интерактива.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git remote add origin https://oauth2:<PAT>@gitlab.com/cemon345rus/clikit.git
|
|
32
|
+
# если origin уже есть и нужно сменить:
|
|
33
|
+
# git remote set-url origin https://oauth2:<PAT>@gitlab.com/cemon345rus/clikit.git
|
|
34
|
+
git remote -v # проверить
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> Безопасность: PAT в `remote-url` оседает в `.git/config` открытым текстом.
|
|
38
|
+
> Приемлемо для локального репозитория, но НЕ копировать `.git/config` в
|
|
39
|
+
> передаваемые артефакты. Альтернатива без утечки — git credential helper или
|
|
40
|
+
> `GIT_ASKPASS` (токен не попадает в коммитимый файл).
|
|
41
|
+
|
|
42
|
+
## 2. Поставить тег релиза
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git tag -a v0.1.0 -m "clikit v0.1.0 — ядро стандарт-библиотеки CLI"
|
|
46
|
+
git tag # убедиться, что v0.1.0 появился
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Аннотированный тег (`-a`) несёт сообщение и автора — предпочтительнее
|
|
50
|
+
легковесного, потребители ссылаются именно на `@v0.1.0`.
|
|
51
|
+
|
|
52
|
+
## 3. Запушить ветку и тег
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git push -u origin main # код
|
|
56
|
+
git push origin v0.1.0 # тег (теги не уходят с обычным push без --tags)
|
|
57
|
+
# или одной командой: git push origin main --follow-tags
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
После push сверить три точки: локальный `HEAD` == `origin/main` == тег `v0.1.0`
|
|
61
|
+
указывает на тот же коммит (`git log -1 --oneline` локально и в GitLab UI).
|
|
62
|
+
|
|
63
|
+
## 4. Потребление в любом CLI-навыке
|
|
64
|
+
|
|
65
|
+
В `pyproject.toml` навыка-потребителя добавить CLIkit в `dependencies` как
|
|
66
|
+
git-зависимость, закреплённую тегом:
|
|
67
|
+
|
|
68
|
+
```toml
|
|
69
|
+
[project]
|
|
70
|
+
dependencies = [
|
|
71
|
+
"clikit @ git+https://gitlab.com/cemon345rus/clikit.git@v0.1.0",
|
|
72
|
+
# ... доменные зависимости навыка ...
|
|
73
|
+
]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Для прямой git-ссылки нужно разрешить direct references (hatchling):
|
|
77
|
+
|
|
78
|
+
```toml
|
|
79
|
+
[tool.hatch.metadata]
|
|
80
|
+
allow-direct-references = true
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Затем в каталоге навыка:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv sync # uv авто-подтянет clikit из GitLab по тегу v0.1.0
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`uv sync` резолвит git-зависимость, клонирует репозиторий на теге и ставит пакет
|
|
90
|
+
в окружение навыка. Никакого `pip install` вручную, никакого vendoring.
|
|
91
|
+
|
|
92
|
+
Опциональные экстра (когда появятся, см. `BACKLOG.md`) подключаются через
|
|
93
|
+
extras-маркер: `"clikit[browser] @ git+...@v0.2.0"`.
|
|
94
|
+
|
|
95
|
+
### Приватный репозиторий
|
|
96
|
+
|
|
97
|
+
Если репозиторий CLIkit приватный, у потребителя при `uv sync` тоже должен быть
|
|
98
|
+
доступ. Варианты: тот же PAT в URL зависимости (утечёт в lock — нежелательно),
|
|
99
|
+
либо `~/.netrc` / git credential helper / `UV_INDEX`-конфиг с токеном вне
|
|
100
|
+
`pyproject.toml`. Предпочтителен helper — токен не попадает в коммитимый файл.
|
|
101
|
+
|
|
102
|
+
## 5. Следующие релизы
|
|
103
|
+
|
|
104
|
+
1. Разработка фичи — в ветке (`git switch -c feat/<name>`), не на `main`.
|
|
105
|
+
2. Влить в `main`, обновить версию (`pyproject.toml` + `__init__.py`) и
|
|
106
|
+
`CHANGELOG.md` (новая секция `## [X.Y.Z]`).
|
|
107
|
+
3. Повторить шаги 0, 2, 3 для нового тега `vX.Y.Z`.
|
|
108
|
+
4. Потребители обновляются сменой `@vX.Y.Z` в своём `pyproject.toml` + `uv sync`
|
|
109
|
+
(закреплённый тег = воспроизводимость; обновление осознанное, не «плывёт»).
|
|
110
|
+
|
|
111
|
+
SemVer: breaking-изменение публичного API (`clikit.__all__`) → мажор; новые
|
|
112
|
+
символы/поля при сохранённом контракте → минор; фиксы — патч.
|