yandex-audience-cli 0.0.1__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.
- yandex_audience_cli-0.0.1/.env.example +21 -0
- yandex_audience_cli-0.0.1/.github/workflows/ci.yml +44 -0
- yandex_audience_cli-0.0.1/.github/workflows/docs.yml +54 -0
- yandex_audience_cli-0.0.1/.github/workflows/spec-audit.yml +26 -0
- yandex_audience_cli-0.0.1/.gitignore +40 -0
- yandex_audience_cli-0.0.1/CLAUDE.md +121 -0
- yandex_audience_cli-0.0.1/PKG-INFO +120 -0
- yandex_audience_cli-0.0.1/README.md +98 -0
- yandex_audience_cli-0.0.1/docs/architecture.md +74 -0
- yandex_audience_cli-0.0.1/docs/authentication.md +66 -0
- yandex_audience_cli-0.0.1/docs/commands/accounts.md +21 -0
- yandex_audience_cli-0.0.1/docs/commands/delegates.md +51 -0
- yandex_audience_cli-0.0.1/docs/commands/grants.md +53 -0
- yandex_audience_cli-0.0.1/docs/commands/pixels.md +63 -0
- yandex_audience_cli-0.0.1/docs/commands/segments.md +187 -0
- yandex_audience_cli-0.0.1/docs/development.md +98 -0
- yandex_audience_cli-0.0.1/docs/index.md +43 -0
- yandex_audience_cli-0.0.1/docs/installation.md +43 -0
- yandex_audience_cli-0.0.1/docs/usage.md +81 -0
- yandex_audience_cli-0.0.1/mkdocs.yml +60 -0
- yandex_audience_cli-0.0.1/pyproject.toml +67 -0
- yandex_audience_cli-0.0.1/scripts/fetch_spec.py +242 -0
- yandex_audience_cli-0.0.1/scripts/live_smoke.sh +399 -0
- yandex_audience_cli-0.0.1/scripts/throttle.sh +54 -0
- yandex_audience_cli-0.0.1/tests/conftest.py +182 -0
- yandex_audience_cli-0.0.1/tests/data/audience_spec.json +232 -0
- yandex_audience_cli-0.0.1/tests/e2e/__init__.py +1 -0
- yandex_audience_cli-0.0.1/tests/e2e/test_live_api.py +50 -0
- yandex_audience_cli-0.0.1/tests/e2e/test_local_server.py +105 -0
- yandex_audience_cli-0.0.1/tests/integration/__init__.py +1 -0
- yandex_audience_cli-0.0.1/tests/integration/test_cli_commands.py +40 -0
- yandex_audience_cli-0.0.1/tests/integration/test_resources.py +194 -0
- yandex_audience_cli-0.0.1/tests/integration/test_uploads.py +60 -0
- yandex_audience_cli-0.0.1/tests/smoke/__init__.py +1 -0
- yandex_audience_cli-0.0.1/tests/smoke/test_smoke.py +79 -0
- yandex_audience_cli-0.0.1/tests/test_openapi_coverage.py +90 -0
- yandex_audience_cli-0.0.1/tests/unit/__init__.py +1 -0
- yandex_audience_cli-0.0.1/tests/unit/test_http_client.py +167 -0
- yandex_audience_cli-0.0.1/tests/unit/test_live_smoke_throttle.py +116 -0
- yandex_audience_cli-0.0.1/tests/unit/test_registry_coverage.py +117 -0
- yandex_audience_cli-0.0.1/tests/unit/test_registry_tree.py +53 -0
- yandex_audience_cli-0.0.1/yac/__init__.py +5 -0
- yandex_audience_cli-0.0.1/yac/__main__.py +6 -0
- yandex_audience_cli-0.0.1/yac/api/__init__.py +1 -0
- yandex_audience_cli-0.0.1/yac/api/accounts.py +15 -0
- yandex_audience_cli-0.0.1/yac/api/delegates.py +33 -0
- yandex_audience_cli-0.0.1/yac/api/grants.py +36 -0
- yandex_audience_cli-0.0.1/yac/api/pixels.py +29 -0
- yandex_audience_cli-0.0.1/yac/api/segments.py +135 -0
- yandex_audience_cli-0.0.1/yac/base.py +44 -0
- yandex_audience_cli-0.0.1/yac/cli.py +88 -0
- yandex_audience_cli-0.0.1/yac/client.py +204 -0
- yandex_audience_cli-0.0.1/yac/commands/__init__.py +1 -0
- yandex_audience_cli-0.0.1/yac/commands/accounts.py +20 -0
- yandex_audience_cli-0.0.1/yac/commands/delegates.py +52 -0
- yandex_audience_cli-0.0.1/yac/commands/grants.py +65 -0
- yandex_audience_cli-0.0.1/yac/commands/pixels.py +54 -0
- yandex_audience_cli-0.0.1/yac/commands/segments.py +247 -0
- yandex_audience_cli-0.0.1/yac/config.py +80 -0
- yandex_audience_cli-0.0.1/yac/context.py +68 -0
- yandex_audience_cli-0.0.1/yac/inputs.py +33 -0
- yandex_audience_cli-0.0.1/yac/models.py +164 -0
- yandex_audience_cli-0.0.1/yac/openapi_coverage.py +115 -0
- yandex_audience_cli-0.0.1/yac/output.py +53 -0
- yandex_audience_cli-0.0.1/yac/registry/__init__.py +60 -0
- yandex_audience_cli-0.0.1/yac/registry/_core.py +141 -0
- yandex_audience_cli-0.0.1/yac/registry/accounts/__init__.py +1 -0
- yandex_audience_cli-0.0.1/yac/registry/accounts/list.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/delegates/__init__.py +1 -0
- yandex_audience_cli-0.0.1/yac/registry/delegates/add.py +8 -0
- yandex_audience_cli-0.0.1/yac/registry/delegates/list.py +10 -0
- yandex_audience_cli-0.0.1/yac/registry/delegates/remove.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/grants/__init__.py +1 -0
- yandex_audience_cli-0.0.1/yac/registry/grants/add.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/grants/list.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/grants/remove.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/pixels/__init__.py +1 -0
- yandex_audience_cli-0.0.1/yac/registry/pixels/create.py +8 -0
- yandex_audience_cli-0.0.1/yac/registry/pixels/delete.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/pixels/list.py +8 -0
- yandex_audience_cli-0.0.1/yac/registry/pixels/undelete.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/pixels/update.py +10 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/__init__.py +1 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/confirm.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/confirm_client_id.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/create_appmetrica.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/create_geo.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/create_geo_polygon.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/create_lookalike.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/create_metrika.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/create_pixel.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/delete.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/list.py +14 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/modify_data.py +16 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/reprocess.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/update.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/update_geo_points.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/upload_csv_file.py +15 -0
- yandex_audience_cli-0.0.1/yac/registry/segments/upload_file.py +15 -0
- yandex_audience_cli-0.0.1/yac/render.py +38 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Пример переменных окружения для yac.
|
|
2
|
+
#
|
|
3
|
+
# Скопируйте в .env и подставьте значения:
|
|
4
|
+
# cp .env.example .env
|
|
5
|
+
#
|
|
6
|
+
# ВАЖНО: yac НЕ читает .env автоматически (нет зависимости от python-dotenv).
|
|
7
|
+
# Экспортируйте переменные в окружение вручную, например:
|
|
8
|
+
# set -a; . ./.env; set +a
|
|
9
|
+
# (это корректно обрабатывает значения с пробелами/кавычками, в отличие от
|
|
10
|
+
# `export $(grep ... | xargs)`), или через direnv (.envrc: `dotenv`).
|
|
11
|
+
#
|
|
12
|
+
# Альтернативы .env (см. README): флаг `--token` или файл
|
|
13
|
+
# ~/.config/yandex-audience-cli/token — приоритет: --token > env > файл.
|
|
14
|
+
|
|
15
|
+
# OAuth-токен Яндекса. Обязателен, если не передаёте --token и нет
|
|
16
|
+
# ~/.config/yandex-audience-cli/token. Где взять: https://yandex.ru/dev/audience/
|
|
17
|
+
YANDEX_AUDIENCE_TOKEN=
|
|
18
|
+
|
|
19
|
+
# Базовый URL API. Менять НЕ нужно — только для тестов/прокси.
|
|
20
|
+
# По умолчанию: https://api-audience.yandex.ru
|
|
21
|
+
# YANDEX_AUDIENCE_BASE_URL=https://api-audience.yandex.ru
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Прогон тестовой пирамиды на push/PR в main.
|
|
4
|
+
# e2e пропускается по умолчанию (conftest: skip без --run-e2e) — в CI не гоняем.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [main]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
# 3.9 — нижняя граница requires-python; 3.12 — текущая.
|
|
19
|
+
python-version: ["3.9", "3.12"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
cache: pip
|
|
26
|
+
- name: Установить проект с dev-зависимостями
|
|
27
|
+
run: pip install -e ".[dev]"
|
|
28
|
+
- name: Прогнать тесты (e2e пропускается без --run-e2e)
|
|
29
|
+
run: pytest
|
|
30
|
+
|
|
31
|
+
lint:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
- uses: actions/setup-python@v5
|
|
36
|
+
with:
|
|
37
|
+
python-version: "3.12"
|
|
38
|
+
cache: pip
|
|
39
|
+
- name: Установить проект с dev-зависимостями
|
|
40
|
+
run: pip install -e ".[dev]"
|
|
41
|
+
- name: Проверить форматирование (black)
|
|
42
|
+
run: black --check yac tests
|
|
43
|
+
- name: Проверить линтером (ruff)
|
|
44
|
+
run: ruff check yac tests
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
# Сборка и публикация документации (MkDocs Material) на GitHub Pages.
|
|
4
|
+
# Триггерится на push в main, затрагивающий доки/конфиг, и вручную.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
paths:
|
|
9
|
+
- docs/**
|
|
10
|
+
- mkdocs.yml
|
|
11
|
+
- pyproject.toml
|
|
12
|
+
- .github/workflows/docs.yml
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
# Права для публикации на Pages.
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
pages: write
|
|
19
|
+
id-token: write
|
|
20
|
+
|
|
21
|
+
# Не отменять идущий деплой; новые запуски ждут очереди.
|
|
22
|
+
concurrency:
|
|
23
|
+
group: pages
|
|
24
|
+
cancel-in-progress: false
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
build:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 1
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
cache: pip
|
|
37
|
+
- name: Установить зависимости документации
|
|
38
|
+
run: pip install -e ".[docs]"
|
|
39
|
+
- name: Собрать сайт (строгий режим)
|
|
40
|
+
run: mkdocs build --strict
|
|
41
|
+
- uses: actions/configure-pages@v5
|
|
42
|
+
- uses: actions/upload-pages-artifact@v3
|
|
43
|
+
with:
|
|
44
|
+
path: site
|
|
45
|
+
|
|
46
|
+
deploy:
|
|
47
|
+
needs: build
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
environment:
|
|
50
|
+
name: github-pages
|
|
51
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
52
|
+
steps:
|
|
53
|
+
- id: deployment
|
|
54
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Spec audit
|
|
2
|
+
|
|
3
|
+
# Еженедельная сверка снапшота спеки (tests/data/audience_spec.json) с ЖИВЫМ
|
|
4
|
+
# справочником Яндекса. Отдельно от CI и неблокирующе — аутаж доков Яндекса не
|
|
5
|
+
# должен ронять обычный pytest. Падает, если живая спека разошлась с закоммиченным
|
|
6
|
+
# снапшотом (дрейф API) — это сигнал обновить реестр и снапшот.
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "0 6 * * 1" # понедельник 06:00 UTC
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
audit:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
cache: pip
|
|
21
|
+
- name: Установить проект с dev-зависимостями
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
- name: Перетянуть спеку из справочника Яндекса (сеть)
|
|
24
|
+
run: python scripts/fetch_spec.py
|
|
25
|
+
- name: Проверить, что снапшот не дрейфовал
|
|
26
|
+
run: git diff --exit-code tests/data/audience_spec.json
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.eggs/
|
|
9
|
+
|
|
10
|
+
# Документация (артефакты сборки MkDocs — генерируются в CI, локально не коммитим)
|
|
11
|
+
site/
|
|
12
|
+
.cache/
|
|
13
|
+
|
|
14
|
+
# Тесты / кэши инструментов
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
|
|
21
|
+
# Виртуальные окружения (проект ставится без venv, но на всякий случай)
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
|
|
25
|
+
# Редакторы / ОС
|
|
26
|
+
.DS_Store
|
|
27
|
+
*.swp
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
|
|
31
|
+
# Локальное служебное (Claude Code)
|
|
32
|
+
.claude/
|
|
33
|
+
|
|
34
|
+
# Секреты (токен резолвится из env или ~/.config, не из репо)
|
|
35
|
+
token
|
|
36
|
+
*.token
|
|
37
|
+
# .env с реальными значениями — в git не коммитим (шаблон: .env.example)
|
|
38
|
+
.env
|
|
39
|
+
.env.*
|
|
40
|
+
!.env.example
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Что это
|
|
6
|
+
|
|
7
|
+
CLI `yac` — обёртка над Yandex Audience API, зеркалящая структуру API **1:1**: версия (`v1`) →
|
|
8
|
+
пространство (`management`) → 5 ресурсных групп (segments, grants, pixels, accounts, delegates),
|
|
9
|
+
28 эндпоинтов. Команды строятся как `yac <группа> <операция>` (`yac segments list`, `yac grants add ...`).
|
|
10
|
+
|
|
11
|
+
Справочник API: живая локаль — **`ru`**, англоязычный путь `/en/ref/openapi/` сейчас отдаёт 404.
|
|
12
|
+
Единого OpenAPI-файла НЕТ; справочник — Diplodoc, по странице на метод. У каждой есть исходный
|
|
13
|
+
Markdown по детерминированному URL `https://yandex.ru/dev/audience/ru/ref/openapi/<группа>/<slug>.md`
|
|
14
|
+
(метод + полный URL ресурса внутри `.md` парсятся машинно) — это первоисточник для сверки реестра.
|
|
15
|
+
|
|
16
|
+
## Команды
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install -e ".[dev]" # установка editable + dev-зависимости (pytest, respx)
|
|
20
|
+
pytest # вся пирамида КРОМЕ e2e (e2e skip без --run-e2e)
|
|
21
|
+
pytest --run-e2e # + e2e (локальный stub-сервер; боевой — skip без YAC_E2E_TOKEN)
|
|
22
|
+
pytest tests/unit # один уровень по пути (unit|integration|smoke|e2e)
|
|
23
|
+
pytest -m smoke # один уровень по маркеру (unit|integration|smoke|e2e)
|
|
24
|
+
pytest tests/integration/test_resources.py::test_... -q # один тест (по узлу <файл>::<тест>)
|
|
25
|
+
pytest -k "<подстрока>" # один тест/группа по имени без указания файла
|
|
26
|
+
yac --help # справка; глобальные опции ДО группы: yac --token X segments list
|
|
27
|
+
python -m yac --help # эквивалент консольного скрипта
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Тесты не требуют установки: `pyproject.toml` задаёт `pythonpath = ["."]`, поэтому `import yac`
|
|
31
|
+
резолвится по корню репо.
|
|
32
|
+
|
|
33
|
+
**Тестовая пирамида** — `tests/{unit,integration,smoke,e2e}/`, уровень виден из `ls`, на каждом
|
|
34
|
+
файле `pytestmark = pytest.mark.<уровень>` (запуск и по пути, и по `-m`). Общие фикстуры —
|
|
35
|
+
`tests/conftest.py` (`client` для ресурсов; `stub_server` — настоящий stdlib-сервер для e2e).
|
|
36
|
+
- **unit** — изолированный модуль без сети: реестр (coverage/tree) и класс `Client` (respx лишь
|
|
37
|
+
подменяет сокет).
|
|
38
|
+
- **integration** — связка слоёв через **respx** (моки httpx): resource↔client↔HTTP, multipart,
|
|
39
|
+
команды CLI. Реальный токен не нужен.
|
|
40
|
+
- **smoke** — дымовой прогон CLI без сети: справка/версия/структура групп, чистая ошибка без токена.
|
|
41
|
+
- **e2e** — сквозной прогон НАСТОЯЩЕГО `yac` без моков: `test_local_server.py` гоняет CLI против
|
|
42
|
+
локального stdlib-сервера через `--base-url` (реальный сокет); `test_live_api.py` бьёт в боевой
|
|
43
|
+
`api-audience.yandex.ru` и **пропускается** без `YAC_E2E_TOKEN`.
|
|
44
|
+
|
|
45
|
+
e2e по умолчанию пропускается (`conftest.py` метит его skip, пока не передан `--run-e2e` —
|
|
46
|
+
идиоматичный `pytest_addoption` + `pytest_collection_modifyitems`). Запуск: `pytest --run-e2e`
|
|
47
|
+
или `pytest tests/e2e --run-e2e`. Боевой e2e: `YAC_E2E_TOKEN=*** pytest tests/e2e/test_live_api.py
|
|
48
|
+
--run-e2e`.
|
|
49
|
+
|
|
50
|
+
## Архитектура: реестр — единственный источник правды
|
|
51
|
+
|
|
52
|
+
`yac/registry/` — **дерево «файл на эндпоинт»**: подкаталог на группу, в нём по файлу на каждый из
|
|
53
|
+
28 `Endpoint` (`registry/segments/list.py`, `registry/grants/add.py`, ...). Имя файла = `op`,
|
|
54
|
+
каталог = `group`, переменная в файле всегда `ENDPOINT`. `ls registry/segments/` показывает все
|
|
55
|
+
эндпоинты группы. Дерево — единственный источник строк путей; `registry.ENDPOINTS` собирается из
|
|
56
|
+
него **автозагрузкой** (`_core._load` через `pkgutil`; порядок групп — `GROUPS`, внутри группы —
|
|
57
|
+
алфавит по имени файла). `registry/_core.py` держит `Endpoint` (frozen dataclass: `group`, `op`,
|
|
58
|
+
`method`, `path`, `summary`, `multipart`, `unverified`, `path_params`), `GROUP_*`, `GROUPS` и
|
|
59
|
+
функции; `registry/__init__.py` — фасад, реэкспортирующий публичный контракт (`ENDPOINTS`, `get`,
|
|
60
|
+
`by_group`, `render_path`, `Endpoint`, `GROUP_*`, `GROUPS`), поэтому `from . import registry` и
|
|
61
|
+
`from ..registry import GROUP_SEGMENTS` работают как раньше. **Всё остальное произрастает из реестра.**
|
|
62
|
+
Ключевой инвариант: имя `op` эндпоинта = имя метода класса ресурса = имя команды CLI (через `_`→`-`).
|
|
63
|
+
|
|
64
|
+
Поток данных слоями (зависимости однонаправленные, цикла нет):
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
commands/<group>.py (Typer-команды; парсят аргументы, зовут render)
|
|
68
|
+
↓
|
|
69
|
+
api/<group>.py (класс ресурса; метод на op; зовёт self._call(op, ...))
|
|
70
|
+
↓
|
|
71
|
+
base.py Resource._call → registry.get(group, op) + registry.render_path(ep, **path_params)
|
|
72
|
+
↓
|
|
73
|
+
client.py Client.request (httpx; Authorization: OAuth <token>, base .../v1/management, pretty, multipart, 429)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`context.py` хранит глобальные опции в `ctx.obj` (`AppState`), лениво создаёт `Client` через
|
|
77
|
+
`load_config()`, и даёт декоратор `handle_errors` (ConfigError/APIError → `exit(1)` без трейсбека).
|
|
78
|
+
`cli.py` собирает корневой Typer-app (`add_typer` по группам) и держит `__version__`;
|
|
79
|
+
`__init__.py` реэкспортирует `app` — отсюда работает entry point `yac = "yac:app"`.
|
|
80
|
+
|
|
81
|
+
Резолвинг токена (`config.py`, по приоритету): `--token` → env `YANDEX_AUDIENCE_TOKEN` →
|
|
82
|
+
файл `~/.config/yandex-audience-cli/token`. Base-url переопределяется `--base-url`/`YANDEX_AUDIENCE_BASE_URL`
|
|
83
|
+
(нужно тестам/прокси). API имеет только `v1`/`management` — это зафиксировано, не параметризуется.
|
|
84
|
+
|
|
85
|
+
## Как добавлять/менять эндпоинты
|
|
86
|
+
|
|
87
|
+
1. Создай файл `yac/registry/<group>/<op>.py` с одним `ENDPOINT = Endpoint(GROUP_<GROUP>, "<op>", ...)`
|
|
88
|
+
(имя файла = `op`, каталог = группа; импорт `from .._core import Endpoint, GROUP_<GROUP>`).
|
|
89
|
+
Это единственное место со строками путей; список собирается автозагрузкой — больше нигде не
|
|
90
|
+
регистрировать.
|
|
91
|
+
2. Добавь метод с именем = `op` в соответствующий `yac/api/<group>.py` (тело — `self._call("op", ...)`).
|
|
92
|
+
3. Добавь команду с тем же именем (`_`→`-`) в `yac/commands/<group>.py`.
|
|
93
|
+
4. Обнови `EXPECTED_PER_GROUP` в `tests/unit/test_registry_coverage.py` (итог `EXPECTED_TOTAL` =
|
|
94
|
+
`sum(...)`, **не хардкодь итоговое число**).
|
|
95
|
+
|
|
96
|
+
Машинные гарантии (тесты упадут при нарушении): `tests/unit/test_registry_coverage.py` — у эндпоинта
|
|
97
|
+
нет метода ресурса ИЛИ команды CLI; `tests/unit/test_registry_tree.py` — имя файла ≠ `op`, каталог ≠
|
|
98
|
+
`group`, число файлов ≠ числу эндпоинтов. Сама автосборка (`_core._load`) падает громко при импорте, если файл
|
|
99
|
+
не объявляет `ENDPOINT` или возникает дубль `(group, op)`.
|
|
100
|
+
|
|
101
|
+
## Раскладка и окружение (важная причина текущей структуры)
|
|
102
|
+
|
|
103
|
+
Весь код — в пакете `yac/` (подпакеты `api/`, `commands/`; остальные модули плоско в корне
|
|
104
|
+
пакета). Это **намеренно**: проект ставится глобально (`pip install -e .`, **без venv**), и плоские
|
|
105
|
+
top-level имена (`config`, `registry`, ...) раньше конфликтовали с одноимёнными модулями других
|
|
106
|
+
editable-проектов в общем `sys.path`. Namespace `yac.*` устраняет коллизию без venv и без префиксов.
|
|
107
|
+
|
|
108
|
+
- Импорты **внутри пакета — относительные** (`from .config import`, `from . import registry`,
|
|
109
|
+
`from ..base import Resource`). `import registry` записывается как `from . import registry`,
|
|
110
|
+
чтобы `registry.get()`/`registry.ENDPOINTS` в телах работали дословно.
|
|
111
|
+
- Импорты **в тестах — абсолютные пакетные** (`from yac.api.segments import Segments`).
|
|
112
|
+
- Не клади голые `.py` в корень репо: editable-`.pth` добавляет корень репо в `sys.path`
|
|
113
|
+
(т.к. `yac/` лежит в корне, не в `src/`), и такой файл снова станет глобальным top-level именем.
|
|
114
|
+
|
|
115
|
+
## Грабли Typer/тестов
|
|
116
|
+
|
|
117
|
+
- Современный Typer **не экспортирует** `click` как модуль — в тестах используй `typer.main.get_command`,
|
|
118
|
+
а не `import click`.
|
|
119
|
+
- Команды извлекай через корневой app: `typer.main.get_command(root_app).commands[group].commands`
|
|
120
|
+
(под-app с одной командой, как accounts, не имеет `.commands` напрямую).
|
|
121
|
+
- hatchling требует существующий `README.md` для сборки.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: yandex-audience-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: CLI, повторяющий структуру Yandex Audience API 1:1 (100% покрытие эндпоинтов)
|
|
5
|
+
Author: yandex-audience-cli
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: advertising,api,audience,cli,yandex
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: typer>=0.12
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: black>=24.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
18
|
+
Provides-Extra: docs
|
|
19
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
20
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# yandex-audience-cli
|
|
24
|
+
|
|
25
|
+
CLI для [Yandex Audience API](https://yandex.ru/dev/audience/), повторяющий структуру API **«1 в 1»** с гарантией 100% покрытия эндпоинтов.
|
|
26
|
+
|
|
27
|
+
📖 **Документация:** https://axisrow.github.io/yandex-audience-cli/
|
|
28
|
+
|
|
29
|
+
## Структура API (3 уровня)
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
версия (v1) → пространство (management) → ресурс (5 групп)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
База: `https://api-audience.yandex.ru/v1/management`. Авторизация: заголовок `Authorization: OAuth <token>`.
|
|
36
|
+
WSDL неприменим (REST/JSON, не SOAP). Реестр выверен 1:1 по официальному
|
|
37
|
+
[OpenAPI-справочнику Яндекса](https://yandex.ru/dev/audience/ru/ref/openapi/); единый источник правды —
|
|
38
|
+
пакет [`yac/registry/`](yac/registry/) (файл на эндпоинт; **28 эндпоинтов** в 5 группах).
|
|
39
|
+
|
|
40
|
+
## Раскладка
|
|
41
|
+
|
|
42
|
+
Весь код — в пакете [`yac/`](yac/). Имена модулей живут в namespace `yac.*`, поэтому
|
|
43
|
+
не конфликтуют с чужими top-level модулями в глобальном `sys.path` (без venv и без
|
|
44
|
+
префиксов). Структура:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
yac/
|
|
48
|
+
├── __init__.py # реэкспорт app, __version__
|
|
49
|
+
├── __main__.py # python -m yac
|
|
50
|
+
├── cli.py # сборка корневого Typer-приложения
|
|
51
|
+
├── config.py client.py models.py
|
|
52
|
+
├── base.py context.py render.py inputs.py output.py
|
|
53
|
+
├── registry/ # реестр-дерево: подкаталог на группу, файл на эндпоинт
|
|
54
|
+
│ ├── _core.py # Endpoint, GROUPS, автосборка ENDPOINTS
|
|
55
|
+
│ └── segments/ grants/ pixels/ accounts/ delegates/
|
|
56
|
+
├── api/ # HTTP-классы ресурсов, по файлу на группу
|
|
57
|
+
│ └── segments.py grants.py pixels.py accounts.py delegates.py
|
|
58
|
+
└── commands/ # Typer-команды, по файлу на группу
|
|
59
|
+
└── segments.py grants.py pixels.py accounts.py delegates.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
По каждой группе API — пара `api/<group>.py` (HTTP-вызовы) и `commands/<group>.py`
|
|
63
|
+
(команды Typer).
|
|
64
|
+
|
|
65
|
+
| Группа | Эндпоинтов | Команды |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `segments` | 16 | list, create-pixel, create-lookalike, create-metrika, create-appmetrica, create-geo, create-geo-polygon, upload-file, upload-csv-file, update-geo-points, modify-data, confirm, confirm-client-id, update, delete, reprocess |
|
|
68
|
+
| `grants` | 3 | list, add, remove |
|
|
69
|
+
| `pixels` | 5 | list, create, update, delete, undelete |
|
|
70
|
+
| `accounts` | 1 | list |
|
|
71
|
+
| `delegates` | 3 | list, add, remove |
|
|
72
|
+
|
|
73
|
+
> Метод «получить один сегмент» (`GET segment/{id}`) в Audience API **отсутствует** —
|
|
74
|
+
> по официальному справочнику доступен только список (`GET segments`). Поэтому такой команды нет.
|
|
75
|
+
|
|
76
|
+
## Установка
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Токен
|
|
83
|
+
|
|
84
|
+
Любой из способов (по приоритету): `--token <OAUTH>` → env `YANDEX_AUDIENCE_TOKEN` →
|
|
85
|
+
файл `~/.config/yandex-audience-cli/token`. Шаблон переменных — `.env.example`.
|
|
86
|
+
|
|
87
|
+
Где взять токен, как работать с `.env` и про общие права приложения — см.
|
|
88
|
+
[Аутентификация](https://axisrow.github.io/yandex-audience-cli/authentication/).
|
|
89
|
+
|
|
90
|
+
## Примеры
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
yac segments list
|
|
94
|
+
yac -o table segments list --pixel 12345 # глобальная -o ДО группы
|
|
95
|
+
yac segments create-pixel --data '{"name":"my","pixel_id":1,"period_length":30}'
|
|
96
|
+
yac segments create-pixel --data @segment.json # из файла
|
|
97
|
+
yac pixels create --name "Мой пиксель"
|
|
98
|
+
yac grants add 777 --user-login partner@yandex.ru --comment "доступ"
|
|
99
|
+
yac delegates list
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Глобальные опции: `--token`, `--base-url`, `-o/--output json|table`, `--pretty`, `--version`.
|
|
103
|
+
|
|
104
|
+
## Гарантия покрытия
|
|
105
|
+
|
|
106
|
+
`tests/unit/test_registry_coverage.py` падает, если в реестре есть эндпоинт без соответствующего
|
|
107
|
+
метода ресурса или команды CLI. Это машинная проверка структурного соответствия API.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pytest
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Боевой smoke-прогон
|
|
114
|
+
|
|
115
|
+
`scripts/live_smoke.sh` гоняет **все 28 эндпоинтов** через настоящий `yac` на боевом Audience API
|
|
116
|
+
(в отличие от read-only `tests/e2e/test_live_api.py`). ⚠️ Скрипт **мутирует боевой аккаунт**: создаёт
|
|
117
|
+
и удаляет реальные объекты (с префиксом `SMOKE yac`), но самоочищается через `trap EXIT` — работает
|
|
118
|
+
только на своих данных, чужие сегменты не трогает. В CI не запускается (нужен реальный токен и квота
|
|
119
|
+
записи). Запуск: токен в `.env`, затем `bash scripts/live_smoke.sh`. Подробнее —
|
|
120
|
+
[Разработка](https://axisrow.github.io/yandex-audience-cli/development/).
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# yandex-audience-cli
|
|
2
|
+
|
|
3
|
+
CLI для [Yandex Audience API](https://yandex.ru/dev/audience/), повторяющий структуру API **«1 в 1»** с гарантией 100% покрытия эндпоинтов.
|
|
4
|
+
|
|
5
|
+
📖 **Документация:** https://axisrow.github.io/yandex-audience-cli/
|
|
6
|
+
|
|
7
|
+
## Структура API (3 уровня)
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
версия (v1) → пространство (management) → ресурс (5 групп)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
База: `https://api-audience.yandex.ru/v1/management`. Авторизация: заголовок `Authorization: OAuth <token>`.
|
|
14
|
+
WSDL неприменим (REST/JSON, не SOAP). Реестр выверен 1:1 по официальному
|
|
15
|
+
[OpenAPI-справочнику Яндекса](https://yandex.ru/dev/audience/ru/ref/openapi/); единый источник правды —
|
|
16
|
+
пакет [`yac/registry/`](yac/registry/) (файл на эндпоинт; **28 эндпоинтов** в 5 группах).
|
|
17
|
+
|
|
18
|
+
## Раскладка
|
|
19
|
+
|
|
20
|
+
Весь код — в пакете [`yac/`](yac/). Имена модулей живут в namespace `yac.*`, поэтому
|
|
21
|
+
не конфликтуют с чужими top-level модулями в глобальном `sys.path` (без venv и без
|
|
22
|
+
префиксов). Структура:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
yac/
|
|
26
|
+
├── __init__.py # реэкспорт app, __version__
|
|
27
|
+
├── __main__.py # python -m yac
|
|
28
|
+
├── cli.py # сборка корневого Typer-приложения
|
|
29
|
+
├── config.py client.py models.py
|
|
30
|
+
├── base.py context.py render.py inputs.py output.py
|
|
31
|
+
├── registry/ # реестр-дерево: подкаталог на группу, файл на эндпоинт
|
|
32
|
+
│ ├── _core.py # Endpoint, GROUPS, автосборка ENDPOINTS
|
|
33
|
+
│ └── segments/ grants/ pixels/ accounts/ delegates/
|
|
34
|
+
├── api/ # HTTP-классы ресурсов, по файлу на группу
|
|
35
|
+
│ └── segments.py grants.py pixels.py accounts.py delegates.py
|
|
36
|
+
└── commands/ # Typer-команды, по файлу на группу
|
|
37
|
+
└── segments.py grants.py pixels.py accounts.py delegates.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
По каждой группе API — пара `api/<group>.py` (HTTP-вызовы) и `commands/<group>.py`
|
|
41
|
+
(команды Typer).
|
|
42
|
+
|
|
43
|
+
| Группа | Эндпоинтов | Команды |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `segments` | 16 | list, create-pixel, create-lookalike, create-metrika, create-appmetrica, create-geo, create-geo-polygon, upload-file, upload-csv-file, update-geo-points, modify-data, confirm, confirm-client-id, update, delete, reprocess |
|
|
46
|
+
| `grants` | 3 | list, add, remove |
|
|
47
|
+
| `pixels` | 5 | list, create, update, delete, undelete |
|
|
48
|
+
| `accounts` | 1 | list |
|
|
49
|
+
| `delegates` | 3 | list, add, remove |
|
|
50
|
+
|
|
51
|
+
> Метод «получить один сегмент» (`GET segment/{id}`) в Audience API **отсутствует** —
|
|
52
|
+
> по официальному справочнику доступен только список (`GET segments`). Поэтому такой команды нет.
|
|
53
|
+
|
|
54
|
+
## Установка
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install -e ".[dev]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Токен
|
|
61
|
+
|
|
62
|
+
Любой из способов (по приоритету): `--token <OAUTH>` → env `YANDEX_AUDIENCE_TOKEN` →
|
|
63
|
+
файл `~/.config/yandex-audience-cli/token`. Шаблон переменных — `.env.example`.
|
|
64
|
+
|
|
65
|
+
Где взять токен, как работать с `.env` и про общие права приложения — см.
|
|
66
|
+
[Аутентификация](https://axisrow.github.io/yandex-audience-cli/authentication/).
|
|
67
|
+
|
|
68
|
+
## Примеры
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
yac segments list
|
|
72
|
+
yac -o table segments list --pixel 12345 # глобальная -o ДО группы
|
|
73
|
+
yac segments create-pixel --data '{"name":"my","pixel_id":1,"period_length":30}'
|
|
74
|
+
yac segments create-pixel --data @segment.json # из файла
|
|
75
|
+
yac pixels create --name "Мой пиксель"
|
|
76
|
+
yac grants add 777 --user-login partner@yandex.ru --comment "доступ"
|
|
77
|
+
yac delegates list
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Глобальные опции: `--token`, `--base-url`, `-o/--output json|table`, `--pretty`, `--version`.
|
|
81
|
+
|
|
82
|
+
## Гарантия покрытия
|
|
83
|
+
|
|
84
|
+
`tests/unit/test_registry_coverage.py` падает, если в реестре есть эндпоинт без соответствующего
|
|
85
|
+
метода ресурса или команды CLI. Это машинная проверка структурного соответствия API.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pytest
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Боевой smoke-прогон
|
|
92
|
+
|
|
93
|
+
`scripts/live_smoke.sh` гоняет **все 28 эндпоинтов** через настоящий `yac` на боевом Audience API
|
|
94
|
+
(в отличие от read-only `tests/e2e/test_live_api.py`). ⚠️ Скрипт **мутирует боевой аккаунт**: создаёт
|
|
95
|
+
и удаляет реальные объекты (с префиксом `SMOKE yac`), но самоочищается через `trap EXIT` — работает
|
|
96
|
+
только на своих данных, чужие сегменты не трогает. В CI не запускается (нужен реальный токен и квота
|
|
97
|
+
записи). Запуск: токен в `.env`, затем `bash scripts/live_smoke.sh`. Подробнее —
|
|
98
|
+
[Разработка](https://axisrow.github.io/yandex-audience-cli/development/).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Архитектура
|
|
2
|
+
|
|
3
|
+
`yac` устроен так, чтобы соответствие CLI и API было **невозможно нарушить незаметно**. В основе —
|
|
4
|
+
реестр эндпоинтов как единственный источник правды.
|
|
5
|
+
|
|
6
|
+
## Реестр — единственный источник правды
|
|
7
|
+
|
|
8
|
+
`yac/registry/` — это **дерево «файл на эндпоинт»**: подкаталог на группу, внутри по файлу на каждый
|
|
9
|
+
из 28 эндпоинтов (`registry/segments/list.py`, `registry/grants/add.py`, …). Каждый файл объявляет
|
|
10
|
+
один объект `ENDPOINT`:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
# yac/registry/segments/list.py
|
|
14
|
+
ENDPOINT = Endpoint(GROUP_SEGMENTS, "list", "GET", "segments", "Список сегментов")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **Имя файла** = имя операции (`op`).
|
|
18
|
+
- **Каталог** = группа.
|
|
19
|
+
- Список `registry.ENDPOINTS` собирается из дерева **автозагрузкой** — регистрировать эндпоинт
|
|
20
|
+
где-то ещё не нужно.
|
|
21
|
+
|
|
22
|
+
`ls yac/registry/segments/` сразу показывает все 16 эндпоинтов группы — карта API наглядна прямо из
|
|
23
|
+
файловой системы.
|
|
24
|
+
|
|
25
|
+
**Ключевой инвариант:** имя `op` эндпоинта = имя метода класса ресурса = имя CLI-команды
|
|
26
|
+
(с заменой `_` ↔ `-`).
|
|
27
|
+
|
|
28
|
+
## Поток данных по слоям
|
|
29
|
+
|
|
30
|
+
Зависимости однонаправленные, цикла нет:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
commands/<group>.py Typer-команды: парсят аргументы, зовут render
|
|
34
|
+
↓
|
|
35
|
+
api/<group>.py класс ресурса: метод на каждый op; зовёт self._call(op, ...)
|
|
36
|
+
↓
|
|
37
|
+
base.py Resource._call → registry.get(group, op) + registry.render_path(ep, **path_params)
|
|
38
|
+
↓
|
|
39
|
+
client.py Client.request httpx: заголовок OAuth, base .../v1/management, pretty, multipart, 429
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Модуль | Роль |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `cli.py` | корневой Typer-app, глобальные опции, регистрация групп |
|
|
45
|
+
| `config.py` | резолвинг токена и base-url |
|
|
46
|
+
| `context.py` | `AppState`, ленивое создание клиента, декоратор `handle_errors` |
|
|
47
|
+
| `client.py` | httpx-обёртка, `APIError`/`RateLimitError`, разбор ответа |
|
|
48
|
+
| `base.py` | `Resource._call` — мост реестр ↔ клиент |
|
|
49
|
+
| `registry/` | `ENDPOINTS`, модель `Endpoint`, `get`/`render_path`/`by_group` |
|
|
50
|
+
| `render.py` / `output.py` | вывод json/table |
|
|
51
|
+
| `inputs.py` | `parse_data` для `--data` (строка/`@файл`/`-`) |
|
|
52
|
+
|
|
53
|
+
## Машинная гарантия покрытия
|
|
54
|
+
|
|
55
|
+
Соответствие реестра, классов ресурсов и CLI-команд проверяется тестами — они **упадут**, если
|
|
56
|
+
рассинхронизировать:
|
|
57
|
+
|
|
58
|
+
- `tests/unit/test_registry_coverage.py` — у каждого эндпоинта есть и метод ресурса, и CLI-команда;
|
|
59
|
+
счётчики по группам (16/3/5/1/3 = 28).
|
|
60
|
+
- `tests/unit/test_registry_tree.py` — имя файла = `op`, каталог = `group`, число файлов = числу
|
|
61
|
+
эндпоинтов.
|
|
62
|
+
- Сама автосборка реестра падает громко при импорте, если файл не объявляет `ENDPOINT` или возник
|
|
63
|
+
дубль `(group, op)`.
|
|
64
|
+
|
|
65
|
+
Поэтому «100% покрытие эндпоинтов» — не обещание, а инвариант, который держат тесты.
|
|
66
|
+
|
|
67
|
+
## Как добавить эндпоинт
|
|
68
|
+
|
|
69
|
+
1. Создать `yac/registry/<group>/<op>.py` с одним `ENDPOINT`.
|
|
70
|
+
2. Добавить метод с именем `op` в `yac/api/<group>.py` (тело — `self._call("op", ...)`).
|
|
71
|
+
3. Добавить команду с тем же именем (`_`→`-`) в `yac/commands/<group>.py`.
|
|
72
|
+
4. Обновить ожидаемые счётчики в `tests/unit/test_registry_coverage.py`.
|
|
73
|
+
|
|
74
|
+
Подробности — в `CLAUDE.md` репозитория.
|