direct-cli 0.3.7__tar.gz → 0.3.9__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 (230) hide show
  1. {direct_cli-0.3.7 → direct_cli-0.3.9}/.github/workflows/claude.yml +1 -1
  2. direct_cli-0.3.9/CHANGELOG.md +110 -0
  3. {direct_cli-0.3.7 → direct_cli-0.3.9}/CLAUDE.md +22 -1
  4. {direct_cli-0.3.7 → direct_cli-0.3.9}/PKG-INFO +156 -15
  5. {direct_cli-0.3.7 → direct_cli-0.3.9}/README.md +155 -14
  6. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/cli.py +14 -1
  7. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/adgroups.py +53 -9
  8. direct_cli-0.3.9/direct_cli/commands/ads.py +798 -0
  9. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/bidmodifiers.py +93 -45
  10. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/bids.py +5 -4
  11. direct_cli-0.3.9/direct_cli/commands/campaigns.py +1061 -0
  12. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/dynamicads.py +4 -4
  13. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/feeds.py +11 -1
  14. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/keywordbids.py +6 -0
  15. direct_cli-0.3.9/direct_cli/commands/keywords.py +626 -0
  16. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/strategies.py +198 -24
  17. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/output.py +72 -2
  18. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/utils.py +45 -0
  19. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/wsdl_coverage.py +31 -0
  20. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli.egg-info/PKG-INFO +156 -15
  21. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli.egg-info/SOURCES.txt +28 -19
  22. direct_cli-0.3.9/docs/audits/issue-198-mutating-wsdl-audit.md +148 -0
  23. {direct_cli-0.3.7 → direct_cli-0.3.9}/pyproject.toml +1 -1
  24. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/build_api_coverage_report.py +85 -34
  25. direct_cli-0.3.9/scripts/sandbox_write_audit.py +198 -0
  26. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/sandbox_write_live.py +161 -21
  27. direct_cli-0.3.9/scripts/test_sandbox_write.sh +44 -0
  28. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/API_COVERAGE.md +1 -1
  29. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/API_ISSUE_AUDIT.md +1 -1
  30. direct_cli-0.3.9/tests/_orphan_store.py +124 -0
  31. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/api_coverage_payloads.py +536 -31
  32. direct_cli-0.3.9/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +437 -0
  33. direct_cli-0.3.9/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +437 -0
  34. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +15 -15
  35. direct_cli-0.3.9/tests/cassettes/test_integration_write/TestWriteRetargetingUpdate.test_retargeting_update.yaml +166 -0
  36. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +16 -16
  37. direct_cli-0.3.9/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +69 -0
  38. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/conftest.py +47 -9
  39. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_api_coverage.py +382 -3
  40. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_cli.py +85 -0
  41. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_dry_run.py +1807 -290
  42. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_integration.py +117 -0
  43. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_integration_write.py +291 -17
  44. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_low_coverage_payloads.py +340 -19
  45. direct_cli-0.3.9/tests/test_sandbox_write_audit.py +67 -0
  46. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_smoke_matrix.py +202 -0
  47. direct_cli-0.3.9/tests/test_v4_live_contracts.py +341 -0
  48. direct_cli-0.3.7/tests/test_integration_live_write.py → direct_cli-0.3.9/tests/test_v5_live_write.py +27 -28
  49. direct_cli-0.3.9/tests/test_wsdl_parity_gate.py +569 -0
  50. direct_cli-0.3.7/CHANGELOG.md +0 -11
  51. direct_cli-0.3.7/direct_cli/commands/ads.py +0 -447
  52. direct_cli-0.3.7/direct_cli/commands/campaigns.py +0 -558
  53. direct_cli-0.3.7/direct_cli/commands/keywords.py +0 -322
  54. direct_cli-0.3.7/scripts/test_sandbox_write.sh +0 -23
  55. direct_cli-0.3.7/tests/test_v4_live_contracts.py +0 -190
  56. {direct_cli-0.3.7 → direct_cli-0.3.9}/.env.example +0 -0
  57. {direct_cli-0.3.7 → direct_cli-0.3.9}/.github/copilot-instructions.md +0 -0
  58. {direct_cli-0.3.7 → direct_cli-0.3.9}/.github/workflows/api-coverage.yml +0 -0
  59. {direct_cli-0.3.7 → direct_cli-0.3.9}/.github/workflows/quality.yml +0 -0
  60. {direct_cli-0.3.7 → direct_cli-0.3.9}/.gitignore +0 -0
  61. {direct_cli-0.3.7 → direct_cli-0.3.9}/AGENTS.md +0 -0
  62. {direct_cli-0.3.7 → direct_cli-0.3.9}/MANIFEST.in +0 -0
  63. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/__init__.py +0 -0
  64. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_deprecated.py +0 -0
  65. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_smoke_probes.py +0 -0
  66. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/__init__.py +0 -0
  67. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  68. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
  69. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  70. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  71. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +0 -0
  72. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
  73. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  74. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
  75. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  76. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  77. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/api.py +0 -0
  78. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/auth.py +0 -0
  79. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/__init__.py +0 -0
  80. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/adextensions.py +0 -0
  81. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/adimages.py +0 -0
  82. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/advideos.py +0 -0
  83. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/agencyclients.py +0 -0
  84. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/audiencetargets.py +0 -0
  85. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/auth.py +0 -0
  86. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/balance.py +0 -0
  87. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/businesses.py +0 -0
  88. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/changes.py +0 -0
  89. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/clients.py +0 -0
  90. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/creatives.py +0 -0
  91. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/dictionaries.py +0 -0
  92. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/dynamicfeedadtargets.py +0 -0
  93. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/keywordsresearch.py +0 -0
  94. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/leads.py +0 -0
  95. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
  96. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/reports.py +0 -0
  97. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/retargeting.py +0 -0
  98. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/sitelinks.py +0 -0
  99. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/smartadtargets.py +0 -0
  100. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/turbopages.py +0 -0
  101. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/v4account.py +0 -0
  102. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/v4events.py +0 -0
  103. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/v4finance.py +0 -0
  104. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/v4forecast.py +0 -0
  105. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/v4goals.py +0 -0
  106. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/v4shells.py +0 -0
  107. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/v4tags.py +0 -0
  108. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/v4wordstat.py +0 -0
  109. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/commands/vcards.py +0 -0
  110. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/reports_coverage.py +0 -0
  111. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/smoke_matrix.py +0 -0
  112. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/v4/__init__.py +0 -0
  113. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/v4/money.py +0 -0
  114. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli/v4_contracts.py +0 -0
  115. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli.egg-info/dependency_links.txt +0 -0
  116. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli.egg-info/entry_points.txt +0 -0
  117. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli.egg-info/requires.txt +0 -0
  118. {direct_cli-0.3.7 → direct_cli-0.3.9}/direct_cli.egg-info/top_level.txt +0 -0
  119. {direct_cli-0.3.7 → direct_cli-0.3.9}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  120. {direct_cli-0.3.7 → direct_cli-0.3.9}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  121. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/anonymize_cassettes.py +0 -0
  122. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/build_api_coverage_checklist.py +0 -0
  123. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/check_reports_drift.py +0 -0
  124. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/check_wsdl_drift.py +0 -0
  125. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/patch_vendor_imports.py +0 -0
  126. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/refresh_reports_cache.py +0 -0
  127. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/refresh_wsdl_cache.py +0 -0
  128. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/release_pypi.sh +0 -0
  129. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/test_dangerous_commands.sh +0 -0
  130. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/test_safe_commands.sh +0 -0
  131. {direct_cli-0.3.7 → direct_cli-0.3.9}/scripts/update_vendor.sh +0 -0
  132. {direct_cli-0.3.7 → direct_cli-0.3.9}/setup.cfg +0 -0
  133. {direct_cli-0.3.7 → direct_cli-0.3.9}/setup.py +0 -0
  134. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/MANUAL_COVERAGE.md +0 -0
  135. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/__init__.py +0 -0
  136. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  137. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  138. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  139. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  140. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  141. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  142. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  143. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  144. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  145. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  146. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  147. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  148. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  149. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  150. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  151. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  152. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  153. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_adgroups_add_update_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_adgroups_add_update_delete.yaml +0 -0
  154. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_adimages_add_get_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_adimages_add_get_delete.yaml +0 -0
  155. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_ads_add_update_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_add_update_delete.yaml +0 -0
  156. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_ads_suspend_resume_archive_unarchive.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
  157. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_advideos_add_get.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_advideos_add_get.yaml +0 -0
  158. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_add_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_add_delete.yaml +0 -0
  159. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_suspend_resume.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_suspend_resume.yaml +0 -0
  160. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_bids_set.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_bids_set.yaml +0 -0
  161. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_campaign_create_get_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_campaign_create_get_delete.yaml +0 -0
  162. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_creatives_chain_advideo_to_creative.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
  163. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_add_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_add_delete.yaml +0 -0
  164. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_suspend_resume.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_suspend_resume.yaml +0 -0
  165. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_keywordbids_set.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywordbids_set.yaml +0 -0
  166. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_keywords_add_update_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_add_update_delete.yaml +0 -0
  167. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_keywords_suspend_resume.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_suspend_resume.yaml +0 -0
  168. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_sitelinks_add_get_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_sitelinks_add_get_delete.yaml +0 -0
  169. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_add_update_delete.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_add_update_delete.yaml +0 -0
  170. /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_suspend_resume.yaml → /direct_cli-0.3.9/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_suspend_resume.yaml +0 -0
  171. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/fixtures/test-video.mp4 +0 -0
  172. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/reports_cache/raw/fields-list.html +0 -0
  173. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/reports_cache/raw/headers.html +0 -0
  174. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/reports_cache/raw/period.html +0 -0
  175. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/reports_cache/raw/spec.html +0 -0
  176. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/reports_cache/raw/type.html +0 -0
  177. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/reports_cache/spec.json +0 -0
  178. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_auth_bw.py +0 -0
  179. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_auth_oauth.py +0 -0
  180. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_auth_op.py +0 -0
  181. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_balance.py +0 -0
  182. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_cli_contract.py +0 -0
  183. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_comprehensive.py +0 -0
  184. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_reports_drift.py +0 -0
  185. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_reports_parsing.py +0 -0
  186. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_transport_contract.py +0 -0
  187. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4_contracts.py +0 -0
  188. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4_foundation.py +0 -0
  189. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4_safety.py +0 -0
  190. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4account.py +0 -0
  191. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4events.py +0 -0
  192. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4finance_money.py +0 -0
  193. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4finance_read.py +0 -0
  194. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4forecast.py +0 -0
  195. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4goals.py +0 -0
  196. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4tags.py +0 -0
  197. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_v4wordstat.py +0 -0
  198. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/test_vendor_imports.py +0 -0
  199. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/adextensions.xml +0 -0
  200. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/adgroups.xml +0 -0
  201. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/adimages.xml +0 -0
  202. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/ads.xml +0 -0
  203. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/advideos.xml +0 -0
  204. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/agencyclients.xml +0 -0
  205. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/audiencetargets.xml +0 -0
  206. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  207. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/bids.xml +0 -0
  208. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/businesses.xml +0 -0
  209. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/campaigns.xml +0 -0
  210. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/changes.xml +0 -0
  211. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/clients.xml +0 -0
  212. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/creatives.xml +0 -0
  213. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/dictionaries.xml +0 -0
  214. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  215. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  216. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/feeds.xml +0 -0
  217. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  218. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/imports/general.xsd +0 -0
  219. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  220. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/keywordbids.xml +0 -0
  221. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/keywords.xml +0 -0
  222. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  223. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/leads.xml +0 -0
  224. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  225. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/retargetinglists.xml +0 -0
  226. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/sitelinks.xml +0 -0
  227. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/smartadtargets.xml +0 -0
  228. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/strategies.xml +0 -0
  229. {direct_cli-0.3.7 → direct_cli-0.3.9}/tests/wsdl_cache/turbopages.xml +0 -0
  230. {direct_cli-0.3.7 → direct_cli-0.3.9}/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,110 @@
1
+ # Changelog
2
+
3
+ ## 0.3.9
4
+
5
+ **Added:**
6
+
7
+ - `direct keywords add` now supports batch mode via `--from-file PATH`
8
+ (JSONL, one keyword object per line) or `--keywords-json '[…]'`
9
+ (inline JSON array). The CLI splits input into chunks of 10 — the
10
+ Yandex Direct API limit for `keywords.add` documented at
11
+ https://yandex.ru/dev/direct/doc/dg/objects/keyword.html — preserves
12
+ input order, and merges `AddResults` from every chunk into a single
13
+ response. Item-level errors do not abort the batch. If a chunk-level
14
+ exception breaks the loop, already-created Ids are printed to stderr
15
+ with a "Partial success before failure" header so a retry doesn't
16
+ duplicate them. Pre-flight warning when any AdGroupId in the input
17
+ exceeds the per-ad-group limit of 200 keywords (the API rejects the
18
+ excess with per-item errors; warning surfaces this before any chunk
19
+ is sent). Row keys use WSDL CamelCase (`Keyword`, `AdGroupId`,
20
+ `Bid`, `ContextBid`, `UserParam1`, `UserParam2`); unknown keys are
21
+ rejected with the row number, and JSON booleans are explicitly
22
+ rejected to prevent silent `True → 1` coercion. `--adgroup-id` is
23
+ optional in batch mode and acts as a default, overridable per row.
24
+ `--dry-run` prints the first chunk's payload alongside
25
+ `{chunks, totalItems, chunkSize}`. Single-item mode (`--keyword`)
26
+ is unchanged (#203).
27
+ - `direct campaigns add` typed flags for CPA strategies and
28
+ cross-cutting `CampaignAddItem` fields: `--goal-id` (single
29
+ Metrika goal), `--crr` (CRR percentage for
30
+ `PAY_FOR_CONVERSION_CRR`),
31
+ `--priority-goals goal_id:value,…` (multi-goal CPA via
32
+ WSDL `PriorityGoalsArray`), `--average-cpa MICRO_RUBLES`,
33
+ `--bid-ceiling MICRO_RUBLES`, `--counter-ids`
34
+ (TextCampaign/DynamicTextCampaign), `--notification JSON`
35
+ (`CampaignBase.Notification` with `SmsSettings`/`EmailSettings`
36
+ shape validation), `--time-targeting JSON`
37
+ (`CampaignAddItem.TimeTargeting` with `HolidaysSchedule`
38
+ shape validation). Strategy-subtype compatibility is enforced
39
+ via `UsageError` at CLI level both ways: WSDL-incompatible flags
40
+ are rejected (e.g. `--average-cpa` for `HIGHEST_POSITION`,
41
+ `--crr` outside `PAY_FOR_CONVERSION_CRR`,
42
+ `--bid-ceiling` for `PayForConversionCrr` /
43
+ `PayForConversionMultipleGoals`), and WSDL `minOccurs=1`
44
+ fields are demanded up-front (e.g. picking `AVERAGE_CPA`
45
+ without `--average-cpa`+`--goal-id`, or `PAY_FOR_CONVERSION_CRR`
46
+ without `--crr`+`--goal-id`, or `*_MULTIPLE_GOALS` without
47
+ `--priority-goals`, all fail at the CLI instead of the API).
48
+ Closes #204.
49
+
50
+ **Notes:**
51
+
52
+ - Issue #204 also requested `--goals` (array) and
53
+ `--network-settings`; both were dropped after WSDL audit. Yandex
54
+ `Strategy*Add` complex types declare only scalar `GoalId`, so
55
+ multi-goal CPA is shipped through `--priority-goals` instead
56
+ (correct WSDL path: `TextCampaign.PriorityGoals.Items[].GoalId/Value`).
57
+ No `NetworkSettings` field exists on `CampaignAddItem` /
58
+ `TextCampaignAddItem` / `DynamicTextCampaignAddItem` /
59
+ `SmartCampaignAddItem` in the current `campaigns.xml` WSDL.
60
+
61
+ **Fixed:**
62
+
63
+ - Refreshed `TestWriteFeeds` and `TestWriteSmartAdTargets` VCR cassettes against a real sandbox, dropped the `_FEED_REGRESSION_PATTERNS` skip workaround, and updated `sandbox_feed` / `sandbox_smart_adgroup` fixtures to pass the now-WSDL-required `--business-type RETAIL` (FeedAddItem) and `--counter-id` (SmartCampaignAddItem). Tests now skip only on genuine sandbox limitations, not on the missing-option proxy that the workaround papered over (#206, fallout from #201). Test invocation now also passes `--login` and prefers env vars over an active `direct auth` profile, matching the inversion documented in CLAUDE.md.
64
+ - WSDL parity gate now fails fast when `COMMAND_WSDL_MAP` points at a container that does not exist in the WSDL request schema. The previous skip-on-empty-required-list silently masked typo'd container names (#206, Copilot follow-up from #205).
65
+ - `WSDL_FIELD_TO_CLI_OPTION` no longer references the non-existent generic `--file` flag. `SourceType` maps to `{--url}` and `ImageData` maps to `{--image-data, --image-file}`, matching the real CLI surface (#206, Copilot follow-up from #205).
66
+ - `direct bidmodifiers set --help` no longer advertises the rejected `--campaign-id`/`--type` legacy path; the rejection now happens via an eager Click callback (same pattern as deprecated `keywords update` options), preserving the existing `UsageError` message for regression coverage (#206, Copilot follow-up from #214).
67
+
68
+ **Refs:** Closes issues #122, #138, #198, #202, #203, #204, #206, #207.
69
+
70
+ ## 0.3.8
71
+
72
+ **BREAKING CHANGES:**
73
+
74
+ - `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).
75
+ - `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).
76
+ - `direct ads add --type MOBILE_APP_AD --href` rejected — MobileAppAd uses `--tracking-url`, not `--href` (PR #196).
77
+ - `direct feeds add` now requires `--business-type {RETAIL,HOTELS,REALTY,AUTOMOBILES,FLIGHTS,OTHER}`. Mirrors WSDL FeedAddItem.BusinessType (minOccurs=1) (PR #201).
78
+
79
+ **Schema gate — mutating ops parity:**
80
+
81
+ - 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).
82
+ - 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).
83
+ - Added MOBILE_APP_AD branch to `ads add` mirroring WSDL `MobileAppAdAdd` (PR #190).
84
+ - `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.
85
+
86
+ **Strict WSDL parity policy:**
87
+
88
+ - 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).
89
+
90
+ **Integration test coverage:**
91
+
92
+ - Added read-only sandbox integration tests for `changes`, `keywordsresearch`, `balance` (PR #186).
93
+ - Added v5 write integration coverage for `strategies` lifecycle, `retargeting update`, `bids get/set-auto`, plus `auth status/list` read-only tests (PR #189).
94
+ - Re-recorded TestWriteBidsRead cassettes against live API and rewrote host to sandbox so the bids endpoints get real coverage in replay mode (PR #193).
95
+
96
+ **CI infrastructure:**
97
+
98
+ - Switched Claude code-review GitHub Action from default (Sonnet 4.5) to Claude Opus 4.7 for deeper PR review (PR #192).
99
+
100
+ **Refs:** Closes issues #118, #136, #137, #175, #176, #180, #183, #191, #199.
101
+
102
+ ## 0.3.3
103
+
104
+ **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.
105
+
106
+ - Added refresh token persistence for OAuth profiles.
107
+ - Added automatic OAuth access token refresh before expiry.
108
+ - Added `expires_in` details to `direct auth status`.
109
+ - Added JSON output for `direct auth status`.
110
+ - 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,24 @@ 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
+
55
+ **WSDL parity gate:** `tests/test_wsdl_parity_gate.py` runs four invariant checks across every `add`/`update`/`set` command in `WRITE_SANDBOX`:
56
+
57
+ 1. *Empty subtype no-op* — a mutating command with only the resource ID must refuse to send the payload (no silent no-op on the live API).
58
+ 2. *Silent data loss* — a typed flag that does not belong to the chosen `--type` must raise `UsageError`, not be dropped.
59
+ 3. *WSDL `minOccurs=1` not validated* — every required WSDL item field must be enforced either via Click `required=True` *or* a documented `UsageError` body check (listed in `INTERNAL_VALIDATION`).
60
+ 4. *Strategy enum drift* — `STRATEGY_TYPES` (`direct_cli/commands/strategies.py`) must equal the subtype-of-one field names in `StrategyAddItem`.
61
+
62
+ Adding a new mutating command requires extending `COMMAND_WSDL_MAP` in `tests/test_wsdl_parity_gate.py` (the coverage test fails otherwise) and, if the WSDL request has a non-mechanical field name, also `WSDL_FIELD_TO_CLI_OPTION`. Tracked in issue #198.
63
+
44
64
  **SelectionCriteria:** Resources like `adgroups`, `ads`, `keywords` require at least one of `Ids`, `CampaignIds`, or `AdGroupIds` — otherwise API error 4001.
45
65
 
46
66
  **Error handling:** All commands wrap API calls in `try/except Exception` → `print_error(str(e))` + `raise click.Abort()`.
@@ -55,6 +75,7 @@ Click group-of-groups. Each Yandex Direct API resource = one file in `direct_cli
55
75
 
56
76
  - **Unit** (`test_cli.py`, `test_comprehensive.py`) — no API calls, no token needed.
57
77
  - **Integration** (`test_integration.py`, `@pytest.mark.integration`) — require `.env` with `YANDEX_DIRECT_TOKEN` and `YANDEX_DIRECT_LOGIN`. Auto-skip if absent.
78
+ - **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
79
 
59
80
  ## Dangerous Commands — Never Auto-Test
60
81
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.3.7
3
+ Version: 0.3.9
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`.
@@ -382,6 +384,15 @@ direct campaigns add --name "My Campaign" --start-date 2024-02-01 --type TEXT_CA
382
384
  direct campaigns add --name "Dynamic Campaign" --start-date 2024-02-01 --type DYNAMIC_TEXT_CAMPAIGN --setting ADD_METRICA_TAG=NO --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --dry-run
383
385
  direct campaigns add --name "Smart Campaign" --start-date 2024-02-01 --type SMART_CAMPAIGN --network-strategy AVERAGE_CPC_PER_FILTER --filter-average-cpc 1000000 --counter-id 123 --dry-run
384
386
 
387
+ # CPA strategy (single goal): --goal-id required, --average-cpa / --bid-ceiling are micro-rubles
388
+ direct campaigns add --name "CPA Campaign" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy AVERAGE_CPA --network-strategy SERVING_OFF --goal-id 1234567 --average-cpa 500000000 --bid-ceiling 1000000000 --counter-ids 111,222 --dry-run
389
+
390
+ # Multi-goal CPA via PriorityGoals (goal_id:value pairs, WSDL PriorityGoalsItem)
391
+ direct campaigns add --name "Multi-Goal CPA" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy AVERAGE_CPA_MULTIPLE_GOALS --network-strategy SERVING_OFF --priority-goals 1234567:80,9876543:20 --bid-ceiling 1000000000 --dry-run
392
+
393
+ # Notification (Sms/Email) and TimeTargeting accept JSON with WSDL CamelCase keys
394
+ direct campaigns add --name "Notify+Schedule" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --notification '{"EmailSettings":{"Email":"ops@example.com","SendWarnings":"YES"}}' --time-targeting '{"Schedule":["1A0123456789ABCDEFGHIJKL"],"ConsiderWorkingWeekends":"YES"}' --dry-run
395
+
385
396
  # Update / lifecycle
386
397
  direct campaigns update --id 12345 --name "New Name" --status SUSPENDED --budget 100000000 --start-date 2024-02-10 --end-date 2024-03-01
387
398
  direct campaigns suspend --id 12345
@@ -408,11 +419,21 @@ direct adgroups delete --id 67890
408
419
  direct ads get --campaign-ids 1,2,3
409
420
  direct ads get --adgroup-ids 45678 --format table
410
421
  direct ads add --adgroup-id 12345 --type TEXT_AD --title "Title" --text "Ad text" --href "https://example.com" --dry-run
411
- direct ads add --adgroup-id 12345 --type TEXT_IMAGE_AD --image-hash abcdefghijklmnopqrst --href "https://example.com" --title "Banner" --text "Image ad" --dry-run
412
- direct ads update --id 99999 --status PAUSED --title "New Title" --text "New text" --href "https://example.com" --image-hash abcdefghijklmnopqrst
422
+ direct ads add --adgroup-id 12345 --type TEXT_AD --title "Title" --text "Ad text" --href "https://example.com" --title2 "Second headline" --display-url-path "deals" --mobile YES --vcard-id 111 --sitelink-set-id 222 --turbo-page-id 333 --ad-extensions "444,555" --dry-run
423
+ direct ads add --adgroup-id 12345 --type TEXT_IMAGE_AD --image-hash abcdefghijklmnopqrst --href "https://example.com" --turbo-page-id 555 --dry-run
424
+ direct ads update --id 99999 --type TEXT_AD --title "New Title" --text "New text" --href "https://example.com"
425
+ direct ads update --id 99999 --type TEXT_AD --image-hash abcdefghijklmnopqrst
426
+ direct ads update --id 99999 --type TEXT_AD --title2 "New second headline" --vcard-id 222
413
427
  direct ads delete --id 99999
414
428
  ```
415
429
 
430
+ Available TEXT_AD typed flags for `ads add` / `ads update`: `--title`, `--text`,
431
+ `--href`, `--image-hash`, `--title2`, `--display-url-path`, `--vcard-id`,
432
+ `--sitelink-set-id`, `--turbo-page-id`. `--mobile` (default `NO`) and
433
+ `--ad-extensions` are `ads add`-only — `TextAdUpdate` does not contain `Mobile`,
434
+ and ad-extension updates go through the `CalloutSetting` WSDL field, which is
435
+ not yet exposed by the CLI. TEXT_IMAGE_AD additionally accepts `--turbo-page-id`.
436
+
416
437
  #### Keywords
417
438
 
418
439
  ```bash
@@ -422,6 +443,33 @@ direct keywords update --id 88888 --keyword "updated keyword text"
422
443
  direct keywords delete --id 88888
423
444
  ```
424
445
 
446
+ **Batch keyword upload** (CLI auto-chunks to the API limit of 10 per request):
447
+
448
+ ```bash
449
+ # From a JSONL file (one keyword object per line)
450
+ direct keywords add --adgroup-id 12345 --from-file keywords.jsonl
451
+
452
+ # Inline JSON array
453
+ direct keywords add --keywords-json '[{"Keyword":"buy laptop","Bid":10000000},{"Keyword":"buy desktop"}]'
454
+ ```
455
+
456
+ Example `keywords.jsonl`:
457
+
458
+ ```jsonl
459
+ {"Keyword":"buy laptop","Bid":10000000,"UserParam1":"src=ad1"}
460
+ {"Keyword":"buy desktop","ContextBid":5000000}
461
+ {"Keyword":"купить ноутбук","AdGroupId":99999}
462
+ ```
463
+
464
+ - Row keys use WSDL CamelCase: `Keyword`, `AdGroupId`, `Bid`, `ContextBid`, `UserParam1`, `UserParam2`.
465
+ - `--adgroup-id` provides the default group ID; rows can override it via per-row `AdGroupId`.
466
+ - Each effective row must resolve `Keyword` and `AdGroupId`; unknown fields are rejected with the row number.
467
+ - API limit: 10 items per `keywords.add` request — see [Yandex Direct docs](https://yandex.ru/dev/direct/doc/dg/objects/keyword.html). The CLI sends as many chunks as needed and merges `AddResults`.
468
+ - API limit: 200 keywords per ad group. The CLI prints a warning if any `AdGroupId` in the input exceeds it; the API rejects the excess as per-item errors.
469
+ - Item-level errors from the API do not abort the batch; the merged output includes successes and per-item errors.
470
+ - If a chunk fails with a network-level error mid-batch, already-created Ids are printed to stderr (`Partial success before failure`) so a retry doesn't duplicate them.
471
+ - `--dry-run` shows the first chunk's payload plus `{chunks, totalItems, chunkSize}`.
472
+
425
473
  #### Reports
426
474
 
427
475
  ```bash
@@ -478,8 +526,8 @@ direct dynamicads set-bids --id 789 --bid 12500000 --context-bid 9000000 --prior
478
526
 
479
527
  # Shared bidding strategies
480
528
  direct strategies get --limit 5
481
- direct strategies add --name "Shared Clicks" --type WbMaximumClicks --spend-limit 1000000000 --average-cpc 30000000 --dry-run
482
- direct strategies update --id 42 --type WbMaximumClicks --average-cpc 35000000 --dry-run
529
+ direct strategies add --name "Shared Clicks" --type WbMaximumClicks --weekly-spend-limit 1000000000 --bid-ceiling 30000000 --dry-run
530
+ direct strategies update --id 42 --type WbMaximumClicks --weekly-spend-limit 35000000 --dry-run
483
531
  direct strategies archive --id 42 --dry-run
484
532
 
485
533
  # Dynamic feed ad targets
@@ -493,7 +541,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
493
541
  direct adextensions add --callout-text "Free shipping" --dry-run
494
542
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
495
543
  direct creatives add --video-id video-id --dry-run
496
- direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --dry-run
544
+ direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
497
545
  direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
498
546
  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
499
547
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -556,6 +604,17 @@ Use `--dry-run` on `add` / `update` commands to preview the API request before s
556
604
  direct campaigns add --name "Test" --start-date 2024-01-01 --dry-run
557
605
  ```
558
606
 
607
+ ### API Errors
608
+
609
+ Yandex Direct can return a successful HTTP response that still contains
610
+ item-level `Errors` for one object. Direct CLI treats those responses as
611
+ failed operations: it exits non-zero and prints the error code, message, and
612
+ details.
613
+
614
+ Code `8800` with `Object not found` usually means the object is not available
615
+ under the current `Client-Login` or account. Check the selected `--login`,
616
+ `YANDEX_DIRECT_LOGIN`, or auth profile before retrying.
617
+
559
618
  ### Testing
560
619
 
561
620
  Four tiers of tests live under `tests/`:
@@ -565,17 +624,22 @@ Four tiers of tests live under `tests/`:
565
624
  | Unit / CLI wiring / dry-run | *(none)* | No | No |
566
625
  | Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
567
626
  | Write integration | `-m integration_write` | No (replays VCR cassettes) | No |
568
- | Live draft write integration | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
627
+ | Live draft write integration (v5) | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
628
+ | v4 live read | `-m v4_live_read` | Yes (production v4 JSON API, read-only) | Yes |
629
+ | 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` |
569
630
 
570
631
  ```bash
571
632
  pip install -e ".[dev]"
572
633
  pytest # fast tier — no token
573
634
  pytest -m integration -v # read-only integration tests (needs token)
574
635
  pytest -m integration_write -v # write cassette replay (no token needed)
575
- YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay
636
+ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5)
576
637
  YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # re-record live draft cassette
638
+ 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
577
639
  ```
578
640
 
641
+ 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`).
642
+
579
643
  #### Smoke command scripts
580
644
 
581
645
  Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
@@ -659,6 +723,12 @@ For `v4account` sandbox smoke, `enable-shared-account` uses
659
723
  `account-management` requires `YANDEX_DIRECT_V4ACCOUNT_ACCOUNT_ID`; without it
660
724
  the runner reports `NOT_COVERED` for that command.
661
725
 
726
+ `clients.update` is opt-in because it mutates client-level account metadata.
727
+ Set `YANDEX_DIRECT_CLIENTS_UPDATE_LOGIN` to an expendable sandbox
728
+ `Client-Login`; the runner passes it through `--login` and updates only
729
+ `ClientInfo` with a unique smoke marker. Without that variable, the runner
730
+ reports `NOT_COVERED` for `clients.update`.
731
+
662
732
  #### Re-recording write cassettes
663
733
 
664
734
  The `integration_write` pytest tier still replays stored write-test traffic
@@ -813,6 +883,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
813
883
  логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
814
884
  или профильные env vars, а не базовые credentials.
815
885
 
886
+ > **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
887
+
816
888
  Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
817
889
  через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
818
890
  подсказкой `use direct instead of direct-cli`.
@@ -1002,6 +1074,15 @@ direct campaigns add --name "Моя кампания" --start-date 2024-02-01 --
1002
1074
  direct campaigns add --name "Динамическая кампания" --start-date 2024-02-01 --type DYNAMIC_TEXT_CAMPAIGN --setting ADD_METRICA_TAG=NO --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --dry-run
1003
1075
  direct campaigns add --name "Смарт-кампания" --start-date 2024-02-01 --type SMART_CAMPAIGN --network-strategy AVERAGE_CPC_PER_FILTER --filter-average-cpc 1000000 --counter-id 123 --dry-run
1004
1076
 
1077
+ # CPA-стратегия (одна цель): --goal-id обязателен, --average-cpa/--bid-ceiling — micro-рубли
1078
+ direct campaigns add --name "CPA-кампания" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy AVERAGE_CPA --network-strategy SERVING_OFF --goal-id 1234567 --average-cpa 500000000 --bid-ceiling 1000000000 --counter-ids 111,222 --dry-run
1079
+
1080
+ # Мульти-целевой CPA через PriorityGoals (пары goal_id:value, WSDL PriorityGoalsItem)
1081
+ direct campaigns add --name "Мульти-целевой CPA" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy AVERAGE_CPA_MULTIPLE_GOALS --network-strategy SERVING_OFF --priority-goals 1234567:80,9876543:20 --bid-ceiling 1000000000 --dry-run
1082
+
1083
+ # Notification (Sms/Email) и TimeTargeting принимают JSON с CamelCase ключами WSDL
1084
+ direct campaigns add --name "Уведомления+Расписание" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --notification '{"EmailSettings":{"Email":"ops@example.com","SendWarnings":"YES"}}' --time-targeting '{"Schedule":["1A0123456789ABCDEFGHIJKL"],"ConsiderWorkingWeekends":"YES"}' --dry-run
1085
+
1005
1086
  # Обновление и управление статусом
1006
1087
  direct campaigns update --id 12345 --name "Новое название" --status SUSPENDED --budget 100000000 --start-date 2024-02-10 --end-date 2024-03-01
1007
1088
  direct campaigns suspend --id 12345
@@ -1028,11 +1109,22 @@ direct adgroups delete --id 67890
1028
1109
  direct ads get --campaign-ids 1,2,3
1029
1110
  direct ads get --adgroup-ids 45678 --format table
1030
1111
  direct ads add --adgroup-id 12345 --type TEXT_AD --title "Заголовок" --text "Текст объявления" --href "https://example.com" --dry-run
1031
- direct ads add --adgroup-id 12345 --type TEXT_IMAGE_AD --image-hash abcdefghijklmnopqrst --href "https://example.com" --title "Баннер" --text "Имиджевое объявление" --dry-run
1032
- direct ads update --id 99999 --status PAUSED --title "Новый заголовок" --text "Новый текст" --href "https://example.com" --image-hash abcdefghijklmnopqrst
1112
+ direct ads add --adgroup-id 12345 --type TEXT_AD --title "Заголовок" --text "Текст" --href "https://example.com" --title2 "Второй заголовок" --display-url-path "deals" --mobile YES --vcard-id 111 --sitelink-set-id 222 --turbo-page-id 333 --ad-extensions "444,555" --dry-run
1113
+ direct ads add --adgroup-id 12345 --type TEXT_IMAGE_AD --image-hash abcdefghijklmnopqrst --href "https://example.com" --turbo-page-id 555 --dry-run
1114
+ direct ads update --id 99999 --type TEXT_AD --title "Новый заголовок" --text "Новый текст" --href "https://example.com"
1115
+ direct ads update --id 99999 --type TEXT_AD --image-hash abcdefghijklmnopqrst
1116
+ direct ads update --id 99999 --type TEXT_AD --title2 "Новый второй заголовок" --vcard-id 222
1033
1117
  direct ads delete --id 99999
1034
1118
  ```
1035
1119
 
1120
+ Доступные типизированные флаги TEXT_AD для `ads add` / `ads update`:
1121
+ `--title`, `--text`, `--href`, `--image-hash`, `--title2`, `--display-url-path`,
1122
+ `--vcard-id`, `--sitelink-set-id`, `--turbo-page-id`. `--mobile`
1123
+ (default `NO`) и `--ad-extensions` доступны только в `ads add` — WSDL
1124
+ `TextAdUpdate` не содержит `Mobile`, а обновление расширений идёт через поле
1125
+ `CalloutSetting`, которое пока не покрыто CLI. Для TEXT_IMAGE_AD дополнительно
1126
+ доступен `--turbo-page-id`.
1127
+
1036
1128
  #### Ключевые слова
1037
1129
 
1038
1130
  ```bash
@@ -1042,6 +1134,33 @@ direct keywords update --id 88888 --keyword "updated keyword text"
1042
1134
  direct keywords delete --id 88888
1043
1135
  ```
1044
1136
 
1137
+ **Пакетная загрузка ключевых слов** (CLI автоматически режет на куски по API-лимиту 10/запрос):
1138
+
1139
+ ```bash
1140
+ # Из JSONL-файла (по одному объекту ключевого слова на строку)
1141
+ direct keywords add --adgroup-id 12345 --from-file keywords.jsonl
1142
+
1143
+ # Inline JSON-массив
1144
+ direct keywords add --keywords-json '[{"Keyword":"купить ноутбук","Bid":10000000},{"Keyword":"купить ПК"}]'
1145
+ ```
1146
+
1147
+ Пример `keywords.jsonl`:
1148
+
1149
+ ```jsonl
1150
+ {"Keyword":"купить ноутбук","Bid":10000000,"UserParam1":"src=ad1"}
1151
+ {"Keyword":"купить ПК","ContextBid":5000000}
1152
+ {"Keyword":"buy laptop","AdGroupId":99999}
1153
+ ```
1154
+
1155
+ - Ключи строки — WSDL CamelCase: `Keyword`, `AdGroupId`, `Bid`, `ContextBid`, `UserParam1`, `UserParam2`.
1156
+ - `--adgroup-id` задаёт значение по умолчанию; в строке можно переопределить через `AdGroupId`.
1157
+ - В каждой строке должны разрешаться `Keyword` и `AdGroupId`; неизвестные поля отклоняются с указанием номера строки.
1158
+ - API-лимит: 10 элементов на запрос `keywords.add` — см. [документацию Yandex Direct](https://yandex.ru/dev/direct/doc/dg/objects/keyword.html). CLI отправит нужное число чанков и склеит `AddResults`.
1159
+ - API-лимит: 200 ключевых слов на одну группу объявлений. CLI печатает предупреждение, если в каком-то `AdGroupId` во входе их больше; API отклонит излишек item-level ошибками.
1160
+ - Item-level ошибки от API не прерывают batch; объединённый вывод содержит и успешные Id, и ошибки.
1161
+ - При сетевой ошибке в середине batch уже созданные Id выводятся в stderr (`Partial success before failure`), чтобы при retry не возникли дубли.
1162
+ - `--dry-run` показывает payload первого чанка плюс `{chunks, totalItems, chunkSize}`.
1163
+
1045
1164
  #### Отчёты
1046
1165
 
1047
1166
  ```bash
@@ -1098,8 +1217,8 @@ direct dynamicads set-bids --id 789 --bid 12500000 --context-bid 9000000 --prior
1098
1217
 
1099
1218
  # Общие стратегии ставок
1100
1219
  direct strategies get --limit 5
1101
- direct strategies add --name "Общая стратегия" --type WbMaximumClicks --spend-limit 1000000000 --average-cpc 30000000 --dry-run
1102
- direct strategies update --id 42 --type WbMaximumClicks --average-cpc 35000000 --dry-run
1220
+ direct strategies add --name "Общая стратегия" --type WbMaximumClicks --weekly-spend-limit 1000000000 --bid-ceiling 30000000 --dry-run
1221
+ direct strategies update --id 42 --type WbMaximumClicks --weekly-spend-limit 35000000 --dry-run
1103
1222
  direct strategies archive --id 42 --dry-run
1104
1223
 
1105
1224
  # Динамические таргеты по фиду
@@ -1113,7 +1232,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
1113
1232
  direct adextensions add --callout-text "Free shipping" --dry-run
1114
1233
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
1115
1234
  direct creatives add --video-id video-id --dry-run
1116
- direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --dry-run
1235
+ direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
1117
1236
  direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
1118
1237
  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
1119
1238
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -1177,6 +1296,17 @@ direct campaigns get --fetch-all # все страницы
1177
1296
  direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
1178
1297
  ```
1179
1298
 
1299
+ ### Ошибки API
1300
+
1301
+ Яндекс Директ может вернуть успешный HTTP-ответ, внутри которого есть
1302
+ item-level `Errors` для конкретного объекта. Direct CLI считает такой ответ
1303
+ ошибкой операции: команда завершается с ненулевым кодом и печатает код ошибки,
1304
+ сообщение и детали.
1305
+
1306
+ Код `8800` с `Object not found` обычно означает, что объект недоступен в
1307
+ текущем `Client-Login` или аккаунте. Перед повтором проверьте выбранный
1308
+ `--login`, `YANDEX_DIRECT_LOGIN` или auth profile.
1309
+
1180
1310
  ### Тестирование
1181
1311
 
1182
1312
  В `tests/` четыре уровня тестов:
@@ -1186,17 +1316,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
1186
1316
  | Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
1187
1317
  | Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
1188
1318
  | Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
1189
- | Live draft write интеграция | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
1319
+ | Live draft write интеграция (v5) | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
1320
+ | v4 live read | `-m v4_live_read` | Да (prod v4 JSON API, только чтение) | Да |
1321
+ | v4 live запись отчётов на уровне аккаунта (opt-in) | `-k _opt_in_write` в `tests/test_v4_live_contracts.py` | Да (prod v4) | Да + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
1190
1322
 
1191
1323
  ```bash
1192
1324
  pip install -e ".[dev]"
1193
1325
  pytest # быстрый уровень — без токена
1194
1326
  pytest -m integration -v # read-only интеграция (нужен токен)
1195
1327
  pytest -m integration_write -v # replay write-кассет (токен не нужен)
1196
- YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты
1328
+ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты (v5)
1197
1329
  YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # перезапись live draft-кассеты
1330
+ YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # жизненный цикл v4 wordstat/forecast
1198
1331
  ```
1199
1332
 
1333
+ Уровень 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`).
1334
+
1200
1335
  #### Smoke-скрипты команд
1201
1336
 
1202
1337
  Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.
@@ -1248,6 +1383,12 @@ sandbox-токен не нужен.
1248
1383
  Для `account-management` нужна переменная
1249
1384
  `YANDEX_DIRECT_V4ACCOUNT_ACCOUNT_ID`; без неё runner покажет `NOT_COVERED`.
1250
1385
 
1386
+ `clients.update` включается только явно, потому что меняет client-level
1387
+ metadata аккаунта. Укажите `YANDEX_DIRECT_CLIENTS_UPDATE_LOGIN` с disposable
1388
+ sandbox `Client-Login`; runner передаст его через `--login` и изменит только
1389
+ `ClientInfo` на уникальный smoke marker. Без этой переменной runner покажет
1390
+ `NOT_COVERED` для `clients.update`.
1391
+
1251
1392
  #### Перезапись write-кассет
1252
1393
 
1253
1394
  Уровень `integration_write` в pytest всё ещё воспроизводит сохранённый