direct-cli 0.4.0__tar.gz → 0.4.1__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 (334) hide show
  1. {direct_cli-0.4.0 → direct_cli-0.4.1}/CHANGELOG.md +154 -0
  2. {direct_cli-0.4.0 → direct_cli-0.4.1}/PKG-INFO +50 -4
  3. {direct_cli-0.4.0 → direct_cli-0.4.1}/README.md +49 -3
  4. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +1 -1
  5. direct_cli-0.4.1/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +27 -0
  6. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +36 -14
  7. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +5 -1
  8. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +10 -6
  9. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +1 -0
  10. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/auth.py +91 -0
  11. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/cli.py +125 -23
  12. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/adextensions.py +2 -1
  13. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/adgroups.py +82 -42
  14. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/adimages.py +2 -1
  15. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/ads.py +125 -62
  16. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/advideos.py +4 -1
  17. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/agencyclients.py +13 -6
  18. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/audiencetargets.py +4 -3
  19. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/auth.py +14 -11
  20. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/balance.py +4 -1
  21. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/bidmodifiers.py +21 -13
  22. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/bids.py +25 -10
  23. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/campaigns.py +150 -87
  24. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/changes.py +16 -10
  25. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/clients.py +5 -2
  26. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/creatives.py +4 -1
  27. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/dynamicads.py +6 -2
  28. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/dynamicfeedadtargets.py +6 -3
  29. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/feeds.py +32 -20
  30. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/keywordbids.py +23 -12
  31. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/keywords.py +107 -55
  32. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/negativekeywordsharedsets.py +8 -0
  33. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/reports.py +5 -0
  34. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/retargeting.py +12 -4
  35. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/sitelinks.py +41 -16
  36. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/smartadtargets.py +7 -4
  37. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/strategies.py +48 -23
  38. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/v4account.py +48 -28
  39. direct_cli-0.4.1/direct_cli/commands/v4adimage.py +209 -0
  40. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/v4events.py +14 -6
  41. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/v4finance.py +160 -35
  42. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/v4forecast.py +3 -2
  43. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/v4goals.py +2 -1
  44. direct_cli-0.4.1/direct_cli/commands/v4keywords.py +81 -0
  45. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/v4tags.py +29 -18
  46. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/v4wordstat.py +3 -2
  47. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/vcards.py +10 -3
  48. direct_cli-0.4.1/direct_cli/i18n.py +158 -0
  49. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/smoke_matrix.py +4 -0
  50. direct_cli-0.4.1/direct_cli/translations/adextensions.json +11 -0
  51. direct_cli-0.4.1/direct_cli/translations/adgroups.json +74 -0
  52. direct_cli-0.4.1/direct_cli/translations/adimages.json +13 -0
  53. direct_cli-0.4.1/direct_cli/translations/ads.json +134 -0
  54. direct_cli-0.4.1/direct_cli/translations/advideos.json +11 -0
  55. direct_cli-0.4.1/direct_cli/translations/agencyclients.json +32 -0
  56. direct_cli-0.4.1/direct_cli/translations/audiencetargets.json +17 -0
  57. direct_cli-0.4.1/direct_cli/translations/auth.json +23 -0
  58. direct_cli-0.4.1/direct_cli/translations/balance.json +4 -0
  59. direct_cli-0.4.1/direct_cli/translations/bidmodifiers.json +46 -0
  60. direct_cli-0.4.1/direct_cli/translations/bids.json +31 -0
  61. direct_cli-0.4.1/direct_cli/translations/businesses.json +5 -0
  62. direct_cli-0.4.1/direct_cli/translations/campaigns.json +313 -0
  63. direct_cli-0.4.1/direct_cli/translations/changes.json +18 -0
  64. direct_cli-0.4.1/direct_cli/translations/clients.json +41 -0
  65. direct_cli-0.4.1/direct_cli/translations/common.json +20 -0
  66. direct_cli-0.4.1/direct_cli/translations/creatives.json +12 -0
  67. direct_cli-0.4.1/direct_cli/translations/dictionaries.json +10 -0
  68. direct_cli-0.4.1/direct_cli/translations/dynamicads.json +10 -0
  69. direct_cli-0.4.1/direct_cli/translations/dynamicfeedadtargets.json +12 -0
  70. direct_cli-0.4.1/direct_cli/translations/feeds.json +37 -0
  71. direct_cli-0.4.1/direct_cli/translations/keywordbids.json +20 -0
  72. direct_cli-0.4.1/direct_cli/translations/keywords.json +62 -0
  73. direct_cli-0.4.1/direct_cli/translations/keywordsresearch.json +7 -0
  74. direct_cli-0.4.1/direct_cli/translations/leads.json +7 -0
  75. direct_cli-0.4.1/direct_cli/translations/negativekeywordsharedsets.json +11 -0
  76. direct_cli-0.4.1/direct_cli/translations/reports.json +23 -0
  77. direct_cli-0.4.1/direct_cli/translations/retargeting.json +16 -0
  78. direct_cli-0.4.1/direct_cli/translations/sitelinks.json +26 -0
  79. direct_cli-0.4.1/direct_cli/translations/smartadtargets.json +19 -0
  80. direct_cli-0.4.1/direct_cli/translations/strategies.json +60 -0
  81. direct_cli-0.4.1/direct_cli/translations/turbopages.json +6 -0
  82. direct_cli-0.4.1/direct_cli/translations/v4account.json +51 -0
  83. direct_cli-0.4.1/direct_cli/translations/v4adimage.json +15 -0
  84. direct_cli-0.4.1/direct_cli/translations/v4events.json +19 -0
  85. direct_cli-0.4.1/direct_cli/translations/v4finance.json +37 -0
  86. direct_cli-0.4.1/direct_cli/translations/v4forecast.json +14 -0
  87. direct_cli-0.4.1/direct_cli/translations/v4goals.json +5 -0
  88. direct_cli-0.4.1/direct_cli/translations/v4keywords.json +6 -0
  89. direct_cli-0.4.1/direct_cli/translations/v4shells.json +3 -0
  90. direct_cli-0.4.1/direct_cli/translations/v4tags.json +30 -0
  91. direct_cli-0.4.1/direct_cli/translations/v4wordstat.json +10 -0
  92. direct_cli-0.4.1/direct_cli/translations/vcards.json +34 -0
  93. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/utils.py +1 -1
  94. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/v4_contracts.py +1 -2
  95. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli.egg-info/PKG-INFO +50 -4
  96. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli.egg-info/SOURCES.txt +50 -1
  97. direct_cli-0.4.0/docs/audits/PROJECT_WIRE_SHAPE_AUDIT_2026-05-29.md → direct_cli-0.4.1/docs/audits/PROJECT_WIRE_SHAPE_AUDIT_2026-05-30.md +11 -64
  98. direct_cli-0.4.1/docs/audits/WIRE_SHAPE_TRIAGE_2026-05-30.md +85 -0
  99. {direct_cli-0.4.0 → direct_cli-0.4.1}/docs/audits/wire_shape.json +17 -441
  100. {direct_cli-0.4.0 → direct_cli-0.4.1}/pyproject.toml +6 -1
  101. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/audit_wire_shape.py +49 -10
  102. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/build_api_coverage_report.py +5 -0
  103. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/check_all_docs_urls.py +15 -0
  104. direct_cli-0.4.1/scripts/probe_drift_urls.sh +59 -0
  105. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/api_coverage_payloads.py +4 -0
  106. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/conftest.py +45 -2
  107. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_api_coverage.py +13 -0
  108. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_audit_wire_shape.py +40 -0
  109. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_auth_oauth.py +135 -0
  110. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_balance.py +20 -0
  111. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_cli.py +90 -9
  112. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_comprehensive.py +2 -0
  113. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_dry_run.py +93 -0
  114. direct_cli-0.4.1/tests/test_i18n.py +388 -0
  115. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_reports_parsing.py +55 -0
  116. direct_cli-0.4.1/tests/test_v4adimage.py +176 -0
  117. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4finance_money.py +79 -6
  118. direct_cli-0.4.1/tests/test_v4keywords.py +112 -0
  119. direct_cli-0.4.0/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -14
  120. direct_cli-0.4.0/direct_cli/i18n.py +0 -107
  121. direct_cli-0.4.0/tests/test_i18n.py +0 -118
  122. {direct_cli-0.4.0 → direct_cli-0.4.1}/.env.example +0 -0
  123. {direct_cli-0.4.0 → direct_cli-0.4.1}/.github/copilot-instructions.md +0 -0
  124. {direct_cli-0.4.0 → direct_cli-0.4.1}/.github/workflows/api-coverage.yml +0 -0
  125. {direct_cli-0.4.0 → direct_cli-0.4.1}/.github/workflows/claude.yml +0 -0
  126. {direct_cli-0.4.0 → direct_cli-0.4.1}/.github/workflows/quality.yml +0 -0
  127. {direct_cli-0.4.0 → direct_cli-0.4.1}/.gitignore +0 -0
  128. {direct_cli-0.4.0 → direct_cli-0.4.1}/AGENTS.md +0 -0
  129. {direct_cli-0.4.0 → direct_cli-0.4.1}/CLAUDE.md +0 -0
  130. {direct_cli-0.4.0 → direct_cli-0.4.1}/MANIFEST.in +0 -0
  131. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/__init__.py +0 -0
  132. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_bidding_strategy.py +0 -0
  133. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_deprecated.py +0 -0
  134. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_smoke_probes.py +0 -0
  135. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/__init__.py +0 -0
  136. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  137. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  138. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  139. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  140. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/api.py +0 -0
  141. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/__init__.py +0 -0
  142. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/businesses.py +0 -0
  143. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/dictionaries.py +0 -0
  144. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/keywordsresearch.py +0 -0
  145. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/leads.py +0 -0
  146. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/turbopages.py +0 -0
  147. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/commands/v4shells.py +0 -0
  148. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/output.py +0 -0
  149. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/reports_coverage.py +0 -0
  150. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/v4/__init__.py +0 -0
  151. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/v4/money.py +0 -0
  152. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli/wsdl_coverage.py +0 -0
  153. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli.egg-info/dependency_links.txt +0 -0
  154. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli.egg-info/entry_points.txt +0 -0
  155. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli.egg-info/requires.txt +0 -0
  156. {direct_cli-0.4.0 → direct_cli-0.4.1}/direct_cli.egg-info/top_level.txt +0 -0
  157. {direct_cli-0.4.0 → direct_cli-0.4.1}/docs/audits/API_COVERAGE.md +0 -0
  158. {direct_cli-0.4.0 → direct_cli-0.4.1}/docs/audits/issue-198-mutating-wsdl-audit.md +0 -0
  159. {direct_cli-0.4.0 → direct_cli-0.4.1}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  160. {direct_cli-0.4.0 → direct_cli-0.4.1}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  161. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/anonymize_cassettes.py +0 -0
  162. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/build_api_coverage_checklist.py +0 -0
  163. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/build_wsdl_optional_field_audit.py +0 -0
  164. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/check_reports_drift.py +0 -0
  165. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/check_wsdl_drift.py +0 -0
  166. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/patch_vendor_imports.py +0 -0
  167. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/preflight_check.sh +0 -0
  168. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/refresh_reports_cache.py +0 -0
  169. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/refresh_wsdl_cache.py +0 -0
  170. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/release_pypi.sh +0 -0
  171. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/sandbox_write_audit.py +0 -0
  172. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/sandbox_write_live.py +0 -0
  173. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/test_dangerous_commands.sh +0 -0
  174. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/test_safe_commands.sh +0 -0
  175. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/test_sandbox_write.sh +0 -0
  176. {direct_cli-0.4.0 → direct_cli-0.4.1}/scripts/update_vendor.sh +0 -0
  177. {direct_cli-0.4.0 → direct_cli-0.4.1}/setup.cfg +0 -0
  178. {direct_cli-0.4.0 → direct_cli-0.4.1}/setup.py +0 -0
  179. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/API_COVERAGE.md +0 -0
  180. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/API_ISSUE_AUDIT.md +0 -0
  181. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/MANUAL_COVERAGE.md +0 -0
  182. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/WSDL_OPTIONAL_FIELD_AUDIT.md +0 -0
  183. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/__init__.py +0 -0
  184. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/_orphan_store.py +0 -0
  185. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  186. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  187. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  188. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  189. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  190. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  191. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  192. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  193. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +0 -0
  194. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +0 -0
  195. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  196. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  197. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  198. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +0 -0
  199. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  200. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  201. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  202. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  203. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteRetargetingUpdate.test_retargeting_update.yaml +0 -0
  204. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  205. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
  206. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +0 -0
  207. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  208. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[adextensions_get].yaml +0 -0
  209. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[adgroups_get].yaml +0 -0
  210. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[adimages_get].yaml +0 -0
  211. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[ads_get].yaml +0 -0
  212. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[advideos_get].yaml +0 -0
  213. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[audiencetargets_get].yaml +0 -0
  214. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[bidmodifiers_get].yaml +0 -0
  215. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[bids_get].yaml +0 -0
  216. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[businesses_get].yaml +0 -0
  217. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[campaigns_get].yaml +0 -0
  218. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[changes_check].yaml +0 -0
  219. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[changes_check_campaigns].yaml +0 -0
  220. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[changes_check_dictionaries].yaml +0 -0
  221. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[clients_get].yaml +0 -0
  222. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[creatives_get].yaml +0 -0
  223. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[dictionaries_get].yaml +0 -0
  224. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[dynamicads_get].yaml +0 -0
  225. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[dynamicfeedadtargets_get].yaml +0 -0
  226. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[feeds_get].yaml +0 -0
  227. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[keywordbids_get].yaml +0 -0
  228. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[keywords_get].yaml +0 -0
  229. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_deduplicate].yaml +0 -0
  230. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_has_search_volume].yaml +0 -0
  231. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[leads_get].yaml +0 -0
  232. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[negativekeywordsharedsets_get].yaml +0 -0
  233. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[reports_get].yaml +0 -0
  234. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[retargeting_get].yaml +0 -0
  235. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[sitelinks_get].yaml +0 -0
  236. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[smartadtargets_get].yaml +0 -0
  237. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[strategies_get].yaml +0 -0
  238. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[turbopages_get].yaml +0 -0
  239. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[v4events_get_events_log].yaml +0 -0
  240. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[v4finance_get_clients_units].yaml +0 -0
  241. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[v4forecast_list].yaml +0 -0
  242. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[v4goals_get_stat_goals].yaml +0 -0
  243. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[v4tags_get_campaigns].yaml +0 -0
  244. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[v4wordstat_list_reports].yaml +0 -0
  245. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_read_command[vcards_get].yaml +0 -0
  246. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_read_cassettes/test_v4finance_check_payment_unknown_transaction.yaml +0 -0
  247. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_adgroups_add_update_delete.yaml +0 -0
  248. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_adimages_add_get_delete.yaml +0 -0
  249. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_add_update_delete.yaml +0 -0
  250. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
  251. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_advideos_add_get.yaml +0 -0
  252. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_add_delete.yaml +0 -0
  253. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_suspend_resume.yaml +0 -0
  254. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_bids_set.yaml +0 -0
  255. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_campaign_create_get_delete.yaml +0 -0
  256. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
  257. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_add_delete.yaml +0 -0
  258. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_suspend_resume.yaml +0 -0
  259. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywordbids_set.yaml +0 -0
  260. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_add_update_delete.yaml +0 -0
  261. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_suspend_resume.yaml +0 -0
  262. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_sitelinks_add_get_delete.yaml +0 -0
  263. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_add_update_delete.yaml +0 -0
  264. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_suspend_resume.yaml +0 -0
  265. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/fixtures/test-video.mp4 +0 -0
  266. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/reports_cache/raw/fields-list.html +0 -0
  267. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/reports_cache/raw/headers.html +0 -0
  268. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/reports_cache/raw/period.html +0 -0
  269. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/reports_cache/raw/spec.html +0 -0
  270. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/reports_cache/raw/type.html +0 -0
  271. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/reports_cache/spec.json +0 -0
  272. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_auth_bw.py +0 -0
  273. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_auth_op.py +0 -0
  274. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_auth_write_json.py +0 -0
  275. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_changes.py +0 -0
  276. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_cli_contract.py +0 -0
  277. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_env_loading.py +0 -0
  278. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_integration.py +0 -0
  279. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_integration_write.py +0 -0
  280. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_low_coverage_payloads.py +0 -0
  281. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_read_cassettes.py +0 -0
  282. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_reports_drift.py +0 -0
  283. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_sandbox_write_audit.py +0 -0
  284. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_smoke_matrix.py +0 -0
  285. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_transport_contract.py +0 -0
  286. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_unknown_option_hints.py +0 -0
  287. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4_contracts.py +0 -0
  288. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4_exit_codes.py +0 -0
  289. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4_foundation.py +0 -0
  290. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4_live_contracts.py +0 -0
  291. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4_runtime_shape.py +0 -0
  292. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4_safety.py +0 -0
  293. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4account.py +0 -0
  294. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4events.py +0 -0
  295. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4finance_read.py +0 -0
  296. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4forecast.py +0 -0
  297. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4goals.py +0 -0
  298. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4tags.py +0 -0
  299. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v4wordstat.py +0 -0
  300. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_v5_live_write.py +0 -0
  301. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_vendor_imports.py +0 -0
  302. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/test_wsdl_parity_gate.py +0 -0
  303. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/adextensions.xml +0 -0
  304. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/adgroups.xml +0 -0
  305. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/adimages.xml +0 -0
  306. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/ads.xml +0 -0
  307. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/advideos.xml +0 -0
  308. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/agencyclients.xml +0 -0
  309. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/audiencetargets.xml +0 -0
  310. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  311. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/bids.xml +0 -0
  312. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/businesses.xml +0 -0
  313. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/campaigns.xml +0 -0
  314. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/changes.xml +0 -0
  315. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/clients.xml +0 -0
  316. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/creatives.xml +0 -0
  317. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/dictionaries.xml +0 -0
  318. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  319. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  320. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/feeds.xml +0 -0
  321. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  322. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/imports/general.xsd +0 -0
  323. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  324. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/keywordbids.xml +0 -0
  325. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/keywords.xml +0 -0
  326. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  327. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/leads.xml +0 -0
  328. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  329. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/retargetinglists.xml +0 -0
  330. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/sitelinks.xml +0 -0
  331. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/smartadtargets.xml +0 -0
  332. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/strategies.xml +0 -0
  333. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/turbopages.xml +0 -0
  334. {direct_cli-0.4.0 → direct_cli-0.4.1}/tests/wsdl_cache/vcards.xml +0 -0
@@ -1,5 +1,159 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ Russian-default CLI localization across all command modules (epic #466).
6
+
7
+ **Fixed — bug hunt (#483):**
8
+
9
+ - `bids get` / `keywordbids get`: refuse an empty `SelectionCriteria` before the
10
+ API call, raising a `UsageError` that asks for at least one filter
11
+ (`--campaign-ids` / `--adgroup-ids` / `--keyword-ids` / `--serving-statuses`)
12
+ instead of letting the API reject it with the opaque error 4001.
13
+ - `bids set-auto`: require exactly one of `--campaign-id`, `--adgroup-id`, or
14
+ `--keyword-id` via the shared `add_single_id_selector` (the three are mutually
15
+ exclusive per the API docs), matching `bids set`.
16
+ - `reports get`: reject a `--fields` value that parses to an empty list (for
17
+ example `",,,"`) before building the request, instead of sending an invalid
18
+ `FieldNames: []` (API error 8000).
19
+ - Error-handling consistency: `get`/lifecycle handlers across `bids`,
20
+ `keywordbids`, `negativekeywordsharedsets`, `balance`, `strategies`,
21
+ `retargeting`, `ads` (all 8 commands), and `advideos` now re-raise
22
+ `click.UsageError` / `click.ClickException` before the generic
23
+ `except Exception`, so validation errors keep their Click formatting and
24
+ exit code 2 instead of being downgraded to an `Abort`.
25
+ - Vendor `tapi_yandex_direct`: `to_columns()` no longer raises `IndexError` on
26
+ report rows shorter than the header (pads with `""`); the error handler reads
27
+ `error_detail` with `.get()` so an unfamiliar error structure no longer masks
28
+ the original API error with a `KeyError`.
29
+ - `utils.parse_priority_goals_spec`: corrected the item type annotation to
30
+ `List[Dict[str, Any]]` (items hold `"YES"/"NO"` strings, not only ints).
31
+
32
+ **Fixed — `--help` hung on a client-login network call (#480 follow-up):**
33
+
34
+ - After #480, `get_credentials` resolved the bare Client-Login via a network
35
+ `clients.get` on every CLI invocation — including `<group> --help` — whenever
36
+ an OAuth profile with an email login had not yet been migrated. That call had
37
+ no timeout, so a slow link or a Yandex SmartCaptcha gateway could hang the
38
+ CLI. Help/version passes now skip the resolver, the resolver is capped with a
39
+ hard timeout, and the unit suite neutralizes it so tests never touch the
40
+ network.
41
+
42
+ **Fixed — auth login saved Passport email, breaking v4 (#480):**
43
+
44
+ - `direct auth login` (OAuth / PKCE) stored the Passport email
45
+ (`<login>@yandex.ru`) in `auth.json`, which Direct v4 AccountManagement
46
+ rejects with `FaultCode 259` ("This client does not exist") — breaking
47
+ `direct balance` and `direct v4account ...`. Login now resolves the bare
48
+ **Client-Login** via a one-shot v5 `clients.get` (`resolve_account_login`),
49
+ falling back to the Passport login only if that call fails.
50
+ - `get_credentials` migrates older profiles in place: when a stored OAuth login
51
+ is an email whose local part matches the token owner's resolved Client-Login,
52
+ it is rewritten to the bare login (one-time, persisted). Agency profiles whose
53
+ login differs from the token owner are never clobbered, and an explicit
54
+ `--login` is never overridden.
55
+
56
+ **Localized — interpolated error messages (#478), completing epic #466:**
57
+
58
+ - Rewrote all 121 interpolated `click.UsageError` / `click.BadParameter` /
59
+ `print_*` messages (114 f-strings + 7 string concatenations) across the
60
+ command modules into the stable `t("<template>").format(**kwargs)` pattern,
61
+ with 96 unique English templates translated to Russian (6 shared templates,
62
+ e.g. `Provide a non-empty comma-separated {wsdl_key} list.`, in
63
+ `common.json`). Placeholder names, conversions, and format specs are
64
+ preserved verbatim in both locales, so the rendered English text is
65
+ byte-identical to before and the Russian render fills the same fields.
66
+ - `t()` gained `@overload` signatures (`str -> str`, `None -> None`) so
67
+ `t(...).format(...)` type-checks without touching call sites.
68
+ - `tests/test_i18n.py` now (a) flags f-string / concat — not just static —
69
+ bare literals in `_RUNTIME_MESSAGE_FUNCS` calls (also covering
70
+ `print_success`), accepting only `t(...)` / `t(...).format(...)`; and
71
+ (b) asserts placeholder parity between every English template key and its
72
+ Russian translation. This completes the error-message localization checklist
73
+ of epic #466.
74
+
75
+ **Localized — static error messages (#477):**
76
+
77
+ - Wrapped all 179 static `click.UsageError` / `click.BadParameter` string
78
+ literals (and the `auth` OAuth-code `click.prompt`) across 33 command
79
+ modules in `t(...)`, with Russian translations added to the per-module
80
+ `translations/*.json` catalogs; seven messages shared across modules
81
+ (e.g. `Provide at least one field to update`,
82
+ `--status and --statuses are mutually exclusive`) live in `common.json`.
83
+ Validation errors now render in Russian by default and in English under
84
+ `--locale en`. Flag names, enum values, and WSDL field names are unchanged.
85
+ - Extended `tests/test_i18n.py::test_localized_groups_wrap_runtime_messages`
86
+ to also scan `UsageError` / `BadParameter` / `prompt` / `confirm` (bare or
87
+ `click.<Name>`); a bare string literal first argument is rejected. Only
88
+ static `ast.Constant` literals are enforced for now — interpolated
89
+ (f-string / concat) messages are stage B (#478).
90
+ - Test suite defaults the CLI locale to English (`tests/conftest.py` autouse
91
+ fixture) so the existing English error-contract assertions stay valid; the
92
+ Russian default remains covered by `tests/test_i18n.py`.
93
+
94
+ **Added — scalable i18n mechanism (#467):**
95
+
96
+ - Source-string-keyed translation catalog: the English `help=` / docstring /
97
+ epilog text is the catalog key, with Russian translations in external
98
+ `direct_cli/translations/*.json` files (one per module, plus shared
99
+ `common.json`). No `cls=`/`help_key` edits in command modules —
100
+ `cli._apply_directcli_classes` retypes every plain `click.Option` to
101
+ `LocalizedOption` and localizes command/group docstrings and epilogs at
102
+ render time.
103
+ - `t()` is now source-keyed and context-free safe (`set_active_locale`), so
104
+ `print_*` runtime messages localize too. `--locale` is eager so the root
105
+ `--help` epilog honors an inline `--locale`.
106
+ - `tests/test_i18n.py` gains a `LOCALIZED_GROUPS` registry with two enforced
107
+ invariants per localized module: translation completeness (no silent English
108
+ leak under the Russian default) and `print_*` runtime-message wrapping.
109
+ - `v4finance` migrated to the new mechanism and fully localized as the
110
+ reference module.
111
+
112
+ **Localized — Core search (#468):**
113
+
114
+ - Russian help/docstrings for `campaigns`, `ads`, `adgroups`, `keywords`,
115
+ `keywordbids`, and `bids` (510 unique strings across the six modules).
116
+ WSDL field paths, enum values, and flag names are kept verbatim; only
117
+ human-readable text is translated. These groups join `LOCALIZED_GROUPS`,
118
+ so their translation completeness is now enforced by `test_i18n.py`.
119
+
120
+ **Localized — Targeting & creatives (#469):**
121
+
122
+ - Russian help/docstrings for `strategies`, `bidmodifiers`, `smartadtargets`,
123
+ `vcards`, `feeds`, `dynamicads`, `audiencetargets`, `dynamicfeedadtargets`,
124
+ `retargeting`, `negativekeywordsharedsets`, `adextensions`, `adimages`,
125
+ `sitelinks`, `creatives`, `advideos`, and `turbopages` (247 unique strings
126
+ across the sixteen modules). WSDL field paths, enum values, and flag names
127
+ are kept verbatim; only human-readable text is translated. These groups
128
+ join `LOCALIZED_GROUPS`, so their translation completeness is now enforced
129
+ by `test_i18n.py`.
130
+
131
+ **Localized — Account, clients, reporting (#470):**
132
+
133
+ - Russian help/docstrings for `clients`, `agencyclients`, `reports`, `changes`,
134
+ `auth`, `leads`, `dictionaries`, `keywordsresearch`, `businesses`, and
135
+ `balance` (142 source strings across the ten modules). WSDL field paths,
136
+ enum values, and flag names are kept verbatim; only human-readable text is
137
+ translated.
138
+ - First modules with localized **runtime messages**: `print_*` calls carrying a
139
+ human-readable literal (`auth` interactive prompts, the `agencyclients delete`
140
+ not-supported notice) are now wrapped in `t()` so they follow the active
141
+ locale. `print_error(str(e))` API-error passthroughs are unchanged. These
142
+ groups join `LOCALIZED_GROUPS`, enforcing both translation completeness and
143
+ the runtime-message wrapping invariant.
144
+
145
+ **Localized — v4 Live services (#471), completing epic #466:**
146
+
147
+ - Russian help/docstrings for `v4account`, `v4tags`, `v4forecast`,
148
+ `v4wordstat`, `v4events`, `v4adimage`, `v4goals`, `v4keywords`, and `v4meta`
149
+ (87 source strings across the nine modules). `v4finance` was already
150
+ localized as the reference module in #467.
151
+ - With these groups, **all 42 CLI command groups are in `LOCALIZED_GROUPS`**:
152
+ the completeness invariant now covers the entire command tree, so every
153
+ English help/docstring string ships with a Russian translation under the
154
+ Russian default. The `v4 Live` epilog (single-sourced docs URL) stays
155
+ verbatim per the URL-registry rule. This closes the localization epic #466.
156
+
3
157
  ## 0.4.0
4
158
 
5
159
  Milestone release closing the 0.4.0 roadmap (#123): typed Yandex Direct
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Command-line interface for Yandex Direct API
5
5
  Author: axisrow
6
6
  License: MIT
@@ -208,6 +208,31 @@ direct v4wordstat get-report --report-id 123 --format table
208
208
  direct v4wordstat delete-report --report-id 123
209
209
  ```
210
210
 
211
+ ### V4 Live Keyword Suggestions
212
+
213
+ `get-suggestion` returns up to 20 related phrases for the seed phrases. Repeat
214
+ `--keyword` for multiple seeds. The method consumes API points.
215
+
216
+ ```bash
217
+ direct v4keywords get-suggestion --keyword холодильник --keyword камера
218
+ direct v4keywords get-suggestion --keyword "buy laptop" --format table
219
+ direct v4keywords get-suggestion --keyword холодильник --dry-run
220
+ ```
221
+
222
+ ### V4 Live Ad-Image Associations
223
+
224
+ `AdImageAssociation` is exposed as two typed commands. `get` reads ad-to-image
225
+ associations via an optional selection filter (an empty filter returns up to
226
+ 10000 associations). `set` attaches or detaches images: `--association AD_ID=HASH`
227
+ attaches an image, `--association AD_ID` (no hash) detaches the current image
228
+ (max 10000 associations per call).
229
+
230
+ ```bash
231
+ direct v4adimage get --ad-ids 123,456 --status-moderate Yes --limit 20
232
+ direct v4adimage get --campaign-ids 789 --format table
233
+ direct v4adimage set --association 123=abc123hash --association 456 --dry-run
234
+ ```
235
+
211
236
  ### V4 Live Budget Forecasts
212
237
 
213
238
  Budget forecasts are asynchronous. Direct CLI makes exactly one API call per
@@ -233,9 +258,15 @@ per-request token from `--master-token`, `--operation-num`, and
233
258
  Environment variables are
234
259
  `YANDEX_DIRECT_MASTER_TOKEN`, `YANDEX_DIRECT_FINANCE_LOGIN`,
235
260
  `YANDEX_DIRECT_FINANCE_TOKEN`, and `YANDEX_DIRECT_OPERATION_NUM`.
236
- `transfer-money` and `pay-campaigns` are dry-run-only in this release and
237
- always require `--dry-run`; `create-invoice` can be sent live when `--dry-run`
238
- is omitted. Dry-run output masks the financial token.
261
+ `transfer-money`, `pay-campaigns`, and `pay-campaigns-by-card` are dry-run-only
262
+ in this release and always require `--dry-run`; `create-invoice` can be sent
263
+ live when `--dry-run` is omitted. Dry-run output masks the financial token.
264
+ `pay-campaigns-by-card` has an undocumented request shape; its dry-run body
265
+ mirrors the documented `pay-campaigns` shape as a best-effort preview.
266
+
267
+ > ⚠ **Finance commands have not been tested against the live API.** Treat the
268
+ > request shapes as best-effort and always verify with `--dry-run` before
269
+ > sending anything that runs live (`create-invoice`).
239
270
 
240
271
  ```bash
241
272
  direct v4finance get-clients-units --logins client-login,other-client --format table
@@ -244,6 +275,7 @@ direct v4finance create-invoice --payment 123=100.50 --payment 456=25 --currency
244
275
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
245
276
  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
277
  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
278
+ direct v4finance pay-campaigns-by-card --campaign-ids 123,456 --amount 100.50 --currency RUB --master-token MASTER_TOKEN --operation-num 128 --finance-login agency-login --dry-run
247
279
  ```
248
280
 
249
281
  ### V4 Live Shared Account
@@ -272,6 +304,20 @@ direct v4account account-management --action TransferMoney --from-account-id 132
272
304
  direct --sandbox v4account enable-shared-account --client-login client-login
273
305
  ```
274
306
 
307
+ ### V4 Live — Intentionally Omitted Methods
308
+
309
+ Some v4 Live methods present in the API registry are intentionally **not**
310
+ exposed as CLI commands:
311
+
312
+ - `DeleteReport` / `DeleteOfflineReport` — disabled by Yandex (the official
313
+ docs list them under "Отключенные методы" / "Метод отключен. Используйте API
314
+ версии 5"). Use the v5 reports API instead.
315
+ - `PingAPI`, `PingAPI_X`, `GetVersion`, `GetAvailableVersions` — service
316
+ diagnostics / version probes with no documented request shape; not useful as
317
+ user-facing CLI commands.
318
+ - `PayCampaignsByCard` is exposed but **dry-run-only** (undocumented,
319
+ financially sensitive — see V4 Live Finance above).
320
+
275
321
  ### CLI Convention
276
322
 
277
323
  The current CLI convention is defined as follows.
@@ -165,6 +165,31 @@ direct v4wordstat get-report --report-id 123 --format table
165
165
  direct v4wordstat delete-report --report-id 123
166
166
  ```
167
167
 
168
+ ### V4 Live Keyword Suggestions
169
+
170
+ `get-suggestion` returns up to 20 related phrases for the seed phrases. Repeat
171
+ `--keyword` for multiple seeds. The method consumes API points.
172
+
173
+ ```bash
174
+ direct v4keywords get-suggestion --keyword холодильник --keyword камера
175
+ direct v4keywords get-suggestion --keyword "buy laptop" --format table
176
+ direct v4keywords get-suggestion --keyword холодильник --dry-run
177
+ ```
178
+
179
+ ### V4 Live Ad-Image Associations
180
+
181
+ `AdImageAssociation` is exposed as two typed commands. `get` reads ad-to-image
182
+ associations via an optional selection filter (an empty filter returns up to
183
+ 10000 associations). `set` attaches or detaches images: `--association AD_ID=HASH`
184
+ attaches an image, `--association AD_ID` (no hash) detaches the current image
185
+ (max 10000 associations per call).
186
+
187
+ ```bash
188
+ direct v4adimage get --ad-ids 123,456 --status-moderate Yes --limit 20
189
+ direct v4adimage get --campaign-ids 789 --format table
190
+ direct v4adimage set --association 123=abc123hash --association 456 --dry-run
191
+ ```
192
+
168
193
  ### V4 Live Budget Forecasts
169
194
 
170
195
  Budget forecasts are asynchronous. Direct CLI makes exactly one API call per
@@ -190,9 +215,15 @@ per-request token from `--master-token`, `--operation-num`, and
190
215
  Environment variables are
191
216
  `YANDEX_DIRECT_MASTER_TOKEN`, `YANDEX_DIRECT_FINANCE_LOGIN`,
192
217
  `YANDEX_DIRECT_FINANCE_TOKEN`, and `YANDEX_DIRECT_OPERATION_NUM`.
193
- `transfer-money` and `pay-campaigns` are dry-run-only in this release and
194
- always require `--dry-run`; `create-invoice` can be sent live when `--dry-run`
195
- is omitted. Dry-run output masks the financial token.
218
+ `transfer-money`, `pay-campaigns`, and `pay-campaigns-by-card` are dry-run-only
219
+ in this release and always require `--dry-run`; `create-invoice` can be sent
220
+ live when `--dry-run` is omitted. Dry-run output masks the financial token.
221
+ `pay-campaigns-by-card` has an undocumented request shape; its dry-run body
222
+ mirrors the documented `pay-campaigns` shape as a best-effort preview.
223
+
224
+ > ⚠ **Finance commands have not been tested against the live API.** Treat the
225
+ > request shapes as best-effort and always verify with `--dry-run` before
226
+ > sending anything that runs live (`create-invoice`).
196
227
 
197
228
  ```bash
198
229
  direct v4finance get-clients-units --logins client-login,other-client --format table
@@ -201,6 +232,7 @@ direct v4finance create-invoice --payment 123=100.50 --payment 456=25 --currency
201
232
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
202
233
  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
234
  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
235
+ direct v4finance pay-campaigns-by-card --campaign-ids 123,456 --amount 100.50 --currency RUB --master-token MASTER_TOKEN --operation-num 128 --finance-login agency-login --dry-run
204
236
  ```
205
237
 
206
238
  ### V4 Live Shared Account
@@ -229,6 +261,20 @@ direct v4account account-management --action TransferMoney --from-account-id 132
229
261
  direct --sandbox v4account enable-shared-account --client-login client-login
230
262
  ```
231
263
 
264
+ ### V4 Live — Intentionally Omitted Methods
265
+
266
+ Some v4 Live methods present in the API registry are intentionally **not**
267
+ exposed as CLI commands:
268
+
269
+ - `DeleteReport` / `DeleteOfflineReport` — disabled by Yandex (the official
270
+ docs list them under "Отключенные методы" / "Метод отключен. Используйте API
271
+ версии 5"). Use the v5 reports API instead.
272
+ - `PingAPI`, `PingAPI_X`, `GetVersion`, `GetAvailableVersions` — service
273
+ diagnostics / version probes with no documented request shape; not useful as
274
+ user-facing CLI commands.
275
+ - `PayCampaignsByCard` is exposed but **dry-run-only** (undocumented,
276
+ financially sensitive — see V4 Live Finance above).
277
+
232
278
  ### CLI Convention
233
279
 
234
280
  The current CLI convention is defined as follows.
@@ -1,7 +1,7 @@
1
1
 
2
2
  __author__ = 'Pavel Maksimov'
3
3
  __email__ = 'vur21@ya.ru'
4
- __version__ = '2026.4.28'
4
+ __version__ = '2026.5.29'
5
5
 
6
6
 
7
7
  from .resource_mapping import *
@@ -0,0 +1,27 @@
1
+ """Runtime endpoints for Yandex Direct API transports.
2
+
3
+ The v5 JSON API historically uses the ``.com`` TLD while the v4 Live API uses
4
+ ``.ru``; both hosts are valid Yandex Direct entrypoints. ``get_direct_api_root``
5
+ keeps that per-transport distinction via the ``tld`` argument so neither
6
+ adapter silently changes the domain its callers rely on.
7
+ """
8
+
9
+ from typing import Any, Dict
10
+
11
+ DIRECT_API_PRODUCTION_ROOT = "https://api.direct.yandex.{tld}/"
12
+ DIRECT_API_SANDBOX_ROOT = "https://api-sandbox.direct.yandex.{tld}/"
13
+ DIRECT_DEBUG_ROOT = "https://"
14
+
15
+
16
+ def get_direct_api_root(api_params: Dict[str, Any], tld: str = "com") -> str:
17
+ """Return the Direct API root for production or sandbox requests.
18
+
19
+ ``tld`` selects the top-level domain: ``"com"`` for the v5 JSON API
20
+ (default, preserving the historical host) and ``"ru"`` for the v4 Live API.
21
+ """
22
+ template = (
23
+ DIRECT_API_SANDBOX_ROOT
24
+ if api_params.get("is_sandbox")
25
+ else DIRECT_API_PRODUCTION_ROOT
26
+ )
27
+ return template.format(tld=tld)
@@ -250,29 +250,51 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
250
250
  return True
251
251
 
252
252
  if error_code == 152:
253
+ # "Not enough units" is an API-points quota error, distinct from the
254
+ # rate-limit codes below: points replenish on a sliding 60-minute
255
+ # window, so the retry waits 5 minutes and uses its own budget
256
+ # (retries_if_not_enough_units) rather than the rate-limit budget.
257
+ # Without a cap the loop would be infinite (sleep, retry, forever).
253
258
  if api_params.get("retry_if_not_enough_units", False):
254
- logger.warning("Not enough units, re-request after 5 minutes")
255
- time.sleep(60 * 5)
256
- return True
259
+ if repeat_number < api_params.get("retries_if_not_enough_units", 5):
260
+ logger.warning("Not enough units, re-request after 5 minutes")
261
+ time.sleep(60 * 5)
262
+ return True
263
+ logger.error("Not enough units to request, retries exhausted")
264
+ return False
257
265
  else:
258
266
  logger.error("Not enough units to request")
259
267
 
260
268
  elif error_code == 506 and api_params.get("retry_if_exceeded_limit", True):
261
- logger.warning("API requests exceeded, re-request after 10 seconds")
262
- time.sleep(10)
263
- return True
269
+ # Bound rate-limit retries with their own attempt budget. Without
270
+ # this cap the loop is infinite (sleep 10s, retry, forever) whenever
271
+ # the API keeps returning the limit code — which hangs the caller.
272
+ if repeat_number < api_params.get("retries_if_exceeded_limit", 5):
273
+ logger.warning("API requests exceeded, re-request after 10 seconds")
274
+ time.sleep(10)
275
+ return True
276
+ logger.error("API requests exceeded, retries exhausted")
277
+ return False
264
278
 
265
279
  elif error_code == 56 and api_params.get("retry_if_exceeded_limit", True):
266
- logger.warning("Method request limit exceeded. Re-request after 10 seconds")
267
- time.sleep(10)
268
- return True
280
+ if repeat_number < api_params.get("retries_if_exceeded_limit", 5):
281
+ logger.warning(
282
+ "Method request limit exceeded. Re-request after 10 seconds"
283
+ )
284
+ time.sleep(10)
285
+ return True
286
+ logger.error("Method request limit exceeded, retries exhausted")
287
+ return False
269
288
 
270
289
  elif error_code == 9000 and api_params.get("retry_if_exceeded_limit", True):
271
- logger.warning(
272
- "Created by max number of reports. Re-request after 10 seconds"
273
- )
274
- time.sleep(10)
275
- return True
290
+ if repeat_number < api_params.get("retries_if_exceeded_limit", 5):
291
+ logger.warning(
292
+ "Created by max number of reports. Re-request after 10 seconds"
293
+ )
294
+ time.sleep(10)
295
+ return True
296
+ logger.error("Max number of reports limit exceeded, retries exhausted")
297
+ return False
276
298
 
277
299
  elif error_code in (52, 1000, 1001, 1002) or status_code == 500:
278
300
  if repeat_number < api_params.get("retries_if_server_error", 5):
@@ -101,7 +101,9 @@ class YandexDirect:
101
101
  login: str = None,
102
102
  is_sandbox: bool = False,
103
103
  retry_if_not_enough_units: bool = False,
104
+ retries_if_not_enough_units: int = 5,
104
105
  retry_if_exceeded_limit: bool = True,
106
+ retries_if_exceeded_limit: int = 5,
105
107
  retries_if_server_error: int = 5,
106
108
  language: str = None,
107
109
  processing_mode: str = "offline",
@@ -119,8 +121,10 @@ class YandexDirect:
119
121
  :param login: If you are making inquiries from an agent account, you must be sure to specify the account login.
120
122
  :param is_sandbox: Enable sandbox.
121
123
  :param retry_if_not_enough_units: Repeat request when units run out
124
+ :param retries_if_not_enough_units: Maximum total request attempts when units run out (includes the initial attempt; default 5 = 1 initial + 4 retries).
122
125
  :param retry_if_exceeded_limit: Repeat the request if the limits on the number of reports or requests are exceeded.
123
- :param retries_if_server_error: Number of retries when server errors occur.
126
+ :param retries_if_exceeded_limit: Maximum total request attempts when report/request limits are exceeded (includes the initial attempt; default 5 = 1 initial + 4 retries).
127
+ :param retries_if_server_error: Maximum total request attempts when server errors occur (includes the initial attempt; default 5 = 1 initial + 4 retries).
124
128
  :param language: The language in which the data for directories and errors will be returned.
125
129
 
126
130
  :param processing_mode: (report resource) Report generation mode: online, offline or auto.
@@ -34,7 +34,8 @@ class V4LiveClientAdapter(JSONAdapterMixin, TapiAdapter):
34
34
  super().__init__(*args, **kwargs)
35
35
 
36
36
  def get_api_root(self, api_params: dict, resource_name: str) -> str:
37
- return get_direct_api_root(api_params)
37
+ # v4 Live lives on the .ru host (the v5 JSON API uses .com).
38
+ return get_direct_api_root(api_params, tld="ru")
38
39
 
39
40
  def get_request_kwargs(self, api_params: dict, *args, **kwargs) -> dict:
40
41
  params = super().get_request_kwargs(api_params, *args, **kwargs)
@@ -176,11 +177,14 @@ class V4LiveClientAdapter(JSONAdapterMixin, TapiAdapter):
176
177
  code = 0
177
178
 
178
179
  if code in (54, 55) and api_params.get("retry_if_exceeded_limit", 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):
180
+ # Bound the limit-exceeded retries with their own attempt budget.
181
+ # Without a cap the loop is infinite (sleep 10s, retry, forever)
182
+ # whenever the API keeps returning code 54/55 which hangs the
183
+ # caller indefinitely. Rate-limit retries use a dedicated
184
+ # ``retries_if_exceeded_limit`` budget so they stay independent of
185
+ # the server-error budget (a caller may want, e.g., zero
186
+ # server-error retries but still tolerate rate limiting).
187
+ if repeat_number < api_params.get("retries_if_exceeded_limit", 5):
184
188
  logger.warning("v4 Live limit exceeded (code=%s), retry in 10s", code)
185
189
  time.sleep(10)
186
190
  return True
@@ -48,6 +48,7 @@ class YandexDirectV4Live:
48
48
  is_sandbox: bool = False,
49
49
  language: str = "en",
50
50
  retry_if_exceeded_limit: bool = True,
51
+ retries_if_exceeded_limit: int = 5,
51
52
  retries_if_server_error: int = 5,
52
53
  finance_token: Optional[str] = None,
53
54
  operation_num: Optional[int] = None,
@@ -33,6 +33,9 @@ DEFAULT_OAUTH_CLIENT_ID = "dcf15d9625f6471d94d6d054d52017ba"
33
33
  AUTH_STORE_PATH = Path.home() / ".direct-cli" / "auth.json"
34
34
  OAUTH_REFRESH_SKEW_SECONDS = 60
35
35
  PENDING_PKCE_TTL_SECONDS = 600
36
+ # Hard ceiling for the best-effort client-login resolution network call so it
37
+ # can never hang the credential-resolution path (see #480 follow-up).
38
+ LOGIN_RESOLVE_TIMEOUT_SECONDS = 8
36
39
 
37
40
 
38
41
  def op_read(ref: str) -> str:
@@ -601,6 +604,58 @@ def resolve_login(token: str) -> Optional[str]:
601
604
  return None
602
605
 
603
606
 
607
+ def _resolve_client_login_via_api(token: str) -> Optional[str]:
608
+ """Return the bare Direct **Client-Login** for *token* via v5 ``clients.get``.
609
+
610
+ The Passport ``/info`` login can be a full email (``<login>@yandex.ru``);
611
+ Direct v4 AccountManagement rejects that with ``FaultCode 259`` (issue
612
+ #480). ``clients.get`` (no ``SelectionCriteria``) returns the authoritative
613
+ Client-Login for the token's own account, avoiding any unreliable
614
+ ``split('@')`` guessing. Best-effort: returns ``None`` on any failure.
615
+ """
616
+ try:
617
+ # Lazy import: api.py imports this module, so importing the vendored
618
+ # client at module load would create a cycle. The vendor never imports
619
+ # auth, so a function-local import is safe.
620
+ from ._vendor.tapi_yandex_direct import YandexDirect
621
+
622
+ # Best-effort, on the credential-resolution hot path: cap the wait so a
623
+ # slow network or a Yandex SmartCaptcha gateway can never hang the CLI
624
+ # indefinitely (requests defaults to no timeout). Retries are disabled
625
+ # so the ceiling stays a single connect+read window.
626
+ client = YandexDirect(
627
+ access_token=token,
628
+ retry_if_exceeded_limit=False,
629
+ retries_if_server_error=0,
630
+ )
631
+ result = client.clients().post(
632
+ data={"method": "get", "params": {"FieldNames": ["Login"]}},
633
+ timeout=LOGIN_RESOLVE_TIMEOUT_SECONDS,
634
+ )
635
+ data = result().extract()
636
+ clients = data.get("Clients") if isinstance(data, dict) else None
637
+ if clients and isinstance(clients[0], dict):
638
+ login = clients[0].get("Login")
639
+ if login:
640
+ return login
641
+ except Exception as exc: # best-effort; never block login on this
642
+ logging.debug("resolve client login via API failed: %s", exc)
643
+ return None
644
+
645
+
646
+ def resolve_account_login(token: str) -> Optional[str]:
647
+ """Resolve the bare Direct Client-Login for an OAuth *token*.
648
+
649
+ Prefers the authoritative v5 ``clients.get`` Login (which v4 accepts);
650
+ falls back to the Passport login only when the API call fails (that value
651
+ may be a full email and can break v4 — see :func:`_resolve_client_login_via_api`).
652
+ """
653
+ login = _resolve_client_login_via_api(token)
654
+ if login:
655
+ return login
656
+ return resolve_login(token)
657
+
658
+
604
659
  def get_credentials(
605
660
  token: Optional[str] = None,
606
661
  login: Optional[str] = None,
@@ -610,6 +665,7 @@ def get_credentials(
610
665
  bw_token_ref: Optional[str] = None,
611
666
  bw_login_ref: Optional[str] = None,
612
667
  profile: Optional[str] = None,
668
+ allow_login_resolve: bool = True,
613
669
  ) -> Tuple[str, Optional[str]]:
614
670
  """
615
671
  Get credentials with priority:
@@ -655,6 +711,41 @@ def get_credentials(
655
711
  final_token = oauth_profile["token"]
656
712
  if not final_login:
657
713
  final_login = oauth_profile["login"]
714
+ stored_login = oauth_profile.get("login")
715
+ if (
716
+ allow_login_resolve
717
+ and not login
718
+ and oauth_profile.get("source") == "oauth"
719
+ and isinstance(stored_login, str)
720
+ and "@" in stored_login
721
+ and not oauth_profile.get("login_migration_checked")
722
+ ):
723
+ # One-time migration (#480): older OAuth profiles saved the
724
+ # Passport email in `login`, which Direct v4 rejects with
725
+ # FaultCode 259. Resolve the bare Client-Login and rewrite the
726
+ # profile — but only when the stored email's local part matches
727
+ # the resolved login, so agency profiles whose login differs
728
+ # from the token owner are never clobbered. Either way we stamp
729
+ # `login_migration_checked` once the resolver answered, so the
730
+ # network round-trip never repeats per command for profiles that
731
+ # are intentionally left unmigrated (e.g. agency logins).
732
+ resolved = _resolve_client_login_via_api(final_token)
733
+ if resolved:
734
+ matches = (
735
+ stored_login.split("@", 1)[0].lower() == resolved.lower()
736
+ )
737
+ if matches:
738
+ final_login = resolved
739
+ try:
740
+ store = load_auth_store()
741
+ prof = store["profiles"].get(selected_profile)
742
+ if isinstance(prof, dict):
743
+ prof["login_migration_checked"] = True
744
+ if matches:
745
+ prof["login"] = resolved
746
+ save_auth_store(store)
747
+ except Exception as exc: # best-effort persistence
748
+ logging.debug("login migration persist failed: %s", exc)
658
749
 
659
750
  if selected_profile and not final_token:
660
751
  env_token, env_login = get_env_profile(selected_profile)