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.
Files changed (133) hide show
  1. direct_cli-0.2.6/.github/workflows/api-coverage.yml +153 -0
  2. {direct_cli-0.2.4 → direct_cli-0.2.6}/PKG-INFO +32 -1
  3. {direct_cli-0.2.4 → direct_cli-0.2.6}/README.md +28 -0
  4. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/api.py +18 -5
  5. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/cli.py +2 -0
  6. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/adextensions.py +19 -11
  7. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/adgroups.py +38 -11
  8. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/adimages.py +11 -6
  9. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/ads.py +86 -27
  10. direct_cli-0.2.4/direct_cli/commands/agencyclients.py → direct_cli-0.2.6/direct_cli/commands/advideos.py +37 -33
  11. direct_cli-0.2.6/direct_cli/commands/agencyclients.py +217 -0
  12. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/audiencetargets.py +85 -27
  13. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/bidmodifiers.py +154 -37
  14. direct_cli-0.2.6/direct_cli/commands/bids.py +183 -0
  15. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/campaigns.py +87 -37
  16. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/creatives.py +29 -3
  17. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/dictionaries.py +30 -0
  18. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/dynamicads.py +89 -23
  19. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/feeds.py +47 -8
  20. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/keywordbids.py +41 -7
  21. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/keywords.py +57 -32
  22. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/keywordsresearch.py +0 -30
  23. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/negativekeywordsharedsets.py +8 -3
  24. direct_cli-0.2.6/direct_cli/commands/reports.py +387 -0
  25. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/retargeting.py +60 -10
  26. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/sitelinks.py +8 -3
  27. direct_cli-0.2.6/direct_cli/commands/smartadtargets.py +305 -0
  28. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/vcards.py +8 -3
  29. direct_cli-0.2.6/direct_cli/reports_coverage.py +242 -0
  30. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/utils.py +1 -0
  31. direct_cli-0.2.6/direct_cli/wsdl_coverage.py +347 -0
  32. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/PKG-INFO +32 -1
  33. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/SOURCES.txt +45 -2
  34. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/requires.txt +3 -0
  35. direct_cli-0.2.6/docs/superpowers/plans/2026-04-12-issue-32-completion.md +548 -0
  36. {direct_cli-0.2.4 → direct_cli-0.2.6}/pyproject.toml +6 -1
  37. direct_cli-0.2.6/scripts/build_api_coverage_report.py +109 -0
  38. direct_cli-0.2.6/scripts/check_reports_drift.py +136 -0
  39. direct_cli-0.2.6/scripts/check_wsdl_drift.py +62 -0
  40. direct_cli-0.2.6/scripts/refresh_reports_cache.py +44 -0
  41. direct_cli-0.2.6/scripts/refresh_wsdl_cache.py +39 -0
  42. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +8 -8
  43. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +27 -27
  44. direct_cli-0.2.6/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +67 -0
  45. {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
  46. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +141 -32
  47. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +22 -22
  48. {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
  49. direct_cli-0.2.6/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +275 -0
  50. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +33 -92
  51. direct_cli-0.2.6/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +59 -0
  52. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +16 -16
  53. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +27 -27
  54. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +27 -27
  55. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +17 -17
  56. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +10 -10
  57. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +5 -5
  58. 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
  59. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +23 -23
  60. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/conftest.py +110 -9
  61. direct_cli-0.2.6/tests/reports_cache/raw/fields-list.html +3316 -0
  62. direct_cli-0.2.6/tests/reports_cache/raw/headers.html +88 -0
  63. direct_cli-0.2.6/tests/reports_cache/raw/spec.html +119 -0
  64. direct_cli-0.2.6/tests/reports_cache/raw/type.html +279 -0
  65. direct_cli-0.2.6/tests/reports_cache/spec.json +583 -0
  66. direct_cli-0.2.6/tests/test_api_coverage.py +1176 -0
  67. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_comprehensive.py +1 -0
  68. direct_cli-0.2.6/tests/test_dry_run.py +1587 -0
  69. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_integration.py +61 -0
  70. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_integration_write.py +191 -77
  71. direct_cli-0.2.6/tests/test_reports_drift.py +102 -0
  72. direct_cli-0.2.6/tests/wsdl_cache/adextensions.xml +189 -0
  73. direct_cli-0.2.6/tests/wsdl_cache/adgroups.xml +551 -0
  74. direct_cli-0.2.6/tests/wsdl_cache/adimages.xml +270 -0
  75. direct_cli-0.2.6/tests/wsdl_cache/ads.xml +1622 -0
  76. direct_cli-0.2.6/tests/wsdl_cache/advideos.xml +153 -0
  77. direct_cli-0.2.6/tests/wsdl_cache/agencyclients.xml +310 -0
  78. direct_cli-0.2.6/tests/wsdl_cache/audiencetargets.xml +339 -0
  79. direct_cli-0.2.6/tests/wsdl_cache/bidmodifiers.xml +536 -0
  80. direct_cli-0.2.6/tests/wsdl_cache/bids.xml +262 -0
  81. direct_cli-0.2.6/tests/wsdl_cache/businesses.xml +106 -0
  82. direct_cli-0.2.6/tests/wsdl_cache/campaigns.xml +2809 -0
  83. direct_cli-0.2.6/tests/wsdl_cache/changes.xml +190 -0
  84. direct_cli-0.2.6/tests/wsdl_cache/clients.xml +132 -0
  85. direct_cli-0.2.6/tests/wsdl_cache/creatives.xml +239 -0
  86. direct_cli-0.2.6/tests/wsdl_cache/dictionaries.xml +327 -0
  87. direct_cli-0.2.6/tests/wsdl_cache/dynamictextadtargets.xml +338 -0
  88. direct_cli-0.2.6/tests/wsdl_cache/feeds.xml +339 -0
  89. direct_cli-0.2.6/tests/wsdl_cache/keywordbids.xml +291 -0
  90. direct_cli-0.2.6/tests/wsdl_cache/keywords.xml +362 -0
  91. direct_cli-0.2.6/tests/wsdl_cache/keywordsresearch.xml +174 -0
  92. direct_cli-0.2.6/tests/wsdl_cache/leads.xml +107 -0
  93. direct_cli-0.2.6/tests/wsdl_cache/negativekeywordsharedsets.xml +232 -0
  94. direct_cli-0.2.6/tests/wsdl_cache/retargetinglists.xml +283 -0
  95. direct_cli-0.2.6/tests/wsdl_cache/sitelinks.xml +189 -0
  96. direct_cli-0.2.6/tests/wsdl_cache/smartadtargets.xml +395 -0
  97. direct_cli-0.2.6/tests/wsdl_cache/turbopages.xml +103 -0
  98. direct_cli-0.2.6/tests/wsdl_cache/vcards.xml +238 -0
  99. direct_cli-0.2.4/direct_cli/commands/bids.py +0 -110
  100. direct_cli-0.2.4/direct_cli/commands/reports.py +0 -124
  101. direct_cli-0.2.4/direct_cli/commands/smartadtargets.py +0 -180
  102. direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -67
  103. direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteBidModifiers.test_toggle_existing.yaml +0 -110
  104. direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -275
  105. direct_cli-0.2.4/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -275
  106. direct_cli-0.2.4/tests/test_dry_run.py +0 -663
  107. {direct_cli-0.2.4 → direct_cli-0.2.6}/.env.example +0 -0
  108. {direct_cli-0.2.4 → direct_cli-0.2.6}/.github/copilot-instructions.md +0 -0
  109. {direct_cli-0.2.4 → direct_cli-0.2.6}/.github/workflows/claude-code-review.yml +0 -0
  110. {direct_cli-0.2.4 → direct_cli-0.2.6}/.github/workflows/claude.yml +0 -0
  111. {direct_cli-0.2.4 → direct_cli-0.2.6}/.gitignore +0 -0
  112. {direct_cli-0.2.4 → direct_cli-0.2.6}/AGENTS.md +0 -0
  113. {direct_cli-0.2.4 → direct_cli-0.2.6}/CLAUDE.md +0 -0
  114. {direct_cli-0.2.4 → direct_cli-0.2.6}/MANIFEST.in +0 -0
  115. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/__init__.py +0 -0
  116. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/auth.py +0 -0
  117. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/__init__.py +0 -0
  118. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/businesses.py +0 -0
  119. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/changes.py +0 -0
  120. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/clients.py +0 -0
  121. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/leads.py +0 -0
  122. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/commands/turbopages.py +0 -0
  123. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli/output.py +0 -0
  124. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/dependency_links.txt +0 -0
  125. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/entry_points.txt +0 -0
  126. {direct_cli-0.2.4 → direct_cli-0.2.6}/direct_cli.egg-info/top_level.txt +0 -0
  127. {direct_cli-0.2.4 → direct_cli-0.2.6}/scripts/release_pypi.sh +0 -0
  128. {direct_cli-0.2.4 → direct_cli-0.2.6}/setup.cfg +0 -0
  129. {direct_cli-0.2.4 → direct_cli-0.2.6}/setup.py +0 -0
  130. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/__init__.py +0 -0
  131. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_auth_bw.py +0 -0
  132. {direct_cli-0.2.4 → direct_cli-0.2.6}/tests/test_auth_op.py +0 -0
  133. {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.4
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="auto",
54
+ processing_mode=processing_mode,
43
55
  wait_report=True,
44
- return_money_in_micros=False,
45
- skip_report_header=True,
46
- skip_column_header=False,
47
- skip_report_summary=True,
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
- "Extension type (UX hint Callout, Sitelinks, Vcard, …). "
74
- "Not sent to the API; the API derives the type from the nested "
75
- "field name inside --json."
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 # consumed only for argument validation / UX clarity
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("--type", "group_type", default="TEXT_AD_GROUP", help="Ad group type")
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
- # The --type CLI option is preserved for backward compatibility but
100
- # is no longer forwarded to the API; users wanting non-text group
101
- # types must pass the matching sub-object via --json.
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