direct-cli 0.2.2__tar.gz → 0.2.5__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.
- {direct_cli-0.2.2 → direct_cli-0.2.5}/.env.example +6 -0
- direct_cli-0.2.5/.github/copilot-instructions.md +91 -0
- direct_cli-0.2.5/.github/workflows/claude-code-review.yml +44 -0
- direct_cli-0.2.5/.github/workflows/claude.yml +50 -0
- direct_cli-0.2.5/.gitignore +57 -0
- direct_cli-0.2.5/AGENTS.md +382 -0
- direct_cli-0.2.5/CLAUDE.md +59 -0
- {direct_cli-0.2.2/direct_cli.egg-info → direct_cli-0.2.5}/PKG-INFO +93 -1
- {direct_cli-0.2.2 → direct_cli-0.2.5}/README.md +90 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/adextensions.py +24 -4
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/adgroups.py +27 -5
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/ads.py +41 -12
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/audiencetargets.py +1 -1
- direct_cli-0.2.5/direct_cli/commands/bidmodifiers.py +385 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/bids.py +4 -4
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/campaigns.py +32 -7
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/feeds.py +53 -8
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/keywordbids.py +2 -2
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/keywords.py +2 -2
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/reports.py +13 -4
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/retargeting.py +20 -1
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/smartadtargets.py +37 -9
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/turbopages.py +5 -36
- {direct_cli-0.2.2 → direct_cli-0.2.5/direct_cli.egg-info}/PKG-INFO +93 -1
- direct_cli-0.2.5/direct_cli.egg-info/SOURCES.txt +80 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli.egg-info/requires.txt +2 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/pyproject.toml +5 -1
- direct_cli-0.2.5/scripts/release_pypi.sh +116 -0
- direct_cli-0.2.5/tests/__init__.py +1 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +111 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +274 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +67 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +275 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +384 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +219 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +166 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +275 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +335 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +59 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +166 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +275 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +275 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +164 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +111 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +57 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +168 -0
- direct_cli-0.2.5/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +219 -0
- direct_cli-0.2.5/tests/conftest.py +502 -0
- direct_cli-0.2.5/tests/test_dry_run.py +1273 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/tests/test_integration.py +14 -41
- direct_cli-0.2.5/tests/test_integration_write.py +866 -0
- direct_cli-0.2.2/direct_cli/commands/bidmodifiers.py +0 -175
- direct_cli-0.2.2/direct_cli.egg-info/SOURCES.txt +0 -52
- direct_cli-0.2.2/tests/test_dry_run.py +0 -661
- {direct_cli-0.2.2 → direct_cli-0.2.5}/MANIFEST.in +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/__init__.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/api.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/auth.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/cli.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/__init__.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/adimages.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/agencyclients.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/businesses.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/changes.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/clients.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/creatives.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/dictionaries.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/dynamicads.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/keywordsresearch.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/leads.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/sitelinks.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/commands/vcards.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/output.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli/utils.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli.egg-info/dependency_links.txt +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli.egg-info/entry_points.txt +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/direct_cli.egg-info/top_level.txt +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/setup.cfg +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/setup.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/tests/test_auth_bw.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/tests/test_auth_op.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/tests/test_cli.py +0 -0
- {direct_cli-0.2.2 → direct_cli-0.2.5}/tests/test_comprehensive.py +0 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
YANDEX_DIRECT_TOKEN=your_access_token_here
|
|
2
2
|
YANDEX_DIRECT_LOGIN=your_yandex_login_here
|
|
3
3
|
|
|
4
|
+
# The same OAuth token also works against the Yandex Direct sandbox
|
|
5
|
+
# (https://api-sandbox.direct.yandex.com) — no separate sandbox token is
|
|
6
|
+
# required. Use the ``--sandbox`` CLI flag to route calls there, or run
|
|
7
|
+
# ``pytest -m integration_write --record-mode=rewrite`` to re-record
|
|
8
|
+
# the sandbox VCR cassettes under ``tests/cassettes/``.
|
|
9
|
+
|
|
4
10
|
# 1Password secret references (optional, used as fallback)
|
|
5
11
|
# YANDEX_DIRECT_OP_TOKEN_REF=op://vault/item/token
|
|
6
12
|
# YANDEX_DIRECT_OP_LOGIN_REF=op://vault/item/login
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Copilot Instructions — Direct CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for the Yandex Direct API, built with Python and Click.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install in dev mode
|
|
9
|
+
pip install -e ".[dev]"
|
|
10
|
+
|
|
11
|
+
# Run all unit tests
|
|
12
|
+
pytest
|
|
13
|
+
|
|
14
|
+
# Run integration tests (requires .env with real token)
|
|
15
|
+
pytest -m integration -v
|
|
16
|
+
|
|
17
|
+
# Run a single test
|
|
18
|
+
pytest tests/test_cli.py::TestCLI::test_cli_help
|
|
19
|
+
|
|
20
|
+
# Run tests matching pattern
|
|
21
|
+
pytest -k "campaigns"
|
|
22
|
+
|
|
23
|
+
# Format
|
|
24
|
+
black .
|
|
25
|
+
|
|
26
|
+
# Lint
|
|
27
|
+
flake8 direct_cli tests
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
The CLI is a Click group-of-groups. Each API resource maps to one file in `direct_cli/commands/` that defines a Click group (e.g. `campaigns`) with subcommands (`get`, `add`, `update`, `delete`, etc.). All groups are imported and registered in `direct_cli/cli.py`.
|
|
33
|
+
|
|
34
|
+
Request flow: `cli.py` → `auth.py` (resolves token/login) → `api.py` (`create_client`) → `tapi_yandex_direct.YandexDirect` → Yandex Direct API → `output.py` (format and print).
|
|
35
|
+
|
|
36
|
+
`utils.py` holds shared helpers: `parse_ids`, `parse_json`, `build_selection_criteria`, `build_common_params`, `get_default_fields`. All command modules import from these; don't duplicate logic there.
|
|
37
|
+
|
|
38
|
+
## Key Conventions
|
|
39
|
+
|
|
40
|
+
**Adding a new command module:**
|
|
41
|
+
1. Create `direct_cli/commands/<resource>.py` with a `@click.group()` and subcommands.
|
|
42
|
+
2. Import and register it in `direct_cli/cli.py` with `cli.add_command(...)`.
|
|
43
|
+
3. Add the command name to `TestCommandsRegistered.EXPECTED_COMMANDS` in `tests/test_comprehensive.py`.
|
|
44
|
+
|
|
45
|
+
**`--dry-run` flag:** `add` and `update` commands support `--dry-run` — they print the request body as JSON without making an API call. Use this as a test seam: these subcommands can be tested without mocking the HTTP layer.
|
|
46
|
+
|
|
47
|
+
**SelectionCriteria requirements:** Some resources (`adgroups`, `ads`, `keywords`) require at least one of `Ids`, `CampaignIds`, or `AdGroupIds` in `SelectionCriteria`. Calling `get` without any filter raises API error 4001. Integration tests handle this by fetching a campaign ID first via `get_first_campaign_id()`.
|
|
48
|
+
|
|
49
|
+
**Default fields in `utils.py`:** `COMMON_FIELDS` maps resource names to default `FieldNames`. Not all fields are valid for all resources — for example, `adimages` uses `AdImageHash` (not `Id`) as its key, and neither `clients` nor `creatives` accept `Status`. When adding a resource, verify field names against the Yandex Direct API docs.
|
|
50
|
+
|
|
51
|
+
**Error handling in commands:** All command functions wrap API calls in `try/except Exception` and call `print_error(str(e))` + `raise click.Abort()`. Keep this pattern consistent.
|
|
52
|
+
|
|
53
|
+
**Test types:**
|
|
54
|
+
- Unit tests (`tests/test_cli.py`, `tests/test_comprehensive.py`) — no API calls, run without a token.
|
|
55
|
+
- Integration tests (`tests/test_integration.py`, `@pytest.mark.integration`) — require `.env` with `YANDEX_DIRECT_TOKEN` and `YANDEX_DIRECT_LOGIN`. Auto-skip if token is absent.
|
|
56
|
+
|
|
57
|
+
**Credentials:** `.env` in the project root. `YANDEX_DIRECT_LOGIN` is the Yandex advertiser login (required). `load_dotenv()` is called at `cli.py` module import, so it is loaded before any Click invocation.
|
|
58
|
+
|
|
59
|
+
## Dangerous Commands — Do Not Auto-Test
|
|
60
|
+
|
|
61
|
+
Never invoke these commands in automated tests against a real account.
|
|
62
|
+
|
|
63
|
+
### 🔴 Irreversible — permanently destroy data
|
|
64
|
+
| Command | Risk |
|
|
65
|
+
|---------|------|
|
|
66
|
+
| `campaigns delete` | Permanently deletes a campaign and all its content |
|
|
67
|
+
| `adgroups delete` | Permanently deletes an ad group and its ads/keywords |
|
|
68
|
+
| `ads delete` | Permanently deletes an ad |
|
|
69
|
+
| `keywords delete` | Permanently deletes a keyword |
|
|
70
|
+
| `audiencetargets delete` | Permanently deletes an audience target |
|
|
71
|
+
|
|
72
|
+
### 🟠 Financial impact — change bids or spending
|
|
73
|
+
| Command | Risk |
|
|
74
|
+
|---------|------|
|
|
75
|
+
| `bids set` | Changes search/network bids on campaigns — direct cost impact |
|
|
76
|
+
| `keywordbids set` | Changes per-keyword bids |
|
|
77
|
+
| `bidmodifiers set` | Changes bid multipliers (device, region, time, etc.) |
|
|
78
|
+
|
|
79
|
+
### 🟡 Reversible but affect live traffic
|
|
80
|
+
`campaigns suspend/resume/archive/unarchive`, `ads suspend/resume/archive/unarchive`, `keywords suspend/resume/archive/unarchive`, `audiencetargets suspend/resume`
|
|
81
|
+
|
|
82
|
+
### 🟡 Account-wide mutations
|
|
83
|
+
`clients update` — modifies account-level settings.
|
|
84
|
+
|
|
85
|
+
### 🟡 Content creation (hard to clean up in bulk)
|
|
86
|
+
All `add` and `update` subcommands across: `campaigns`, `adgroups`, `ads`, `keywords`, `feeds`, `retargeting`, `sitelinks`, `turbopages`, `vcards`, `adextensions`, `negativekeywordsharedsets`, `smartadtargets`, `dynamicads`, `audiencetargets`.
|
|
87
|
+
|
|
88
|
+
These can be safely tested using `--dry-run` (outputs the request body as JSON without sending it).
|
|
89
|
+
|
|
90
|
+
### ✅ Safe to auto-test (read-only, no side effects)
|
|
91
|
+
All `get` subcommands, plus: `changes check*`, `dictionaries list-names`, `keywordsresearch has-search-volume`, `reports list-types`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Claude Code Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, ready_for_review, reopened]
|
|
6
|
+
# Optional: Only run on specific file changes
|
|
7
|
+
# paths:
|
|
8
|
+
# - "src/**/*.ts"
|
|
9
|
+
# - "src/**/*.tsx"
|
|
10
|
+
# - "src/**/*.js"
|
|
11
|
+
# - "src/**/*.jsx"
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
claude-review:
|
|
15
|
+
# Optional: Filter by PR author
|
|
16
|
+
# if: |
|
|
17
|
+
# github.event.pull_request.user.login == 'external-contributor' ||
|
|
18
|
+
# github.event.pull_request.user.login == 'new-developer' ||
|
|
19
|
+
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
|
20
|
+
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
pull-requests: read
|
|
25
|
+
issues: read
|
|
26
|
+
id-token: write
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout repository
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 1
|
|
33
|
+
|
|
34
|
+
- name: Run Claude Code Review
|
|
35
|
+
id: claude-review
|
|
36
|
+
uses: anthropics/claude-code-action@v1
|
|
37
|
+
with:
|
|
38
|
+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
39
|
+
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
|
40
|
+
plugins: 'code-review@claude-code-plugins'
|
|
41
|
+
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
|
42
|
+
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
43
|
+
# or https://code.claude.com/docs/en/cli-reference for available options
|
|
44
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Claude Code
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
types: [created]
|
|
6
|
+
pull_request_review_comment:
|
|
7
|
+
types: [created]
|
|
8
|
+
issues:
|
|
9
|
+
types: [opened, assigned]
|
|
10
|
+
pull_request_review:
|
|
11
|
+
types: [submitted]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
claude:
|
|
15
|
+
if: |
|
|
16
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
17
|
+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
18
|
+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
19
|
+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
pull-requests: read
|
|
24
|
+
issues: read
|
|
25
|
+
id-token: write
|
|
26
|
+
actions: read # Required for Claude to read CI results on PRs
|
|
27
|
+
steps:
|
|
28
|
+
- name: Checkout repository
|
|
29
|
+
uses: actions/checkout@v4
|
|
30
|
+
with:
|
|
31
|
+
fetch-depth: 1
|
|
32
|
+
|
|
33
|
+
- name: Run Claude Code
|
|
34
|
+
id: claude
|
|
35
|
+
uses: anthropics/claude-code-action@v1
|
|
36
|
+
with:
|
|
37
|
+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
38
|
+
|
|
39
|
+
# This is an optional setting that allows Claude to read CI results on PRs
|
|
40
|
+
additional_permissions: |
|
|
41
|
+
actions: read
|
|
42
|
+
|
|
43
|
+
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
|
44
|
+
# prompt: 'Update the pull request description to include a summary of changes.'
|
|
45
|
+
|
|
46
|
+
# Optional: Add claude_args to customize behavior and configuration
|
|
47
|
+
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
48
|
+
# or https://code.claude.com/docs/en/cli-reference for available options
|
|
49
|
+
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
|
50
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLLs
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.txt
|
|
23
|
+
pip-log.txt
|
|
24
|
+
Pipfile
|
|
25
|
+
MANIFEST
|
|
26
|
+
|
|
27
|
+
# Unit test / coverage reports
|
|
28
|
+
htmlcov/
|
|
29
|
+
.tox/
|
|
30
|
+
.nox
|
|
31
|
+
.coverate
|
|
32
|
+
.cache
|
|
33
|
+
nosetests.xml
|
|
34
|
+
coverage.xml
|
|
35
|
+
*.cover
|
|
36
|
+
|
|
37
|
+
# Environments
|
|
38
|
+
.env
|
|
39
|
+
.venv
|
|
40
|
+
|
|
41
|
+
# IDE
|
|
42
|
+
.vscode/
|
|
43
|
+
.idea/
|
|
44
|
+
*.swp
|
|
45
|
+
*.swo
|
|
46
|
+
*~
|
|
47
|
+
|
|
48
|
+
# OS
|
|
49
|
+
.DS_Store
|
|
50
|
+
Thumbs.db
|
|
51
|
+
|
|
52
|
+
# Temporary files
|
|
53
|
+
*.tmp
|
|
54
|
+
*.temp
|
|
55
|
+
*.log
|
|
56
|
+
*.bak
|
|
57
|
+
*.orig
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# AGENTS.md - Guidelines for AI Coding Agents
|
|
2
|
+
|
|
3
|
+
This document provides essential information for AI coding agents working on the Direct CLI codebase.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
Direct CLI is a command-line interface for the Yandex Direct API, built with Python and Click. It provides commands for managing campaigns, ad groups, ads, keywords, reports, and other Yandex Direct resources.
|
|
8
|
+
|
|
9
|
+
## Build, Test, and Lint Commands
|
|
10
|
+
|
|
11
|
+
### Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Install package in development mode
|
|
15
|
+
pip install -e .
|
|
16
|
+
|
|
17
|
+
# Install with development dependencies
|
|
18
|
+
pip install -e ".[dev]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Testing
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Run all tests
|
|
25
|
+
pytest
|
|
26
|
+
|
|
27
|
+
# Run all tests with verbose output
|
|
28
|
+
pytest -v
|
|
29
|
+
|
|
30
|
+
# Run a single test file
|
|
31
|
+
pytest tests/test_cli.py
|
|
32
|
+
|
|
33
|
+
# Run a single test by name
|
|
34
|
+
pytest tests/test_cli.py::TestCLI::test_cli_help
|
|
35
|
+
|
|
36
|
+
# Run tests matching a pattern
|
|
37
|
+
pytest -k "campaigns"
|
|
38
|
+
|
|
39
|
+
# Run tests with coverage
|
|
40
|
+
pytest --cov=direct_cli
|
|
41
|
+
|
|
42
|
+
# Run tests with coverage report
|
|
43
|
+
pytest --cov=direct_cli --cov-report=html
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Code Quality
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Format code with Black
|
|
50
|
+
black .
|
|
51
|
+
|
|
52
|
+
# Format specific files
|
|
53
|
+
black direct_cli/ tests/
|
|
54
|
+
|
|
55
|
+
# Check formatting without changes
|
|
56
|
+
black --check .
|
|
57
|
+
|
|
58
|
+
# Lint with flake8
|
|
59
|
+
flake8 direct_cli tests
|
|
60
|
+
|
|
61
|
+
# Lint specific file
|
|
62
|
+
flake8 direct_cli/cli.py
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Building
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Build package
|
|
69
|
+
python -m build
|
|
70
|
+
|
|
71
|
+
# Build wheel only
|
|
72
|
+
pip wheel . --no-deps -w dist/
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Code Style Guidelines
|
|
76
|
+
|
|
77
|
+
### Import Organization
|
|
78
|
+
|
|
79
|
+
Organize imports in three groups, separated by blank lines:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# 1. Standard library imports (alphabetical)
|
|
83
|
+
import json
|
|
84
|
+
import os
|
|
85
|
+
from datetime import datetime
|
|
86
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
87
|
+
|
|
88
|
+
# 2. Third-party imports (alphabetical)
|
|
89
|
+
import click
|
|
90
|
+
from dotenv import load_dotenv
|
|
91
|
+
|
|
92
|
+
# 3. Local imports (alphabetical)
|
|
93
|
+
from .api import create_client
|
|
94
|
+
from .auth import get_credentials
|
|
95
|
+
from .output import format_output, print_error
|
|
96
|
+
from .utils import parse_ids, build_selection_criteria
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Type Hints
|
|
100
|
+
|
|
101
|
+
Always use type hints for function parameters and return values:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
def parse_ids(ids_str: Optional[str]) -> Optional[List[int]]:
|
|
105
|
+
"""Parse comma-separated IDs"""
|
|
106
|
+
if not ids_str:
|
|
107
|
+
return None
|
|
108
|
+
return [int(x.strip()) for x in ids_str.split(",")]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def create_client(
|
|
112
|
+
token: Optional[str] = None,
|
|
113
|
+
login: Optional[str] = None,
|
|
114
|
+
sandbox: bool = False,
|
|
115
|
+
) -> YandexDirect:
|
|
116
|
+
"""Create YandexDirect client"""
|
|
117
|
+
...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Docstrings
|
|
121
|
+
|
|
122
|
+
Use descriptive docstrings with Args, Returns, and Raises sections:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
def get_credentials(
|
|
126
|
+
token: Optional[str] = None,
|
|
127
|
+
login: Optional[str] = None,
|
|
128
|
+
env_path: Optional[str] = None,
|
|
129
|
+
) -> Tuple[str, Optional[str]]:
|
|
130
|
+
"""
|
|
131
|
+
Get credentials with priority:
|
|
132
|
+
1. Direct arguments
|
|
133
|
+
2. Environment variables
|
|
134
|
+
3. .env file
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
token: API access token
|
|
138
|
+
login: Client login (for agency accounts)
|
|
139
|
+
env_path: Path to .env file
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (token, login)
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If token is not provided
|
|
146
|
+
"""
|
|
147
|
+
...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Naming Conventions
|
|
151
|
+
|
|
152
|
+
- **Functions and variables**: `snake_case`
|
|
153
|
+
- **Classes**: `PascalCase`
|
|
154
|
+
- **Constants**: `UPPER_SNAKE_CASE`
|
|
155
|
+
- **CLI command names**: `kebab-case` (e.g., `get-campaigns`, `add-adgroup`)
|
|
156
|
+
|
|
157
|
+
### Error Handling
|
|
158
|
+
|
|
159
|
+
Use try/except blocks with `print_error()` and `raise click.Abort()`:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
try:
|
|
163
|
+
client = create_client(
|
|
164
|
+
token=ctx.obj.get("token"),
|
|
165
|
+
login=ctx.obj.get("login"),
|
|
166
|
+
sandbox=ctx.obj.get("sandbox"),
|
|
167
|
+
)
|
|
168
|
+
result = client.campaigns().post(data=body)
|
|
169
|
+
format_output(result().extract(), output_format, output)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print_error(str(e))
|
|
172
|
+
raise click.Abort()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Click Command Structure
|
|
176
|
+
|
|
177
|
+
Use Click decorators consistently:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
@click.group()
|
|
181
|
+
def campaigns():
|
|
182
|
+
"""Manage campaigns"""
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@campaigns.command()
|
|
187
|
+
@click.option("--ids", help="Comma-separated campaign IDs")
|
|
188
|
+
@click.option("--status", help="Filter by status")
|
|
189
|
+
@click.option("--limit", type=int, help="Limit number of results")
|
|
190
|
+
@click.pass_context
|
|
191
|
+
def get(ctx, ids, status, limit):
|
|
192
|
+
"""Get campaigns"""
|
|
193
|
+
...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Project Structure
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
direct-cli/
|
|
200
|
+
├── direct_cli/ # Main package
|
|
201
|
+
│ ├── __init__.py # Package initialization
|
|
202
|
+
│ ├── cli.py # Main CLI entry point
|
|
203
|
+
│ ├── api.py # YandexDirect API client wrapper
|
|
204
|
+
│ ├── auth.py # Authentication module
|
|
205
|
+
│ ├── utils.py # Utility functions
|
|
206
|
+
│ ├── output.py # Output formatting (json, table, csv, tsv)
|
|
207
|
+
│ └── commands/ # Command modules by resource
|
|
208
|
+
│ ├── __init__.py
|
|
209
|
+
│ ├── campaigns.py
|
|
210
|
+
│ ├── adgroups.py
|
|
211
|
+
│ ├── ads.py
|
|
212
|
+
│ ├── keywords.py
|
|
213
|
+
│ ├── reports.py
|
|
214
|
+
│ └── ... # Other resource commands
|
|
215
|
+
├── tests/ # Test directory
|
|
216
|
+
│ ├── __init__.py
|
|
217
|
+
│ └── test_cli.py
|
|
218
|
+
├── pyproject.toml # Project configuration
|
|
219
|
+
└── README.md # User documentation
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Key Patterns
|
|
223
|
+
|
|
224
|
+
### Creating a New Command Module
|
|
225
|
+
|
|
226
|
+
1. Create a new file in `direct_cli/commands/`
|
|
227
|
+
2. Define a Click group with commands
|
|
228
|
+
3. Register in `direct_cli/cli.py`
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
# direct_cli/commands/newresource.py
|
|
232
|
+
import click
|
|
233
|
+
from ..api import create_client
|
|
234
|
+
from ..output import format_output, print_error
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@click.group()
|
|
238
|
+
def newresource():
|
|
239
|
+
"""Manage new resource"""
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@newresource.command()
|
|
244
|
+
@click.pass_context
|
|
245
|
+
def get(ctx):
|
|
246
|
+
"""Get new resource"""
|
|
247
|
+
try:
|
|
248
|
+
client = create_client(
|
|
249
|
+
token=ctx.obj.get("token"),
|
|
250
|
+
login=ctx.obj.get("login"),
|
|
251
|
+
sandbox=ctx.obj.get("sandbox"),
|
|
252
|
+
)
|
|
253
|
+
# ... API call
|
|
254
|
+
format_output(data, "json", None)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print_error(str(e))
|
|
257
|
+
raise click.Abort()
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Then register in `cli.py`:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from .commands.newresource import newresource
|
|
264
|
+
cli.add_command(newresource)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Output Formatting
|
|
268
|
+
|
|
269
|
+
Use `format_output()` for consistent output:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from ..output import format_output
|
|
273
|
+
|
|
274
|
+
# JSON output (default)
|
|
275
|
+
format_output(data, "json", None)
|
|
276
|
+
|
|
277
|
+
# Table output
|
|
278
|
+
format_output(data, "table", None)
|
|
279
|
+
|
|
280
|
+
# CSV/TSV to file
|
|
281
|
+
format_output(data, "csv", "output.csv")
|
|
282
|
+
format_output(data, "tsv", "output.tsv")
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Pagination
|
|
286
|
+
|
|
287
|
+
For large result sets, use `--fetch-all` flag:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
if fetch_all:
|
|
291
|
+
items = []
|
|
292
|
+
for item in result().iter_items():
|
|
293
|
+
items.append(item)
|
|
294
|
+
format_output(items, output_format, output)
|
|
295
|
+
else:
|
|
296
|
+
data = result().extract()
|
|
297
|
+
format_output(data, output_format, output)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Code Formatting Standards
|
|
301
|
+
|
|
302
|
+
- **Line length**: 88 characters (Black default)
|
|
303
|
+
- **String quotes**: Prefer double quotes (`"`)
|
|
304
|
+
- **Indentation**: 4 spaces (no tabs)
|
|
305
|
+
- **Blank lines**:
|
|
306
|
+
- 2 blank lines before class/function definitions at module level
|
|
307
|
+
- 1 blank line between methods
|
|
308
|
+
- 1 blank line between logical sections within functions
|
|
309
|
+
|
|
310
|
+
## Testing Guidelines
|
|
311
|
+
|
|
312
|
+
### Test Structure
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
import unittest
|
|
316
|
+
from click.testing import CliRunner
|
|
317
|
+
from direct_cli.cli import cli
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class TestCLI(unittest.TestCase):
|
|
321
|
+
"""Test CLI commands"""
|
|
322
|
+
|
|
323
|
+
def setUp(self):
|
|
324
|
+
self.runner = CliRunner()
|
|
325
|
+
|
|
326
|
+
def test_command_help(self):
|
|
327
|
+
"""Test command help"""
|
|
328
|
+
result = self.runner.invoke(cli, ["command", "--help"])
|
|
329
|
+
self.assertEqual(result.exit_code, 0)
|
|
330
|
+
self.assertIn("expected text", result.output)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Test Naming
|
|
334
|
+
|
|
335
|
+
- Test files: `test_*.py`
|
|
336
|
+
- Test classes: `Test*`
|
|
337
|
+
- Test methods: `test_*`
|
|
338
|
+
|
|
339
|
+
## Dependencies
|
|
340
|
+
|
|
341
|
+
Main dependencies (see `pyproject.toml`):
|
|
342
|
+
- `tapi-yandex-direct` - Yandex Direct API wrapper
|
|
343
|
+
- `click` - CLI framework
|
|
344
|
+
- `python-dotenv` - Environment variable management
|
|
345
|
+
- `tabulate` - Table formatting
|
|
346
|
+
- `colorama` - Terminal colors
|
|
347
|
+
- `tqdm` - Progress bars
|
|
348
|
+
|
|
349
|
+
Development dependencies:
|
|
350
|
+
- `pytest` - Testing framework
|
|
351
|
+
- `pytest-cov` - Coverage plugin
|
|
352
|
+
- `black` - Code formatter
|
|
353
|
+
- `flake8` - Linter
|
|
354
|
+
|
|
355
|
+
## Common Tasks
|
|
356
|
+
|
|
357
|
+
### Adding a New CLI Option
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
@campaigns.command()
|
|
361
|
+
@click.option("--new-option", help="Description of new option")
|
|
362
|
+
@click.pass_context
|
|
363
|
+
def get(ctx, new_option):
|
|
364
|
+
"""Get campaigns"""
|
|
365
|
+
if new_option:
|
|
366
|
+
# Handle option
|
|
367
|
+
pass
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Adding a New Output Format
|
|
371
|
+
|
|
372
|
+
1. Add format function in `output.py`
|
|
373
|
+
2. Update `format_output()` to handle new format type
|
|
374
|
+
3. Update `--format` option help text in commands
|
|
375
|
+
|
|
376
|
+
## Important Notes
|
|
377
|
+
|
|
378
|
+
- Always handle missing credentials gracefully with helpful error messages
|
|
379
|
+
- Use `--dry-run` flag for operations that modify data
|
|
380
|
+
- Support pagination for list operations with `--fetch-all` and `--limit`
|
|
381
|
+
- Maintain backward compatibility when changing command signatures
|
|
382
|
+
- Test changes with both sandbox and production API when possible
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
CLI for the Yandex Direct API, built with Python and Click. Installed via pip, published to PyPI.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install -e ".[dev]" # Install in dev mode
|
|
13
|
+
pytest # Unit tests (no token needed)
|
|
14
|
+
pytest -m integration -v # Integration tests (needs .env with token)
|
|
15
|
+
pytest tests/test_cli.py::TestCLI::test_cli_help # Single test
|
|
16
|
+
pytest -k "campaigns" # Pattern match
|
|
17
|
+
black . # Format
|
|
18
|
+
flake8 direct_cli tests # Lint
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
Click group-of-groups. Each Yandex Direct API resource = one file in `direct_cli/commands/` with a Click group and subcommands (`get`, `add`, `update`, `delete`, `suspend`, `resume`, etc.). All groups registered in `direct_cli/cli.py` via `cli.add_command()`.
|
|
24
|
+
|
|
25
|
+
**Request flow:** `cli.py` → `auth.py` (resolves token/login) → `api.py` (`create_client`) → `tapi_yandex_direct.YandexDirect` → Yandex API → `output.py` (format/print).
|
|
26
|
+
|
|
27
|
+
**Credentials priority:** CLI flags (`--token`, `--login`) > env vars (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN`) > `.env` file. `load_dotenv()` runs at `cli.py` import time.
|
|
28
|
+
|
|
29
|
+
**Shared utilities** (`utils.py`): `parse_ids`, `parse_json`, `build_selection_criteria`, `build_common_params`, `get_default_fields`, `COMMON_FIELDS` dict. All command modules import from here — don't duplicate.
|
|
30
|
+
|
|
31
|
+
**Output** (`output.py`): `format_output()` supports json (default), table, csv, tsv. Colored helpers: `print_success`, `print_error`, `print_warning`, `print_info`.
|
|
32
|
+
|
|
33
|
+
## Key Conventions
|
|
34
|
+
|
|
35
|
+
**Adding a new command module:**
|
|
36
|
+
1. Create `direct_cli/commands/<resource>.py` with `@click.group()` + subcommands.
|
|
37
|
+
2. Register in `cli.py` with `cli.add_command(...)`.
|
|
38
|
+
3. Add command name to `TestCommandsRegistered.EXPECTED_COMMANDS` in `tests/test_comprehensive.py`.
|
|
39
|
+
|
|
40
|
+
**`--dry-run`:** `add`/`update` commands print request JSON without calling the API. Use as test seam.
|
|
41
|
+
|
|
42
|
+
**SelectionCriteria:** Resources like `adgroups`, `ads`, `keywords` require at least one of `Ids`, `CampaignIds`, or `AdGroupIds` — otherwise API error 4001.
|
|
43
|
+
|
|
44
|
+
**Error handling:** All commands wrap API calls in `try/except Exception` → `print_error(str(e))` + `raise click.Abort()`.
|
|
45
|
+
|
|
46
|
+
**Default fields:** `COMMON_FIELDS` in `utils.py` maps resource names to `FieldNames`. Not all fields are valid for all resources (e.g., `adimages` uses `AdImageHash`, not `Id`).
|
|
47
|
+
|
|
48
|
+
## Tests
|
|
49
|
+
|
|
50
|
+
- **Unit** (`test_cli.py`, `test_comprehensive.py`) — no API calls, no token needed.
|
|
51
|
+
- **Integration** (`test_integration.py`, `@pytest.mark.integration`) — require `.env` with `YANDEX_DIRECT_TOKEN` and `YANDEX_DIRECT_LOGIN`. Auto-skip if absent.
|
|
52
|
+
|
|
53
|
+
## Dangerous Commands — Never Auto-Test
|
|
54
|
+
|
|
55
|
+
- **Irreversible (delete):** `campaigns/adgroups/ads/keywords/audiencetargets delete`
|
|
56
|
+
- **Financial:** `bids set`, `keywordbids set`, `bidmodifiers set`
|
|
57
|
+
- **Live traffic:** `suspend/resume/archive/unarchive` on campaigns, ads, keywords
|
|
58
|
+
- **Mutations:** all `add`/`update` subcommands (test with `--dry-run`)
|
|
59
|
+
- **Safe (read-only):** all `get` subcommands, `changes check*`, `dictionaries list-names`, `keywordsresearch has-search-volume`, `reports list-types`
|