direct-cli 0.2.4__tar.gz → 0.2.6__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.6/.github/workflows/api-coverage.yml +153 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/PKG-INFO +32 -1
- {direct_cli-0.2.4 → direct_cli-0.2.6}/README.md +28 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/api.py +18 -5
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/cli.py +2 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/adextensions.py +19 -11
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/adgroups.py +38 -11
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/adimages.py +11 -6
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/ads.py +86 -27
- direct_cli-0.2.4/direct_cli/commands/agencyclients.py → direct_cli-0.2.6/direct_cli/commands/advideos.py +37 -33
- direct_cli-0.2.6/direct_cli/commands/agencyclients.py +217 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/audiencetargets.py +85 -27
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/bidmodifiers.py +154 -37
- direct_cli-0.2.6/direct_cli/commands/bids.py +183 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/campaigns.py +87 -37
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/creatives.py +29 -3
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/dictionaries.py +30 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/dynamicads.py +89 -23
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/feeds.py +47 -8
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/keywordbids.py +41 -7
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/keywords.py +57 -32
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/keywordsresearch.py +0 -30
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/negativekeywordsharedsets.py +8 -3
- direct_cli-0.2.6/direct_cli/commands/reports.py +387 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/retargeting.py +60 -10
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/sitelinks.py +8 -3
- direct_cli-0.2.6/direct_cli/commands/smartadtargets.py +305 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/vcards.py +8 -3
- direct_cli-0.2.6/direct_cli/reports_coverage.py +242 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/utils.py +1 -0
- direct_cli-0.2.6/direct_cli/wsdl_coverage.py +347 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/PKG-INFO +32 -1
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/SOURCES.txt +45 -2
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/requires.txt +3 -0
- direct_cli-0.2.6/docs/superpowers/plans/2026-04-12-issue-32-completion.md +548 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/pyproject.toml +6 -1
- direct_cli-0.2.6/scripts/build_api_coverage_report.py +109 -0
- direct_cli-0.2.6/scripts/check_reports_drift.py +136 -0
- direct_cli-0.2.6/scripts/check_wsdl_drift.py +62 -0
- direct_cli-0.2.6/scripts/refresh_reports_cache.py +44 -0
- direct_cli-0.2.6/scripts/refresh_wsdl_cache.py +39 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +8 -8
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +27 -27
- direct_cli-0.2.6/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +67 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +28 -28
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +141 -32
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +22 -22
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +17 -17
- direct_cli-0.2.6/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +275 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +33 -92
- direct_cli-0.2.6/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +59 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +16 -16
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +27 -27
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +27 -27
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +17 -17
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +10 -10
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +5 -5
- direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml → direct_cli-0.2.6/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +31 -28
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +23 -23
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/conftest.py +110 -9
- direct_cli-0.2.6/tests/reports_cache/raw/fields-list.html +3316 -0
- direct_cli-0.2.6/tests/reports_cache/raw/headers.html +88 -0
- direct_cli-0.2.6/tests/reports_cache/raw/spec.html +119 -0
- direct_cli-0.2.6/tests/reports_cache/raw/type.html +279 -0
- direct_cli-0.2.6/tests/reports_cache/spec.json +583 -0
- direct_cli-0.2.6/tests/test_api_coverage.py +1176 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_comprehensive.py +1 -0
- direct_cli-0.2.6/tests/test_dry_run.py +1587 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_integration.py +61 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_integration_write.py +191 -77
- direct_cli-0.2.6/tests/test_reports_drift.py +102 -0
- direct_cli-0.2.6/tests/wsdl_cache/adextensions.xml +189 -0
- direct_cli-0.2.6/tests/wsdl_cache/adgroups.xml +551 -0
- direct_cli-0.2.6/tests/wsdl_cache/adimages.xml +270 -0
- direct_cli-0.2.6/tests/wsdl_cache/ads.xml +1622 -0
- direct_cli-0.2.6/tests/wsdl_cache/advideos.xml +153 -0
- direct_cli-0.2.6/tests/wsdl_cache/agencyclients.xml +310 -0
- direct_cli-0.2.6/tests/wsdl_cache/audiencetargets.xml +339 -0
- direct_cli-0.2.6/tests/wsdl_cache/bidmodifiers.xml +536 -0
- direct_cli-0.2.6/tests/wsdl_cache/bids.xml +262 -0
- direct_cli-0.2.6/tests/wsdl_cache/businesses.xml +106 -0
- direct_cli-0.2.6/tests/wsdl_cache/campaigns.xml +2809 -0
- direct_cli-0.2.6/tests/wsdl_cache/changes.xml +190 -0
- direct_cli-0.2.6/tests/wsdl_cache/clients.xml +132 -0
- direct_cli-0.2.6/tests/wsdl_cache/creatives.xml +239 -0
- direct_cli-0.2.6/tests/wsdl_cache/dictionaries.xml +327 -0
- direct_cli-0.2.6/tests/wsdl_cache/dynamictextadtargets.xml +338 -0
- direct_cli-0.2.6/tests/wsdl_cache/feeds.xml +339 -0
- direct_cli-0.2.6/tests/wsdl_cache/keywordbids.xml +291 -0
- direct_cli-0.2.6/tests/wsdl_cache/keywords.xml +362 -0
- direct_cli-0.2.6/tests/wsdl_cache/keywordsresearch.xml +174 -0
- direct_cli-0.2.6/tests/wsdl_cache/leads.xml +107 -0
- direct_cli-0.2.6/tests/wsdl_cache/negativekeywordsharedsets.xml +232 -0
- direct_cli-0.2.6/tests/wsdl_cache/retargetinglists.xml +283 -0
- direct_cli-0.2.6/tests/wsdl_cache/sitelinks.xml +189 -0
- direct_cli-0.2.6/tests/wsdl_cache/smartadtargets.xml +395 -0
- direct_cli-0.2.6/tests/wsdl_cache/turbopages.xml +103 -0
- direct_cli-0.2.6/tests/wsdl_cache/vcards.xml +238 -0
- direct_cli-0.2.4/direct_cli/commands/bids.py +0 -110
- direct_cli-0.2.4/direct_cli/commands/reports.py +0 -124
- direct_cli-0.2.4/direct_cli/commands/smartadtargets.py +0 -180
- direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -67
- direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteBidModifiers.test_toggle_existing.yaml +0 -110
- direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -275
- direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -275
- direct_cli-0.2.4/tests/test_dry_run.py +0 -663
- {direct_cli-0.2.4 → direct_cli-0.2.6}/.env.example +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/.github/copilot-instructions.md +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/.github/workflows/claude-code-review.yml +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/.github/workflows/claude.yml +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/.gitignore +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/AGENTS.md +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/CLAUDE.md +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/MANIFEST.in +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/__init__.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/auth.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/__init__.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/businesses.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/changes.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/clients.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/leads.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/turbopages.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/output.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/dependency_links.txt +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/entry_points.txt +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/top_level.txt +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/scripts/release_pypi.sh +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/setup.cfg +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/setup.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/__init__.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_auth_bw.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_auth_op.py +0 -0
- {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_cli.py +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
name: API Coverage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 6 * * 1"
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test-and-report:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.9", "3.11", "3.13"]
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install package with dev dependencies
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install -e ".[dev]"
|
|
31
|
+
|
|
32
|
+
- name: Run fast coverage suites
|
|
33
|
+
run: |
|
|
34
|
+
pytest -q tests/test_api_coverage.py tests/test_dry_run.py tests/test_cli.py tests/test_comprehensive.py tests/test_reports_drift.py
|
|
35
|
+
|
|
36
|
+
- name: Build API coverage report
|
|
37
|
+
run: |
|
|
38
|
+
python scripts/build_api_coverage_report.py > api_coverage_report.json
|
|
39
|
+
|
|
40
|
+
- name: Publish API coverage summary
|
|
41
|
+
run: |
|
|
42
|
+
python - <<'PY' >> "$GITHUB_STEP_SUMMARY"
|
|
43
|
+
import json
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
report = json.loads(Path("api_coverage_report.json").read_text())
|
|
47
|
+
summary = report["summary"]
|
|
48
|
+
print("## API coverage summary")
|
|
49
|
+
print()
|
|
50
|
+
print(f"- Services checked: {summary['services_checked']}")
|
|
51
|
+
print(f"- Missing methods: {summary['missing_service_methods']}")
|
|
52
|
+
print(f"- Unexpected methods: {summary['unexpected_service_methods']}")
|
|
53
|
+
print(f"- Strict parity OK: {summary['strict_parity_ok']}")
|
|
54
|
+
print(f"- Alias groups: {len(report['aliases'])}")
|
|
55
|
+
print(f"- Non-WSDL services: {len(report['non_wsdl_services'])}")
|
|
56
|
+
print(f"- CLI helpers: {len(report['cli_helpers'])}")
|
|
57
|
+
PY
|
|
58
|
+
|
|
59
|
+
- name: Upload API coverage report
|
|
60
|
+
uses: actions/upload-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: api-coverage-report-py${{ matrix.python-version }}
|
|
63
|
+
path: api_coverage_report.json
|
|
64
|
+
|
|
65
|
+
monitor-live-wsdl:
|
|
66
|
+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- name: Checkout repository
|
|
70
|
+
uses: actions/checkout@v4
|
|
71
|
+
|
|
72
|
+
- name: Set up Python
|
|
73
|
+
uses: actions/setup-python@v5
|
|
74
|
+
with:
|
|
75
|
+
python-version: "3.11"
|
|
76
|
+
|
|
77
|
+
- name: Install package with dev dependencies
|
|
78
|
+
run: |
|
|
79
|
+
python -m pip install --upgrade pip
|
|
80
|
+
pip install -e ".[dev]"
|
|
81
|
+
|
|
82
|
+
- name: Check live WSDL drift
|
|
83
|
+
run: |
|
|
84
|
+
python scripts/check_wsdl_drift.py > wsdl_drift_report.json
|
|
85
|
+
|
|
86
|
+
- name: Publish WSDL drift summary
|
|
87
|
+
if: always()
|
|
88
|
+
run: |
|
|
89
|
+
python - <<'PY' >> "$GITHUB_STEP_SUMMARY"
|
|
90
|
+
import json
|
|
91
|
+
from pathlib import Path
|
|
92
|
+
|
|
93
|
+
report = json.loads(Path("wsdl_drift_report.json").read_text())
|
|
94
|
+
print("## WSDL drift summary")
|
|
95
|
+
print()
|
|
96
|
+
print(f"- Services checked: {report['services_checked']}")
|
|
97
|
+
print(f"- Missing cache files: {report['missing_cache_count']}")
|
|
98
|
+
print(f"- Drifted services: {report['drift_count']}")
|
|
99
|
+
for item in report["drift"][:10]:
|
|
100
|
+
print(f"- Drift: {item['service']}")
|
|
101
|
+
PY
|
|
102
|
+
|
|
103
|
+
- name: Upload WSDL drift report
|
|
104
|
+
if: always()
|
|
105
|
+
uses: actions/upload-artifact@v4
|
|
106
|
+
with:
|
|
107
|
+
name: wsdl-drift-report
|
|
108
|
+
path: wsdl_drift_report.json
|
|
109
|
+
|
|
110
|
+
monitor-live-reports:
|
|
111
|
+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
112
|
+
runs-on: ubuntu-latest
|
|
113
|
+
steps:
|
|
114
|
+
- name: Checkout repository
|
|
115
|
+
uses: actions/checkout@v4
|
|
116
|
+
|
|
117
|
+
- name: Set up Python
|
|
118
|
+
uses: actions/setup-python@v5
|
|
119
|
+
with:
|
|
120
|
+
python-version: "3.11"
|
|
121
|
+
|
|
122
|
+
- name: Install package with dev dependencies
|
|
123
|
+
run: |
|
|
124
|
+
python -m pip install --upgrade pip
|
|
125
|
+
pip install -e ".[dev]"
|
|
126
|
+
|
|
127
|
+
- name: Check live reports drift
|
|
128
|
+
run: |
|
|
129
|
+
python scripts/check_reports_drift.py > reports_drift_report.json
|
|
130
|
+
|
|
131
|
+
- name: Publish reports drift summary
|
|
132
|
+
if: always()
|
|
133
|
+
run: |
|
|
134
|
+
python - <<'PY' >> "$GITHUB_STEP_SUMMARY"
|
|
135
|
+
import json
|
|
136
|
+
from pathlib import Path
|
|
137
|
+
|
|
138
|
+
report = json.loads(Path("reports_drift_report.json").read_text())
|
|
139
|
+
print("## Reports drift summary")
|
|
140
|
+
print()
|
|
141
|
+
print(f"- Sources checked: {report.get('sources_checked', 0)}")
|
|
142
|
+
print(f"- Missing cache files: {report.get('missing_cache_count', 0)}")
|
|
143
|
+
print(f"- Drifted sections: {report.get('drift_count', 0)}")
|
|
144
|
+
for item in report.get("drift", [])[:10]:
|
|
145
|
+
print(f"- Drift: {item['section']}")
|
|
146
|
+
PY
|
|
147
|
+
|
|
148
|
+
- name: Upload reports drift report
|
|
149
|
+
if: always()
|
|
150
|
+
uses: actions/upload-artifact@v4
|
|
151
|
+
with:
|
|
152
|
+
name: reports-drift-report
|
|
153
|
+
path: reports_drift_report.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: direct-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Command-line interface for Yandex Direct API
|
|
5
5
|
Author: axisrow
|
|
6
6
|
License: MIT
|
|
@@ -31,6 +31,9 @@ Requires-Dist: pytest-recording>=0.13; extra == "dev"
|
|
|
31
31
|
Requires-Dist: vcrpy>=6.0; extra == "dev"
|
|
32
32
|
Requires-Dist: black>=22.0; extra == "dev"
|
|
33
33
|
Requires-Dist: flake8>=4.0; extra == "dev"
|
|
34
|
+
Requires-Dist: requests>=2.0; extra == "dev"
|
|
35
|
+
Requires-Dist: beautifulsoup4>=4.12; extra == "dev"
|
|
36
|
+
Requires-Dist: lxml>=4.9; extra == "dev"
|
|
34
37
|
|
|
35
38
|
# Direct CLI
|
|
36
39
|
|
|
@@ -231,6 +234,34 @@ pytest -m integration -v # read-only integration tests (needs token)
|
|
|
231
234
|
pytest -m integration_write -v # write cassette replay (no token needed)
|
|
232
235
|
```
|
|
233
236
|
|
|
237
|
+
### API Coverage And Drift Monitoring
|
|
238
|
+
|
|
239
|
+
The project now distinguishes four surfaces:
|
|
240
|
+
|
|
241
|
+
| Surface | Coverage strategy |
|
|
242
|
+
|---|---|
|
|
243
|
+
| Canonical WSDL-backed SOAP services | `tests/test_api_coverage.py` verifies strict service/method parity and dry-run request-schema coverage or explicit exclusions |
|
|
244
|
+
| Non-WSDL services (`reports`) | Explicit contract tests |
|
|
245
|
+
| Canonical CLI aliases | Checked as aliases, not counted as separate API surface |
|
|
246
|
+
| Intentional CLI-only helpers | Explicitly allowlisted with reasons in `direct_cli/wsdl_coverage.py` |
|
|
247
|
+
|
|
248
|
+
`100% coverage` in this project means full coverage of the supported
|
|
249
|
+
**canonical API surface**. Alias groups and CLI-only helpers remain supported,
|
|
250
|
+
but they are tracked outside the strict parity metric.
|
|
251
|
+
|
|
252
|
+
Useful maintenance commands:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
python scripts/build_api_coverage_report.py
|
|
256
|
+
python scripts/refresh_wsdl_cache.py
|
|
257
|
+
python scripts/check_wsdl_drift.py
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
CI runs a scheduled API coverage workflow that:
|
|
261
|
+
- runs the fast coverage suites;
|
|
262
|
+
- uploads a machine-readable API coverage report artifact;
|
|
263
|
+
- checks the cached WSDL files against the live Yandex Direct API on schedule.
|
|
264
|
+
|
|
234
265
|
#### Re-recording write cassettes
|
|
235
266
|
|
|
236
267
|
The write tests replay HTTP traffic captured from the Yandex Direct **sandbox**
|
|
@@ -197,6 +197,34 @@ pytest -m integration -v # read-only integration tests (needs token)
|
|
|
197
197
|
pytest -m integration_write -v # write cassette replay (no token needed)
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
### API Coverage And Drift Monitoring
|
|
201
|
+
|
|
202
|
+
The project now distinguishes four surfaces:
|
|
203
|
+
|
|
204
|
+
| Surface | Coverage strategy |
|
|
205
|
+
|---|---|
|
|
206
|
+
| Canonical WSDL-backed SOAP services | `tests/test_api_coverage.py` verifies strict service/method parity and dry-run request-schema coverage or explicit exclusions |
|
|
207
|
+
| Non-WSDL services (`reports`) | Explicit contract tests |
|
|
208
|
+
| Canonical CLI aliases | Checked as aliases, not counted as separate API surface |
|
|
209
|
+
| Intentional CLI-only helpers | Explicitly allowlisted with reasons in `direct_cli/wsdl_coverage.py` |
|
|
210
|
+
|
|
211
|
+
`100% coverage` in this project means full coverage of the supported
|
|
212
|
+
**canonical API surface**. Alias groups and CLI-only helpers remain supported,
|
|
213
|
+
but they are tracked outside the strict parity metric.
|
|
214
|
+
|
|
215
|
+
Useful maintenance commands:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
python scripts/build_api_coverage_report.py
|
|
219
|
+
python scripts/refresh_wsdl_cache.py
|
|
220
|
+
python scripts/check_wsdl_drift.py
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
CI runs a scheduled API coverage workflow that:
|
|
224
|
+
- runs the fast coverage suites;
|
|
225
|
+
- uploads a machine-readable API coverage report artifact;
|
|
226
|
+
- checks the cached WSDL files against the live Yandex Direct API on schedule.
|
|
227
|
+
|
|
200
228
|
#### Re-recording write cassettes
|
|
201
229
|
|
|
202
230
|
The write tests replay HTTP traffic captured from the Yandex Direct **sandbox**
|
|
@@ -14,6 +14,12 @@ def create_client(
|
|
|
14
14
|
sandbox: bool = False,
|
|
15
15
|
op_token_ref: Optional[str] = None,
|
|
16
16
|
op_login_ref: Optional[str] = None,
|
|
17
|
+
processing_mode: str = "auto",
|
|
18
|
+
return_money_in_micros: bool = False,
|
|
19
|
+
skip_report_header: bool = True,
|
|
20
|
+
skip_column_header: bool = False,
|
|
21
|
+
skip_report_summary: bool = True,
|
|
22
|
+
language: Optional[str] = None,
|
|
17
23
|
) -> YandexDirect:
|
|
18
24
|
"""
|
|
19
25
|
Create YandexDirect client
|
|
@@ -24,6 +30,12 @@ def create_client(
|
|
|
24
30
|
sandbox: Use sandbox API
|
|
25
31
|
op_token_ref: 1Password secret reference for token
|
|
26
32
|
op_login_ref: 1Password secret reference for login
|
|
33
|
+
processing_mode: Report processing mode (auto/online/offline)
|
|
34
|
+
return_money_in_micros: Return monetary values in micros
|
|
35
|
+
skip_report_header: Omit report header row
|
|
36
|
+
skip_column_header: Omit column header row
|
|
37
|
+
skip_report_summary: Omit report summary row
|
|
38
|
+
language: Accept-Language for report (ru/en)
|
|
27
39
|
|
|
28
40
|
Returns:
|
|
29
41
|
YandexDirect client instance
|
|
@@ -39,12 +51,13 @@ def create_client(
|
|
|
39
51
|
retry_if_exceeded_limit=True,
|
|
40
52
|
retries_if_server_error=5,
|
|
41
53
|
# Report settings
|
|
42
|
-
processing_mode=
|
|
54
|
+
processing_mode=processing_mode,
|
|
43
55
|
wait_report=True,
|
|
44
|
-
return_money_in_micros=
|
|
45
|
-
skip_report_header=
|
|
46
|
-
skip_column_header=
|
|
47
|
-
skip_report_summary=
|
|
56
|
+
return_money_in_micros=return_money_in_micros,
|
|
57
|
+
skip_report_header=skip_report_header,
|
|
58
|
+
skip_column_header=skip_column_header,
|
|
59
|
+
skip_report_summary=skip_report_summary,
|
|
60
|
+
language=language,
|
|
48
61
|
)
|
|
49
62
|
|
|
50
63
|
|
|
@@ -35,6 +35,7 @@ from .commands.smartadtargets import smartadtargets
|
|
|
35
35
|
from .commands.businesses import businesses
|
|
36
36
|
from .commands.keywordsresearch import keywordsresearch
|
|
37
37
|
from .commands.dynamicads import dynamicads
|
|
38
|
+
from .commands.advideos import advideos
|
|
38
39
|
|
|
39
40
|
# Load .env file
|
|
40
41
|
load_dotenv()
|
|
@@ -133,6 +134,7 @@ cli.add_command(smartadtargets)
|
|
|
133
134
|
cli.add_command(businesses)
|
|
134
135
|
cli.add_command(keywordsresearch)
|
|
135
136
|
cli.add_command(dynamicads)
|
|
137
|
+
cli.add_command(advideos)
|
|
136
138
|
|
|
137
139
|
# Canonical aliases expected by external integrations.
|
|
138
140
|
cli.add_command(dynamicads, name="dynamictargets")
|
|
@@ -68,11 +68,14 @@ def get(ctx, ids, types, limit, fetch_all, output_format, output, fields):
|
|
|
68
68
|
@click.option(
|
|
69
69
|
"--type",
|
|
70
70
|
"ext_type",
|
|
71
|
-
required=True,
|
|
72
71
|
help=(
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
72
|
+
"Legacy UX hint; NOT forwarded to the API. The Yandex Direct "
|
|
73
|
+
"API derives the extension type from the nested field name "
|
|
74
|
+
"inside --json (Callout / Sitelinks / Vcard / ...), so the "
|
|
75
|
+
"only flag that actually matters is --json. Previously this "
|
|
76
|
+
"option was required=True but silently discarded, which "
|
|
77
|
+
"forced every caller to pass a value that did nothing. See "
|
|
78
|
+
"axisrow/direct-cli#25."
|
|
76
79
|
),
|
|
77
80
|
)
|
|
78
81
|
@click.option("--json", "extra_json", required=True, help="Extension data in JSON")
|
|
@@ -88,7 +91,7 @@ def add(ctx, ext_type, extra_json, dry_run):
|
|
|
88
91
|
``{"Type": ext_type, ...}`` and the sandbox rejected the extra
|
|
89
92
|
key as ``unknown parameter Type``.
|
|
90
93
|
"""
|
|
91
|
-
_ = ext_type #
|
|
94
|
+
_ = ext_type # intentionally unused — kept as UX hint only
|
|
92
95
|
try:
|
|
93
96
|
ext_data = json.loads(extra_json)
|
|
94
97
|
|
|
@@ -114,21 +117,26 @@ def add(ctx, ext_type, extra_json, dry_run):
|
|
|
114
117
|
|
|
115
118
|
@adextensions.command()
|
|
116
119
|
@click.option("--id", "extension_id", required=True, type=int, help="Extension ID")
|
|
120
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
117
121
|
@click.pass_context
|
|
118
|
-
def delete(ctx, extension_id):
|
|
122
|
+
def delete(ctx, extension_id, dry_run):
|
|
119
123
|
"""Delete ad extension"""
|
|
120
124
|
try:
|
|
125
|
+
body = {
|
|
126
|
+
"method": "delete",
|
|
127
|
+
"params": {"SelectionCriteria": {"Ids": [extension_id]}},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if dry_run:
|
|
131
|
+
format_output(body, "json", None)
|
|
132
|
+
return
|
|
133
|
+
|
|
121
134
|
client = create_client(
|
|
122
135
|
token=ctx.obj.get("token"),
|
|
123
136
|
login=ctx.obj.get("login"),
|
|
124
137
|
sandbox=ctx.obj.get("sandbox"),
|
|
125
138
|
)
|
|
126
139
|
|
|
127
|
-
body = {
|
|
128
|
-
"method": "delete",
|
|
129
|
-
"params": {"SelectionCriteria": {"Ids": [extension_id]}},
|
|
130
|
-
}
|
|
131
|
-
|
|
132
140
|
result = client.adextensions().post(data=body)
|
|
133
141
|
format_output(result().extract(), "json", None)
|
|
134
142
|
|
|
@@ -84,7 +84,18 @@ def get(
|
|
|
84
84
|
@adgroups.command()
|
|
85
85
|
@click.option("--name", required=True, help="Ad group name")
|
|
86
86
|
@click.option("--campaign-id", required=True, type=int, help="Campaign ID")
|
|
87
|
-
@click.option(
|
|
87
|
+
@click.option(
|
|
88
|
+
"--type",
|
|
89
|
+
"group_type",
|
|
90
|
+
default="TEXT_AD_GROUP",
|
|
91
|
+
help=(
|
|
92
|
+
"Ad group type (case-insensitive). The Yandex Direct API derives "
|
|
93
|
+
"the group type from nested objects (MobileAppAdGroup / "
|
|
94
|
+
"DynamicTextAdGroup / SmartAdGroup / ...), not from a top-level "
|
|
95
|
+
"Type discriminator. Convenience flags only build a TEXT_AD_GROUP; "
|
|
96
|
+
"for other types pass the matching nested object via --json."
|
|
97
|
+
),
|
|
98
|
+
)
|
|
88
99
|
@click.option("--region-ids", help="Comma-separated region IDs")
|
|
89
100
|
@click.option("--json", "extra_json", help="Additional JSON parameters")
|
|
90
101
|
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
@@ -96,12 +107,21 @@ def add(ctx, name, campaign_id, group_type, region_ids, extra_json, dry_run):
|
|
|
96
107
|
# AdGroupAddItem — the group type is inferred from the presence of
|
|
97
108
|
# MobileAppAdGroup / DynamicTextAdGroup / SmartAdGroup / etc.
|
|
98
109
|
# sub-objects, exactly like Ads (see fix in commands/ads.py).
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
110
|
+
# Previously --type was accepted but silently discarded — users
|
|
111
|
+
# passing --type MOBILE_APP_AD_GROUP got a TEXT_AD_GROUP with no
|
|
112
|
+
# warning. Now we normalize case and fail loudly if the caller
|
|
113
|
+
# asks for anything except TEXT_AD_GROUP. See axisrow/direct-cli#23.
|
|
102
114
|
# Refs: https://yandex.ru/dev/direct/doc/ref-v5/adgroups/add.html
|
|
115
|
+
group_type_norm = (group_type or "TEXT_AD_GROUP").upper().replace("-", "_")
|
|
116
|
+
|
|
117
|
+
if group_type_norm != "TEXT_AD_GROUP" and not extra_json:
|
|
118
|
+
raise click.UsageError(
|
|
119
|
+
f"--type {group_type} requires --json with the "
|
|
120
|
+
f"ad-group-type-specific nested object "
|
|
121
|
+
f"(e.g. DynamicTextAdGroup, SmartAdGroup, MobileAppAdGroup)."
|
|
122
|
+
)
|
|
123
|
+
|
|
103
124
|
adgroup_data = {"Name": name, "CampaignId": campaign_id}
|
|
104
|
-
_ = group_type # acknowledged-but-unused
|
|
105
125
|
|
|
106
126
|
if region_ids:
|
|
107
127
|
adgroup_data["RegionIds"] = parse_ids(region_ids)
|
|
@@ -125,6 +145,8 @@ def add(ctx, name, campaign_id, group_type, region_ids, extra_json, dry_run):
|
|
|
125
145
|
result = client.adgroups().post(data=body)
|
|
126
146
|
format_output(result().extract(), "json", None)
|
|
127
147
|
|
|
148
|
+
except click.UsageError:
|
|
149
|
+
raise
|
|
128
150
|
except Exception as e:
|
|
129
151
|
print_error(str(e))
|
|
130
152
|
raise click.Abort()
|
|
@@ -174,21 +196,26 @@ def update(ctx, adgroup_id, name, status, extra_json, dry_run):
|
|
|
174
196
|
|
|
175
197
|
@adgroups.command()
|
|
176
198
|
@click.option("--id", "adgroup_id", required=True, type=int, help="Ad group ID")
|
|
199
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
177
200
|
@click.pass_context
|
|
178
|
-
def delete(ctx, adgroup_id):
|
|
201
|
+
def delete(ctx, adgroup_id, dry_run):
|
|
179
202
|
"""Delete ad group"""
|
|
180
203
|
try:
|
|
204
|
+
body = {
|
|
205
|
+
"method": "delete",
|
|
206
|
+
"params": {"SelectionCriteria": {"Ids": [adgroup_id]}},
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if dry_run:
|
|
210
|
+
format_output(body, "json", None)
|
|
211
|
+
return
|
|
212
|
+
|
|
181
213
|
client = create_client(
|
|
182
214
|
token=ctx.obj.get("token"),
|
|
183
215
|
login=ctx.obj.get("login"),
|
|
184
216
|
sandbox=ctx.obj.get("sandbox"),
|
|
185
217
|
)
|
|
186
218
|
|
|
187
|
-
body = {
|
|
188
|
-
"method": "delete",
|
|
189
|
-
"params": {"SelectionCriteria": {"Ids": [adgroup_id]}},
|
|
190
|
-
}
|
|
191
|
-
|
|
192
219
|
result = client.adgroups().post(data=body)
|
|
193
220
|
format_output(result().extract(), "json", None)
|
|
194
221
|
|
|
@@ -92,21 +92,26 @@ def add(ctx, image_json, dry_run):
|
|
|
92
92
|
|
|
93
93
|
@adimages.command()
|
|
94
94
|
@click.option("--hash", "image_hash", required=True, help="Ad image hash")
|
|
95
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
95
96
|
@click.pass_context
|
|
96
|
-
def delete(ctx, image_hash):
|
|
97
|
+
def delete(ctx, image_hash, dry_run):
|
|
97
98
|
"""Delete ad image"""
|
|
98
99
|
try:
|
|
100
|
+
body = {
|
|
101
|
+
"method": "delete",
|
|
102
|
+
"params": {"SelectionCriteria": {"AdImageHashes": [image_hash]}},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if dry_run:
|
|
106
|
+
format_output(body, "json", None)
|
|
107
|
+
return
|
|
108
|
+
|
|
99
109
|
client = create_client(
|
|
100
110
|
token=ctx.obj.get("token"),
|
|
101
111
|
login=ctx.obj.get("login"),
|
|
102
112
|
sandbox=ctx.obj.get("sandbox"),
|
|
103
113
|
)
|
|
104
114
|
|
|
105
|
-
body = {
|
|
106
|
-
"method": "delete",
|
|
107
|
-
"params": {"SelectionCriteria": {"AdImageHashes": [image_hash]}},
|
|
108
|
-
}
|
|
109
|
-
|
|
110
115
|
result = client.adimages().post(data=body)
|
|
111
116
|
format_output(result().extract(), "json", None)
|
|
112
117
|
|