direct-cli 0.3.7__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 (223) hide show
  1. {direct_cli-0.3.7 → 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.7 → direct_cli-0.3.8}/CLAUDE.md +13 -1
  4. {direct_cli-0.3.7 → direct_cli-0.3.8}/PKG-INFO +21 -7
  5. {direct_cli-0.3.7 → direct_cli-0.3.8}/README.md +20 -6
  6. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/ads.py +145 -26
  7. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/feeds.py +11 -1
  8. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/PKG-INFO +21 -7
  9. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/SOURCES.txt +24 -19
  10. {direct_cli-0.3.7 → direct_cli-0.3.8}/pyproject.toml +1 -1
  11. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/build_api_coverage_report.py +85 -34
  12. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/sandbox_write_live.py +18 -7
  13. direct_cli-0.3.8/scripts/test_sandbox_write.sh +38 -0
  14. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/API_COVERAGE.md +1 -1
  15. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/API_ISSUE_AUDIT.md +1 -1
  16. direct_cli-0.3.8/tests/_orphan_store.py +124 -0
  17. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/api_coverage_payloads.py +416 -31
  18. direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +437 -0
  19. direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +437 -0
  20. direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +58 -0
  21. direct_cli-0.3.7/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
  22. direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +69 -0
  23. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/conftest.py +9 -0
  24. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_api_coverage.py +353 -3
  25. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_dry_run.py +55 -12
  26. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_integration.py +117 -0
  27. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_integration_write.py +256 -0
  28. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_low_coverage_payloads.py +213 -8
  29. direct_cli-0.3.8/tests/test_v4_live_contracts.py +341 -0
  30. direct_cli-0.3.7/tests/test_integration_live_write.py → direct_cli-0.3.8/tests/test_v5_live_write.py +27 -28
  31. direct_cli-0.3.7/CHANGELOG.md +0 -11
  32. direct_cli-0.3.7/scripts/test_sandbox_write.sh +0 -23
  33. direct_cli-0.3.7/tests/test_v4_live_contracts.py +0 -190
  34. {direct_cli-0.3.7 → direct_cli-0.3.8}/.env.example +0 -0
  35. {direct_cli-0.3.7 → direct_cli-0.3.8}/.github/copilot-instructions.md +0 -0
  36. {direct_cli-0.3.7 → direct_cli-0.3.8}/.github/workflows/api-coverage.yml +0 -0
  37. {direct_cli-0.3.7 → direct_cli-0.3.8}/.github/workflows/quality.yml +0 -0
  38. {direct_cli-0.3.7 → direct_cli-0.3.8}/.gitignore +0 -0
  39. {direct_cli-0.3.7 → direct_cli-0.3.8}/AGENTS.md +0 -0
  40. {direct_cli-0.3.7 → direct_cli-0.3.8}/MANIFEST.in +0 -0
  41. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/__init__.py +0 -0
  42. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_deprecated.py +0 -0
  43. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_smoke_probes.py +0 -0
  44. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/__init__.py +0 -0
  45. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  46. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
  47. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  48. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  49. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +0 -0
  50. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
  51. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  52. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
  53. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  54. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  55. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/api.py +0 -0
  56. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/auth.py +0 -0
  57. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/cli.py +0 -0
  58. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/__init__.py +0 -0
  59. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/adextensions.py +0 -0
  60. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/adgroups.py +0 -0
  61. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/adimages.py +0 -0
  62. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/advideos.py +0 -0
  63. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/agencyclients.py +0 -0
  64. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/audiencetargets.py +0 -0
  65. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/auth.py +0 -0
  66. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/balance.py +0 -0
  67. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/bidmodifiers.py +0 -0
  68. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/bids.py +0 -0
  69. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/businesses.py +0 -0
  70. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/campaigns.py +0 -0
  71. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/changes.py +0 -0
  72. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/clients.py +0 -0
  73. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/creatives.py +0 -0
  74. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/dictionaries.py +0 -0
  75. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/dynamicads.py +0 -0
  76. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/dynamicfeedadtargets.py +0 -0
  77. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/keywordbids.py +0 -0
  78. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/keywords.py +0 -0
  79. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/keywordsresearch.py +0 -0
  80. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/leads.py +0 -0
  81. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
  82. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/reports.py +0 -0
  83. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/retargeting.py +0 -0
  84. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/sitelinks.py +0 -0
  85. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/smartadtargets.py +0 -0
  86. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/strategies.py +0 -0
  87. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/turbopages.py +0 -0
  88. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4account.py +0 -0
  89. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4events.py +0 -0
  90. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4finance.py +0 -0
  91. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4forecast.py +0 -0
  92. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4goals.py +0 -0
  93. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4shells.py +0 -0
  94. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4tags.py +0 -0
  95. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4wordstat.py +0 -0
  96. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/vcards.py +0 -0
  97. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/output.py +0 -0
  98. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/reports_coverage.py +0 -0
  99. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/smoke_matrix.py +0 -0
  100. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/utils.py +0 -0
  101. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/v4/__init__.py +0 -0
  102. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/v4/money.py +0 -0
  103. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/v4_contracts.py +0 -0
  104. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/wsdl_coverage.py +0 -0
  105. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/dependency_links.txt +0 -0
  106. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/entry_points.txt +0 -0
  107. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/requires.txt +0 -0
  108. {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/top_level.txt +0 -0
  109. {direct_cli-0.3.7 → direct_cli-0.3.8}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  110. {direct_cli-0.3.7 → direct_cli-0.3.8}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  111. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/anonymize_cassettes.py +0 -0
  112. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/build_api_coverage_checklist.py +0 -0
  113. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/check_reports_drift.py +0 -0
  114. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/check_wsdl_drift.py +0 -0
  115. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/patch_vendor_imports.py +0 -0
  116. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/refresh_reports_cache.py +0 -0
  117. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/refresh_wsdl_cache.py +0 -0
  118. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/release_pypi.sh +0 -0
  119. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/test_dangerous_commands.sh +0 -0
  120. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/test_safe_commands.sh +0 -0
  121. {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/update_vendor.sh +0 -0
  122. {direct_cli-0.3.7 → direct_cli-0.3.8}/setup.cfg +0 -0
  123. {direct_cli-0.3.7 → direct_cli-0.3.8}/setup.py +0 -0
  124. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/MANUAL_COVERAGE.md +0 -0
  125. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/__init__.py +0 -0
  126. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  127. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  128. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  129. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  130. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  131. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  132. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  133. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  134. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  135. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  136. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  137. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  138. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  139. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  140. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  141. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  142. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
  143. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  144. /direct_cli-0.3.7/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
  145. /direct_cli-0.3.7/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
  146. /direct_cli-0.3.7/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
  147. /direct_cli-0.3.7/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
  148. /direct_cli-0.3.7/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
  149. /direct_cli-0.3.7/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
  150. /direct_cli-0.3.7/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
  151. /direct_cli-0.3.7/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
  152. /direct_cli-0.3.7/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
  153. /direct_cli-0.3.7/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
  154. /direct_cli-0.3.7/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
  155. /direct_cli-0.3.7/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
  156. /direct_cli-0.3.7/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
  157. /direct_cli-0.3.7/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
  158. /direct_cli-0.3.7/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
  159. /direct_cli-0.3.7/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
  160. /direct_cli-0.3.7/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
  161. /direct_cli-0.3.7/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
  162. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/fixtures/test-video.mp4 +0 -0
  163. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/fields-list.html +0 -0
  164. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/headers.html +0 -0
  165. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/period.html +0 -0
  166. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/spec.html +0 -0
  167. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/type.html +0 -0
  168. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/spec.json +0 -0
  169. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_auth_bw.py +0 -0
  170. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_auth_oauth.py +0 -0
  171. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_auth_op.py +0 -0
  172. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_balance.py +0 -0
  173. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_cli.py +0 -0
  174. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_cli_contract.py +0 -0
  175. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_comprehensive.py +0 -0
  176. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_reports_drift.py +0 -0
  177. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_reports_parsing.py +0 -0
  178. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_smoke_matrix.py +0 -0
  179. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_transport_contract.py +0 -0
  180. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4_contracts.py +0 -0
  181. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4_foundation.py +0 -0
  182. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4_safety.py +0 -0
  183. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4account.py +0 -0
  184. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4events.py +0 -0
  185. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4finance_money.py +0 -0
  186. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4finance_read.py +0 -0
  187. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4forecast.py +0 -0
  188. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4goals.py +0 -0
  189. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4tags.py +0 -0
  190. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4wordstat.py +0 -0
  191. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_vendor_imports.py +0 -0
  192. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/adextensions.xml +0 -0
  193. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/adgroups.xml +0 -0
  194. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/adimages.xml +0 -0
  195. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/ads.xml +0 -0
  196. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/advideos.xml +0 -0
  197. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/agencyclients.xml +0 -0
  198. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/audiencetargets.xml +0 -0
  199. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  200. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/bids.xml +0 -0
  201. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/businesses.xml +0 -0
  202. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/campaigns.xml +0 -0
  203. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/changes.xml +0 -0
  204. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/clients.xml +0 -0
  205. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/creatives.xml +0 -0
  206. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/dictionaries.xml +0 -0
  207. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  208. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  209. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/feeds.xml +0 -0
  210. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  211. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/imports/general.xsd +0 -0
  212. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  213. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/keywordbids.xml +0 -0
  214. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/keywords.xml +0 -0
  215. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  216. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/leads.xml +0 -0
  217. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  218. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/retargetinglists.xml +0 -0
  219. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/sitelinks.xml +0 -0
  220. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/smartadtargets.xml +0 -0
  221. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/strategies.xml +0 -0
  222. {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/turbopages.xml +0 -0
  223. {direct_cli-0.3.7 → 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.7
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`.
@@ -493,7 +495,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
493
495
  direct adextensions add --callout-text "Free shipping" --dry-run
494
496
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
495
497
  direct creatives add --video-id video-id --dry-run
496
- 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
497
499
  direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
498
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
499
501
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -565,17 +567,22 @@ Four tiers of tests live under `tests/`:
565
567
  | Unit / CLI wiring / dry-run | *(none)* | No | No |
566
568
  | Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
567
569
  | 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` |
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` |
569
573
 
570
574
  ```bash
571
575
  pip install -e ".[dev]"
572
576
  pytest # fast tier — no token
573
577
  pytest -m integration -v # read-only integration tests (needs token)
574
578
  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
579
+ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5)
576
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
577
582
  ```
578
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
+
579
586
  #### Smoke command scripts
580
587
 
581
588
  Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
@@ -813,6 +820,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
813
820
  логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
814
821
  или профильные env vars, а не базовые credentials.
815
822
 
823
+ > **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
824
+
816
825
  Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
817
826
  через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
818
827
  подсказкой `use direct instead of direct-cli`.
@@ -1113,7 +1122,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
1113
1122
  direct adextensions add --callout-text "Free shipping" --dry-run
1114
1123
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
1115
1124
  direct creatives add --video-id video-id --dry-run
1116
- 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
1117
1126
  direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
1118
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
1119
1128
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -1186,17 +1195,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
1186
1195
  | Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
1187
1196
  | Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
1188
1197
  | Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
1189
- | 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` |
1190
1201
 
1191
1202
  ```bash
1192
1203
  pip install -e ".[dev]"
1193
1204
  pytest # быстрый уровень — без токена
1194
1205
  pytest -m integration -v # read-only интеграция (нужен токен)
1195
1206
  pytest -m integration_write -v # replay write-кассет (токен не нужен)
1196
- 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)
1197
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
1198
1210
  ```
1199
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
+
1200
1214
  #### Smoke-скрипты команд
1201
1215
 
1202
1216
  Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.
@@ -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`.
@@ -450,7 +452,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
450
452
  direct adextensions add --callout-text "Free shipping" --dry-run
451
453
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
452
454
  direct creatives add --video-id video-id --dry-run
453
- 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
454
456
  direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
455
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
456
458
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -522,17 +524,22 @@ Four tiers of tests live under `tests/`:
522
524
  | Unit / CLI wiring / dry-run | *(none)* | No | No |
523
525
  | Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
524
526
  | Write integration | `-m integration_write` | No (replays VCR cassettes) | No |
525
- | 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` |
526
530
 
527
531
  ```bash
528
532
  pip install -e ".[dev]"
529
533
  pytest # fast tier — no token
530
534
  pytest -m integration -v # read-only integration tests (needs token)
531
535
  pytest -m integration_write -v # write cassette replay (no token needed)
532
- 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)
533
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
534
539
  ```
535
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
+
536
543
  #### Smoke command scripts
537
544
 
538
545
  Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
@@ -770,6 +777,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
770
777
  логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
771
778
  или профильные env vars, а не базовые credentials.
772
779
 
780
+ > **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
781
+
773
782
  Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
774
783
  через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
775
784
  подсказкой `use direct instead of direct-cli`.
@@ -1070,7 +1079,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
1070
1079
  direct adextensions add --callout-text "Free shipping" --dry-run
1071
1080
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
1072
1081
  direct creatives add --video-id video-id --dry-run
1073
- 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
1074
1083
  direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
1075
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
1076
1085
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -1143,17 +1152,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
1143
1152
  | Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
1144
1153
  | Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
1145
1154
  | Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
1146
- | 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` |
1147
1158
 
1148
1159
  ```bash
1149
1160
  pip install -e ".[dev]"
1150
1161
  pytest # быстрый уровень — без токена
1151
1162
  pytest -m integration -v # read-only интеграция (нужен токен)
1152
1163
  pytest -m integration_write -v # replay write-кассет (токен не нужен)
1153
- 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)
1154
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
1155
1167
  ```
1156
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
+
1157
1171
  #### Smoke-скрипты команд
1158
1172
 
1159
1173
  Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.
@@ -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
 
@@ -70,13 +70,23 @@ def get(ctx, ids, limit, fetch_all, output_format, output, fields, dry_run):
70
70
  @feeds.command()
71
71
  @click.option("--name", required=True, help="Feed name")
72
72
  @click.option("--url", required=True, help="Feed URL")
73
+ @click.option(
74
+ "--business-type",
75
+ required=True,
76
+ type=click.Choice(
77
+ ["RETAIL", "HOTELS", "REALTY", "AUTOMOBILES", "FLIGHTS", "OTHER"],
78
+ case_sensitive=False,
79
+ ),
80
+ help="Business type (BusinessTypeEnum)",
81
+ )
73
82
  @click.option("--dry-run", is_flag=True, help="Show request without sending")
74
83
  @click.pass_context
75
- def add(ctx, name, url, dry_run):
84
+ def add(ctx, name, url, business_type, dry_run):
76
85
  """Add feed"""
77
86
  try:
78
87
  feed_data = {
79
88
  "Name": name,
89
+ "BusinessType": business_type.upper(),
80
90
  "SourceType": "URL",
81
91
  "UrlFeed": {"Url": url},
82
92
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.3.7
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`.
@@ -493,7 +495,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
493
495
  direct adextensions add --callout-text "Free shipping" --dry-run
494
496
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
495
497
  direct creatives add --video-id video-id --dry-run
496
- 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
497
499
  direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
498
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
499
501
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -565,17 +567,22 @@ Four tiers of tests live under `tests/`:
565
567
  | Unit / CLI wiring / dry-run | *(none)* | No | No |
566
568
  | Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
567
569
  | 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` |
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` |
569
573
 
570
574
  ```bash
571
575
  pip install -e ".[dev]"
572
576
  pytest # fast tier — no token
573
577
  pytest -m integration -v # read-only integration tests (needs token)
574
578
  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
579
+ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5)
576
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
577
582
  ```
578
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
+
579
586
  #### Smoke command scripts
580
587
 
581
588
  Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
@@ -813,6 +820,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
813
820
  логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
814
821
  или профильные env vars, а не базовые credentials.
815
822
 
823
+ > **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
824
+
816
825
  Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
817
826
  через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
818
827
  подсказкой `use direct instead of direct-cli`.
@@ -1113,7 +1122,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
1113
1122
  direct adextensions add --callout-text "Free shipping" --dry-run
1114
1123
  direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
1115
1124
  direct creatives add --video-id video-id --dry-run
1116
- 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
1117
1126
  direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
1118
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
1119
1128
  direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
@@ -1186,17 +1195,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
1186
1195
  | Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
1187
1196
  | Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
1188
1197
  | Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
1189
- | 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` |
1190
1201
 
1191
1202
  ```bash
1192
1203
  pip install -e ".[dev]"
1193
1204
  pytest # быстрый уровень — без токена
1194
1205
  pytest -m integration -v # read-only интеграция (нужен токен)
1195
1206
  pytest -m integration_write -v # replay write-кассет (токен не нужен)
1196
- 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)
1197
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
1198
1210
  ```
1199
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
+
1200
1214
  #### Smoke-скрипты команд
1201
1215
 
1202
1216
  Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.