Metadata-Version: 2.4
Name: direct-cli
Version: 0.2.7
Summary: Command-line interface for Yandex Direct API
Author: axisrow
License: MIT
Project-URL: Homepage, https://github.com/axisrow/direct-cli
Project-URL: Repository, https://github.com/axisrow/direct-cli
Keywords: yandex,direct,cli,api,advertising
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: tapi-yandex-direct>=2021.5.29
Requires-Dist: click>=8.0.0
Requires-Dist: python-dotenv>=0.19.0
Requires-Dist: tabulate>=0.8.0
Requires-Dist: colorama>=0.4.0
Requires-Dist: tqdm>=4.60.0
Provides-Extra: dev
Requires-Dist: pytest>=6.0; extra == "dev"
Requires-Dist: pytest-cov>=2.0; extra == "dev"
Requires-Dist: pytest-recording>=0.13; extra == "dev"
Requires-Dist: vcrpy>=6.0; extra == "dev"
Requires-Dist: black>=22.0; extra == "dev"
Requires-Dist: flake8>=4.0; extra == "dev"
Requires-Dist: requests>=2.0; extra == "dev"
Requires-Dist: beautifulsoup4>=4.12; extra == "dev"
Requires-Dist: lxml>=4.9; extra == "dev"

# Direct CLI

[English](#english) | [Русский](#русский)

---

## English

Command-line interface for the Yandex Direct API.

### Installation

```bash
pip install direct-cli
```

### Configuration

Create a `.env` file in your working directory:

```env
YANDEX_DIRECT_TOKEN=your_access_token
YANDEX_DIRECT_LOGIN=your_yandex_login
```

Or pass credentials directly per command:

```bash
direct --token YOUR_TOKEN --login YOUR_LOGIN campaigns get
```

Install with `pip install direct-cli`, then run commands with `direct`.

### Global Options

| Option | Description |
|--------|-------------|
| `--token` | API access token |
| `--login` | Yandex advertiser login |
| `--sandbox` | Use sandbox API |

### CLI Convention

The current CLI convention is defined as follows.

#### CLI Contract

The canonical command shape is:

```bash
direct <group> <command> [flags]
```

Naming rules:

- `group`:
  - lowercase ASCII only
  - no underscores
  - multiword groups are concatenated
  - examples: `dynamicads`, `smartadtargets`, `negativekeywordsharedsets`

- `command`:
  - lowercase only
  - multiword commands use kebab-case
  - examples: `get`, `set-bids`, `check-campaigns`, `has-search-volume`

direct-cli owns the public naming contract. `tapi-yandex-direct` may
influence the internal transport layer, but it does not define canonical CLI
names.

The current policy is canonical-only. Historical aliases are not preserved in
the runtime CLI by default. If compatibility is ever needed, an alias must be
added as an explicit exception with the concrete legacy syntax that still has to
be supported.

#### Input Rules

- All user-facing input must be passed only through typed CLI flags.
- `--json` is not part of the public CLI contract.
- User-facing parameters must not be passed through `--json`.
- The CLI must not accept `SelectionCriteria`, nested payloads, update payloads, bidding rules, or any other user-facing command input through `--json`.
- Typed flags and JSON blobs must not be mixed as part of one public command contract.
- If the API requires a complex object, the CLI must expose explicit flags or subcommands instead of forwarding raw JSON.

#### Command Formatting Rules
- Every canonical CLI command must be written strictly on a single line.
- Multi-line command formatting is not allowed.
- Shell line continuation using `\` is forbidden in canonical documentation, help text, tests, and examples.

Allowed:

```bash
direct dictionaries get-geo-regions --region-ids 225,187 --fields GeoRegionId,GeoRegionName
```

Not allowed:

```bash
direct dictionaries get-geo-regions \
  --region-ids 225,187 \
  --fields GeoRegionId,GeoRegionName
```

#### Flag Design Rules

- List inputs use comma-separated CLI syntax where appropriate.
- Money and bid values stay human-readable in CLI input and are converted internally to the API wire format.
- Selector fields remain explicit flags, for example:
  - `--id`
  - `--campaign-id`
  - `--adgroup-id`
- Nested API structures must be projected into typed flags instead of blob JSON.
- Help text must not advertise JSON as an alternative input path.

#### Datetime Rules

- Datetime parameters must be passed in the format `YYYY-MM-DDTHH:MM:SS`.
- Datetime values must be passed as a single shell token.
- Canonical examples must not use timezone suffixes like `Z`.
- Canonical examples must not use quoted space-separated datetime values.

Use:

```bash
direct changes check-campaigns --timestamp 2026-04-14T00:00:00
```

Do not use:

```bash
direct changes check-campaigns --timestamp 2026-04-14T00:00:00Z
direct changes check-campaigns --timestamp "2026-04-14 00:00:00"
```

#### Documentation Contract

- `README` must use only canonical syntax.
- `README` must use only single-line command examples.
- Canonical examples must not contain `--json`.
- Help output and tests must enforce the same contract.

#### Examples

Valid canonical examples:

```bash
direct campaigns get --ids 1,2,3
direct changes check-campaigns --timestamp 2026-04-14T00:00:00
direct keywordsresearch has-search-volume --keywords "buy laptop,buy desktop"
direct dynamicads set-bids --id 789 --bid 12.5 --context-bid 9 --priority HIGH
direct dictionaries get-geo-regions --name Moscow --region-ids 225,187 --exact-names Москва,Санкт-Петербург --fields GeoRegionId,GeoRegionName
```

Invalid examples:

```bash
direct dictionaries get-geo-regions --json '{"GeoRegionIds":[225]}' --fields GeoRegionId,GeoRegionName
direct dynamicads set-bids --id 789 --bid 12.5 --json '{"StrategyPriority":"HIGH"}'
direct dictionaries get-geo-regions \
  --region-ids 225 \
  --fields GeoRegionId,GeoRegionName
direct changes check-campaigns --timestamp 2026-04-14T00:00:00Z
direct changes check-campaigns --timestamp "2026-04-14 00:00:00"
```

#### Campaigns

```bash
# Get campaigns
direct campaigns get
direct campaigns get --status ACTIVE
direct campaigns get --ids 1,2,3 --format table
direct campaigns get --fetch-all --format csv --output campaigns.csv

# Create (use --dry-run to preview the request)
direct campaigns add --name "My Campaign" --start-date 2024-02-01 --type TEXT_CAMPAIGN --budget 1000 --setting ADD_METRICA_TAG=YES --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --dry-run
direct campaigns add --name "Dynamic Campaign" --start-date 2024-02-01 --type DYNAMIC_TEXT_CAMPAIGN --setting ADD_METRICA_TAG=NO --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --dry-run
direct campaigns add --name "Smart Campaign" --start-date 2024-02-01 --type SMART_CAMPAIGN --network-strategy AVERAGE_CPC_PER_FILTER --filter-average-cpc 1 --counter-id 123 --dry-run

# Update / lifecycle
direct campaigns update --id 12345 --name "New Name" --status SUSPENDED --budget 100 --start-date 2024-02-10 --end-date 2024-03-01
direct campaigns suspend --id 12345
direct campaigns resume --id 12345
direct campaigns archive --id 12345
direct campaigns unarchive --id 12345
direct campaigns delete --id 12345
```

#### Ad Groups

```bash
direct adgroups get --campaign-ids 1,2,3 --limit 50
direct adgroups add --name "Group 1" --campaign-id 12345 --region-ids 1,225 --dry-run
direct adgroups add --name "Dynamic Group" --campaign-id 12345 --type DYNAMIC_TEXT_AD_GROUP --region-ids 1,225 --domain-url example.com --dry-run
direct adgroups add --name "Smart Group" --campaign-id 12345 --type SMART_AD_GROUP --region-ids 1,225 --feed-id 170 --ad-title-source FEED_NAME --ad-body-source FEED_NAME --dry-run
direct adgroups update --id 67890 --name "New Name" --status SUSPENDED --region-ids 1,225
direct adgroups delete --id 67890
```

#### Ads

```bash
direct ads get --campaign-ids 1,2,3
direct ads get --adgroup-ids 45678 --format table
direct ads add --adgroup-id 12345 --type TEXT_AD --title "Title" --text "Ad text" --href "https://example.com" --dry-run
direct ads add --adgroup-id 12345 --type TEXT_IMAGE_AD --image-hash abcdefghijklmnopqrst --href "https://example.com" --title "Banner" --text "Image ad" --dry-run
direct ads update --id 99999 --status PAUSED --title "New Title" --text "New text" --href "https://example.com" --image-hash abcdefghijklmnopqrst
direct ads delete --id 99999
```

#### Keywords

```bash
direct keywords get --campaign-ids 1,2,3
direct keywords add --adgroup-id 12345 --keyword "buy laptop" --bid 10.50 --context-bid 5.25 --user-param-1 segment-a --user-param-2 segment-b --dry-run
direct keywords update --id 88888 --bid 15.00 --context-bid 6.00 --status SUSPENDED
direct keywords delete --id 88888
```

#### Reports

```bash
# Get a report (saved to file)
direct reports get --type CAMPAIGN_PERFORMANCE_REPORT --from 2024-01-01 --to 2024-01-31 --name "January Report" --fields "Date,CampaignId,Clicks,Cost" --format csv --output report.csv

# List available report types
direct reports list-types
```

Available report types: `CAMPAIGN_PERFORMANCE_REPORT`, `ADGROUP_PERFORMANCE_REPORT`, `AD_PERFORMANCE_REPORT`, `CRITERIA_PERFORMANCE_REPORT`, `CUSTOM_REPORT`, `REACH_AND_FREQUENCY_CAMPAIGN_REPORT`, `SEARCH_QUERY_PERFORMANCE_REPORT`

#### Other Resources

```bash
# Reference dictionaries and changes
direct dictionaries get --names Currencies,GeoRegions
direct dictionaries get-geo-regions --name Moscow --region-ids 225,187 --exact-names Москва,Санкт-Петербург --fields GeoRegionId,GeoRegionName
direct changes check --campaign-ids 1,2,3 --timestamp 2026-04-14T00:00:00
direct changes check-campaigns --timestamp 2026-04-14T00:00:00
direct changes check-dictionaries
direct clients get --fields ClientId,Login,Currency

# Keyword research and retargeting
direct keywordsresearch has-search-volume --keywords "buy laptop,buy desktop"
direct retargeting add --name "List A" --type AUDIENCE --rule "ALL:12345:30|67890:7" --dry-run
direct retargeting update --id 55 --name "Renamed" --rule "ANY:12345:30" --dry-run

# Bids and modifiers
direct bids set --keyword-id 123 --bid 15
direct bids set-auto --keyword-id 123 --max-bid 20 --position PREMIUMBLOCK --scope SEARCH --dry-run
direct keywordbids set --keyword-id 321 --search-bid 8 --network-bid 3
direct keywordbids set-auto --keyword-id 321 --target-traffic-volume 100 --increase-percent 10 --bid-ceiling 12.5 --dry-run
direct bidmodifiers add --campaign-id 123 --type DEMOGRAPHICS_ADJUSTMENT --value 150 --gender GENDER_MALE --age AGE_25_34 --dry-run
direct bidmodifiers set --id 99 --value 130 --dry-run

# Canonical multiword groups
direct negativekeywordsharedsets update --id 123 --keywords "foo,bar"
direct audiencetargets add --adgroup-id 100 --retargeting-list-id 200 --bid 12 --priority HIGH --dry-run
direct audiencetargets set-bids --id 101 --context-bid 7 --priority LOW --dry-run
direct dynamicads add --adgroup-id 33 --name "Webpage A" --condition "URL:CONTAINS_ANY:test|shop" --condition "PAGE_CONTENT:CONTAINS:baz" --bid 3 --context-bid 2 --priority HIGH --dry-run
direct smartadtargets add --adgroup-id 55 --name "Audience A" --audience ALL_SEGMENTS --condition "CATEGORY_ID:EQUALS:42" --average-cpc 3 --average-cpa 4 --priority HIGH --available-items-only YES --dry-run
direct smartadtargets update --id 456 --priority HIGH
direct smartadtargets set-bids --id 456 --average-cpc 10.5 --average-cpa 15 --priority HIGH
direct dynamicads set-bids --id 789 --bid 12.5 --context-bid 9 --priority HIGH

# Extensions, assets, feeds, and clients
direct sitelinks add --sitelink "Docs|https://example.com/docs" --sitelink "Help|https://example.com/help|Desk" --dry-run
direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company-name "Acme" --work-time 1#5#9#0#18#0 --phone-country-code +7 --phone-city-code 495 --phone-number 1234567 --dry-run
direct adextensions add --callout-text "Free shipping" --dry-run
direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
direct creatives add --video-id video-id --dry-run
direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --dry-run
direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
direct clients update --client-id 999 --phone +70000000000 --fax +70000000001 --email user@example.com --city Moscow --dry-run
direct agencyclients add --login client-login --first-name Alice --last-name Smith --currency RUB --notification-email ops@example.com --notification-lang RU --send-account-news --no-send-warnings --dry-run
direct agencyclients add-passport-organization --name "Org" --currency RUB --notification-email ops@example.com --notification-lang EN --no-send-account-news --send-warnings --dry-run
direct agencyclients add-passport-organization-member --passport-organization-login org-login --role CHIEF --invite-email user@example.com --dry-run
direct agencyclients update --client-id 42 --phone +70000000000 --email user@example.com --grant EDIT_CAMPAIGNS --grant IMPORT_XLS --dry-run
```

### Known Transport Gap

`dynamicads update` is still a transport gap. Downstream wrappers may assume
that operation exists, but the current installed CLI help surface does not
expose `dynamicads update`. Treat it as unsupported until a real command is
added.

### Output Formats

All `get` commands support `--format`:

| Format | Description |
|--------|-------------|
| `json` | JSON (default) |
| `table` | Formatted table |
| `csv` | CSV |
| `tsv` | TSV |

```bash
direct campaigns get --format table
direct campaigns get --format csv --output campaigns.csv
```

### Pagination

```bash
direct campaigns get --limit 10        # first 10 results
direct campaigns get --fetch-all       # all pages
```

### ⚠️ Destructive Commands

The following commands make **irreversible changes** — use with caution:

| Command | Effect |
|---------|--------|
| `campaigns delete --id` | Permanently deletes a campaign and all its contents |
| `adgroups delete --id` | Permanently deletes an ad group |
| `ads delete --id` | Permanently deletes an ad |
| `keywords delete --id` | Permanently deletes a keyword |
| `audiencetargets delete --id` | Permanently deletes an audience target |

Commands that affect live ad delivery: `suspend`, `resume`, `archive`, `unarchive` (available on `campaigns`, `ads`, `keywords`).

Commands that affect bids and spending: `bids set`, `keywordbids set`, `bidmodifiers set`.

Use `--dry-run` on `add` / `update` commands to preview the API request before sending:

```bash
direct campaigns add --name "Test" --start-date 2024-01-01 --dry-run
```

### Testing

Three tiers of tests live under `tests/`:

| Tier | Marker | Network | Token required |
|---|---|---|---|
| Unit / CLI wiring / dry-run | *(none)* | No | No |
| Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
| Write integration | `-m integration_write` | No (replays VCR cassettes) | No |

```bash
pip install -e ".[dev]"
pytest                              # fast tier — no token
pytest -m integration -v            # read-only integration tests (needs token)
pytest -m integration_write -v      # write cassette replay (no token needed)
```

### API Coverage And Drift Monitoring

The project now distinguishes four surfaces:

| Surface | Coverage strategy |
|---|---|
| Canonical WSDL-backed SOAP services | `tests/test_api_coverage.py` verifies strict service/method parity and dry-run request-schema coverage or explicit exclusions |
| Non-WSDL services (`reports`) | Explicit contract tests |
| Historical aliases retained by exception | None currently retained |
| Intentional CLI-only helpers | Explicitly allowlisted with reasons in `direct_cli/wsdl_coverage.py` |

`100% coverage` in this project means full coverage of the supported
**canonical API surface**. Alias groups and CLI-only helpers remain supported,
but they are tracked outside the strict parity metric.

Useful maintenance commands:

```bash
python scripts/build_api_coverage_report.py
python scripts/refresh_wsdl_cache.py
python scripts/check_wsdl_drift.py
```

CI runs a scheduled API coverage workflow that:
- runs the fast coverage suites;
- uploads a machine-readable API coverage report artifact;
- checks the cached WSDL files against the live Yandex Direct API on schedule.

#### Re-recording write cassettes

The write tests replay HTTP traffic captured from the Yandex Direct **sandbox**
(`--sandbox` is injected automatically).  Cassettes live under
`tests/cassettes/test_integration_write/` and are checked into git.

If you change the request payload of any write command (e.g. adding a field),
the matching cassette stops replaying and the test fails with a body-mismatch
error.  To regenerate:

```bash
set -a && source .env && set +a        # load YANDEX_DIRECT_TOKEN / LOGIN
pytest -m integration_write -v --record-mode=rewrite
```

**The same OAuth token works for both production and the sandbox** — no
separate sandbox token is needed.  After recording, **always audit the
generated YAMLs for leaked secrets**:

```bash
grep -r "$YANDEX_DIRECT_TOKEN" tests/cassettes/   # must return nothing
grep -r "$YANDEX_DIRECT_LOGIN" tests/cassettes/   # must return nothing
```

The VCR config in `tests/conftest.py` already strips `Authorization`,
`Client-Login`, cookies and any response header containing the substring
`login`, but manual verification is mandatory before committing.

### Release Process

Build, validate and upload to PyPI:

```bash
pip install -e ".[dev]"
scripts/release_pypi.sh testpypi   # upload to TestPyPI
scripts/release_pypi.sh pypi       # upload to PyPI
scripts/release_pypi.sh all        # both
```

The script reads credentials from `.env`:

```dotenv
TWINE_USERNAME=__token__
TEST_PYPI_TOKEN=pypi-...
PYPI_TOKEN=pypi-...
```

#### PyPI Token Scoping

PyPI API tokens can be **account-wide** or **project-scoped**:

- **Project-scoped** tokens only allow uploads to the specific project they were created for. A token scoped to `telethon-cli` cannot upload `direct-cli` — you will get **403 Forbidden**.
- **Account-wide** tokens allow uploads to any project under your account.
- For the **first publication** of a new project, you **must** use an account-wide token (project-scoped tokens cannot be created until the project exists on PyPI).
- After the first successful upload, create a project-scoped token at https://pypi.org/manage/account/token/ and replace the account-wide token in `.env`.

Bump `version` in `pyproject.toml` before each release — PyPI rejects duplicate versions.

### License

MIT

---

## Русский

Интерфейс командной строки для Яндекс.Директ API.

### Установка

```bash
pip install direct-cli
```

### Настройка

Создайте файл `.env` в рабочей директории:

```env
YANDEX_DIRECT_TOKEN=ваш_токен
YANDEX_DIRECT_LOGIN=ваш_логин_на_яндексе
```

Или передавайте credentials напрямую в команду:

```bash
direct --token ВАШ_ТОКЕН --login ВАШ_ЛОГИН campaigns get
```

Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет через `direct`.

### Глобальные опции

| Опция | Описание |
|-------|----------|
| `--token` | OAuth-токен доступа к API |
| `--login` | Логин рекламодателя на Яндексе |
| `--sandbox` | Использовать тестовое API (песочница) |

### Использование

Канонический transport-контракт выглядит так:

```bash
direct <group> <command> [flags]
```

Group naming rules:
- только lowercase ASCII
- без `_`
- многословные группы склеиваются, например `negativekeywordsharedsets`

Command naming rules:
- только lowercase
- kebab-case для многословных действий, например `check-campaigns`
- в документации и примерах каноническими считаются `get`,
  `check-dictionaries` и `has-search-volume`

`direct-cli` владеет публичным naming contract. `tapi-yandex-direct` может
влиять на внутренний transport layer, но не определяет канонические CLI-имена.

Текущая политика — canonical-only. Исторические aliases по умолчанию не
сохраняются в runtime CLI. Если совместимость когда-нибудь понадобится, alias
должен быть добавлен как явное exception-правило с конкретным legacy syntax из
`tapi-yandex-direct`, который действительно нужно поддержать.

`direct-cli` — это транспортный слой над API Яндекс Директа. Канонические
имена CLI-групп следуют нормализованным Python-именам из
`tapi-yandex-direct`, а имена подкоманд — это kebab-case проекции API-методов.

Базовые соответствия:

- API `dynamictextadtargets` -> Python `dynamicads` -> CLI `direct dynamicads`
- API `retargetinglists` -> Python `retargeting` -> CLI `direct retargeting`
- API `checkCampaigns` -> CLI `direct changes check-campaigns`
- API `checkDictionaries` -> CLI `direct changes check-dictionaries`
- API `hasSearchVolume` -> CLI `direct keywordsresearch has-search-volume`

### CLI Convention

The current CLI convention is defined as follows.

#### CLI Contract

The canonical command shape is:

```bash
direct <group> <command> [flags]
```

Naming rules:

- `group`:
  - lowercase ASCII only
  - no underscores
  - multiword groups are concatenated
  - examples: `dynamicads`, `smartadtargets`, `negativekeywordsharedsets`

- `command`:
  - lowercase only
  - multiword commands use kebab-case
  - examples: `get`, `set-bids`, `check-campaigns`, `has-search-volume`

`direct-cli` владеет публичным naming contract. `tapi-yandex-direct` может
влиять на внутренний transport layer, но не определяет канонические CLI-имена.

Текущая политика — canonical-only. Исторические aliases по умолчанию не
сохраняются в runtime CLI. Если совместимость когда-нибудь понадобится, alias
должен быть добавлен как явное explicit exception-правило с конкретным legacy
syntax, который действительно нужно поддержать.

#### Input Rules

- All user-facing input must be passed only through typed CLI flags.
- `--json` is not part of the public CLI contract.
- User-facing parameters must not be passed through `--json`.
- The CLI must not accept `SelectionCriteria`, nested payloads, update payloads, bidding rules, or any other user-facing command input through `--json`.
- Typed flags and JSON blobs must not be mixed as part of one public command contract.
- If the API requires a complex object, the CLI must expose explicit flags or subcommands instead of forwarding raw JSON.

#### Command Formatting Rules

- Every canonical CLI command must be written strictly on a single line.
- Multi-line command formatting is not allowed.
- Shell line continuation using `\` is forbidden in canonical documentation, help text, tests, and examples.

Allowed:

```bash
direct dictionaries get-geo-regions --region-ids 225,187 --fields GeoRegionId,GeoRegionName
```

Not allowed:

```bash
direct dictionaries get-geo-regions \
  --region-ids 225,187 \
  --fields GeoRegionId,GeoRegionName
```

#### Flag Design Rules

- List inputs use comma-separated CLI syntax where appropriate.
- Money and bid values stay human-readable in CLI input and are converted internally to the API wire format.
- Selector fields remain explicit flags, for example:
  - `--id`
  - `--campaign-id`
  - `--adgroup-id`
- Nested API structures must be projected into typed flags instead of blob JSON.
- Help text must not advertise JSON as an alternative input path.

#### Datetime Rules

- Datetime parameters must be passed in the format `YYYY-MM-DDTHH:MM:SS`.
- Datetime values must be passed as a single shell token.
- Canonical examples must not use timezone suffixes like `Z`.
- Canonical examples must not use quoted space-separated datetime values.

Use:

```bash
direct changes check-campaigns --timestamp 2026-04-14T00:00:00
```

Do not use:

```bash
direct changes check-campaigns --timestamp 2026-04-14T00:00:00Z
direct changes check-campaigns --timestamp "2026-04-14 00:00:00"
```

#### Documentation Contract

- `README` must use only canonical syntax.
- `README` must use only single-line command examples.
- Canonical examples must not contain `--json`.
- Help output and tests must enforce the same contract.

#### Examples

Valid canonical examples:

```bash
direct campaigns get --ids 1,2,3
direct changes check-campaigns --timestamp 2026-04-14T00:00:00
direct keywordsresearch has-search-volume --keywords "buy laptop,buy desktop"
direct dynamicads set-bids --id 789 --bid 12.5
direct dictionaries get-geo-regions --region-ids 225 --fields GeoRegionId,GeoRegionName
```

Invalid examples:

```bash
direct dictionaries get-geo-regions --json '{"GeoRegionIds":[225]}' --fields GeoRegionId,GeoRegionName
direct dynamicads set-bids --id 789 --bid 12.5 --json '{"StrategyPriority":"HIGH"}'
direct dictionaries get-geo-regions \
  --region-ids 225 \
  --fields GeoRegionId,GeoRegionName
direct changes check-campaigns --timestamp 2026-04-14T00:00:00Z
direct changes check-campaigns --timestamp "2026-04-14 00:00:00"
```

#### Кампании

```bash
# Получить кампании
direct campaigns get
direct campaigns get --status ACTIVE
direct campaigns get --ids 1,2,3 --format table
direct campaigns get --fetch-all --format csv --output campaigns.csv

# Создать (--dry-run покажет запрос без отправки)
direct campaigns add --name "Моя кампания" --start-date 2024-02-01 --type TEXT_CAMPAIGN --budget 1000 --setting ADD_METRICA_TAG=YES --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --dry-run
direct campaigns add --name "Динамическая кампания" --start-date 2024-02-01 --type DYNAMIC_TEXT_CAMPAIGN --setting ADD_METRICA_TAG=NO --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --dry-run
direct campaigns add --name "Смарт-кампания" --start-date 2024-02-01 --type SMART_CAMPAIGN --network-strategy AVERAGE_CPC_PER_FILTER --filter-average-cpc 1 --counter-id 123 --dry-run

# Обновление и управление статусом
direct campaigns update --id 12345 --name "Новое название" --status SUSPENDED --budget 100 --start-date 2024-02-10 --end-date 2024-03-01
direct campaigns suspend --id 12345
direct campaigns resume --id 12345
direct campaigns archive --id 12345
direct campaigns unarchive --id 12345
direct campaigns delete --id 12345
```

#### Группы объявлений

```bash
direct adgroups get --campaign-ids 1,2,3 --limit 50
direct adgroups add --name "Группа 1" --campaign-id 12345 --region-ids 1,225 --dry-run
direct adgroups add --name "Динамическая группа" --campaign-id 12345 --type DYNAMIC_TEXT_AD_GROUP --region-ids 1,225 --domain-url example.com --dry-run
direct adgroups add --name "Смарт-группа" --campaign-id 12345 --type SMART_AD_GROUP --region-ids 1,225 --feed-id 170 --ad-title-source FEED_NAME --ad-body-source FEED_NAME --dry-run
direct adgroups update --id 67890 --name "Новое название" --status SUSPENDED --region-ids 1,225
direct adgroups delete --id 67890
```

#### Объявления

```bash
direct ads get --campaign-ids 1,2,3
direct ads get --adgroup-ids 45678 --format table
direct ads add --adgroup-id 12345 --type TEXT_AD --title "Заголовок" --text "Текст объявления" --href "https://example.com" --dry-run
direct ads add --adgroup-id 12345 --type TEXT_IMAGE_AD --image-hash abcdefghijklmnopqrst --href "https://example.com" --title "Баннер" --text "Имиджевое объявление" --dry-run
direct ads update --id 99999 --status PAUSED --title "Новый заголовок" --text "Новый текст" --href "https://example.com" --image-hash abcdefghijklmnopqrst
direct ads delete --id 99999
```

#### Ключевые слова

```bash
direct keywords get --campaign-ids 1,2,3
direct keywords add --adgroup-id 12345 --keyword "купить ноутбук" --bid 10.50 --context-bid 5.25 --user-param-1 segment-a --user-param-2 segment-b --dry-run
direct keywords update --id 88888 --bid 15.00 --context-bid 6.00 --status SUSPENDED
direct keywords delete --id 88888
```

#### Отчёты

```bash
# Сформировать отчёт (сохраняется в файл)
direct reports get --type CAMPAIGN_PERFORMANCE_REPORT --from 2024-01-01 --to 2024-01-31 --name "Отчёт за январь" --fields "Date,CampaignId,Clicks,Cost" --format csv --output report.csv

# Список доступных типов отчётов
direct reports list-types
```

Доступные типы: `CAMPAIGN_PERFORMANCE_REPORT`, `ADGROUP_PERFORMANCE_REPORT`, `AD_PERFORMANCE_REPORT`, `CRITERIA_PERFORMANCE_REPORT`, `CUSTOM_REPORT`, `REACH_AND_FREQUENCY_CAMPAIGN_REPORT`, `SEARCH_QUERY_PERFORMANCE_REPORT`

#### Другие ресурсы

```bash
# Справочники и изменения
direct dictionaries get --names Currencies,GeoRegions
direct dictionaries get-geo-regions --name Москва --region-ids 225,187 --exact-names Москва,Санкт-Петербург --fields GeoRegionId,GeoRegionName
direct changes check --campaign-ids 1,2,3 --timestamp 2026-04-14T00:00:00
direct changes check-campaigns --timestamp 2026-04-14T00:00:00
direct changes check-dictionaries
direct clients get --fields ClientId,Login,Currency

# Исследование ключевых слов и ретаргетинг
direct keywordsresearch has-search-volume --keywords "купить ноутбук,купить компьютер"
direct retargeting add --name "Список A" --type AUDIENCE --rule "ALL:12345:30|67890:7" --dry-run
direct retargeting update --id 55 --name "Переименованный список" --rule "ANY:12345:30" --dry-run

# Ставки и модификаторы
direct bids set --keyword-id 123 --bid 15
direct bids set-auto --keyword-id 123 --max-bid 20 --position PREMIUMBLOCK --scope SEARCH --dry-run
direct keywordbids set --keyword-id 321 --search-bid 8 --network-bid 3
direct keywordbids set-auto --keyword-id 321 --target-traffic-volume 100 --increase-percent 10 --bid-ceiling 12.5 --dry-run
direct bidmodifiers add --campaign-id 123 --type DEMOGRAPHICS_ADJUSTMENT --value 150 --gender GENDER_MALE --age AGE_25_34 --dry-run
direct bidmodifiers set --id 99 --value 130 --dry-run

# Канонические многословные группы
direct negativekeywordsharedsets update --id 123 --keywords "foo,bar"
direct audiencetargets add --adgroup-id 100 --retargeting-list-id 200 --bid 12 --priority HIGH --dry-run
direct audiencetargets set-bids --id 101 --context-bid 7 --priority LOW --dry-run
direct dynamicads add --adgroup-id 33 --name "Webpage A" --condition "URL:CONTAINS_ANY:test|shop" --condition "PAGE_CONTENT:CONTAINS:baz" --bid 3 --context-bid 2 --priority HIGH --dry-run
direct smartadtargets add --adgroup-id 55 --name "Audience A" --audience ALL_SEGMENTS --condition "CATEGORY_ID:EQUALS:42" --average-cpc 3 --average-cpa 4 --priority HIGH --available-items-only YES --dry-run
direct smartadtargets update --id 456 --priority HIGH
direct smartadtargets set-bids --id 456 --average-cpc 10.5 --average-cpa 15 --priority HIGH
direct dynamicads set-bids --id 789 --bid 12.5 --context-bid 9 --priority HIGH

# Расширения, ассеты, фиды и клиенты
direct sitelinks add --sitelink "Docs|https://example.com/docs" --sitelink "Help|https://example.com/help|Desk" --dry-run
direct vcards add --campaign-id 555 --country "Россия" --city "Москва" --company-name "Acme" --work-time 1#5#9#0#18#0 --phone-country-code +7 --phone-city-code 495 --phone-number 1234567 --dry-run
direct adextensions add --callout-text "Free shipping" --dry-run
direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
direct creatives add --video-id video-id --dry-run
direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --dry-run
direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
direct clients update --client-id 999 --phone +70000000000 --fax +70000000001 --email user@example.com --city Moscow --dry-run
direct agencyclients add --login client-login --first-name Alice --last-name Smith --currency RUB --notification-email ops@example.com --notification-lang RU --send-account-news --no-send-warnings --dry-run
direct agencyclients add-passport-organization --name "Org" --currency RUB --notification-email ops@example.com --notification-lang EN --no-send-account-news --send-warnings --dry-run
direct agencyclients add-passport-organization-member --passport-organization-login org-login --role CHIEF --invite-email user@example.com --dry-run
direct agencyclients update --client-id 42 --phone +70000000000 --email user@example.com --grant EDIT_CAMPAIGNS --grant IMPORT_XLS --dry-run
```

### Известный Transport Gap

`dynamicads update` пока остаётся transport gap. Downstream-обёртки могут
ожидать эту операцию, но текущая help-поверхность установленного CLI не
экспортирует `dynamicads update`. До появления реальной команды считайте её
неподдерживаемой.

### Форматы вывода

Все команды `get` поддерживают `--format`:

| Формат | Описание |
|--------|----------|
| `json` | JSON (по умолчанию) |
| `table` | Таблица |
| `csv` | CSV |
| `tsv` | TSV |

```bash
direct campaigns get --format table
direct campaigns get --format csv --output campaigns.csv
```

### Пагинация

```bash
direct campaigns get --limit 10    # первые 10 результатов
direct campaigns get --fetch-all   # все страницы
```

### ⚠️ Опасные команды

Следующие команды вносят **необратимые изменения** — используйте осторожно:

| Команда | Эффект |
|---------|--------|
| `campaigns delete --id` | Безвозвратно удаляет кампанию и весь её контент |
| `adgroups delete --id` | Безвозвратно удаляет группу объявлений |
| `ads delete --id` | Безвозвратно удаляет объявление |
| `keywords delete --id` | Безвозвратно удаляет ключевое слово |
| `audiencetargets delete --id` | Безвозвратно удаляет условие подбора аудитории |

Команды, влияющие на показ рекламы: `suspend`, `resume`, `archive`, `unarchive` (доступны для `campaigns`, `ads`, `keywords`).

Команды, влияющие на ставки и расходы: `bids set`, `keywordbids set`, `bidmodifiers set`.

Используйте `--dry-run` в командах `add` / `update`, чтобы увидеть тело запроса до отправки:

```bash
direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
```

### Тестирование

В `tests/` три уровня тестов:

| Уровень | Маркер | Сеть | Нужен токен |
|---|---|---|---|
| Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
| Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
| Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |

```bash
pip install -e ".[dev]"
pytest                              # быстрый уровень — без токена
pytest -m integration -v            # read-only интеграция (нужен токен)
pytest -m integration_write -v      # replay write-кассет (токен не нужен)
```

#### Перезапись write-кассет

Write-тесты воспроизводят HTTP-трафик, записанный против **sandbox-окружения**
Яндекс Директа (`--sandbox` подставляется автоматически). Кассеты лежат в
`tests/cassettes/test_integration_write/` и закоммичены в git.

Если меняется payload какой-то из write-команд (например, добавили поле),
соответствующая кассета перестанет воспроизводиться, тест упадёт с
body-mismatch. Перезапись:

```bash
set -a && source .env && set +a        # загрузить YANDEX_DIRECT_TOKEN / LOGIN
pytest -m integration_write -v --record-mode=rewrite
```

**Один и тот же OAuth-токен работает и для продакшена, и для sandbox** —
отдельный sandbox-токен не нужен. После перезаписи **обязательно проверьте
YAML-ы на утечку секретов**:

```bash
grep -r "$YANDEX_DIRECT_TOKEN" tests/cassettes/   # должно быть пусто
grep -r "$YANDEX_DIRECT_LOGIN" tests/cassettes/   # должно быть пусто
```

VCR-конфиг в `tests/conftest.py` уже стрипает `Authorization`, `Client-Login`,
куки и любые response-заголовки с подстрокой `login`, но ручная проверка
перед коммитом обязательна.

### Публикация на PyPI

Сборка, проверка и загрузка на PyPI:

```bash
pip install -e ".[dev]"
scripts/release_pypi.sh testpypi   # загрузить на TestPyPI
scripts/release_pypi.sh pypi       # загрузить на PyPI
scripts/release_pypi.sh all        # оба
```

Скрипт читает credentials из `.env`:

```dotenv
TWINE_USERNAME=__token__
TEST_PYPI_TOKEN=pypi-...
PYPI_TOKEN=pypi-...
```

#### Области действия токенов PyPI

API-токены PyPI могут быть **account-wide** (на весь аккаунт) или **project-scoped** (на конкретный проект):

- **Project-scoped** токены работают только для конкретного проекта. Токен от `telethon-cli` не может загрузить `direct-cli` — будет **403 Forbidden**.
- **Account-wide** токены позволяют загружать в любой проект аккаунта.
- Для **первой публикации** нового проекта **необходим** account-wide токен (project-scoped нельзя создать, пока проект не зарегистрирован на PyPI).
- После первой успешной загрузки создайте project-scoped токен на https://pypi.org/manage/account/token/ и замените account-wide токен в `.env`.

Перед каждым релизом обновите `version` в `pyproject.toml` — PyPI отклоняет дубли версий.

### Лицензия

MIT
