direct-cli 0.3.15__tar.gz → 0.3.16__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 (283) hide show
  1. {direct_cli-0.3.15 → direct_cli-0.3.16}/.github/workflows/claude.yml +1 -1
  2. {direct_cli-0.3.15 → direct_cli-0.3.16}/CHANGELOG.md +28 -0
  3. {direct_cli-0.3.15 → direct_cli-0.3.16}/PKG-INFO +12 -4
  4. {direct_cli-0.3.15 → direct_cli-0.3.16}/README.md +11 -3
  5. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +9 -3
  6. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/cli.py +28 -0
  7. direct_cli-0.3.16/direct_cli/commands/v4events.py +232 -0
  8. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/v4finance.py +53 -21
  9. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/v4forecast.py +52 -3
  10. direct_cli-0.3.16/direct_cli/i18n.py +107 -0
  11. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/v4/__init__.py +2 -2
  12. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/v4_contracts.py +131 -42
  13. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli.egg-info/PKG-INFO +12 -4
  14. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli.egg-info/SOURCES.txt +47 -0
  15. direct_cli-0.3.16/docs/audits/API_COVERAGE.md +93 -0
  16. direct_cli-0.3.16/docs/audits/PROJECT_WIRE_SHAPE_AUDIT_2026-05-29.md +193 -0
  17. direct_cli-0.3.16/docs/audits/wire_shape.json +1394 -0
  18. {direct_cli-0.3.15 → direct_cli-0.3.16}/pyproject.toml +1 -1
  19. direct_cli-0.3.16/scripts/audit_wire_shape.py +791 -0
  20. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/test_dangerous_commands.sh +2 -2
  21. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/API_COVERAGE.md +11 -6
  22. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[adextensions_get].yaml +56 -0
  23. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[adgroups_get].yaml +56 -0
  24. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[adimages_get].yaml +66 -0
  25. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[ads_get].yaml +56 -0
  26. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[advideos_get].yaml +56 -0
  27. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[audiencetargets_get].yaml +56 -0
  28. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[bidmodifiers_get].yaml +56 -0
  29. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[bids_get].yaml +56 -0
  30. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[businesses_get].yaml +56 -0
  31. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[campaigns_get].yaml +57 -0
  32. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[changes_check].yaml +56 -0
  33. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[changes_check_campaigns].yaml +56 -0
  34. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[changes_check_dictionaries].yaml +56 -0
  35. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[clients_get].yaml +56 -0
  36. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[creatives_get].yaml +56 -0
  37. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[dictionaries_get].yaml +11696 -0
  38. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[dynamicads_get].yaml +56 -0
  39. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[dynamicfeedadtargets_get].yaml +56 -0
  40. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[feeds_get].yaml +56 -0
  41. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[keywordbids_get].yaml +56 -0
  42. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[keywords_get].yaml +56 -0
  43. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_deduplicate].yaml +59 -0
  44. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_has_search_volume].yaml +58 -0
  45. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[leads_get].yaml +56 -0
  46. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[negativekeywordsharedsets_get].yaml +56 -0
  47. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[reports_get].yaml +64 -0
  48. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[retargeting_get].yaml +56 -0
  49. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[sitelinks_get].yaml +56 -0
  50. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[smartadtargets_get].yaml +56 -0
  51. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[strategies_get].yaml +66 -0
  52. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[turbopages_get].yaml +56 -0
  53. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[v4events_get_events_log].yaml +52 -0
  54. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[v4finance_get_clients_units].yaml +52 -0
  55. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[v4forecast_list].yaml +52 -0
  56. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[v4goals_get_stat_goals].yaml +52 -0
  57. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[v4tags_get_campaigns].yaml +52 -0
  58. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[v4wordstat_list_reports].yaml +52 -0
  59. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_read_command[vcards_get].yaml +57 -0
  60. direct_cli-0.3.16/tests/cassettes/test_read_cassettes/test_v4finance_check_payment_unknown_transaction.yaml +52 -0
  61. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/conftest.py +68 -17
  62. direct_cli-0.3.16/tests/test_audit_wire_shape.py +158 -0
  63. direct_cli-0.3.16/tests/test_i18n.py +118 -0
  64. direct_cli-0.3.16/tests/test_read_cassettes.py +252 -0
  65. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4_contracts.py +20 -5
  66. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4_runtime_shape.py +29 -3
  67. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4events.py +110 -0
  68. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4finance_money.py +81 -9
  69. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4forecast.py +68 -1
  70. direct_cli-0.3.15/direct_cli/commands/v4events.py +0 -120
  71. {direct_cli-0.3.15 → direct_cli-0.3.16}/.env.example +0 -0
  72. {direct_cli-0.3.15 → direct_cli-0.3.16}/.github/copilot-instructions.md +0 -0
  73. {direct_cli-0.3.15 → direct_cli-0.3.16}/.github/workflows/api-coverage.yml +0 -0
  74. {direct_cli-0.3.15 → direct_cli-0.3.16}/.github/workflows/quality.yml +0 -0
  75. {direct_cli-0.3.15 → direct_cli-0.3.16}/.gitignore +0 -0
  76. {direct_cli-0.3.15 → direct_cli-0.3.16}/AGENTS.md +0 -0
  77. {direct_cli-0.3.15 → direct_cli-0.3.16}/CLAUDE.md +0 -0
  78. {direct_cli-0.3.15 → direct_cli-0.3.16}/MANIFEST.in +0 -0
  79. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/__init__.py +0 -0
  80. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_bidding_strategy.py +0 -0
  81. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_deprecated.py +0 -0
  82. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_smoke_probes.py +0 -0
  83. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/__init__.py +0 -0
  84. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  85. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
  86. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  87. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  88. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +0 -0
  89. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
  90. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  91. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  92. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  93. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/api.py +0 -0
  94. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/auth.py +0 -0
  95. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/__init__.py +0 -0
  96. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/adextensions.py +0 -0
  97. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/adgroups.py +0 -0
  98. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/adimages.py +0 -0
  99. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/ads.py +0 -0
  100. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/advideos.py +0 -0
  101. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/agencyclients.py +0 -0
  102. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/audiencetargets.py +0 -0
  103. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/auth.py +0 -0
  104. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/balance.py +0 -0
  105. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/bidmodifiers.py +0 -0
  106. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/bids.py +0 -0
  107. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/businesses.py +0 -0
  108. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/campaigns.py +0 -0
  109. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/changes.py +0 -0
  110. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/clients.py +0 -0
  111. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/creatives.py +0 -0
  112. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/dictionaries.py +0 -0
  113. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/dynamicads.py +0 -0
  114. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/dynamicfeedadtargets.py +0 -0
  115. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/feeds.py +0 -0
  116. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/keywordbids.py +0 -0
  117. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/keywords.py +0 -0
  118. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/keywordsresearch.py +0 -0
  119. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/leads.py +0 -0
  120. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
  121. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/reports.py +0 -0
  122. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/retargeting.py +0 -0
  123. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/sitelinks.py +0 -0
  124. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/smartadtargets.py +0 -0
  125. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/strategies.py +0 -0
  126. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/turbopages.py +0 -0
  127. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/v4account.py +0 -0
  128. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/v4goals.py +0 -0
  129. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/v4shells.py +0 -0
  130. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/v4tags.py +0 -0
  131. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/v4wordstat.py +0 -0
  132. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/commands/vcards.py +0 -0
  133. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/output.py +0 -0
  134. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/reports_coverage.py +0 -0
  135. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/smoke_matrix.py +0 -0
  136. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/utils.py +0 -0
  137. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/v4/money.py +0 -0
  138. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli/wsdl_coverage.py +0 -0
  139. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli.egg-info/dependency_links.txt +0 -0
  140. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli.egg-info/entry_points.txt +0 -0
  141. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli.egg-info/requires.txt +0 -0
  142. {direct_cli-0.3.15 → direct_cli-0.3.16}/direct_cli.egg-info/top_level.txt +0 -0
  143. {direct_cli-0.3.15 → direct_cli-0.3.16}/docs/audits/issue-198-mutating-wsdl-audit.md +0 -0
  144. {direct_cli-0.3.15 → direct_cli-0.3.16}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  145. {direct_cli-0.3.15 → direct_cli-0.3.16}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  146. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/anonymize_cassettes.py +0 -0
  147. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/build_api_coverage_checklist.py +0 -0
  148. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/build_api_coverage_report.py +0 -0
  149. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/build_wsdl_optional_field_audit.py +0 -0
  150. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/check_all_docs_urls.py +0 -0
  151. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/check_reports_drift.py +0 -0
  152. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/check_wsdl_drift.py +0 -0
  153. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/patch_vendor_imports.py +0 -0
  154. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/preflight_check.sh +0 -0
  155. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/refresh_reports_cache.py +0 -0
  156. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/refresh_wsdl_cache.py +0 -0
  157. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/release_pypi.sh +0 -0
  158. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/sandbox_write_audit.py +0 -0
  159. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/sandbox_write_live.py +0 -0
  160. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/test_safe_commands.sh +0 -0
  161. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/test_sandbox_write.sh +0 -0
  162. {direct_cli-0.3.15 → direct_cli-0.3.16}/scripts/update_vendor.sh +0 -0
  163. {direct_cli-0.3.15 → direct_cli-0.3.16}/setup.cfg +0 -0
  164. {direct_cli-0.3.15 → direct_cli-0.3.16}/setup.py +0 -0
  165. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/API_ISSUE_AUDIT.md +0 -0
  166. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/MANUAL_COVERAGE.md +0 -0
  167. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/WSDL_OPTIONAL_FIELD_AUDIT.md +0 -0
  168. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/__init__.py +0 -0
  169. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/_orphan_store.py +0 -0
  170. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/api_coverage_payloads.py +0 -0
  171. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  172. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  173. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  174. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  175. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  176. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  177. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  178. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  179. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +0 -0
  180. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +0 -0
  181. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  182. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  183. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  184. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +0 -0
  185. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  186. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  187. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  188. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  189. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteRetargetingUpdate.test_retargeting_update.yaml +0 -0
  190. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  191. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
  192. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +0 -0
  193. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  194. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_adgroups_add_update_delete.yaml +0 -0
  195. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_adimages_add_get_delete.yaml +0 -0
  196. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_add_update_delete.yaml +0 -0
  197. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
  198. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_advideos_add_get.yaml +0 -0
  199. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_add_delete.yaml +0 -0
  200. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_suspend_resume.yaml +0 -0
  201. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_bids_set.yaml +0 -0
  202. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_campaign_create_get_delete.yaml +0 -0
  203. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
  204. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_add_delete.yaml +0 -0
  205. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_suspend_resume.yaml +0 -0
  206. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywordbids_set.yaml +0 -0
  207. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_add_update_delete.yaml +0 -0
  208. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_suspend_resume.yaml +0 -0
  209. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_sitelinks_add_get_delete.yaml +0 -0
  210. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_add_update_delete.yaml +0 -0
  211. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_suspend_resume.yaml +0 -0
  212. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/fixtures/test-video.mp4 +0 -0
  213. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/reports_cache/raw/fields-list.html +0 -0
  214. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/reports_cache/raw/headers.html +0 -0
  215. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/reports_cache/raw/period.html +0 -0
  216. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/reports_cache/raw/spec.html +0 -0
  217. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/reports_cache/raw/type.html +0 -0
  218. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/reports_cache/spec.json +0 -0
  219. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_api_coverage.py +0 -0
  220. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_auth_bw.py +0 -0
  221. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_auth_oauth.py +0 -0
  222. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_auth_op.py +0 -0
  223. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_auth_write_json.py +0 -0
  224. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_balance.py +0 -0
  225. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_changes.py +0 -0
  226. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_cli.py +0 -0
  227. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_cli_contract.py +0 -0
  228. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_comprehensive.py +0 -0
  229. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_dry_run.py +0 -0
  230. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_env_loading.py +0 -0
  231. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_integration.py +0 -0
  232. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_integration_write.py +0 -0
  233. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_low_coverage_payloads.py +0 -0
  234. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_reports_drift.py +0 -0
  235. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_reports_parsing.py +0 -0
  236. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_sandbox_write_audit.py +0 -0
  237. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_smoke_matrix.py +0 -0
  238. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_transport_contract.py +0 -0
  239. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_unknown_option_hints.py +0 -0
  240. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4_exit_codes.py +0 -0
  241. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4_foundation.py +0 -0
  242. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4_live_contracts.py +0 -0
  243. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4_safety.py +0 -0
  244. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4account.py +0 -0
  245. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4finance_read.py +0 -0
  246. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4goals.py +0 -0
  247. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4tags.py +0 -0
  248. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v4wordstat.py +0 -0
  249. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_v5_live_write.py +0 -0
  250. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_vendor_imports.py +0 -0
  251. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/test_wsdl_parity_gate.py +0 -0
  252. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/adextensions.xml +0 -0
  253. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/adgroups.xml +0 -0
  254. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/adimages.xml +0 -0
  255. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/ads.xml +0 -0
  256. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/advideos.xml +0 -0
  257. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/agencyclients.xml +0 -0
  258. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/audiencetargets.xml +0 -0
  259. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  260. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/bids.xml +0 -0
  261. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/businesses.xml +0 -0
  262. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/campaigns.xml +0 -0
  263. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/changes.xml +0 -0
  264. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/clients.xml +0 -0
  265. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/creatives.xml +0 -0
  266. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/dictionaries.xml +0 -0
  267. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  268. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  269. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/feeds.xml +0 -0
  270. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  271. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/imports/general.xsd +0 -0
  272. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  273. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/keywordbids.xml +0 -0
  274. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/keywords.xml +0 -0
  275. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  276. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/leads.xml +0 -0
  277. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  278. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/retargetinglists.xml +0 -0
  279. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/sitelinks.xml +0 -0
  280. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/smartadtargets.xml +0 -0
  281. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/strategies.xml +0 -0
  282. {direct_cli-0.3.15 → direct_cli-0.3.16}/tests/wsdl_cache/turbopages.xml +0 -0
  283. {direct_cli-0.3.15 → direct_cli-0.3.16}/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: "--model claude-opus-4-7"
49
+ claude_args: "--model claude-opus-4-8"
50
50
 
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.16
4
+
5
+ **BREAKING CHANGES (regression fix — reverts 0.3.15 wire-shape changes):**
6
+
7
+ - `direct v4finance transfer-money` now requires `--currency` again and
8
+ re-emits `Currency` on every `FromCampaigns` / `ToCampaigns` item.
9
+ The 0.3.15 removal verified against `dg-v4/reference/TransferMoney`
10
+ (legacy v4); the actual Live 4 docs at
11
+ `dg-v4/live/TransferMoney` define `PayCampElement` with
12
+ `CampaignID`, `Sum`, and `Currency`, and explicitly mark `Currency` as
13
+ obligatory in the Live 4 changelog. The CLI now matches the live
14
+ docs 1:1. See audit comment on #125 for the reproducible diff.
15
+ - `direct v4finance pay-campaigns` now requires `--currency` again and
16
+ re-emits `Currency` on every `Payments[]` item. Same root cause:
17
+ `dg-v4/reference/PayCampaigns` (legacy) lacks `Currency`,
18
+ `dg-v4/live/PayCampaigns` (Live 4) requires it.
19
+ - `direct v4finance pay-campaigns` accepts `--pay-method Overdraft`
20
+ again. The Live 4 changelog explicitly adds `Overdraft` for direct
21
+ advertisers (paired with `Bank` for agencies). Only `Bank` keeps the
22
+ `--contract-id` requirement.
23
+ - `direct v4finance create-invoice` now requires `--currency` again and
24
+ re-emits `Currency` on every `Payments[]` item, mirroring
25
+ `dg-v4/live/CreateInvoice`.
26
+
27
+ This release reverts the wire-shape changes shipped by PRs #441, #442,
28
+ #443 (which closed #432, #433, #434). The CLI lives in the `v4finance`
29
+ Live group and must mirror `dg-v4/live/*`, not `dg-v4/reference/*`.
30
+
3
31
  ## 0.3.15
4
32
 
5
33
  **BREAKING CHANGES:**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.3.15
3
+ Version: 0.3.16
4
4
  Summary: Command-line interface for Yandex Direct API
5
5
  Author: axisrow
6
6
  License: MIT
@@ -154,6 +154,12 @@ direct balance --logins client-login --dry-run
154
154
  | `--login` | Direct client login |
155
155
  | `--profile` | Credential profile name |
156
156
  | `--sandbox` | Use sandbox API |
157
+ | `--locale` | Help/message language: `ru` (default) or `en`. Also set via `YANDEX_DIRECT_CLI_LOCALE`. Command and flag names are unchanged. |
158
+
159
+ Help text is shown in Russian by default. Switch to English with
160
+ `direct --locale en ...` or `export YANDEX_DIRECT_CLI_LOCALE=en`. For example,
161
+ `direct v4finance --help` shows the financial master-token setup steps using
162
+ the locale-appropriate Yandex Direct UI labels.
157
163
 
158
164
  ### V4 Live Goals
159
165
 
@@ -186,6 +192,7 @@ direct v4tags update-banners --banner-ids 2571700 --clear-tags --dry-run
186
192
  ```bash
187
193
  direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00
188
194
  direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00 --currency RUB --limit 100 --offset 0 --format table
195
+ direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00 --last-event-only Yes --with-text-description Yes --filter-campaign-ids 123,456 --filter-event-type MoneyOut,MoneyIn
189
196
  ```
190
197
 
191
198
  ### V4 Live Wordstat Reports
@@ -209,6 +216,7 @@ the forecast is ready.
209
216
 
210
217
  ```bash
211
218
  direct v4forecast create --phrases "buy laptop,buy desktop" --geo-ids 213 --currency RUB
219
+ direct v4forecast create --phrases "buy laptop" --geo-ids 213 --auction-bids Yes --common-minus-words "used,broken"
212
220
  direct v4forecast list --format table
213
221
  direct v4forecast get --forecast-id 123 --format table
214
222
  direct v4forecast delete --forecast-id 123
@@ -232,10 +240,10 @@ is omitted. Dry-run output masks the financial token.
232
240
  ```bash
233
241
  direct v4finance get-clients-units --logins client-login,other-client --format table
234
242
  direct v4finance get-credit-limits --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login
235
- direct v4finance create-invoice --payment 123=100.50 --payment 456=25 --master-token MASTER_TOKEN --operation-num 124 --finance-login agency-login --dry-run
243
+ direct v4finance create-invoice --payment 123=100.50 --payment 456=25 --currency RUB --master-token MASTER_TOKEN --operation-num 124 --finance-login agency-login --dry-run
236
244
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
237
- direct v4finance transfer-money --from-campaign-id 123 --to-campaign-id 456 --amount 100.50 --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
238
- direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --contract-id CONTRACT_ID --pay-method Bank --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
245
+ direct v4finance transfer-money --from-campaign-id 123 --to-campaign-id 456 --amount 100.50 --currency RUB --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
246
+ direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --contract-id CONTRACT_ID --pay-method Bank --currency RUB --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
239
247
  ```
240
248
 
241
249
  ### V4 Live Shared Account
@@ -111,6 +111,12 @@ direct balance --logins client-login --dry-run
111
111
  | `--login` | Direct client login |
112
112
  | `--profile` | Credential profile name |
113
113
  | `--sandbox` | Use sandbox API |
114
+ | `--locale` | Help/message language: `ru` (default) or `en`. Also set via `YANDEX_DIRECT_CLI_LOCALE`. Command and flag names are unchanged. |
115
+
116
+ Help text is shown in Russian by default. Switch to English with
117
+ `direct --locale en ...` or `export YANDEX_DIRECT_CLI_LOCALE=en`. For example,
118
+ `direct v4finance --help` shows the financial master-token setup steps using
119
+ the locale-appropriate Yandex Direct UI labels.
114
120
 
115
121
  ### V4 Live Goals
116
122
 
@@ -143,6 +149,7 @@ direct v4tags update-banners --banner-ids 2571700 --clear-tags --dry-run
143
149
  ```bash
144
150
  direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00
145
151
  direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00 --currency RUB --limit 100 --offset 0 --format table
152
+ direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00 --last-event-only Yes --with-text-description Yes --filter-campaign-ids 123,456 --filter-event-type MoneyOut,MoneyIn
146
153
  ```
147
154
 
148
155
  ### V4 Live Wordstat Reports
@@ -166,6 +173,7 @@ the forecast is ready.
166
173
 
167
174
  ```bash
168
175
  direct v4forecast create --phrases "buy laptop,buy desktop" --geo-ids 213 --currency RUB
176
+ direct v4forecast create --phrases "buy laptop" --geo-ids 213 --auction-bids Yes --common-minus-words "used,broken"
169
177
  direct v4forecast list --format table
170
178
  direct v4forecast get --forecast-id 123 --format table
171
179
  direct v4forecast delete --forecast-id 123
@@ -189,10 +197,10 @@ is omitted. Dry-run output masks the financial token.
189
197
  ```bash
190
198
  direct v4finance get-clients-units --logins client-login,other-client --format table
191
199
  direct v4finance get-credit-limits --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login
192
- direct v4finance create-invoice --payment 123=100.50 --payment 456=25 --master-token MASTER_TOKEN --operation-num 124 --finance-login agency-login --dry-run
200
+ direct v4finance create-invoice --payment 123=100.50 --payment 456=25 --currency RUB --master-token MASTER_TOKEN --operation-num 124 --finance-login agency-login --dry-run
193
201
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
194
- direct v4finance transfer-money --from-campaign-id 123 --to-campaign-id 456 --amount 100.50 --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
195
- direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --contract-id CONTRACT_ID --pay-method Bank --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
202
+ direct v4finance transfer-money --from-campaign-id 123 --to-campaign-id 456 --amount 100.50 --currency RUB --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
203
+ direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --contract-id CONTRACT_ID --pay-method Bank --currency RUB --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
196
204
  ```
197
205
 
198
206
  ### V4 Live Shared Account
@@ -176,9 +176,15 @@ class V4LiveClientAdapter(JSONAdapterMixin, TapiAdapter):
176
176
  code = 0
177
177
 
178
178
  if code in (54, 55) and api_params.get("retry_if_exceeded_limit", True):
179
- logger.warning("v4 Live limit exceeded (code=%s), retry in 10s", code)
180
- time.sleep(10)
181
- return True
179
+ # Bound the limit-exceeded retries with the same attempt budget as
180
+ # the server-error branch below. Without this guard the loop is
181
+ # infinite (sleep 10s, retry, forever) whenever the API keeps
182
+ # returning code 54/55 — which hangs the caller indefinitely.
183
+ if repeat_number < api_params.get("retries_if_server_error", 5):
184
+ logger.warning("v4 Live limit exceeded (code=%s), retry in 10s", code)
185
+ time.sleep(10)
186
+ return True
187
+ return False
182
188
 
183
189
  if code in (52, 1000, 1001, 1002) or response.status_code == 500:
184
190
  if repeat_number < api_params.get("retries_if_server_error", 5):
@@ -8,6 +8,11 @@ from click.core import ParameterSource
8
8
 
9
9
  from . import __version__
10
10
  from .auth import get_active_profile, get_credentials, load_env_file
11
+ from .i18n import (
12
+ LOCALE_ENV_VAR,
13
+ resolve_locale,
14
+ t,
15
+ )
11
16
  from .utils import get_docs_url
12
17
 
13
18
  from .commands.campaigns import campaigns
@@ -155,6 +160,21 @@ class DirectCliGroup(_NoSuchOptionHintMixin, click.Group):
155
160
 
156
161
  command_class = DirectCliCommand
157
162
 
163
+ def format_epilog(self, ctx, formatter):
164
+ """Render a locale-aware epilog when the group opts into i18n.
165
+
166
+ A group sets ``localized_epilog_key`` (a key in ``i18n.CATALOG``) and,
167
+ optionally, ``localized_epilog_suffix`` (an already-built tail appended
168
+ verbatim so single-sourced text such as ``V4_EPILOG`` is not
169
+ duplicated). Groups without the attribute keep their static epilog.
170
+ """
171
+ key = getattr(self, "localized_epilog_key", None)
172
+ if key:
173
+ text = t(key, resolve_locale(ctx))
174
+ suffix = getattr(self, "localized_epilog_suffix", None)
175
+ self.epilog = f"{text}\n\n{suffix}" if suffix else text
176
+ super().format_epilog(ctx, formatter)
177
+
158
178
 
159
179
  def _apply_directcli_classes(command: click.Command) -> None:
160
180
  """Recursively retype *command* (and subcommands) so every node in the
@@ -182,6 +202,12 @@ def _apply_directcli_classes(command: click.Command) -> None:
182
202
  @click.option("--login", envvar="YANDEX_DIRECT_LOGIN", help="Client login")
183
203
  @click.option("--profile", help="Credential profile name")
184
204
  @click.option("--sandbox", is_flag=True, help="Use sandbox API")
205
+ @click.option(
206
+ "--locale",
207
+ envvar=LOCALE_ENV_VAR,
208
+ default=None,
209
+ help="Language for help and messages (ru or en; default: ru)",
210
+ )
185
211
  @click.option(
186
212
  "--op-token-ref",
187
213
  envvar="YANDEX_DIRECT_OP_TOKEN_REF",
@@ -209,6 +235,7 @@ def cli(
209
235
  login,
210
236
  profile,
211
237
  sandbox,
238
+ locale,
212
239
  op_token_ref,
213
240
  op_login_ref,
214
241
  bw_token_ref,
@@ -218,6 +245,7 @@ def cli(
218
245
  ctx.ensure_object(dict)
219
246
  ctx.obj["sandbox"] = sandbox
220
247
  ctx.obj["profile"] = profile
248
+ ctx.obj["locale"] = locale
221
249
  active_profile = None
222
250
  if ctx.invoked_subcommand != "auth":
223
251
  active_profile = get_active_profile()
@@ -0,0 +1,232 @@
1
+ """Yandex Direct v4 Live events commands."""
2
+
3
+ import re
4
+ from datetime import datetime
5
+ from typing import Optional
6
+
7
+ import click
8
+
9
+ from ..api import create_v4_client
10
+ from ..output import format_output, print_error
11
+ from ..utils import parse_csv_strings, parse_ids
12
+ from ..v4 import build_v4_body, call_v4
13
+ from ..v4_contracts import v4_method_contract
14
+ from .v4shells import V4_EPILOG
15
+
16
+ V4_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
17
+ V4_DATETIME_RE = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}")
18
+
19
+ # Documented GetEventsLogFilter.EventType enum (dg-v4/live/GetEventsLog).
20
+ EVENT_TYPES = [
21
+ "BannerModerated",
22
+ "CampaignFinished",
23
+ "LowCTR",
24
+ "MoneyOut",
25
+ "MoneyWarning",
26
+ "MoneyIn",
27
+ "PausedByDayBudget",
28
+ "WarnMinPrice",
29
+ "WarnPlace",
30
+ ]
31
+
32
+
33
+ def _events_log_filter(
34
+ campaign_ids: Optional[str],
35
+ banner_ids: Optional[str],
36
+ phrase_ids: Optional[str],
37
+ account_ids: Optional[str],
38
+ event_type: Optional[str],
39
+ ) -> Optional[dict]:
40
+ """Build the GetEventsLogFilter object; None when no filter is provided."""
41
+ filter_obj: dict = {}
42
+ for option_name, raw, key in (
43
+ ("--filter-campaign-ids", campaign_ids, "CampaignIDS"),
44
+ ("--filter-banner-ids", banner_ids, "BannerIDS"),
45
+ ("--filter-phrase-ids", phrase_ids, "PhraseIDS"),
46
+ ("--filter-account-ids", account_ids, "AccountIDS"),
47
+ ):
48
+ if not raw:
49
+ continue
50
+ try:
51
+ parsed = parse_ids(raw)
52
+ except ValueError as exc:
53
+ raise click.UsageError(f"{option_name}: {exc}") from exc
54
+ if parsed:
55
+ filter_obj[key] = parsed
56
+ if event_type:
57
+ types = parse_csv_strings(event_type)
58
+ if types:
59
+ unknown = [value for value in types if value not in EVENT_TYPES]
60
+ if unknown:
61
+ raise click.UsageError(
62
+ "--filter-event-type has unknown values: "
63
+ f"{', '.join(unknown)}. Valid values: {', '.join(EVENT_TYPES)}"
64
+ )
65
+ filter_obj["EventType"] = types
66
+ return filter_obj or None
67
+
68
+
69
+ def _parse_v4_datetime(value: str, option_name: str) -> datetime:
70
+ """Parse a strict v4 Live CLI datetime token."""
71
+ if not V4_DATETIME_RE.fullmatch(value):
72
+ raise click.UsageError(
73
+ f"Invalid {option_name}: {value}. Expected YYYY-MM-DDTHH:MM:SS"
74
+ )
75
+ try:
76
+ return datetime.strptime(value, V4_DATETIME_FORMAT)
77
+ except ValueError:
78
+ raise click.UsageError(
79
+ f"Invalid {option_name}: {value}. Expected YYYY-MM-DDTHH:MM:SS"
80
+ )
81
+
82
+
83
+ def _events_log_param(
84
+ timestamp_from: str,
85
+ timestamp_to: str,
86
+ currency: str,
87
+ limit: Optional[int],
88
+ offset: Optional[int],
89
+ last_event_only: Optional[str] = None,
90
+ with_text_description: Optional[str] = None,
91
+ logins: Optional[str] = None,
92
+ campaign_ids: Optional[str] = None,
93
+ banner_ids: Optional[str] = None,
94
+ phrase_ids: Optional[str] = None,
95
+ account_ids: Optional[str] = None,
96
+ event_type: Optional[str] = None,
97
+ ) -> dict:
98
+ """Build the v4 Live GetEventsLog parameter."""
99
+ from_dt = _parse_v4_datetime(timestamp_from, "--from")
100
+ to_dt = _parse_v4_datetime(timestamp_to, "--to")
101
+ if from_dt > to_dt:
102
+ raise click.UsageError("--from must be earlier than or equal to --to")
103
+
104
+ param = {
105
+ "TimestampFrom": timestamp_from,
106
+ "TimestampTo": timestamp_to,
107
+ "Currency": currency,
108
+ }
109
+ if last_event_only:
110
+ param["LastEventOnly"] = last_event_only
111
+ if with_text_description:
112
+ param["WithTextDescription"] = with_text_description
113
+ if logins:
114
+ login_list = parse_csv_strings(logins)
115
+ if login_list:
116
+ param["Logins"] = login_list
117
+ filter_obj = _events_log_filter(
118
+ campaign_ids, banner_ids, phrase_ids, account_ids, event_type
119
+ )
120
+ if filter_obj is not None:
121
+ param["Filter"] = filter_obj
122
+ if limit is not None:
123
+ param["Limit"] = limit
124
+ if offset is not None:
125
+ param["Offset"] = offset
126
+ return param
127
+
128
+
129
+ @click.group(epilog=V4_EPILOG)
130
+ def v4events():
131
+ """Yandex Direct v4 Live events commands."""
132
+
133
+
134
+ @v4_method_contract("GetEventsLog")
135
+ @v4events.command(name="get-events-log")
136
+ @click.option(
137
+ "--from",
138
+ "timestamp_from",
139
+ required=True,
140
+ help="Start timestamp in YYYY-MM-DDTHH:MM:SS format",
141
+ )
142
+ @click.option(
143
+ "--to",
144
+ "timestamp_to",
145
+ required=True,
146
+ help="End timestamp in YYYY-MM-DDTHH:MM:SS format",
147
+ )
148
+ @click.option(
149
+ "--last-event-only",
150
+ type=click.Choice(["Yes", "No"]),
151
+ help="Return only the latest record per event type — Yes/No",
152
+ )
153
+ @click.option(
154
+ "--with-text-description",
155
+ type=click.Choice(["Yes", "No"]),
156
+ help="Include event text descriptions in the response — Yes/No",
157
+ )
158
+ @click.option("--currency", default="RUB", show_default=True, help="Currency")
159
+ @click.option("--logins", help="Comma-separated client logins")
160
+ @click.option("--filter-campaign-ids", help="Filter: comma-separated campaign IDs")
161
+ @click.option("--filter-banner-ids", help="Filter: comma-separated banner IDs")
162
+ @click.option("--filter-phrase-ids", help="Filter: comma-separated phrase IDs")
163
+ @click.option("--filter-account-ids", help="Filter: comma-separated account IDs")
164
+ @click.option(
165
+ "--filter-event-type",
166
+ help="Filter: comma-separated event types (" + ", ".join(EVENT_TYPES) + ")",
167
+ )
168
+ @click.option("--limit", type=click.IntRange(min=0), help="Result limit")
169
+ @click.option("--offset", type=click.IntRange(min=0), help="Result offset")
170
+ @click.option(
171
+ "--format",
172
+ "output_format",
173
+ default="json",
174
+ type=click.Choice(["json", "table", "csv", "tsv"]),
175
+ help="Output format",
176
+ )
177
+ @click.option("--output", help="Output file")
178
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
179
+ @click.pass_context
180
+ def get_events_log(
181
+ ctx,
182
+ timestamp_from,
183
+ timestamp_to,
184
+ last_event_only,
185
+ with_text_description,
186
+ currency,
187
+ logins,
188
+ filter_campaign_ids,
189
+ filter_banner_ids,
190
+ filter_phrase_ids,
191
+ filter_account_ids,
192
+ filter_event_type,
193
+ limit,
194
+ offset,
195
+ output_format,
196
+ output,
197
+ dry_run,
198
+ ):
199
+ """Get v4 Live events log entries."""
200
+ param = _events_log_param(
201
+ timestamp_from,
202
+ timestamp_to,
203
+ currency,
204
+ limit,
205
+ offset,
206
+ last_event_only=last_event_only,
207
+ with_text_description=with_text_description,
208
+ logins=logins,
209
+ campaign_ids=filter_campaign_ids,
210
+ banner_ids=filter_banner_ids,
211
+ phrase_ids=filter_phrase_ids,
212
+ account_ids=filter_account_ids,
213
+ event_type=filter_event_type,
214
+ )
215
+ if dry_run:
216
+ format_output(build_v4_body("GetEventsLog", param), "json", None)
217
+ return
218
+
219
+ try:
220
+ client = create_v4_client(
221
+ token=ctx.obj.get("token"),
222
+ login=ctx.obj.get("login"),
223
+ profile=ctx.obj.get("profile"),
224
+ sandbox=ctx.obj.get("sandbox"),
225
+ )
226
+ data = call_v4(client, "GetEventsLog", param)
227
+ format_output(data, output_format, output)
228
+ except click.ClickException:
229
+ raise
230
+ except Exception as e:
231
+ print_error(str(e))
232
+ raise click.Abort()
@@ -6,6 +6,7 @@ from typing import Any, Optional
6
6
  import click
7
7
 
8
8
  from ..api import create_v4_client
9
+ from ..i18n import LocalizedOption
9
10
  from ..output import format_output, print_error
10
11
  from ..utils import parse_csv_strings, parse_ids
11
12
  from ..v4 import build_v4_body, call_v4
@@ -15,7 +16,8 @@ from .v4shells import V4_EPILOG
15
16
 
16
17
  FINANCE_TOKEN_MASK = "<redacted>"
17
18
  CUSTOM_TRANSACTION_ID_RE = re.compile(r"[A-Za-z0-9]{32}")
18
- V4_PAY_METHODS = ["Bank"]
19
+ V4_PAY_METHODS = ["Bank", "Overdraft"]
20
+ V4_FINANCE_CURRENCIES = ("RUB", "CHF", "EUR", "KZT", "TRY", "UAH", "USD", "BYN")
19
21
  FINANCE_HELP_EPILOG = (
20
22
  "To issue a master token in the Yandex Direct UI, open Tools -> API -> "
21
23
  "Financial operations, enable the 'Allow financial operations' checkbox, "
@@ -33,11 +35,12 @@ def _logins_param(logins: str) -> list[str]:
33
35
  return login_list
34
36
 
35
37
 
36
- def _invoice_payments_param(payments: tuple[str, ...]) -> dict:
38
+ def _invoice_payments_param(payments: tuple[str, ...], currency: str) -> dict:
37
39
  """Build the v4 Live CreateInvoice payment object parameter."""
38
40
  if not payments:
39
41
  raise click.UsageError("--payment is required")
40
42
 
43
+ normalized_currency = currency.upper()
41
44
  parsed_payments = []
42
45
  seen_campaign_ids = set()
43
46
  for payment in payments:
@@ -62,6 +65,7 @@ def _invoice_payments_param(payments: tuple[str, ...]) -> dict:
62
65
  {
63
66
  "CampaignID": campaign_id,
64
67
  "Sum": parse_v4_money_sum(amount_text),
68
+ "Currency": normalized_currency,
65
69
  }
66
70
  )
67
71
 
@@ -156,6 +160,13 @@ def v4finance():
156
160
  """Yandex Direct v4 Live finance commands."""
157
161
 
158
162
 
163
+ # Localize the group epilog at render time (Russian by default, English via
164
+ # --locale en / YANDEX_DIRECT_CLI_LOCALE). The V4_EPILOG tail is appended
165
+ # verbatim so its single-sourced docs URL is not duplicated. See i18n.py.
166
+ v4finance.localized_epilog_key = "v4finance.master_token_setup"
167
+ v4finance.localized_epilog_suffix = V4_EPILOG
168
+
169
+
159
170
  @v4_method_contract("GetClientsUnits")
160
171
  @v4finance.command(name="get-clients-units")
161
172
  @click.option("--logins", required=True, help="Comma-separated client logins")
@@ -202,11 +213,9 @@ def get_clients_units(ctx, logins, output_format, output, dry_run):
202
213
  )
203
214
  @click.option(
204
215
  "--master-token",
216
+ cls=LocalizedOption,
217
+ help_key="v4finance.master_token_option",
205
218
  envvar="YANDEX_DIRECT_MASTER_TOKEN",
206
- help=(
207
- "Financial master token issued after enabling and saving financial "
208
- "operations in Tools -> API -> Financial operations"
209
- ),
210
219
  )
211
220
  @click.option(
212
221
  "--operation-num",
@@ -330,6 +339,12 @@ def check_payment(
330
339
  required=True,
331
340
  help="Invoice payment as CAMPAIGN_ID=AMOUNT; repeat for multiple campaigns",
332
341
  )
342
+ @click.option(
343
+ "--currency",
344
+ required=True,
345
+ type=click.Choice(V4_FINANCE_CURRENCIES, case_sensitive=False),
346
+ help="Payment currency (RUB/CHF/EUR/KZT/TRY/UAH/USD/BYN)",
347
+ )
333
348
  @click.option(
334
349
  "--finance-token",
335
350
  envvar="YANDEX_DIRECT_FINANCE_TOKEN",
@@ -337,11 +352,9 @@ def check_payment(
337
352
  )
338
353
  @click.option(
339
354
  "--master-token",
355
+ cls=LocalizedOption,
356
+ help_key="v4finance.master_token_option",
340
357
  envvar="YANDEX_DIRECT_MASTER_TOKEN",
341
- help=(
342
- "Financial master token issued after enabling and saving financial "
343
- "operations in Tools -> API -> Financial operations"
344
- ),
345
358
  )
346
359
  @click.option(
347
360
  "--operation-num",
@@ -367,6 +380,7 @@ def check_payment(
367
380
  def create_invoice(
368
381
  ctx,
369
382
  payments,
383
+ currency,
370
384
  finance_token,
371
385
  master_token,
372
386
  operation_num,
@@ -384,7 +398,7 @@ def create_invoice(
384
398
  "CreateInvoice",
385
399
  ctx.obj.get("login"),
386
400
  )
387
- param = _invoice_payments_param(payments)
401
+ param = _invoice_payments_param(payments, currency)
388
402
 
389
403
  if dry_run:
390
404
  format_output(
@@ -427,6 +441,12 @@ def create_invoice(
427
441
  help="Destination campaign ID",
428
442
  )
429
443
  @click.option("--amount", required=True, help="Positive amount, for example 100.50")
444
+ @click.option(
445
+ "--currency",
446
+ required=True,
447
+ type=click.Choice(V4_FINANCE_CURRENCIES, case_sensitive=False),
448
+ help="Transfer currency (RUB/CHF/EUR/KZT/TRY/UAH/USD/BYN)",
449
+ )
430
450
  @click.option(
431
451
  "--finance-token",
432
452
  envvar="YANDEX_DIRECT_FINANCE_TOKEN",
@@ -434,11 +454,9 @@ def create_invoice(
434
454
  )
435
455
  @click.option(
436
456
  "--master-token",
457
+ cls=LocalizedOption,
458
+ help_key="v4finance.master_token_option",
437
459
  envvar="YANDEX_DIRECT_MASTER_TOKEN",
438
- help=(
439
- "Financial master token issued after enabling and saving financial "
440
- "operations in Tools -> API -> Financial operations"
441
- ),
442
460
  )
443
461
  @click.option(
444
462
  "--operation-num",
@@ -462,6 +480,7 @@ def transfer_money(
462
480
  from_campaign_id,
463
481
  to_campaign_id,
464
482
  amount,
483
+ currency,
465
484
  finance_token,
466
485
  master_token,
467
486
  operation_num,
@@ -479,17 +498,20 @@ def transfer_money(
479
498
  ctx.obj.get("login"),
480
499
  )
481
500
  parsed_amount = parse_v4_money_sum(amount)
501
+ normalized_currency = currency.upper()
482
502
  param = {
483
503
  "FromCampaigns": [
484
504
  {
485
505
  "CampaignID": from_campaign_id,
486
506
  "Sum": parsed_amount,
507
+ "Currency": normalized_currency,
487
508
  },
488
509
  ],
489
510
  "ToCampaigns": [
490
511
  {
491
512
  "CampaignID": to_campaign_id,
492
513
  "Sum": parsed_amount,
514
+ "Currency": normalized_currency,
493
515
  },
494
516
  ],
495
517
  }
@@ -514,7 +536,13 @@ def transfer_money(
514
536
  "--pay-method",
515
537
  required=True,
516
538
  type=click.Choice(V4_PAY_METHODS, case_sensitive=False),
517
- help="Payment method",
539
+ help="Payment method: Bank (agency credit) or Overdraft (direct advertiser)",
540
+ )
541
+ @click.option(
542
+ "--currency",
543
+ required=True,
544
+ type=click.Choice(V4_FINANCE_CURRENCIES, case_sensitive=False),
545
+ help="Payment currency (RUB/CHF/EUR/KZT/TRY/UAH/USD/BYN)",
518
546
  )
519
547
  @click.option(
520
548
  "--finance-token",
@@ -523,11 +551,9 @@ def transfer_money(
523
551
  )
524
552
  @click.option(
525
553
  "--master-token",
554
+ cls=LocalizedOption,
555
+ help_key="v4finance.master_token_option",
526
556
  envvar="YANDEX_DIRECT_MASTER_TOKEN",
527
- help=(
528
- "Financial master token issued after enabling and saving financial "
529
- "operations in Tools -> API -> Financial operations"
530
- ),
531
557
  )
532
558
  @click.option(
533
559
  "--operation-num",
@@ -552,6 +578,7 @@ def pay_campaigns(
552
578
  amount,
553
579
  contract_id,
554
580
  pay_method,
581
+ currency,
555
582
  finance_token,
556
583
  master_token,
557
584
  operation_num,
@@ -571,12 +598,17 @@ def pay_campaigns(
571
598
  parsed_amount = parse_v4_money_sum(amount)
572
599
  parsed_campaign_ids = _campaign_ids_param(campaign_ids)
573
600
  pay_method = _non_empty_option(pay_method, "--pay-method")
601
+ normalized_currency = currency.upper()
574
602
  contract_id = (contract_id or "").strip()
575
603
  if pay_method == "Bank" and not contract_id:
576
604
  raise click.UsageError("--contract-id is required when --pay-method Bank")
577
605
  param = {
578
606
  "Payments": [
579
- {"CampaignID": campaign_id, "Sum": parsed_amount}
607
+ {
608
+ "CampaignID": campaign_id,
609
+ "Sum": parsed_amount,
610
+ "Currency": normalized_currency,
611
+ }
580
612
  for campaign_id in parsed_campaign_ids
581
613
  ],
582
614
  "PayMethod": pay_method,