direct-cli 0.3.6__tar.gz → 0.3.7__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 (216) hide show
  1. {direct_cli-0.3.6 → direct_cli-0.3.7}/PKG-INFO +38 -7
  2. {direct_cli-0.3.6 → direct_cli-0.3.7}/README.md +37 -6
  3. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/cli.py +4 -1
  4. direct_cli-0.3.7/direct_cli/commands/v4forecast.py +168 -0
  5. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/v4shells.py +0 -5
  6. direct_cli-0.3.7/direct_cli/commands/v4tags.py +263 -0
  7. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/smoke_matrix.py +8 -0
  8. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/v4_contracts.py +86 -24
  9. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli.egg-info/PKG-INFO +38 -7
  10. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli.egg-info/SOURCES.txt +4 -1
  11. {direct_cli-0.3.6 → direct_cli-0.3.7}/pyproject.toml +1 -1
  12. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/sandbox_write_live.py +60 -3
  13. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/test_safe_commands.sh +12 -0
  14. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/API_COVERAGE.md +3 -3
  15. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/api_coverage_payloads.py +8 -0
  16. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_cli_contract.py +1 -0
  17. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_comprehensive.py +1 -0
  18. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_smoke_matrix.py +75 -4
  19. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4_contracts.py +86 -0
  20. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4_foundation.py +2 -0
  21. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4_safety.py +14 -0
  22. direct_cli-0.3.7/tests/test_v4forecast.py +238 -0
  23. direct_cli-0.3.7/tests/test_v4tags.py +321 -0
  24. direct_cli-0.3.6/.github/workflows/claude-code-review.yml +0 -44
  25. {direct_cli-0.3.6 → direct_cli-0.3.7}/.env.example +0 -0
  26. {direct_cli-0.3.6 → direct_cli-0.3.7}/.github/copilot-instructions.md +0 -0
  27. {direct_cli-0.3.6 → direct_cli-0.3.7}/.github/workflows/api-coverage.yml +0 -0
  28. {direct_cli-0.3.6 → direct_cli-0.3.7}/.github/workflows/claude.yml +0 -0
  29. {direct_cli-0.3.6 → direct_cli-0.3.7}/.github/workflows/quality.yml +0 -0
  30. {direct_cli-0.3.6 → direct_cli-0.3.7}/.gitignore +0 -0
  31. {direct_cli-0.3.6 → direct_cli-0.3.7}/AGENTS.md +0 -0
  32. {direct_cli-0.3.6 → direct_cli-0.3.7}/CHANGELOG.md +0 -0
  33. {direct_cli-0.3.6 → direct_cli-0.3.7}/CLAUDE.md +0 -0
  34. {direct_cli-0.3.6 → direct_cli-0.3.7}/MANIFEST.in +0 -0
  35. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/__init__.py +0 -0
  36. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_deprecated.py +0 -0
  37. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_smoke_probes.py +0 -0
  38. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/__init__.py +0 -0
  39. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  40. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
  41. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  42. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  43. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +0 -0
  44. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
  45. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  46. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
  47. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  48. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  49. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/api.py +0 -0
  50. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/auth.py +0 -0
  51. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/__init__.py +0 -0
  52. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/adextensions.py +0 -0
  53. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/adgroups.py +0 -0
  54. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/adimages.py +0 -0
  55. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/ads.py +0 -0
  56. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/advideos.py +0 -0
  57. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/agencyclients.py +0 -0
  58. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/audiencetargets.py +0 -0
  59. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/auth.py +0 -0
  60. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/balance.py +0 -0
  61. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/bidmodifiers.py +0 -0
  62. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/bids.py +0 -0
  63. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/businesses.py +0 -0
  64. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/campaigns.py +0 -0
  65. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/changes.py +0 -0
  66. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/clients.py +0 -0
  67. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/creatives.py +0 -0
  68. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/dictionaries.py +0 -0
  69. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/dynamicads.py +0 -0
  70. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/dynamicfeedadtargets.py +0 -0
  71. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/feeds.py +0 -0
  72. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/keywordbids.py +0 -0
  73. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/keywords.py +0 -0
  74. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/keywordsresearch.py +0 -0
  75. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/leads.py +0 -0
  76. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
  77. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/reports.py +0 -0
  78. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/retargeting.py +0 -0
  79. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/sitelinks.py +0 -0
  80. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/smartadtargets.py +0 -0
  81. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/strategies.py +0 -0
  82. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/turbopages.py +0 -0
  83. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/v4account.py +0 -0
  84. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/v4events.py +0 -0
  85. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/v4finance.py +0 -0
  86. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/v4goals.py +0 -0
  87. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/v4wordstat.py +0 -0
  88. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/commands/vcards.py +0 -0
  89. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/output.py +0 -0
  90. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/reports_coverage.py +0 -0
  91. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/utils.py +0 -0
  92. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/v4/__init__.py +0 -0
  93. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/v4/money.py +0 -0
  94. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli/wsdl_coverage.py +0 -0
  95. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli.egg-info/dependency_links.txt +0 -0
  96. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli.egg-info/entry_points.txt +0 -0
  97. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli.egg-info/requires.txt +0 -0
  98. {direct_cli-0.3.6 → direct_cli-0.3.7}/direct_cli.egg-info/top_level.txt +0 -0
  99. {direct_cli-0.3.6 → direct_cli-0.3.7}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  100. {direct_cli-0.3.6 → direct_cli-0.3.7}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  101. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/anonymize_cassettes.py +0 -0
  102. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/build_api_coverage_checklist.py +0 -0
  103. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/build_api_coverage_report.py +0 -0
  104. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/check_reports_drift.py +0 -0
  105. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/check_wsdl_drift.py +0 -0
  106. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/patch_vendor_imports.py +0 -0
  107. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/refresh_reports_cache.py +0 -0
  108. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/refresh_wsdl_cache.py +0 -0
  109. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/release_pypi.sh +0 -0
  110. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/test_dangerous_commands.sh +0 -0
  111. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/test_sandbox_write.sh +0 -0
  112. {direct_cli-0.3.6 → direct_cli-0.3.7}/scripts/update_vendor.sh +0 -0
  113. {direct_cli-0.3.6 → direct_cli-0.3.7}/setup.cfg +0 -0
  114. {direct_cli-0.3.6 → direct_cli-0.3.7}/setup.py +0 -0
  115. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/API_ISSUE_AUDIT.md +0 -0
  116. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/MANUAL_COVERAGE.md +0 -0
  117. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/__init__.py +0 -0
  118. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_adgroups_add_update_delete.yaml +0 -0
  119. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_adimages_add_get_delete.yaml +0 -0
  120. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_ads_add_update_delete.yaml +0 -0
  121. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
  122. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_advideos_add_get.yaml +0 -0
  123. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_add_delete.yaml +0 -0
  124. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_suspend_resume.yaml +0 -0
  125. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_bids_set.yaml +0 -0
  126. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_campaign_create_get_delete.yaml +0 -0
  127. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
  128. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_add_delete.yaml +0 -0
  129. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_suspend_resume.yaml +0 -0
  130. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_keywordbids_set.yaml +0 -0
  131. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_keywords_add_update_delete.yaml +0 -0
  132. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_keywords_suspend_resume.yaml +0 -0
  133. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_sitelinks_add_get_delete.yaml +0 -0
  134. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_add_update_delete.yaml +0 -0
  135. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_suspend_resume.yaml +0 -0
  136. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  137. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  138. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  139. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  140. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  141. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  142. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  143. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  144. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  145. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  146. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  147. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +0 -0
  148. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  149. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  150. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  151. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  152. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  153. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
  154. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  155. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/conftest.py +0 -0
  156. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/fixtures/test-video.mp4 +0 -0
  157. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/reports_cache/raw/fields-list.html +0 -0
  158. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/reports_cache/raw/headers.html +0 -0
  159. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/reports_cache/raw/period.html +0 -0
  160. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/reports_cache/raw/spec.html +0 -0
  161. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/reports_cache/raw/type.html +0 -0
  162. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/reports_cache/spec.json +0 -0
  163. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_api_coverage.py +0 -0
  164. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_auth_bw.py +0 -0
  165. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_auth_oauth.py +0 -0
  166. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_auth_op.py +0 -0
  167. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_balance.py +0 -0
  168. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_cli.py +0 -0
  169. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_dry_run.py +0 -0
  170. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_integration.py +0 -0
  171. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_integration_live_write.py +0 -0
  172. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_integration_write.py +0 -0
  173. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_low_coverage_payloads.py +0 -0
  174. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_reports_drift.py +0 -0
  175. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_reports_parsing.py +0 -0
  176. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_transport_contract.py +0 -0
  177. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4_live_contracts.py +0 -0
  178. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4account.py +0 -0
  179. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4events.py +0 -0
  180. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4finance_money.py +0 -0
  181. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4finance_read.py +0 -0
  182. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4goals.py +0 -0
  183. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_v4wordstat.py +0 -0
  184. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/test_vendor_imports.py +0 -0
  185. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/adextensions.xml +0 -0
  186. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/adgroups.xml +0 -0
  187. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/adimages.xml +0 -0
  188. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/ads.xml +0 -0
  189. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/advideos.xml +0 -0
  190. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/agencyclients.xml +0 -0
  191. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/audiencetargets.xml +0 -0
  192. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  193. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/bids.xml +0 -0
  194. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/businesses.xml +0 -0
  195. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/campaigns.xml +0 -0
  196. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/changes.xml +0 -0
  197. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/clients.xml +0 -0
  198. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/creatives.xml +0 -0
  199. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/dictionaries.xml +0 -0
  200. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  201. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  202. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/feeds.xml +0 -0
  203. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  204. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/imports/general.xsd +0 -0
  205. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  206. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/keywordbids.xml +0 -0
  207. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/keywords.xml +0 -0
  208. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  209. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/leads.xml +0 -0
  210. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  211. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/retargetinglists.xml +0 -0
  212. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/sitelinks.xml +0 -0
  213. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/smartadtargets.xml +0 -0
  214. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/strategies.xml +0 -0
  215. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/turbopages.xml +0 -0
  216. {direct_cli-0.3.6 → direct_cli-0.3.7}/tests/wsdl_cache/vcards.xml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Command-line interface for Yandex Direct API
5
5
  Author: axisrow
6
6
  License: MIT
@@ -158,6 +158,24 @@ direct v4goals get-retargeting-goals --campaign-ids 123,456 --format table
158
158
  direct v4goals get-stat-goals --campaign-ids 123 --dry-run
159
159
  ```
160
160
 
161
+ ### V4 Live Tags
162
+
163
+ Campaign tags are managed as `{TagID, Tag}` pairs. Use `TagID=0` to create a
164
+ new campaign tag. Banner/ad tags are assigned by campaign tag IDs. Update
165
+ methods replace the full tag list for the target campaign or banner, so pass
166
+ existing tags again if they must remain assigned. Ad group tags are filter-only
167
+ through `direct adgroups get --tag-ids/--tags`; this release does not add ad
168
+ group tag mutation commands.
169
+
170
+ ```bash
171
+ direct v4tags get-campaigns --campaign-ids 3193279,1634563
172
+ direct v4tags get-banners --banner-ids 2571700,2571745
173
+ direct v4tags get-banners --campaign-ids 3193279
174
+ direct v4tags update-campaigns --campaign-id 3193279 --tag 0=akapulko --tag 16590=orange --dry-run
175
+ direct v4tags update-banners --banner-ids 2571700,2571745 --tag-ids 16590,16734 --dry-run
176
+ direct v4tags update-banners --banner-ids 2571700 --clear-tags --dry-run
177
+ ```
178
+
161
179
  ### V4 Live Events
162
180
 
163
181
  ```bash
@@ -178,6 +196,19 @@ direct v4wordstat get-report --report-id 123 --format table
178
196
  direct v4wordstat delete-report --report-id 123
179
197
  ```
180
198
 
199
+ ### V4 Live Budget Forecasts
200
+
201
+ Budget forecasts are asynchronous. Direct CLI makes exactly one API call per
202
+ command and does not poll automatically; repeat `list` or `get` yourself until
203
+ the forecast is ready.
204
+
205
+ ```bash
206
+ direct v4forecast create --phrases "buy laptop,buy desktop" --geo-ids 213 --currency RUB
207
+ direct v4forecast list --format table
208
+ direct v4forecast get --forecast-id 123 --format table
209
+ direct v4forecast delete --forecast-id 123
210
+ ```
211
+
181
212
  ### V4 Live Finance
182
213
 
183
214
  Finance methods require an extra financial token for money operations. In the
@@ -562,9 +593,9 @@ Current command surface:
562
593
  | WSDL-backed API services | 29 |
563
594
  | Supported API services including Reports | 30 |
564
595
  | WSDL operations | 112 |
565
- | CLI groups including `auth` | 39 |
566
- | CLI subcommands including `auth` | 130 |
567
- | API CLI subcommands excluding `auth` | 126 |
596
+ | CLI groups including `auth` | 40 |
597
+ | CLI subcommands including `auth` | 144 |
598
+ | API CLI subcommands excluding `auth` | 140 |
568
599
 
569
600
  ### API Coverage And Drift Monitoring
570
601
 
@@ -1183,9 +1214,9 @@ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rew
1183
1214
  | WSDL-backed API services | 29 |
1184
1215
  | API services с учётом Reports | 30 |
1185
1216
  | WSDL operations | 112 |
1186
- | CLI groups с `auth` | 39 |
1187
- | CLI subcommands с `auth` | 130 |
1188
- | API CLI subcommands без `auth` | 126 |
1217
+ | CLI groups с `auth` | 40 |
1218
+ | CLI subcommands с `auth` | 144 |
1219
+ | API CLI subcommands без `auth` | 140 |
1189
1220
 
1190
1221
  #### Live sandbox write smoke
1191
1222
 
@@ -115,6 +115,24 @@ direct v4goals get-retargeting-goals --campaign-ids 123,456 --format table
115
115
  direct v4goals get-stat-goals --campaign-ids 123 --dry-run
116
116
  ```
117
117
 
118
+ ### V4 Live Tags
119
+
120
+ Campaign tags are managed as `{TagID, Tag}` pairs. Use `TagID=0` to create a
121
+ new campaign tag. Banner/ad tags are assigned by campaign tag IDs. Update
122
+ methods replace the full tag list for the target campaign or banner, so pass
123
+ existing tags again if they must remain assigned. Ad group tags are filter-only
124
+ through `direct adgroups get --tag-ids/--tags`; this release does not add ad
125
+ group tag mutation commands.
126
+
127
+ ```bash
128
+ direct v4tags get-campaigns --campaign-ids 3193279,1634563
129
+ direct v4tags get-banners --banner-ids 2571700,2571745
130
+ direct v4tags get-banners --campaign-ids 3193279
131
+ direct v4tags update-campaigns --campaign-id 3193279 --tag 0=akapulko --tag 16590=orange --dry-run
132
+ direct v4tags update-banners --banner-ids 2571700,2571745 --tag-ids 16590,16734 --dry-run
133
+ direct v4tags update-banners --banner-ids 2571700 --clear-tags --dry-run
134
+ ```
135
+
118
136
  ### V4 Live Events
119
137
 
120
138
  ```bash
@@ -135,6 +153,19 @@ direct v4wordstat get-report --report-id 123 --format table
135
153
  direct v4wordstat delete-report --report-id 123
136
154
  ```
137
155
 
156
+ ### V4 Live Budget Forecasts
157
+
158
+ Budget forecasts are asynchronous. Direct CLI makes exactly one API call per
159
+ command and does not poll automatically; repeat `list` or `get` yourself until
160
+ the forecast is ready.
161
+
162
+ ```bash
163
+ direct v4forecast create --phrases "buy laptop,buy desktop" --geo-ids 213 --currency RUB
164
+ direct v4forecast list --format table
165
+ direct v4forecast get --forecast-id 123 --format table
166
+ direct v4forecast delete --forecast-id 123
167
+ ```
168
+
138
169
  ### V4 Live Finance
139
170
 
140
171
  Finance methods require an extra financial token for money operations. In the
@@ -519,9 +550,9 @@ Current command surface:
519
550
  | WSDL-backed API services | 29 |
520
551
  | Supported API services including Reports | 30 |
521
552
  | WSDL operations | 112 |
522
- | CLI groups including `auth` | 39 |
523
- | CLI subcommands including `auth` | 130 |
524
- | API CLI subcommands excluding `auth` | 126 |
553
+ | CLI groups including `auth` | 40 |
554
+ | CLI subcommands including `auth` | 144 |
555
+ | API CLI subcommands excluding `auth` | 140 |
525
556
 
526
557
  ### API Coverage And Drift Monitoring
527
558
 
@@ -1140,9 +1171,9 @@ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rew
1140
1171
  | WSDL-backed API services | 29 |
1141
1172
  | API services с учётом Reports | 30 |
1142
1173
  | WSDL operations | 112 |
1143
- | CLI groups с `auth` | 39 |
1144
- | CLI subcommands с `auth` | 130 |
1145
- | API CLI subcommands без `auth` | 126 |
1174
+ | CLI groups с `auth` | 40 |
1175
+ | CLI subcommands с `auth` | 144 |
1176
+ | API CLI subcommands без `auth` | 140 |
1146
1177
 
1147
1178
  #### Live sandbox write smoke
1148
1179
 
@@ -43,10 +43,12 @@ from .commands.strategies import strategies
43
43
  from .commands.auth import auth
44
44
  from .commands.balance import balance
45
45
  from .commands.v4events import v4events
46
+ from .commands.v4forecast import v4forecast
46
47
  from .commands.v4finance import v4finance
47
48
  from .commands.v4account import v4account
48
- from .commands.v4shells import v4forecast, v4meta
49
+ from .commands.v4shells import v4meta
49
50
  from .commands.v4goals import v4goals
51
+ from .commands.v4tags import v4tags
50
52
  from .commands.v4wordstat import v4wordstat
51
53
 
52
54
  # Load .env file
@@ -186,6 +188,7 @@ for command in (
186
188
  v4goals,
187
189
  v4events,
188
190
  v4wordstat,
191
+ v4tags,
189
192
  v4forecast,
190
193
  v4meta,
191
194
  auth,
@@ -0,0 +1,168 @@
1
+ """Yandex Direct v4 Live budget forecast commands."""
2
+
3
+ from typing import Optional
4
+
5
+ import click
6
+
7
+ from ..api import create_v4_client
8
+ from ..output import format_output, print_error
9
+ from ..utils import parse_csv_strings, parse_ids
10
+ from ..v4 import build_v4_body, call_v4
11
+ from ..v4_contracts import v4_method_contract
12
+ from .v4shells import V4_EPILOG
13
+
14
+
15
+ def _forecast_param(
16
+ phrases: str, geo_ids: Optional[str], currency: str
17
+ ) -> dict[str, object]:
18
+ """Build the v4 Live CreateNewForecast parameter."""
19
+ phrase_list = parse_csv_strings(phrases)
20
+ if not phrase_list:
21
+ raise click.UsageError("--phrases must not be empty")
22
+ if len(phrase_list) > 100:
23
+ raise click.UsageError("--phrases accepts at most 100 phrases")
24
+
25
+ param: dict[str, object] = {
26
+ "Phrases": phrase_list,
27
+ "Currency": currency,
28
+ }
29
+ if geo_ids:
30
+ try:
31
+ parsed_geo_ids = parse_ids(geo_ids)
32
+ except ValueError as exc:
33
+ raise click.UsageError(str(exc)) from exc
34
+ if parsed_geo_ids:
35
+ param["GeoID"] = parsed_geo_ids
36
+ return param
37
+
38
+
39
+ def _call_forecast(
40
+ ctx,
41
+ method: str,
42
+ param,
43
+ output_format: str,
44
+ output: Optional[str],
45
+ ) -> None:
46
+ """Call one v4 Live budget forecast method and print formatted output."""
47
+ try:
48
+ client = create_v4_client(
49
+ token=ctx.obj.get("token"),
50
+ login=ctx.obj.get("login"),
51
+ profile=ctx.obj.get("profile"),
52
+ sandbox=ctx.obj.get("sandbox"),
53
+ )
54
+ data = call_v4(client, method, param)
55
+ format_output(data, output_format, output)
56
+ except Exception as e:
57
+ print_error(str(e))
58
+ raise click.Abort()
59
+
60
+
61
+ @click.group(epilog=V4_EPILOG)
62
+ def v4forecast():
63
+ """Yandex Direct v4 Live budget forecast commands."""
64
+
65
+
66
+ @v4_method_contract("CreateNewForecast")
67
+ @v4forecast.command()
68
+ @click.option("--phrases", required=True, help="Comma-separated phrases, up to 100")
69
+ @click.option("--geo-ids", help="Comma-separated geo region IDs")
70
+ @click.option(
71
+ "--currency",
72
+ default="RUB",
73
+ show_default=True,
74
+ help="Forecast currency",
75
+ )
76
+ @click.option(
77
+ "--format",
78
+ "output_format",
79
+ default="json",
80
+ type=click.Choice(["json", "table", "csv", "tsv"]),
81
+ help="Output format",
82
+ )
83
+ @click.option("--output", help="Output file")
84
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
85
+ @click.pass_context
86
+ def create(ctx, phrases, geo_ids, currency, output_format, output, dry_run):
87
+ """Create a v4 Live budget forecast."""
88
+ param = _forecast_param(phrases, geo_ids, currency)
89
+ if dry_run:
90
+ format_output(build_v4_body("CreateNewForecast", param), "json", None)
91
+ return
92
+
93
+ _call_forecast(ctx, "CreateNewForecast", param, output_format, output)
94
+
95
+
96
+ @v4_method_contract("GetForecastList")
97
+ @v4forecast.command(name="list")
98
+ @click.option(
99
+ "--format",
100
+ "output_format",
101
+ default="json",
102
+ type=click.Choice(["json", "table", "csv", "tsv"]),
103
+ help="Output format",
104
+ )
105
+ @click.option("--output", help="Output file")
106
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
107
+ @click.pass_context
108
+ def list_forecasts(ctx, output_format, output, dry_run):
109
+ """List v4 Live budget forecasts."""
110
+ if dry_run:
111
+ format_output(build_v4_body("GetForecastList"), "json", None)
112
+ return
113
+
114
+ _call_forecast(ctx, "GetForecastList", None, output_format, output)
115
+
116
+
117
+ @v4_method_contract("GetForecast")
118
+ @v4forecast.command()
119
+ @click.option(
120
+ "--forecast-id",
121
+ required=True,
122
+ type=click.IntRange(min=1),
123
+ help="Forecast ID",
124
+ )
125
+ @click.option(
126
+ "--format",
127
+ "output_format",
128
+ default="json",
129
+ type=click.Choice(["json", "table", "csv", "tsv"]),
130
+ help="Output format",
131
+ )
132
+ @click.option("--output", help="Output file")
133
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
134
+ @click.pass_context
135
+ def get(ctx, forecast_id, output_format, output, dry_run):
136
+ """Get a ready v4 Live budget forecast."""
137
+ if dry_run:
138
+ format_output(build_v4_body("GetForecast", forecast_id), "json", None)
139
+ return
140
+
141
+ _call_forecast(ctx, "GetForecast", forecast_id, output_format, output)
142
+
143
+
144
+ @v4_method_contract("DeleteForecastReport")
145
+ @v4forecast.command()
146
+ @click.option(
147
+ "--forecast-id",
148
+ required=True,
149
+ type=click.IntRange(min=1),
150
+ help="Forecast ID",
151
+ )
152
+ @click.option(
153
+ "--format",
154
+ "output_format",
155
+ default="json",
156
+ type=click.Choice(["json", "table", "csv", "tsv"]),
157
+ help="Output format",
158
+ )
159
+ @click.option("--output", help="Output file")
160
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
161
+ @click.pass_context
162
+ def delete(ctx, forecast_id, output_format, output, dry_run):
163
+ """Delete a v4 Live budget forecast."""
164
+ if dry_run:
165
+ format_output(build_v4_body("DeleteForecastReport", forecast_id), "json", None)
166
+ return
167
+
168
+ _call_forecast(ctx, "DeleteForecastReport", forecast_id, output_format, output)
@@ -15,11 +15,6 @@ def v4wordstat():
15
15
  """Yandex Direct v4 Live wordstat commands."""
16
16
 
17
17
 
18
- @click.group(epilog=V4_EPILOG)
19
- def v4forecast():
20
- """Yandex Direct v4 Live forecast commands."""
21
-
22
-
23
18
  @click.group(epilog=V4_EPILOG)
24
19
  def v4meta():
25
20
  """Yandex Direct v4 Live metadata commands."""
@@ -0,0 +1,263 @@
1
+ """Yandex Direct v4 Live tag commands."""
2
+
3
+ from typing import Optional
4
+
5
+ import click
6
+
7
+ from ..api import create_v4_client
8
+ from ..output import format_output, print_error
9
+ from ..utils import parse_ids
10
+ from ..v4 import build_v4_body, call_v4
11
+ from ..v4_contracts import v4_method_contract
12
+ from .v4shells import V4_EPILOG
13
+
14
+
15
+ def _positive_ids_param(value: str, option_name: str) -> list[int]:
16
+ """Parse a required comma-separated positive integer list."""
17
+ try:
18
+ ids = parse_ids(value)
19
+ except ValueError as exc:
20
+ raise click.UsageError(str(exc)) from exc
21
+ if not ids:
22
+ raise click.UsageError(f"{option_name} must not be empty")
23
+ if any(item <= 0 for item in ids):
24
+ raise click.UsageError(f"{option_name} must contain only positive integers")
25
+ return ids
26
+
27
+
28
+ def _tag_ids_param(tag_ids: str) -> list[int]:
29
+ """Parse v4 banner tag IDs."""
30
+ parsed = _positive_ids_param(tag_ids, "--tag-ids")
31
+ if len(parsed) > 30:
32
+ raise click.UsageError("--tag-ids accepts at most 30 tag IDs")
33
+ return parsed
34
+
35
+
36
+ def _get_campaigns_tags_param(campaign_ids: str) -> dict:
37
+ """Build the v4 Live GetCampaignsTags parameter."""
38
+ return {"CampaignIDS": _positive_ids_param(campaign_ids, "--campaign-ids")}
39
+
40
+
41
+ def _get_banners_tags_param(
42
+ campaign_ids: Optional[str], banner_ids: Optional[str]
43
+ ) -> dict:
44
+ """Build the v4 Live GetBannersTags parameter."""
45
+ if (campaign_ids is not None) == (banner_ids is not None):
46
+ raise click.UsageError("Use exactly one of --campaign-ids or --banner-ids")
47
+ if campaign_ids is not None:
48
+ ids = _positive_ids_param(campaign_ids, "--campaign-ids")
49
+ if len(ids) > 10:
50
+ raise click.UsageError("--campaign-ids accepts at most 10 campaign IDs")
51
+ return {"CampaignIDS": ids}
52
+
53
+ ids = _positive_ids_param(banner_ids or "", "--banner-ids")
54
+ if len(ids) > 2000:
55
+ raise click.UsageError("--banner-ids accepts at most 2000 banner IDs")
56
+ return {"BannerIDS": ids}
57
+
58
+
59
+ def _campaign_tag_param(tag_specs: tuple[str, ...], clear_tags: bool) -> list[dict]:
60
+ """Build campaign TagInfo objects from repeated TAG_ID=TEXT specs."""
61
+ if clear_tags:
62
+ if tag_specs:
63
+ raise click.UsageError("Use either --tag or --clear-tags, not both")
64
+ return []
65
+ if not tag_specs:
66
+ raise click.UsageError("--tag is required unless --clear-tags is used")
67
+
68
+ tags = []
69
+ seen_texts = set()
70
+ seen_existing_ids = set()
71
+ for spec in tag_specs:
72
+ text = (spec or "").strip()
73
+ tag_id_text, separator, tag_text = text.partition("=")
74
+ if not separator:
75
+ raise click.UsageError("--tag must use TAG_ID=TEXT")
76
+ tag_id_text = tag_id_text.strip()
77
+ tag_text = tag_text.strip()
78
+ try:
79
+ tag_id = int(tag_id_text)
80
+ except ValueError as exc:
81
+ raise click.UsageError("--tag ID must be a non-negative integer") from exc
82
+ if tag_id < 0:
83
+ raise click.UsageError("--tag ID must be a non-negative integer")
84
+ if tag_id > 0:
85
+ if tag_id in seen_existing_ids:
86
+ raise click.UsageError("--tag IDs must be unique")
87
+ seen_existing_ids.add(tag_id)
88
+ if not tag_text:
89
+ raise click.UsageError("--tag text must not be empty")
90
+ if len(tag_text) > 25:
91
+ raise click.UsageError("--tag text must be 25 characters or fewer")
92
+ normalized_text = tag_text.casefold()
93
+ if normalized_text in seen_texts:
94
+ raise click.UsageError("--tag texts must be unique ignoring case")
95
+ seen_texts.add(normalized_text)
96
+ tags.append({"TagID": tag_id, "Tag": tag_text})
97
+
98
+ if len(tags) > 200:
99
+ raise click.UsageError("--tag accepts at most 200 campaign tags")
100
+ return tags
101
+
102
+
103
+ def _update_campaigns_tags_param(
104
+ campaign_id: int, tag_specs: tuple[str, ...], clear_tags: bool
105
+ ) -> list[dict]:
106
+ """Build the v4 Live UpdateCampaignsTags parameter."""
107
+ return [
108
+ {
109
+ "CampaignID": campaign_id,
110
+ "Tags": _campaign_tag_param(tag_specs, clear_tags),
111
+ }
112
+ ]
113
+
114
+
115
+ def _update_banners_tags_param(
116
+ banner_ids: str, tag_ids: Optional[str], clear_tags: bool
117
+ ) -> list[dict]:
118
+ """Build the v4 Live UpdateBannersTags parameter."""
119
+ parsed_banner_ids = _positive_ids_param(banner_ids, "--banner-ids")
120
+ if clear_tags:
121
+ if tag_ids is not None:
122
+ raise click.UsageError("Use either --tag-ids or --clear-tags, not both")
123
+ parsed_tag_ids: list[int] = []
124
+ else:
125
+ if tag_ids is None:
126
+ raise click.UsageError("--tag-ids is required unless --clear-tags is used")
127
+ parsed_tag_ids = _tag_ids_param(tag_ids)
128
+ return [
129
+ {"BannerID": banner_id, "TagIDS": parsed_tag_ids}
130
+ for banner_id in parsed_banner_ids
131
+ ]
132
+
133
+
134
+ def _call_v4tags(ctx, method: str, param, output_format: str, output: str) -> None:
135
+ """Call one v4 Live tag method and print formatted output."""
136
+ try:
137
+ client = create_v4_client(
138
+ token=ctx.obj.get("token"),
139
+ login=ctx.obj.get("login"),
140
+ profile=ctx.obj.get("profile"),
141
+ sandbox=ctx.obj.get("sandbox"),
142
+ )
143
+ data = call_v4(client, method, param)
144
+ format_output(data, output_format, output)
145
+ except Exception as e:
146
+ print_error(str(e))
147
+ raise click.Abort()
148
+
149
+
150
+ @click.group(epilog=V4_EPILOG)
151
+ def v4tags():
152
+ """Yandex Direct v4 Live tag commands."""
153
+
154
+
155
+ @v4_method_contract("GetCampaignsTags")
156
+ @v4tags.command(name="get-campaigns")
157
+ @click.option("--campaign-ids", required=True, help="Comma-separated campaign IDs")
158
+ @click.option(
159
+ "--format",
160
+ "output_format",
161
+ default="json",
162
+ type=click.Choice(["json", "table", "csv", "tsv"]),
163
+ help="Output format",
164
+ )
165
+ @click.option("--output", help="Output file")
166
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
167
+ @click.pass_context
168
+ def get_campaigns(ctx, campaign_ids, output_format, output, dry_run):
169
+ """Get campaign tags."""
170
+ param = _get_campaigns_tags_param(campaign_ids)
171
+ if dry_run:
172
+ format_output(build_v4_body("GetCampaignsTags", param), "json", None)
173
+ return
174
+
175
+ _call_v4tags(ctx, "GetCampaignsTags", param, output_format, output)
176
+
177
+
178
+ @v4_method_contract("GetBannersTags")
179
+ @v4tags.command(name="get-banners")
180
+ @click.option("--campaign-ids", help="Comma-separated campaign IDs, up to 10")
181
+ @click.option("--banner-ids", help="Comma-separated banner IDs, up to 2000")
182
+ @click.option(
183
+ "--format",
184
+ "output_format",
185
+ default="json",
186
+ type=click.Choice(["json", "table", "csv", "tsv"]),
187
+ help="Output format",
188
+ )
189
+ @click.option("--output", help="Output file")
190
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
191
+ @click.pass_context
192
+ def get_banners(ctx, campaign_ids, banner_ids, output_format, output, dry_run):
193
+ """Get banner tag IDs."""
194
+ param = _get_banners_tags_param(campaign_ids, banner_ids)
195
+ if dry_run:
196
+ format_output(build_v4_body("GetBannersTags", param), "json", None)
197
+ return
198
+
199
+ _call_v4tags(ctx, "GetBannersTags", param, output_format, output)
200
+
201
+
202
+ @v4_method_contract("UpdateCampaignsTags")
203
+ @v4tags.command(name="update-campaigns")
204
+ @click.option(
205
+ "--campaign-id",
206
+ required=True,
207
+ type=click.IntRange(min=1),
208
+ help="Campaign ID",
209
+ )
210
+ @click.option(
211
+ "--tag",
212
+ "tag_specs",
213
+ multiple=True,
214
+ help="Campaign tag as TAG_ID=TEXT; use 0 for a new tag",
215
+ )
216
+ @click.option("--clear-tags", is_flag=True, help="Remove all campaign tags")
217
+ @click.option(
218
+ "--format",
219
+ "output_format",
220
+ default="json",
221
+ type=click.Choice(["json", "table", "csv", "tsv"]),
222
+ help="Output format",
223
+ )
224
+ @click.option("--output", help="Output file")
225
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
226
+ @click.pass_context
227
+ def update_campaigns(
228
+ ctx, campaign_id, tag_specs, clear_tags, output_format, output, dry_run
229
+ ):
230
+ """Replace the campaign tag list."""
231
+ param = _update_campaigns_tags_param(campaign_id, tag_specs, clear_tags)
232
+ if dry_run:
233
+ format_output(build_v4_body("UpdateCampaignsTags", param), "json", None)
234
+ return
235
+
236
+ _call_v4tags(ctx, "UpdateCampaignsTags", param, output_format, output)
237
+
238
+
239
+ @v4_method_contract("UpdateBannersTags")
240
+ @v4tags.command(name="update-banners")
241
+ @click.option("--banner-ids", required=True, help="Comma-separated banner IDs")
242
+ @click.option("--tag-ids", help="Comma-separated campaign tag IDs, up to 30")
243
+ @click.option("--clear-tags", is_flag=True, help="Remove all banner tags")
244
+ @click.option(
245
+ "--format",
246
+ "output_format",
247
+ default="json",
248
+ type=click.Choice(["json", "table", "csv", "tsv"]),
249
+ help="Output format",
250
+ )
251
+ @click.option("--output", help="Output file")
252
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
253
+ @click.pass_context
254
+ def update_banners(
255
+ ctx, banner_ids, tag_ids, clear_tags, output_format, output, dry_run
256
+ ):
257
+ """Replace banner tag assignments."""
258
+ param = _update_banners_tags_param(banner_ids, tag_ids, clear_tags)
259
+ if dry_run:
260
+ format_output(build_v4_body("UpdateBannersTags", param), "json", None)
261
+ return
262
+
263
+ _call_v4tags(ctx, "UpdateBannersTags", param, output_format, output)
@@ -58,11 +58,15 @@ SMOKE_MATRIX = {
58
58
  "strategies.get",
59
59
  "turbopages.get",
60
60
  "v4events.get-events-log",
61
+ "v4forecast.get",
62
+ "v4forecast.list",
61
63
  "v4finance.check-payment",
62
64
  "v4finance.get-clients-units",
63
65
  "v4finance.get-credit-limits",
64
66
  "v4goals.get-retargeting-goals",
65
67
  "v4goals.get-stat-goals",
68
+ "v4tags.get-banners",
69
+ "v4tags.get-campaigns",
66
70
  "v4wordstat.get-report",
67
71
  "v4wordstat.list-reports",
68
72
  "vcards.get",
@@ -143,6 +147,10 @@ SMOKE_MATRIX = {
143
147
  "strategies.update",
144
148
  "v4account.account-management",
145
149
  "v4account.enable-shared-account",
150
+ "v4forecast.create",
151
+ "v4forecast.delete",
152
+ "v4tags.update-banners",
153
+ "v4tags.update-campaigns",
146
154
  "v4wordstat.create-report",
147
155
  "v4wordstat.delete-report",
148
156
  "vcards.add",