direct-cli 0.3.6__tar.gz → 0.3.8__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 (224) hide show
  1. {direct_cli-0.3.6 → direct_cli-0.3.8}/.github/workflows/claude.yml +1 -1
  2. direct_cli-0.3.8/CHANGELOG.md +43 -0
  3. {direct_cli-0.3.6 → direct_cli-0.3.8}/CLAUDE.md +13 -1
  4. {direct_cli-0.3.6 → direct_cli-0.3.8}/PKG-INFO +58 -13
  5. {direct_cli-0.3.6 → direct_cli-0.3.8}/README.md +57 -12
  6. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/cli.py +4 -1
  7. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/ads.py +145 -26
  8. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/feeds.py +11 -1
  9. direct_cli-0.3.8/direct_cli/commands/v4forecast.py +168 -0
  10. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/v4shells.py +0 -5
  11. direct_cli-0.3.8/direct_cli/commands/v4tags.py +263 -0
  12. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/smoke_matrix.py +8 -0
  13. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/v4_contracts.py +86 -24
  14. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli.egg-info/PKG-INFO +58 -13
  15. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli.egg-info/SOURCES.txt +28 -20
  16. {direct_cli-0.3.6 → direct_cli-0.3.8}/pyproject.toml +1 -1
  17. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/build_api_coverage_report.py +85 -34
  18. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/sandbox_write_live.py +78 -10
  19. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/test_safe_commands.sh +12 -0
  20. direct_cli-0.3.8/scripts/test_sandbox_write.sh +38 -0
  21. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/API_COVERAGE.md +4 -4
  22. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/API_ISSUE_AUDIT.md +1 -1
  23. direct_cli-0.3.8/tests/_orphan_store.py +124 -0
  24. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/api_coverage_payloads.py +424 -31
  25. direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +437 -0
  26. direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +437 -0
  27. direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +58 -0
  28. direct_cli-0.3.6/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml → direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteRetargetingUpdate.test_retargeting_update.yaml +29 -29
  29. direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +69 -0
  30. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/conftest.py +9 -0
  31. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_api_coverage.py +353 -3
  32. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_cli_contract.py +1 -0
  33. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_comprehensive.py +1 -0
  34. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_dry_run.py +55 -12
  35. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_integration.py +117 -0
  36. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_integration_write.py +256 -0
  37. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_low_coverage_payloads.py +213 -8
  38. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_smoke_matrix.py +75 -4
  39. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4_contracts.py +86 -0
  40. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4_foundation.py +2 -0
  41. direct_cli-0.3.8/tests/test_v4_live_contracts.py +341 -0
  42. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4_safety.py +14 -0
  43. direct_cli-0.3.8/tests/test_v4forecast.py +238 -0
  44. direct_cli-0.3.8/tests/test_v4tags.py +321 -0
  45. direct_cli-0.3.6/tests/test_integration_live_write.py → direct_cli-0.3.8/tests/test_v5_live_write.py +27 -28
  46. direct_cli-0.3.6/.github/workflows/claude-code-review.yml +0 -44
  47. direct_cli-0.3.6/CHANGELOG.md +0 -11
  48. direct_cli-0.3.6/scripts/test_sandbox_write.sh +0 -23
  49. direct_cli-0.3.6/tests/test_v4_live_contracts.py +0 -190
  50. {direct_cli-0.3.6 → direct_cli-0.3.8}/.env.example +0 -0
  51. {direct_cli-0.3.6 → direct_cli-0.3.8}/.github/copilot-instructions.md +0 -0
  52. {direct_cli-0.3.6 → direct_cli-0.3.8}/.github/workflows/api-coverage.yml +0 -0
  53. {direct_cli-0.3.6 → direct_cli-0.3.8}/.github/workflows/quality.yml +0 -0
  54. {direct_cli-0.3.6 → direct_cli-0.3.8}/.gitignore +0 -0
  55. {direct_cli-0.3.6 → direct_cli-0.3.8}/AGENTS.md +0 -0
  56. {direct_cli-0.3.6 → direct_cli-0.3.8}/MANIFEST.in +0 -0
  57. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/__init__.py +0 -0
  58. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_deprecated.py +0 -0
  59. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_smoke_probes.py +0 -0
  60. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/__init__.py +0 -0
  61. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  62. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
  63. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  64. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  65. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +0 -0
  66. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
  67. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  68. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
  69. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  70. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  71. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/api.py +0 -0
  72. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/auth.py +0 -0
  73. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/__init__.py +0 -0
  74. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/adextensions.py +0 -0
  75. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/adgroups.py +0 -0
  76. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/adimages.py +0 -0
  77. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/advideos.py +0 -0
  78. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/agencyclients.py +0 -0
  79. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/audiencetargets.py +0 -0
  80. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/auth.py +0 -0
  81. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/balance.py +0 -0
  82. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/bidmodifiers.py +0 -0
  83. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/bids.py +0 -0
  84. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/businesses.py +0 -0
  85. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/campaigns.py +0 -0
  86. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/changes.py +0 -0
  87. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/clients.py +0 -0
  88. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/creatives.py +0 -0
  89. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/dictionaries.py +0 -0
  90. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/dynamicads.py +0 -0
  91. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/dynamicfeedadtargets.py +0 -0
  92. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/keywordbids.py +0 -0
  93. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/keywords.py +0 -0
  94. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/keywordsresearch.py +0 -0
  95. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/leads.py +0 -0
  96. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
  97. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/reports.py +0 -0
  98. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/retargeting.py +0 -0
  99. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/sitelinks.py +0 -0
  100. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/smartadtargets.py +0 -0
  101. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/strategies.py +0 -0
  102. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/turbopages.py +0 -0
  103. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/v4account.py +0 -0
  104. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/v4events.py +0 -0
  105. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/v4finance.py +0 -0
  106. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/v4goals.py +0 -0
  107. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/v4wordstat.py +0 -0
  108. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/commands/vcards.py +0 -0
  109. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/output.py +0 -0
  110. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/reports_coverage.py +0 -0
  111. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/utils.py +0 -0
  112. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/v4/__init__.py +0 -0
  113. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/v4/money.py +0 -0
  114. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli/wsdl_coverage.py +0 -0
  115. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli.egg-info/dependency_links.txt +0 -0
  116. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli.egg-info/entry_points.txt +0 -0
  117. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli.egg-info/requires.txt +0 -0
  118. {direct_cli-0.3.6 → direct_cli-0.3.8}/direct_cli.egg-info/top_level.txt +0 -0
  119. {direct_cli-0.3.6 → direct_cli-0.3.8}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  120. {direct_cli-0.3.6 → direct_cli-0.3.8}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  121. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/anonymize_cassettes.py +0 -0
  122. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/build_api_coverage_checklist.py +0 -0
  123. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/check_reports_drift.py +0 -0
  124. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/check_wsdl_drift.py +0 -0
  125. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/patch_vendor_imports.py +0 -0
  126. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/refresh_reports_cache.py +0 -0
  127. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/refresh_wsdl_cache.py +0 -0
  128. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/release_pypi.sh +0 -0
  129. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/test_dangerous_commands.sh +0 -0
  130. {direct_cli-0.3.6 → direct_cli-0.3.8}/scripts/update_vendor.sh +0 -0
  131. {direct_cli-0.3.6 → direct_cli-0.3.8}/setup.cfg +0 -0
  132. {direct_cli-0.3.6 → direct_cli-0.3.8}/setup.py +0 -0
  133. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/MANUAL_COVERAGE.md +0 -0
  134. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/__init__.py +0 -0
  135. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  136. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  137. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  138. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  139. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  140. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  141. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  142. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  143. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  144. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  145. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  146. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  147. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  148. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  149. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  150. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  151. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
  152. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  153. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_adgroups_add_update_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_adgroups_add_update_delete.yaml +0 -0
  154. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_adimages_add_get_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_adimages_add_get_delete.yaml +0 -0
  155. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_ads_add_update_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_add_update_delete.yaml +0 -0
  156. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_ads_suspend_resume_archive_unarchive.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
  157. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_advideos_add_get.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_advideos_add_get.yaml +0 -0
  158. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_add_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_add_delete.yaml +0 -0
  159. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_suspend_resume.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_suspend_resume.yaml +0 -0
  160. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_bids_set.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_bids_set.yaml +0 -0
  161. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_campaign_create_get_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_campaign_create_get_delete.yaml +0 -0
  162. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_creatives_chain_advideo_to_creative.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
  163. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_add_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_add_delete.yaml +0 -0
  164. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_suspend_resume.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_suspend_resume.yaml +0 -0
  165. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_keywordbids_set.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywordbids_set.yaml +0 -0
  166. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_keywords_add_update_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_add_update_delete.yaml +0 -0
  167. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_keywords_suspend_resume.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_suspend_resume.yaml +0 -0
  168. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_sitelinks_add_get_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_sitelinks_add_get_delete.yaml +0 -0
  169. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_add_update_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_add_update_delete.yaml +0 -0
  170. /direct_cli-0.3.6/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_suspend_resume.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_suspend_resume.yaml +0 -0
  171. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/fixtures/test-video.mp4 +0 -0
  172. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/reports_cache/raw/fields-list.html +0 -0
  173. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/reports_cache/raw/headers.html +0 -0
  174. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/reports_cache/raw/period.html +0 -0
  175. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/reports_cache/raw/spec.html +0 -0
  176. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/reports_cache/raw/type.html +0 -0
  177. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/reports_cache/spec.json +0 -0
  178. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_auth_bw.py +0 -0
  179. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_auth_oauth.py +0 -0
  180. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_auth_op.py +0 -0
  181. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_balance.py +0 -0
  182. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_cli.py +0 -0
  183. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_reports_drift.py +0 -0
  184. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_reports_parsing.py +0 -0
  185. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_transport_contract.py +0 -0
  186. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4account.py +0 -0
  187. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4events.py +0 -0
  188. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4finance_money.py +0 -0
  189. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4finance_read.py +0 -0
  190. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4goals.py +0 -0
  191. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_v4wordstat.py +0 -0
  192. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/test_vendor_imports.py +0 -0
  193. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/adextensions.xml +0 -0
  194. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/adgroups.xml +0 -0
  195. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/adimages.xml +0 -0
  196. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/ads.xml +0 -0
  197. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/advideos.xml +0 -0
  198. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/agencyclients.xml +0 -0
  199. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/audiencetargets.xml +0 -0
  200. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  201. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/bids.xml +0 -0
  202. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/businesses.xml +0 -0
  203. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/campaigns.xml +0 -0
  204. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/changes.xml +0 -0
  205. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/clients.xml +0 -0
  206. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/creatives.xml +0 -0
  207. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/dictionaries.xml +0 -0
  208. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  209. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  210. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/feeds.xml +0 -0
  211. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  212. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/imports/general.xsd +0 -0
  213. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  214. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/keywordbids.xml +0 -0
  215. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/keywords.xml +0 -0
  216. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  217. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/leads.xml +0 -0
  218. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  219. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/retargetinglists.xml +0 -0
  220. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/sitelinks.xml +0 -0
  221. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/smartadtargets.xml +0 -0
  222. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/strategies.xml +0 -0
  223. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/turbopages.xml +0 -0
  224. {direct_cli-0.3.6 → direct_cli-0.3.8}/tests/wsdl_cache/vcards.xml +0 -0
@@ -46,5 +46,5 @@ jobs:
46
46
  # Optional: Add claude_args to customize behavior and configuration
47
47
  # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
48
48
  # or https://code.claude.com/docs/en/cli-reference for available options
49
- # claude_args: '--allowed-tools Bash(gh pr:*)'
49
+ claude_args: "--model claude-opus-4-7"
50
50
 
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ ## 0.3.8
4
+
5
+ **BREAKING CHANGES:**
6
+
7
+ - `direct ads update` now requires `--type {TEXT_AD,TEXT_IMAGE_AD,MOBILE_APP_AD}`. Scripts that called `ads update` with only field flags will fail with `Missing option '--type'`. Mirrors the WSDL one-of choice between TextAd/TextImageAd/MobileAppAd update subtypes (PR #197).
8
+ - `direct ads add --type TEXT_IMAGE_AD` rejects `--title/--text` (TEXT_IMAGE_AD has no such WSDL fields). `direct ads update --status` rejected — use `ads suspend/resume/archive/unarchive` for status changes (PR #190).
9
+ - `direct ads add --type MOBILE_APP_AD --href` rejected — MobileAppAd uses `--tracking-url`, not `--href` (PR #196).
10
+ - `direct feeds add` now requires `--business-type {RETAIL,HOTELS,REALTY,AUTOMOBILES,FLIGHTS,OTHER}`. Mirrors WSDL FeedAddItem.BusinessType (minOccurs=1) (PR #201).
11
+
12
+ **Schema gate — mutating ops parity:**
13
+
14
+ - Extended the WSDL `*FieldNames` schema gate (introduced for `get` in 0.3.7) to mutating operations (`add/update/set/setBids/lifecycle`). Added per-operation waiver granularity via `SCHEMA_GATE_OPERATION_WAIVERS` (PR #181).
15
+ - Promoted dynamicads, bidmodifiers add/set, adimages/advideos/vcards add (media payloads), adextensions/retargeting/feeds.add typed fixtures to `PAYLOAD_CASES` (PRs #184, #185, #187, #188).
16
+ - Added MOBILE_APP_AD branch to `ads add` mirroring WSDL `MobileAppAdAdd` (PR #190).
17
+ - `bidmodifiers.delete` correctly classified as a real destructive WSDL operation and added to schema gate (PR #194); the earlier "Helper/legacy surface" rationale was a mis-classification — see post-mortem in #199 / PR #200.
18
+
19
+ **Strict WSDL parity policy:**
20
+
21
+ - Documented "Strict WSDL parity" principle in `CLAUDE.md`: `DRY_RUN_PAYLOAD_EXCLUSIONS` may only contain entries from five legitimate categories (read-path `*.get`, runtime-deprecated, v4-not-in-v5-wsdl, custom non-RPC endpoints, methods covered by `tests/test_dry_run.py`). New guard test `test_dry_run_exclusions_have_no_helper_or_legacy_rationale` fails CI if any rationale uses banned phrases (PR #200).
22
+
23
+ **Integration test coverage:**
24
+
25
+ - Added read-only sandbox integration tests for `changes`, `keywordsresearch`, `balance` (PR #186).
26
+ - Added v5 write integration coverage for `strategies` lifecycle, `retargeting update`, `bids get/set-auto`, plus `auth status/list` read-only tests (PR #189).
27
+ - Re-recorded TestWriteBidsRead cassettes against live API and rewrote host to sandbox so the bids endpoints get real coverage in replay mode (PR #193).
28
+
29
+ **CI infrastructure:**
30
+
31
+ - Switched Claude code-review GitHub Action from default (Sonnet 4.5) to Claude Opus 4.7 for deeper PR review (PR #192).
32
+
33
+ **Refs:** Closes issues #118, #136, #137, #175, #176, #180, #183, #191, #199.
34
+
35
+ ## 0.3.3
36
+
37
+ **BREAKING CHANGE:** OAuth profiles created before 0.3.3 (without `refresh_token` and `expires_at`) are no longer accepted. Any such profile will fail immediately with an "incomplete profile" error. Run `direct auth login --profile <name>` to re-authenticate and create a valid 0.3.3 profile.
38
+
39
+ - Added refresh token persistence for OAuth profiles.
40
+ - Added automatic OAuth access token refresh before expiry.
41
+ - Added `expires_in` details to `direct auth status`.
42
+ - Added JSON output for `direct auth status`.
43
+ - Kept `direct auth login --oauth-token` as a manual access-token import without auto-refresh.
@@ -24,7 +24,9 @@ Click group-of-groups. Each Yandex Direct API resource = one file in `direct_cli
24
24
 
25
25
  **Request flow:** `cli.py` → `auth.py` (resolves token/login) → `api.py` (`create_client`) → `tapi_yandex_direct.YandexDirect` → Yandex API → `output.py` (format/print).
26
26
 
27
- **Credentials priority:** CLI flags (`--token`, `--login`) > env vars (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN`) > `.env` file. `load_dotenv()` runs at `cli.py` import time.
27
+ **Credentials priority (CLI):** CLI flags (`--token`, `--login`) > active profile from `direct auth login` > env vars (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN`) > `.env` file > 1Password/Bitwarden refs. See `direct_cli/auth.py:600` (`get_credentials`) and README table for the full chain. `load_dotenv()` runs at `cli.py` import time.
28
+
29
+ **Credentials priority (tests):** **inverted** — env vars > active profile > skip. Tests must not silently hit production when a developer has an active `direct auth` profile, so env vars take precedence over the profile (see `tests/test_v4_live_contracts.py::_credentials`).
28
30
 
29
31
  **Shared utilities** (`utils.py`): `parse_ids`, `parse_json`, `build_selection_criteria`, `build_common_params`, `get_default_fields`, `COMMON_FIELDS` dict. All command modules import from here — don't duplicate.
30
32
 
@@ -41,6 +43,15 @@ Click group-of-groups. Each Yandex Direct API resource = one file in `direct_cli
41
43
 
42
44
  **Runtime-deprecated methods:** WSDL-visible methods that Yandex rejects at runtime belong in `RUNTIME_DEPRECATED_METHODS` (`direct_cli/wsdl_coverage.py`) and must fail with `click.UsageError` before request construction. `agencyclients add` is blocked this way; use `agencyclients add-passport-organization`.
43
45
 
46
+ **Strict WSDL parity:** `DRY_RUN_PAYLOAD_EXCLUSIONS` in `tests/api_coverage_payloads.py` must NOT contain any entry whose rationale claims the CLI surface is a «helper», «legacy», or «not part of strict WSDL parity». If the WSDL declares the operation, the CLI mirrors it 1:1 with a `PAYLOAD_CASES` fixture. Legitimate permanent exclusions are limited to:
47
+ - read-path `*.get` (covered by SelectionCriteria tests);
48
+ - runtime-deprecated methods (see `RUNTIME_DEPRECATED_METHODS`);
49
+ - v4 methods that have no v5 WSDL (covered by `direct_cli/v4_contracts.py`);
50
+ - custom non-RPC endpoints (e.g. `reports.get` — TSV stream);
51
+ - methods explicitly covered by `tests/test_dry_run.py::test_<service>_<op>_payload`.
52
+
53
+ A guard in `tests/test_api_coverage.py::test_dry_run_exclusions_have_no_helper_or_legacy_rationale` enforces this — any rationale outside those five categories that uses the banned phrasing is a mis-classification: write a `PAYLOAD_CASES` fixture instead. See post-mortem in issue #199.
54
+
44
55
  **SelectionCriteria:** Resources like `adgroups`, `ads`, `keywords` require at least one of `Ids`, `CampaignIds`, or `AdGroupIds` — otherwise API error 4001.
45
56
 
46
57
  **Error handling:** All commands wrap API calls in `try/except Exception` → `print_error(str(e))` + `raise click.Abort()`.
@@ -55,6 +66,7 @@ Click group-of-groups. Each Yandex Direct API resource = one file in `direct_cli
55
66
 
56
67
  - **Unit** (`test_cli.py`, `test_comprehensive.py`) — no API calls, no token needed.
57
68
  - **Integration** (`test_integration.py`, `@pytest.mark.integration`) — require `.env` with `YANDEX_DIRECT_TOKEN` and `YANDEX_DIRECT_LOGIN`. Auto-skip if absent.
69
+ - **Credential resolution in tests:** env vars first, then active `direct auth` profile, then skip. This is **inverted** vs. CLI (where the profile wins) on purpose: a developer machine with an active profile must not silently hit production on a plain `pytest`.
58
70
 
59
71
  ## Dangerous Commands — Never Auto-Test
60
72
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: Command-line interface for Yandex Direct API
5
5
  Author: axisrow
6
6
  License: MIT
@@ -125,6 +125,8 @@ fall back to base `YANDEX_DIRECT_LOGIN`; this prevents mixing a profile token
125
125
  with a login from the project `.env`. For multi-account setups, prefer OAuth
126
126
  profiles or profile-specific env vars instead of base credentials.
127
127
 
128
+ > **Tests use the inverted order.** Live-API test suites (e.g. `tests/test_v4_live_contracts.py`) read `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` from the environment first, only then fall back to the active `direct auth` profile, and skip the test if neither is set. This is intentional: a developer machine with an active profile must not silently hit production on a plain `pytest` invocation. See `CLAUDE.md` for the contract.
129
+
128
130
  Install with `pip install direct-cli`, then run commands with `direct`.
129
131
  Invoking the deprecated `direct-cli` entrypoint exits with
130
132
  `use direct instead of direct-cli`.
@@ -158,6 +160,24 @@ direct v4goals get-retargeting-goals --campaign-ids 123,456 --format table
158
160
  direct v4goals get-stat-goals --campaign-ids 123 --dry-run
159
161
  ```
160
162
 
163
+ ### V4 Live Tags
164
+
165
+ Campaign tags are managed as `{TagID, Tag}` pairs. Use `TagID=0` to create a
166
+ new campaign tag. Banner/ad tags are assigned by campaign tag IDs. Update
167
+ methods replace the full tag list for the target campaign or banner, so pass
168
+ existing tags again if they must remain assigned. Ad group tags are filter-only
169
+ through `direct adgroups get --tag-ids/--tags`; this release does not add ad
170
+ group tag mutation commands.
171
+
172
+ ```bash
173
+ direct v4tags get-campaigns --campaign-ids 3193279,1634563
174
+ direct v4tags get-banners --banner-ids 2571700,2571745
175
+ direct v4tags get-banners --campaign-ids 3193279
176
+ direct v4tags update-campaigns --campaign-id 3193279 --tag 0=akapulko --tag 16590=orange --dry-run
177
+ direct v4tags update-banners --banner-ids 2571700,2571745 --tag-ids 16590,16734 --dry-run
178
+ direct v4tags update-banners --banner-ids 2571700 --clear-tags --dry-run
179
+ ```
180
+
161
181
  ### V4 Live Events
162
182
 
163
183
  ```bash
@@ -178,6 +198,19 @@ direct v4wordstat get-report --report-id 123 --format table
178
198
  direct v4wordstat delete-report --report-id 123
179
199
  ```
180
200
 
201
+ ### V4 Live Budget Forecasts
202
+
203
+ Budget forecasts are asynchronous. Direct CLI makes exactly one API call per
204
+ command and does not poll automatically; repeat `list` or `get` yourself until
205
+ the forecast is ready.
206
+
207
+ ```bash
208
+ direct v4forecast create --phrases "buy laptop,buy desktop" --geo-ids 213 --currency RUB
209
+ direct v4forecast list --format table
210
+ direct v4forecast get --forecast-id 123 --format table
211
+ direct v4forecast delete --forecast-id 123
212
+ ```
213
+
181
214
  ### V4 Live Finance
182
215
 
183
216
  Finance methods require an extra financial token for money operations. In the
@@ -462,7 +495,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
462
495
  direct adextensions add --callout-text "Free shipping" --dry-run
463
496
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
464
497
  direct creatives add --video-id video-id --dry-run
465
- direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --dry-run
498
+ direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
466
499
  direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
467
500
  direct clients update --client-info "Priority client" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
468
501
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -534,17 +567,22 @@ Four tiers of tests live under `tests/`:
534
567
  | Unit / CLI wiring / dry-run | *(none)* | No | No |
535
568
  | Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
536
569
  | Write integration | `-m integration_write` | No (replays VCR cassettes) | No |
537
- | Live draft write integration | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
570
+ | Live draft write integration (v5) | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
571
+ | v4 live read | `-m v4_live_read` | Yes (production v4 JSON API, read-only) | Yes |
572
+ | v4 live account-level report write (opt-in) | `-k _opt_in_write` in `tests/test_v4_live_contracts.py` | Yes (production v4) | Yes + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
538
573
 
539
574
  ```bash
540
575
  pip install -e ".[dev]"
541
576
  pytest # fast tier — no token
542
577
  pytest -m integration -v # read-only integration tests (needs token)
543
578
  pytest -m integration_write -v # write cassette replay (no token needed)
544
- YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay
579
+ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5)
545
580
  YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # re-record live draft cassette
581
+ YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # v4 wordstat/forecast account-level lifecycle
546
582
  ```
547
583
 
584
+ The v4 account-level write tier (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) creates real Wordstat and forecast reports in the production account and deletes them in the same run. There are **no cassettes** — these tests run against live API only. Created IDs are tracked in `~/.direct-cli/test-orphans.json` so that if the run is interrupted between create and delete, the next invocation will retry the cleanup automatically (see `tests/_orphan_store.py`).
585
+
548
586
  #### Smoke command scripts
549
587
 
550
588
  Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
@@ -562,9 +600,9 @@ Current command surface:
562
600
  | WSDL-backed API services | 29 |
563
601
  | Supported API services including Reports | 30 |
564
602
  | WSDL operations | 112 |
565
- | CLI groups including `auth` | 39 |
566
- | CLI subcommands including `auth` | 130 |
567
- | API CLI subcommands excluding `auth` | 126 |
603
+ | CLI groups including `auth` | 40 |
604
+ | CLI subcommands including `auth` | 144 |
605
+ | API CLI subcommands excluding `auth` | 140 |
568
606
 
569
607
  ### API Coverage And Drift Monitoring
570
608
 
@@ -782,6 +820,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
782
820
  логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
783
821
  или профильные env vars, а не базовые credentials.
784
822
 
823
+ > **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
824
+
785
825
  Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
786
826
  через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
787
827
  подсказкой `use direct instead of direct-cli`.
@@ -1082,7 +1122,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
1082
1122
  direct adextensions add --callout-text "Free shipping" --dry-run
1083
1123
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
1084
1124
  direct creatives add --video-id video-id --dry-run
1085
- direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --dry-run
1125
+ direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
1086
1126
  direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
1087
1127
  direct clients update --client-info "Приоритетный клиент" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
1088
1128
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -1155,17 +1195,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
1155
1195
  | Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
1156
1196
  | Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
1157
1197
  | Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
1158
- | Live draft write интеграция | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
1198
+ | Live draft write интеграция (v5) | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
1199
+ | v4 live read | `-m v4_live_read` | Да (prod v4 JSON API, только чтение) | Да |
1200
+ | v4 live запись отчётов на уровне аккаунта (opt-in) | `-k _opt_in_write` в `tests/test_v4_live_contracts.py` | Да (prod v4) | Да + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
1159
1201
 
1160
1202
  ```bash
1161
1203
  pip install -e ".[dev]"
1162
1204
  pytest # быстрый уровень — без токена
1163
1205
  pytest -m integration -v # read-only интеграция (нужен токен)
1164
1206
  pytest -m integration_write -v # replay write-кассет (токен не нужен)
1165
- YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты
1207
+ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты (v5)
1166
1208
  YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # перезапись live draft-кассеты
1209
+ YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # жизненный цикл v4 wordstat/forecast
1167
1210
  ```
1168
1211
 
1212
+ Уровень v4 account-level write (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) создаёт настоящие Wordstat-отчёты и прогнозы в боевом аккаунте и удаляет их в том же запуске. **Кассет нет** — эти тесты идут только в живой API. Созданные ID пишутся в `~/.direct-cli/test-orphans.json`: если запуск оборвался между create и delete, при следующем вызове осиротевшие ID будут удалены автоматически (см. `tests/_orphan_store.py`).
1213
+
1169
1214
  #### Smoke-скрипты команд
1170
1215
 
1171
1216
  Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.
@@ -1183,9 +1228,9 @@ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rew
1183
1228
  | WSDL-backed API services | 29 |
1184
1229
  | API services с учётом Reports | 30 |
1185
1230
  | WSDL operations | 112 |
1186
- | CLI groups с `auth` | 39 |
1187
- | CLI subcommands с `auth` | 130 |
1188
- | API CLI subcommands без `auth` | 126 |
1231
+ | CLI groups с `auth` | 40 |
1232
+ | CLI subcommands с `auth` | 144 |
1233
+ | API CLI subcommands без `auth` | 140 |
1189
1234
 
1190
1235
  #### Live sandbox write smoke
1191
1236
 
@@ -82,6 +82,8 @@ fall back to base `YANDEX_DIRECT_LOGIN`; this prevents mixing a profile token
82
82
  with a login from the project `.env`. For multi-account setups, prefer OAuth
83
83
  profiles or profile-specific env vars instead of base credentials.
84
84
 
85
+ > **Tests use the inverted order.** Live-API test suites (e.g. `tests/test_v4_live_contracts.py`) read `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` from the environment first, only then fall back to the active `direct auth` profile, and skip the test if neither is set. This is intentional: a developer machine with an active profile must not silently hit production on a plain `pytest` invocation. See `CLAUDE.md` for the contract.
86
+
85
87
  Install with `pip install direct-cli`, then run commands with `direct`.
86
88
  Invoking the deprecated `direct-cli` entrypoint exits with
87
89
  `use direct instead of direct-cli`.
@@ -115,6 +117,24 @@ direct v4goals get-retargeting-goals --campaign-ids 123,456 --format table
115
117
  direct v4goals get-stat-goals --campaign-ids 123 --dry-run
116
118
  ```
117
119
 
120
+ ### V4 Live Tags
121
+
122
+ Campaign tags are managed as `{TagID, Tag}` pairs. Use `TagID=0` to create a
123
+ new campaign tag. Banner/ad tags are assigned by campaign tag IDs. Update
124
+ methods replace the full tag list for the target campaign or banner, so pass
125
+ existing tags again if they must remain assigned. Ad group tags are filter-only
126
+ through `direct adgroups get --tag-ids/--tags`; this release does not add ad
127
+ group tag mutation commands.
128
+
129
+ ```bash
130
+ direct v4tags get-campaigns --campaign-ids 3193279,1634563
131
+ direct v4tags get-banners --banner-ids 2571700,2571745
132
+ direct v4tags get-banners --campaign-ids 3193279
133
+ direct v4tags update-campaigns --campaign-id 3193279 --tag 0=akapulko --tag 16590=orange --dry-run
134
+ direct v4tags update-banners --banner-ids 2571700,2571745 --tag-ids 16590,16734 --dry-run
135
+ direct v4tags update-banners --banner-ids 2571700 --clear-tags --dry-run
136
+ ```
137
+
118
138
  ### V4 Live Events
119
139
 
120
140
  ```bash
@@ -135,6 +155,19 @@ direct v4wordstat get-report --report-id 123 --format table
135
155
  direct v4wordstat delete-report --report-id 123
136
156
  ```
137
157
 
158
+ ### V4 Live Budget Forecasts
159
+
160
+ Budget forecasts are asynchronous. Direct CLI makes exactly one API call per
161
+ command and does not poll automatically; repeat `list` or `get` yourself until
162
+ the forecast is ready.
163
+
164
+ ```bash
165
+ direct v4forecast create --phrases "buy laptop,buy desktop" --geo-ids 213 --currency RUB
166
+ direct v4forecast list --format table
167
+ direct v4forecast get --forecast-id 123 --format table
168
+ direct v4forecast delete --forecast-id 123
169
+ ```
170
+
138
171
  ### V4 Live Finance
139
172
 
140
173
  Finance methods require an extra financial token for money operations. In the
@@ -419,7 +452,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
419
452
  direct adextensions add --callout-text "Free shipping" --dry-run
420
453
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
421
454
  direct creatives add --video-id video-id --dry-run
422
- direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --dry-run
455
+ direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
423
456
  direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
424
457
  direct clients update --client-info "Priority client" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
425
458
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -491,17 +524,22 @@ Four tiers of tests live under `tests/`:
491
524
  | Unit / CLI wiring / dry-run | *(none)* | No | No |
492
525
  | Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
493
526
  | Write integration | `-m integration_write` | No (replays VCR cassettes) | No |
494
- | Live draft write integration | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
527
+ | Live draft write integration (v5) | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
528
+ | v4 live read | `-m v4_live_read` | Yes (production v4 JSON API, read-only) | Yes |
529
+ | v4 live account-level report write (opt-in) | `-k _opt_in_write` in `tests/test_v4_live_contracts.py` | Yes (production v4) | Yes + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
495
530
 
496
531
  ```bash
497
532
  pip install -e ".[dev]"
498
533
  pytest # fast tier — no token
499
534
  pytest -m integration -v # read-only integration tests (needs token)
500
535
  pytest -m integration_write -v # write cassette replay (no token needed)
501
- YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay
536
+ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5)
502
537
  YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # re-record live draft cassette
538
+ YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # v4 wordstat/forecast account-level lifecycle
503
539
  ```
504
540
 
541
+ The v4 account-level write tier (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) creates real Wordstat and forecast reports in the production account and deletes them in the same run. There are **no cassettes** — these tests run against live API only. Created IDs are tracked in `~/.direct-cli/test-orphans.json` so that if the run is interrupted between create and delete, the next invocation will retry the cleanup automatically (see `tests/_orphan_store.py`).
542
+
505
543
  #### Smoke command scripts
506
544
 
507
545
  Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
@@ -519,9 +557,9 @@ Current command surface:
519
557
  | WSDL-backed API services | 29 |
520
558
  | Supported API services including Reports | 30 |
521
559
  | WSDL operations | 112 |
522
- | CLI groups including `auth` | 39 |
523
- | CLI subcommands including `auth` | 130 |
524
- | API CLI subcommands excluding `auth` | 126 |
560
+ | CLI groups including `auth` | 40 |
561
+ | CLI subcommands including `auth` | 144 |
562
+ | API CLI subcommands excluding `auth` | 140 |
525
563
 
526
564
  ### API Coverage And Drift Monitoring
527
565
 
@@ -739,6 +777,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
739
777
  логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
740
778
  или профильные env vars, а не базовые credentials.
741
779
 
780
+ > **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
781
+
742
782
  Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
743
783
  через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
744
784
  подсказкой `use direct instead of direct-cli`.
@@ -1039,7 +1079,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
1039
1079
  direct adextensions add --callout-text "Free shipping" --dry-run
1040
1080
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
1041
1081
  direct creatives add --video-id video-id --dry-run
1042
- direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --dry-run
1082
+ direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
1043
1083
  direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
1044
1084
  direct clients update --client-info "Приоритетный клиент" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
1045
1085
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -1112,17 +1152,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
1112
1152
  | Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
1113
1153
  | Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
1114
1154
  | Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
1115
- | Live draft write интеграция | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
1155
+ | Live draft write интеграция (v5) | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
1156
+ | v4 live read | `-m v4_live_read` | Да (prod v4 JSON API, только чтение) | Да |
1157
+ | v4 live запись отчётов на уровне аккаунта (opt-in) | `-k _opt_in_write` в `tests/test_v4_live_contracts.py` | Да (prod v4) | Да + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
1116
1158
 
1117
1159
  ```bash
1118
1160
  pip install -e ".[dev]"
1119
1161
  pytest # быстрый уровень — без токена
1120
1162
  pytest -m integration -v # read-only интеграция (нужен токен)
1121
1163
  pytest -m integration_write -v # replay write-кассет (токен не нужен)
1122
- YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты
1164
+ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты (v5)
1123
1165
  YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # перезапись live draft-кассеты
1166
+ YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # жизненный цикл v4 wordstat/forecast
1124
1167
  ```
1125
1168
 
1169
+ Уровень v4 account-level write (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) создаёт настоящие Wordstat-отчёты и прогнозы в боевом аккаунте и удаляет их в том же запуске. **Кассет нет** — эти тесты идут только в живой API. Созданные ID пишутся в `~/.direct-cli/test-orphans.json`: если запуск оборвался между create и delete, при следующем вызове осиротевшие ID будут удалены автоматически (см. `tests/_orphan_store.py`).
1170
+
1126
1171
  #### Smoke-скрипты команд
1127
1172
 
1128
1173
  Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.
@@ -1140,9 +1185,9 @@ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rew
1140
1185
  | WSDL-backed API services | 29 |
1141
1186
  | API services с учётом Reports | 30 |
1142
1187
  | WSDL operations | 112 |
1143
- | CLI groups с `auth` | 39 |
1144
- | CLI subcommands с `auth` | 130 |
1145
- | API CLI subcommands без `auth` | 126 |
1188
+ | CLI groups с `auth` | 40 |
1189
+ | CLI subcommands с `auth` | 144 |
1190
+ | API CLI subcommands без `auth` | 140 |
1146
1191
 
1147
1192
  #### Live sandbox write smoke
1148
1193
 
@@ -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,
@@ -163,21 +163,40 @@ def get(
163
163
  default="TEXT_AD",
164
164
  help="Ad type",
165
165
  )
166
- @click.option("--title", help="Ad title (TEXT_AD only)")
167
- @click.option("--text", help="Ad text (TEXT_AD only)")
168
- @click.option("--href", help="Ad URL (TEXT_AD only)")
169
- @click.option("--image-hash", help="Ad image hash for TEXT_IMAGE_AD")
166
+ @click.option("--title", help="Ad title (TEXT_AD / MOBILE_APP_AD)")
167
+ @click.option("--text", help="Ad text (TEXT_AD / MOBILE_APP_AD)")
168
+ @click.option("--href", help="Ad URL (TEXT_AD / TEXT_IMAGE_AD)")
169
+ @click.option("--image-hash", help="Ad image hash (TEXT_IMAGE_AD / MOBILE_APP_AD)")
170
+ @click.option(
171
+ "--action",
172
+ help="MOBILE_APP_AD call-to-action (MobileAppAdActionEnum, e.g. INSTALL)",
173
+ )
174
+ @click.option("--tracking-url", help="MOBILE_APP_AD tracking URL")
175
+ @click.option("--age-label", help="MOBILE_APP_AD age label (MobAppAgeLabelEnum)")
170
176
  @click.option("--dry-run", is_flag=True, help="Show request without sending")
171
177
  @click.pass_context
172
- def add(ctx, adgroup_id, ad_type, title, text, href, image_hash, dry_run):
178
+ def add(
179
+ ctx,
180
+ adgroup_id,
181
+ ad_type,
182
+ title,
183
+ text,
184
+ href,
185
+ image_hash,
186
+ action,
187
+ tracking_url,
188
+ age_label,
189
+ dry_run,
190
+ ):
173
191
  """Add new ad"""
174
192
  try:
175
193
  ad_type_norm = (ad_type or "TEXT_AD").upper().replace("-", "_")
176
- supported_types = {"TEXT_AD", "TEXT_IMAGE_AD"}
194
+ supported_types = {"TEXT_AD", "TEXT_IMAGE_AD", "MOBILE_APP_AD"}
177
195
  if ad_type_norm not in supported_types:
178
196
  raise click.UsageError(
179
197
  "Invalid value for '--type': "
180
- f"{ad_type!r} is not one of 'TEXT_AD', 'TEXT_IMAGE_AD'."
198
+ f"{ad_type!r} is not one of "
199
+ "'TEXT_AD', 'TEXT_IMAGE_AD', 'MOBILE_APP_AD'."
181
200
  )
182
201
 
183
202
  ad_data = {"AdGroupId": adgroup_id}
@@ -200,6 +219,11 @@ def add(ctx, adgroup_id, ad_type, title, text, href, image_hash, dry_run):
200
219
  "Href": href,
201
220
  }
202
221
  elif ad_type_norm == "TEXT_IMAGE_AD":
222
+ if title or text:
223
+ raise click.UsageError(
224
+ "--title/--text are only valid for TEXT_AD. "
225
+ "For TEXT_IMAGE_AD, use --image-hash and --href."
226
+ )
203
227
  if not image_hash or not href:
204
228
  raise click.UsageError(
205
229
  "TEXT_IMAGE_AD requires both --image-hash and --href"
@@ -208,10 +232,37 @@ def add(ctx, adgroup_id, ad_type, title, text, href, image_hash, dry_run):
208
232
  "AdImageHash": image_hash,
209
233
  "Href": href,
210
234
  }
211
- if title:
212
- ad_data["TextImageAd"]["Title"] = title
213
- if text:
214
- ad_data["TextImageAd"]["Text"] = text
235
+ elif ad_type_norm == "MOBILE_APP_AD":
236
+ if href:
237
+ raise click.UsageError(
238
+ "--href does not apply to MOBILE_APP_AD. "
239
+ "Use --tracking-url instead."
240
+ )
241
+ missing_fields = [
242
+ option_name
243
+ for option_name, value in (
244
+ ("--title", title),
245
+ ("--text", text),
246
+ ("--action", action),
247
+ )
248
+ if not value
249
+ ]
250
+ if missing_fields:
251
+ raise click.UsageError(
252
+ "MOBILE_APP_AD requires " + ", ".join(missing_fields)
253
+ )
254
+ mobile_app_ad = {
255
+ "Title": title,
256
+ "Text": text,
257
+ "Action": action.upper(),
258
+ }
259
+ if image_hash:
260
+ mobile_app_ad["AdImageHash"] = image_hash
261
+ if tracking_url:
262
+ mobile_app_ad["TrackingUrl"] = tracking_url
263
+ if age_label:
264
+ mobile_app_ad["AgeLabel"] = age_label.upper()
265
+ ad_data["MobileAppAd"] = mobile_app_ad
215
266
 
216
267
  body = {"method": "add", "params": {"Ads": [ad_data]}}
217
268
 
@@ -237,30 +288,98 @@ def add(ctx, adgroup_id, ad_type, title, text, href, image_hash, dry_run):
237
288
 
238
289
  @ads.command()
239
290
  @click.option("--id", "ad_id", required=True, type=int, help="Ad ID")
240
- @click.option("--status", help="New status")
241
- @click.option("--title", help="New text ad title")
242
- @click.option("--text", help="New text ad text")
243
- @click.option("--href", help="New text ad URL")
244
- @click.option("--image-hash", help="New text image ad image hash")
291
+ @click.option(
292
+ "--type",
293
+ "ad_type",
294
+ required=True,
295
+ help="Ad subtype: TEXT_AD | TEXT_IMAGE_AD | MOBILE_APP_AD",
296
+ )
297
+ @click.option(
298
+ "--status",
299
+ help=(
300
+ "Deprecated: not part of WSDL AdUpdateItem. "
301
+ "Use 'direct ads suspend/resume/archive/unarchive' instead."
302
+ ),
303
+ )
304
+ @click.option("--title", help="Title (TEXT_AD / MOBILE_APP_AD)")
305
+ @click.option("--text", help="Text (TEXT_AD / MOBILE_APP_AD)")
306
+ @click.option("--href", help="URL (TEXT_AD / TEXT_IMAGE_AD)")
307
+ @click.option("--image-hash", help="Image hash (TEXT_IMAGE_AD / MOBILE_APP_AD)")
308
+ @click.option(
309
+ "--action",
310
+ help="MOBILE_APP_AD call-to-action (MobileAppAdActionEnum, e.g. INSTALL)",
311
+ )
312
+ @click.option("--tracking-url", help="MOBILE_APP_AD tracking URL")
313
+ @click.option("--age-label", help="MOBILE_APP_AD age label (MobAppAgeLabelEnum)")
245
314
  @click.option("--dry-run", is_flag=True, help="Show request without sending")
246
315
  @click.pass_context
247
- def update(ctx, ad_id, status, title, text, href, image_hash, dry_run):
316
+ def update(
317
+ ctx,
318
+ ad_id,
319
+ ad_type,
320
+ status,
321
+ title,
322
+ text,
323
+ href,
324
+ image_hash,
325
+ action,
326
+ tracking_url,
327
+ age_label,
328
+ dry_run,
329
+ ):
248
330
  """Update ad"""
331
+ if status:
332
+ raise click.UsageError(
333
+ "Use 'direct ads suspend/resume/archive/unarchive' to change status. "
334
+ "The --status flag is not supported by WSDL AdUpdateItem."
335
+ )
336
+
337
+ ad_type_norm = ad_type.upper().replace("-", "_")
338
+ supported_types = {"TEXT_AD", "TEXT_IMAGE_AD", "MOBILE_APP_AD"}
339
+ if ad_type_norm not in supported_types:
340
+ raise click.UsageError(
341
+ "Invalid value for '--type': "
342
+ f"{ad_type!r} is not one of "
343
+ "'TEXT_AD', 'TEXT_IMAGE_AD', 'MOBILE_APP_AD'."
344
+ )
345
+
249
346
  try:
250
347
  ad_data = {"Id": ad_id}
251
348
 
252
- if status:
253
- ad_data["Status"] = status
254
- if any([title, text, href]):
255
- ad_data["TextAd"] = {}
349
+ if ad_type_norm == "TEXT_AD":
350
+ text_ad = {}
256
351
  if title:
257
- ad_data["TextAd"]["Title"] = title
352
+ text_ad["Title"] = title
258
353
  if text:
259
- ad_data["TextAd"]["Text"] = text
354
+ text_ad["Text"] = text
355
+ if href:
356
+ text_ad["Href"] = href
357
+ if text_ad:
358
+ ad_data["TextAd"] = text_ad
359
+ elif ad_type_norm == "TEXT_IMAGE_AD":
360
+ text_image_ad = {}
361
+ if image_hash:
362
+ text_image_ad["AdImageHash"] = image_hash
260
363
  if href:
261
- ad_data["TextAd"]["Href"] = href
262
- if image_hash:
263
- ad_data["TextImageAd"] = {"AdImageHash": image_hash}
364
+ text_image_ad["Href"] = href
365
+ if text_image_ad:
366
+ ad_data["TextImageAd"] = text_image_ad
367
+ elif ad_type_norm == "MOBILE_APP_AD":
368
+ mobile_app_ad = {}
369
+ if title:
370
+ mobile_app_ad["Title"] = title
371
+ if text:
372
+ mobile_app_ad["Text"] = text
373
+ if image_hash:
374
+ mobile_app_ad["AdImageHash"] = image_hash
375
+ if action:
376
+ mobile_app_ad["Action"] = action.upper()
377
+ if tracking_url:
378
+ mobile_app_ad["TrackingUrl"] = tracking_url
379
+ if age_label:
380
+ mobile_app_ad["AgeLabel"] = age_label.upper()
381
+ if mobile_app_ad:
382
+ ad_data["MobileAppAd"] = mobile_app_ad
264
383
 
265
384
  body = {"method": "update", "params": {"Ads": [ad_data]}}
266
385