direct-cli 0.2.5__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.5 → direct_cli-0.2.6}/PKG-INFO +32 -1
- {direct_cli-0.2.5 → direct_cli-0.2.6}/README.md +28 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/api.py +18 -5
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/cli.py +2 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/adextensions.py +11 -6
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/adgroups.py +11 -6
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/adimages.py +11 -6
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/ads.py +51 -21
- direct_cli-0.2.5/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.5 → direct_cli-0.2.6}/direct_cli/commands/audiencetargets.py +84 -26
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/bidmodifiers.py +26 -12
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/bids.py +78 -5
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/campaigns.py +55 -30
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/creatives.py +29 -3
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/dictionaries.py +30 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/dynamicads.py +89 -23
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/feeds.py +8 -3
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/keywordbids.py +39 -5
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/keywords.py +55 -30
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/keywordsresearch.py +0 -30
- {direct_cli-0.2.5 → 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.5 → direct_cli-0.2.6}/direct_cli/commands/retargeting.py +46 -15
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/sitelinks.py +8 -3
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/smartadtargets.py +129 -32
- {direct_cli-0.2.5 → 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.5 → 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.5 → direct_cli-0.2.6}/direct_cli.egg-info/PKG-INFO +32 -1
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli.egg-info/SOURCES.txt +45 -1
- {direct_cli-0.2.5 → 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.5 → direct_cli-0.2.6}/pyproject.toml +5 -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.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.5 → direct_cli-0.2.6}/tests/test_comprehensive.py +1 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/test_dry_run.py +341 -27
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/test_integration.py +61 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/test_integration_write.py +3 -10
- 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.5/direct_cli/commands/reports.py +0 -133
- {direct_cli-0.2.5 → direct_cli-0.2.6}/.env.example +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/.github/copilot-instructions.md +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/.github/workflows/claude-code-review.yml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/.github/workflows/claude.yml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/.gitignore +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/AGENTS.md +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/CLAUDE.md +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/MANIFEST.in +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/__init__.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/auth.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/__init__.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/businesses.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/changes.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/clients.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/leads.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/commands/turbopages.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli/output.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli.egg-info/dependency_links.txt +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli.egg-info/entry_points.txt +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/direct_cli.egg-info/top_level.txt +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/scripts/release_pypi.sh +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/setup.cfg +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/setup.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/__init__.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/conftest.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/test_auth_bw.py +0 -0
- {direct_cli-0.2.5 → direct_cli-0.2.6}/tests/test_auth_op.py +0 -0
- {direct_cli-0.2.5 → 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")
|
|
@@ -117,21 +117,26 @@ def add(ctx, ext_type, extra_json, dry_run):
|
|
|
117
117
|
|
|
118
118
|
@adextensions.command()
|
|
119
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")
|
|
120
121
|
@click.pass_context
|
|
121
|
-
def delete(ctx, extension_id):
|
|
122
|
+
def delete(ctx, extension_id, dry_run):
|
|
122
123
|
"""Delete ad extension"""
|
|
123
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
|
+
|
|
124
134
|
client = create_client(
|
|
125
135
|
token=ctx.obj.get("token"),
|
|
126
136
|
login=ctx.obj.get("login"),
|
|
127
137
|
sandbox=ctx.obj.get("sandbox"),
|
|
128
138
|
)
|
|
129
139
|
|
|
130
|
-
body = {
|
|
131
|
-
"method": "delete",
|
|
132
|
-
"params": {"SelectionCriteria": {"Ids": [extension_id]}},
|
|
133
|
-
}
|
|
134
|
-
|
|
135
140
|
result = client.adextensions().post(data=body)
|
|
136
141
|
format_output(result().extract(), "json", None)
|
|
137
142
|
|
|
@@ -196,21 +196,26 @@ def update(ctx, adgroup_id, name, status, extra_json, dry_run):
|
|
|
196
196
|
|
|
197
197
|
@adgroups.command()
|
|
198
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")
|
|
199
200
|
@click.pass_context
|
|
200
|
-
def delete(ctx, adgroup_id):
|
|
201
|
+
def delete(ctx, adgroup_id, dry_run):
|
|
201
202
|
"""Delete ad group"""
|
|
202
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
|
+
|
|
203
213
|
client = create_client(
|
|
204
214
|
token=ctx.obj.get("token"),
|
|
205
215
|
login=ctx.obj.get("login"),
|
|
206
216
|
sandbox=ctx.obj.get("sandbox"),
|
|
207
217
|
)
|
|
208
218
|
|
|
209
|
-
body = {
|
|
210
|
-
"method": "delete",
|
|
211
|
-
"params": {"SelectionCriteria": {"Ids": [adgroup_id]}},
|
|
212
|
-
}
|
|
213
|
-
|
|
214
219
|
result = client.adgroups().post(data=body)
|
|
215
220
|
format_output(result().extract(), "json", None)
|
|
216
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
|
|
|
@@ -221,18 +221,23 @@ def update(ctx, ad_id, status, extra_json, dry_run):
|
|
|
221
221
|
|
|
222
222
|
@ads.command()
|
|
223
223
|
@click.option("--id", "ad_id", required=True, type=int, help="Ad ID")
|
|
224
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
224
225
|
@click.pass_context
|
|
225
|
-
def delete(ctx, ad_id):
|
|
226
|
+
def delete(ctx, ad_id, dry_run):
|
|
226
227
|
"""Delete ad"""
|
|
227
228
|
try:
|
|
229
|
+
body = {"method": "delete", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
230
|
+
|
|
231
|
+
if dry_run:
|
|
232
|
+
format_output(body, "json", None)
|
|
233
|
+
return
|
|
234
|
+
|
|
228
235
|
client = create_client(
|
|
229
236
|
token=ctx.obj.get("token"),
|
|
230
237
|
login=ctx.obj.get("login"),
|
|
231
238
|
sandbox=ctx.obj.get("sandbox"),
|
|
232
239
|
)
|
|
233
240
|
|
|
234
|
-
body = {"method": "delete", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
235
|
-
|
|
236
241
|
result = client.ads().post(data=body)
|
|
237
242
|
format_output(result().extract(), "json", None)
|
|
238
243
|
|
|
@@ -243,18 +248,23 @@ def delete(ctx, ad_id):
|
|
|
243
248
|
|
|
244
249
|
@ads.command()
|
|
245
250
|
@click.option("--id", "ad_id", required=True, type=int, help="Ad ID")
|
|
251
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
246
252
|
@click.pass_context
|
|
247
|
-
def archive(ctx, ad_id):
|
|
253
|
+
def archive(ctx, ad_id, dry_run):
|
|
248
254
|
"""Archive ad"""
|
|
249
255
|
try:
|
|
256
|
+
body = {"method": "archive", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
257
|
+
|
|
258
|
+
if dry_run:
|
|
259
|
+
format_output(body, "json", None)
|
|
260
|
+
return
|
|
261
|
+
|
|
250
262
|
client = create_client(
|
|
251
263
|
token=ctx.obj.get("token"),
|
|
252
264
|
login=ctx.obj.get("login"),
|
|
253
265
|
sandbox=ctx.obj.get("sandbox"),
|
|
254
266
|
)
|
|
255
267
|
|
|
256
|
-
body = {"method": "archive", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
257
|
-
|
|
258
268
|
result = client.ads().post(data=body)
|
|
259
269
|
format_output(result().extract(), "json", None)
|
|
260
270
|
|
|
@@ -265,21 +275,26 @@ def archive(ctx, ad_id):
|
|
|
265
275
|
|
|
266
276
|
@ads.command()
|
|
267
277
|
@click.option("--id", "ad_id", required=True, type=int, help="Ad ID")
|
|
278
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
268
279
|
@click.pass_context
|
|
269
|
-
def unarchive(ctx, ad_id):
|
|
280
|
+
def unarchive(ctx, ad_id, dry_run):
|
|
270
281
|
"""Unarchive ad"""
|
|
271
282
|
try:
|
|
283
|
+
body = {
|
|
284
|
+
"method": "unarchive",
|
|
285
|
+
"params": {"SelectionCriteria": {"Ids": [ad_id]}},
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if dry_run:
|
|
289
|
+
format_output(body, "json", None)
|
|
290
|
+
return
|
|
291
|
+
|
|
272
292
|
client = create_client(
|
|
273
293
|
token=ctx.obj.get("token"),
|
|
274
294
|
login=ctx.obj.get("login"),
|
|
275
295
|
sandbox=ctx.obj.get("sandbox"),
|
|
276
296
|
)
|
|
277
297
|
|
|
278
|
-
body = {
|
|
279
|
-
"method": "unarchive",
|
|
280
|
-
"params": {"SelectionCriteria": {"Ids": [ad_id]}},
|
|
281
|
-
}
|
|
282
|
-
|
|
283
298
|
result = client.ads().post(data=body)
|
|
284
299
|
format_output(result().extract(), "json", None)
|
|
285
300
|
|
|
@@ -290,18 +305,23 @@ def unarchive(ctx, ad_id):
|
|
|
290
305
|
|
|
291
306
|
@ads.command()
|
|
292
307
|
@click.option("--id", "ad_id", required=True, type=int, help="Ad ID")
|
|
308
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
293
309
|
@click.pass_context
|
|
294
|
-
def suspend(ctx, ad_id):
|
|
310
|
+
def suspend(ctx, ad_id, dry_run):
|
|
295
311
|
"""Suspend ad"""
|
|
296
312
|
try:
|
|
313
|
+
body = {"method": "suspend", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
314
|
+
|
|
315
|
+
if dry_run:
|
|
316
|
+
format_output(body, "json", None)
|
|
317
|
+
return
|
|
318
|
+
|
|
297
319
|
client = create_client(
|
|
298
320
|
token=ctx.obj.get("token"),
|
|
299
321
|
login=ctx.obj.get("login"),
|
|
300
322
|
sandbox=ctx.obj.get("sandbox"),
|
|
301
323
|
)
|
|
302
324
|
|
|
303
|
-
body = {"method": "suspend", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
304
|
-
|
|
305
325
|
result = client.ads().post(data=body)
|
|
306
326
|
format_output(result().extract(), "json", None)
|
|
307
327
|
|
|
@@ -312,18 +332,23 @@ def suspend(ctx, ad_id):
|
|
|
312
332
|
|
|
313
333
|
@ads.command()
|
|
314
334
|
@click.option("--id", "ad_id", required=True, type=int, help="Ad ID")
|
|
335
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
315
336
|
@click.pass_context
|
|
316
|
-
def resume(ctx, ad_id):
|
|
337
|
+
def resume(ctx, ad_id, dry_run):
|
|
317
338
|
"""Resume ad"""
|
|
318
339
|
try:
|
|
340
|
+
body = {"method": "resume", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
341
|
+
|
|
342
|
+
if dry_run:
|
|
343
|
+
format_output(body, "json", None)
|
|
344
|
+
return
|
|
345
|
+
|
|
319
346
|
client = create_client(
|
|
320
347
|
token=ctx.obj.get("token"),
|
|
321
348
|
login=ctx.obj.get("login"),
|
|
322
349
|
sandbox=ctx.obj.get("sandbox"),
|
|
323
350
|
)
|
|
324
351
|
|
|
325
|
-
body = {"method": "resume", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
326
|
-
|
|
327
352
|
result = client.ads().post(data=body)
|
|
328
353
|
format_output(result().extract(), "json", None)
|
|
329
354
|
|
|
@@ -334,18 +359,23 @@ def resume(ctx, ad_id):
|
|
|
334
359
|
|
|
335
360
|
@ads.command()
|
|
336
361
|
@click.option("--id", "ad_id", required=True, type=int, help="Ad ID")
|
|
362
|
+
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
337
363
|
@click.pass_context
|
|
338
|
-
def moderate(ctx, ad_id):
|
|
364
|
+
def moderate(ctx, ad_id, dry_run):
|
|
339
365
|
"""Moderate ad"""
|
|
340
366
|
try:
|
|
367
|
+
body = {"method": "moderate", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
368
|
+
|
|
369
|
+
if dry_run:
|
|
370
|
+
format_output(body, "json", None)
|
|
371
|
+
return
|
|
372
|
+
|
|
341
373
|
client = create_client(
|
|
342
374
|
token=ctx.obj.get("token"),
|
|
343
375
|
login=ctx.obj.get("login"),
|
|
344
376
|
sandbox=ctx.obj.get("sandbox"),
|
|
345
377
|
)
|
|
346
378
|
|
|
347
|
-
body = {"method": "moderate", "params": {"SelectionCriteria": {"Ids": [ad_id]}}}
|
|
348
|
-
|
|
349
379
|
result = client.ads().post(data=body)
|
|
350
380
|
format_output(result().extract(), "json", None)
|
|
351
381
|
|