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.
Files changed (100) hide show
  1. yandex_audience_cli-0.0.1/.env.example +21 -0
  2. yandex_audience_cli-0.0.1/.github/workflows/ci.yml +44 -0
  3. yandex_audience_cli-0.0.1/.github/workflows/docs.yml +54 -0
  4. yandex_audience_cli-0.0.1/.github/workflows/spec-audit.yml +26 -0
  5. yandex_audience_cli-0.0.1/.gitignore +40 -0
  6. yandex_audience_cli-0.0.1/CLAUDE.md +121 -0
  7. yandex_audience_cli-0.0.1/PKG-INFO +120 -0
  8. yandex_audience_cli-0.0.1/README.md +98 -0
  9. yandex_audience_cli-0.0.1/docs/architecture.md +74 -0
  10. yandex_audience_cli-0.0.1/docs/authentication.md +66 -0
  11. yandex_audience_cli-0.0.1/docs/commands/accounts.md +21 -0
  12. yandex_audience_cli-0.0.1/docs/commands/delegates.md +51 -0
  13. yandex_audience_cli-0.0.1/docs/commands/grants.md +53 -0
  14. yandex_audience_cli-0.0.1/docs/commands/pixels.md +63 -0
  15. yandex_audience_cli-0.0.1/docs/commands/segments.md +187 -0
  16. yandex_audience_cli-0.0.1/docs/development.md +98 -0
  17. yandex_audience_cli-0.0.1/docs/index.md +43 -0
  18. yandex_audience_cli-0.0.1/docs/installation.md +43 -0
  19. yandex_audience_cli-0.0.1/docs/usage.md +81 -0
  20. yandex_audience_cli-0.0.1/mkdocs.yml +60 -0
  21. yandex_audience_cli-0.0.1/pyproject.toml +67 -0
  22. yandex_audience_cli-0.0.1/scripts/fetch_spec.py +242 -0
  23. yandex_audience_cli-0.0.1/scripts/live_smoke.sh +399 -0
  24. yandex_audience_cli-0.0.1/scripts/throttle.sh +54 -0
  25. yandex_audience_cli-0.0.1/tests/conftest.py +182 -0
  26. yandex_audience_cli-0.0.1/tests/data/audience_spec.json +232 -0
  27. yandex_audience_cli-0.0.1/tests/e2e/__init__.py +1 -0
  28. yandex_audience_cli-0.0.1/tests/e2e/test_live_api.py +50 -0
  29. yandex_audience_cli-0.0.1/tests/e2e/test_local_server.py +105 -0
  30. yandex_audience_cli-0.0.1/tests/integration/__init__.py +1 -0
  31. yandex_audience_cli-0.0.1/tests/integration/test_cli_commands.py +40 -0
  32. yandex_audience_cli-0.0.1/tests/integration/test_resources.py +194 -0
  33. yandex_audience_cli-0.0.1/tests/integration/test_uploads.py +60 -0
  34. yandex_audience_cli-0.0.1/tests/smoke/__init__.py +1 -0
  35. yandex_audience_cli-0.0.1/tests/smoke/test_smoke.py +79 -0
  36. yandex_audience_cli-0.0.1/tests/test_openapi_coverage.py +90 -0
  37. yandex_audience_cli-0.0.1/tests/unit/__init__.py +1 -0
  38. yandex_audience_cli-0.0.1/tests/unit/test_http_client.py +167 -0
  39. yandex_audience_cli-0.0.1/tests/unit/test_live_smoke_throttle.py +116 -0
  40. yandex_audience_cli-0.0.1/tests/unit/test_registry_coverage.py +117 -0
  41. yandex_audience_cli-0.0.1/tests/unit/test_registry_tree.py +53 -0
  42. yandex_audience_cli-0.0.1/yac/__init__.py +5 -0
  43. yandex_audience_cli-0.0.1/yac/__main__.py +6 -0
  44. yandex_audience_cli-0.0.1/yac/api/__init__.py +1 -0
  45. yandex_audience_cli-0.0.1/yac/api/accounts.py +15 -0
  46. yandex_audience_cli-0.0.1/yac/api/delegates.py +33 -0
  47. yandex_audience_cli-0.0.1/yac/api/grants.py +36 -0
  48. yandex_audience_cli-0.0.1/yac/api/pixels.py +29 -0
  49. yandex_audience_cli-0.0.1/yac/api/segments.py +135 -0
  50. yandex_audience_cli-0.0.1/yac/base.py +44 -0
  51. yandex_audience_cli-0.0.1/yac/cli.py +88 -0
  52. yandex_audience_cli-0.0.1/yac/client.py +204 -0
  53. yandex_audience_cli-0.0.1/yac/commands/__init__.py +1 -0
  54. yandex_audience_cli-0.0.1/yac/commands/accounts.py +20 -0
  55. yandex_audience_cli-0.0.1/yac/commands/delegates.py +52 -0
  56. yandex_audience_cli-0.0.1/yac/commands/grants.py +65 -0
  57. yandex_audience_cli-0.0.1/yac/commands/pixels.py +54 -0
  58. yandex_audience_cli-0.0.1/yac/commands/segments.py +247 -0
  59. yandex_audience_cli-0.0.1/yac/config.py +80 -0
  60. yandex_audience_cli-0.0.1/yac/context.py +68 -0
  61. yandex_audience_cli-0.0.1/yac/inputs.py +33 -0
  62. yandex_audience_cli-0.0.1/yac/models.py +164 -0
  63. yandex_audience_cli-0.0.1/yac/openapi_coverage.py +115 -0
  64. yandex_audience_cli-0.0.1/yac/output.py +53 -0
  65. yandex_audience_cli-0.0.1/yac/registry/__init__.py +60 -0
  66. yandex_audience_cli-0.0.1/yac/registry/_core.py +141 -0
  67. yandex_audience_cli-0.0.1/yac/registry/accounts/__init__.py +1 -0
  68. yandex_audience_cli-0.0.1/yac/registry/accounts/list.py +14 -0
  69. yandex_audience_cli-0.0.1/yac/registry/delegates/__init__.py +1 -0
  70. yandex_audience_cli-0.0.1/yac/registry/delegates/add.py +8 -0
  71. yandex_audience_cli-0.0.1/yac/registry/delegates/list.py +10 -0
  72. yandex_audience_cli-0.0.1/yac/registry/delegates/remove.py +14 -0
  73. yandex_audience_cli-0.0.1/yac/registry/grants/__init__.py +1 -0
  74. yandex_audience_cli-0.0.1/yac/registry/grants/add.py +15 -0
  75. yandex_audience_cli-0.0.1/yac/registry/grants/list.py +15 -0
  76. yandex_audience_cli-0.0.1/yac/registry/grants/remove.py +15 -0
  77. yandex_audience_cli-0.0.1/yac/registry/pixels/__init__.py +1 -0
  78. yandex_audience_cli-0.0.1/yac/registry/pixels/create.py +8 -0
  79. yandex_audience_cli-0.0.1/yac/registry/pixels/delete.py +15 -0
  80. yandex_audience_cli-0.0.1/yac/registry/pixels/list.py +8 -0
  81. yandex_audience_cli-0.0.1/yac/registry/pixels/undelete.py +15 -0
  82. yandex_audience_cli-0.0.1/yac/registry/pixels/update.py +10 -0
  83. yandex_audience_cli-0.0.1/yac/registry/segments/__init__.py +1 -0
  84. yandex_audience_cli-0.0.1/yac/registry/segments/confirm.py +15 -0
  85. yandex_audience_cli-0.0.1/yac/registry/segments/confirm_client_id.py +15 -0
  86. yandex_audience_cli-0.0.1/yac/registry/segments/create_appmetrica.py +14 -0
  87. yandex_audience_cli-0.0.1/yac/registry/segments/create_geo.py +14 -0
  88. yandex_audience_cli-0.0.1/yac/registry/segments/create_geo_polygon.py +14 -0
  89. yandex_audience_cli-0.0.1/yac/registry/segments/create_lookalike.py +14 -0
  90. yandex_audience_cli-0.0.1/yac/registry/segments/create_metrika.py +14 -0
  91. yandex_audience_cli-0.0.1/yac/registry/segments/create_pixel.py +14 -0
  92. yandex_audience_cli-0.0.1/yac/registry/segments/delete.py +15 -0
  93. yandex_audience_cli-0.0.1/yac/registry/segments/list.py +14 -0
  94. yandex_audience_cli-0.0.1/yac/registry/segments/modify_data.py +16 -0
  95. yandex_audience_cli-0.0.1/yac/registry/segments/reprocess.py +15 -0
  96. yandex_audience_cli-0.0.1/yac/registry/segments/update.py +15 -0
  97. yandex_audience_cli-0.0.1/yac/registry/segments/update_geo_points.py +15 -0
  98. yandex_audience_cli-0.0.1/yac/registry/segments/upload_csv_file.py +15 -0
  99. yandex_audience_cli-0.0.1/yac/registry/segments/upload_file.py +15 -0
  100. 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` репозитория.