direct-cli 0.3.2__tar.gz → 0.3.4__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 (213) hide show
  1. direct_cli-0.3.4/.github/workflows/quality.yml +33 -0
  2. direct_cli-0.3.4/CHANGELOG.md +11 -0
  3. {direct_cli-0.3.2 → direct_cli-0.3.4}/PKG-INFO +33 -14
  4. {direct_cli-0.3.2 → direct_cli-0.3.4}/README.md +28 -13
  5. direct_cli-0.3.4/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +14 -0
  6. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +110 -18
  7. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +12 -6
  8. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/auth.py +152 -16
  9. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/auth.py +75 -9
  10. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/reports.py +6 -8
  11. direct_cli-0.3.4/direct_cli/commands/v4finance.py +617 -0
  12. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/smoke_matrix.py +2 -0
  13. direct_cli-0.3.4/direct_cli/v4/money.py +78 -0
  14. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/v4_contracts.py +23 -10
  15. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli.egg-info/PKG-INFO +33 -14
  16. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli.egg-info/SOURCES.txt +4 -0
  17. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli.egg-info/requires.txt +4 -0
  18. {direct_cli-0.3.2 → direct_cli-0.3.4}/pyproject.toml +27 -1
  19. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/sandbox_write_live.py +1 -1
  20. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/test_dangerous_commands.sh +2 -2
  21. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/test_safe_commands.sh +12 -0
  22. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/API_COVERAGE.md +8 -0
  23. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/MANUAL_COVERAGE.md +5 -3
  24. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/api_coverage_payloads.py +2 -0
  25. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_adgroups_add_update_delete.yaml +6 -6
  26. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_adimages_add_get_delete.yaml +1 -1
  27. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_ads_add_update_delete.yaml +8 -8
  28. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_ads_suspend_resume_archive_unarchive.yaml +10 -10
  29. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_advideos_add_get.yaml +2 -2
  30. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_add_delete.yaml +5 -5
  31. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_suspend_resume.yaml +5 -5
  32. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_bids_set.yaml +7 -7
  33. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_campaign_create_get_delete.yaml +4 -4
  34. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_creatives_chain_advideo_to_creative.yaml +3 -3
  35. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_add_delete.yaml +1 -1
  36. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_suspend_resume.yaml +1 -1
  37. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_keywordbids_set.yaml +7 -7
  38. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_keywords_add_update_delete.yaml +7 -7
  39. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_keywords_suspend_resume.yaml +8 -8
  40. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_sitelinks_add_get_delete.yaml +3 -3
  41. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_add_update_delete.yaml +3 -3
  42. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_suspend_resume.yaml +3 -3
  43. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +2 -2
  44. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +5 -5
  45. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +1 -1
  46. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +5 -5
  47. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +7 -7
  48. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +4 -4
  49. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +3 -3
  50. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +5 -5
  51. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +4 -4
  52. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +6 -6
  53. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +1 -1
  54. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +3 -3
  55. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +5 -5
  56. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +5 -5
  57. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +3 -3
  58. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +2 -2
  59. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +1 -1
  60. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +3 -3
  61. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +4 -4
  62. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/conftest.py +1 -1
  63. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_api_coverage.py +115 -0
  64. direct_cli-0.3.4/tests/test_auth_oauth.py +703 -0
  65. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_dry_run.py +29 -2
  66. direct_cli-0.3.4/tests/test_reports_parsing.py +245 -0
  67. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_smoke_matrix.py +3 -2
  68. direct_cli-0.3.4/tests/test_transport_contract.py +66 -0
  69. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_v4_contracts.py +17 -4
  70. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_v4_live_contracts.py +36 -0
  71. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_v4_safety.py +3 -1
  72. direct_cli-0.3.4/tests/test_v4finance_money.py +667 -0
  73. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_v4finance_read.py +89 -7
  74. direct_cli-0.3.2/direct_cli/commands/v4finance.py +0 -298
  75. direct_cli-0.3.2/direct_cli/v4/money.py +0 -35
  76. direct_cli-0.3.2/tests/test_auth_oauth.py +0 -206
  77. direct_cli-0.3.2/tests/test_transport_contract.py +0 -23
  78. direct_cli-0.3.2/tests/test_v4finance_money.py +0 -320
  79. {direct_cli-0.3.2 → direct_cli-0.3.4}/.env.example +0 -0
  80. {direct_cli-0.3.2 → direct_cli-0.3.4}/.github/copilot-instructions.md +0 -0
  81. {direct_cli-0.3.2 → direct_cli-0.3.4}/.github/workflows/api-coverage.yml +0 -0
  82. {direct_cli-0.3.2 → direct_cli-0.3.4}/.github/workflows/claude-code-review.yml +0 -0
  83. {direct_cli-0.3.2 → direct_cli-0.3.4}/.github/workflows/claude.yml +0 -0
  84. {direct_cli-0.3.2 → direct_cli-0.3.4}/.gitignore +0 -0
  85. {direct_cli-0.3.2 → direct_cli-0.3.4}/AGENTS.md +0 -0
  86. {direct_cli-0.3.2 → direct_cli-0.3.4}/CLAUDE.md +0 -0
  87. {direct_cli-0.3.2 → direct_cli-0.3.4}/MANIFEST.in +0 -0
  88. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/__init__.py +0 -0
  89. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_deprecated.py +0 -0
  90. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_smoke_probes.py +0 -0
  91. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/__init__.py +0 -0
  92. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  93. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  94. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  95. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
  96. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  97. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  98. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  99. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/api.py +0 -0
  100. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/cli.py +0 -0
  101. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/__init__.py +0 -0
  102. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/adextensions.py +0 -0
  103. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/adgroups.py +0 -0
  104. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/adimages.py +0 -0
  105. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/ads.py +0 -0
  106. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/advideos.py +0 -0
  107. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/agencyclients.py +0 -0
  108. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/audiencetargets.py +0 -0
  109. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/balance.py +0 -0
  110. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/bidmodifiers.py +0 -0
  111. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/bids.py +0 -0
  112. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/businesses.py +0 -0
  113. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/campaigns.py +0 -0
  114. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/changes.py +0 -0
  115. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/clients.py +0 -0
  116. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/creatives.py +0 -0
  117. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/dictionaries.py +0 -0
  118. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/dynamicads.py +0 -0
  119. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/dynamicfeedadtargets.py +0 -0
  120. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/feeds.py +0 -0
  121. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/keywordbids.py +0 -0
  122. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/keywords.py +0 -0
  123. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/keywordsresearch.py +0 -0
  124. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/leads.py +0 -0
  125. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
  126. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/retargeting.py +0 -0
  127. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/sitelinks.py +0 -0
  128. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/smartadtargets.py +0 -0
  129. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/strategies.py +0 -0
  130. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/turbopages.py +0 -0
  131. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/v4account.py +0 -0
  132. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/v4events.py +0 -0
  133. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/v4goals.py +0 -0
  134. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/v4shells.py +0 -0
  135. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/commands/vcards.py +0 -0
  136. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/output.py +0 -0
  137. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/reports_coverage.py +0 -0
  138. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/utils.py +0 -0
  139. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/v4/__init__.py +0 -0
  140. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli/wsdl_coverage.py +0 -0
  141. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli.egg-info/dependency_links.txt +0 -0
  142. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli.egg-info/entry_points.txt +0 -0
  143. {direct_cli-0.3.2 → direct_cli-0.3.4}/direct_cli.egg-info/top_level.txt +0 -0
  144. {direct_cli-0.3.2 → direct_cli-0.3.4}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  145. {direct_cli-0.3.2 → direct_cli-0.3.4}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  146. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/anonymize_cassettes.py +0 -0
  147. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/build_api_coverage_checklist.py +0 -0
  148. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/build_api_coverage_report.py +0 -0
  149. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/check_reports_drift.py +0 -0
  150. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/check_wsdl_drift.py +0 -0
  151. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/patch_vendor_imports.py +0 -0
  152. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/refresh_reports_cache.py +0 -0
  153. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/refresh_wsdl_cache.py +0 -0
  154. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/release_pypi.sh +0 -0
  155. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/test_sandbox_write.sh +0 -0
  156. {direct_cli-0.3.2 → direct_cli-0.3.4}/scripts/update_vendor.sh +0 -0
  157. {direct_cli-0.3.2 → direct_cli-0.3.4}/setup.cfg +0 -0
  158. {direct_cli-0.3.2 → direct_cli-0.3.4}/setup.py +0 -0
  159. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/API_ISSUE_AUDIT.md +0 -0
  160. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/__init__.py +0 -0
  161. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/fixtures/test-video.mp4 +0 -0
  162. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/reports_cache/raw/fields-list.html +0 -0
  163. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/reports_cache/raw/headers.html +0 -0
  164. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/reports_cache/raw/period.html +0 -0
  165. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/reports_cache/raw/spec.html +0 -0
  166. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/reports_cache/raw/type.html +0 -0
  167. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/reports_cache/spec.json +0 -0
  168. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_auth_bw.py +0 -0
  169. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_auth_op.py +0 -0
  170. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_balance.py +0 -0
  171. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_cli.py +0 -0
  172. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_comprehensive.py +0 -0
  173. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_integration.py +0 -0
  174. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_integration_live_write.py +0 -0
  175. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_integration_write.py +0 -0
  176. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_reports_drift.py +0 -0
  177. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_v4_foundation.py +0 -0
  178. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_v4account.py +0 -0
  179. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_v4events.py +0 -0
  180. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_v4goals.py +0 -0
  181. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/test_vendor_imports.py +0 -0
  182. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/adextensions.xml +0 -0
  183. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/adgroups.xml +0 -0
  184. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/adimages.xml +0 -0
  185. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/ads.xml +0 -0
  186. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/advideos.xml +0 -0
  187. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/agencyclients.xml +0 -0
  188. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/audiencetargets.xml +0 -0
  189. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  190. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/bids.xml +0 -0
  191. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/businesses.xml +0 -0
  192. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/campaigns.xml +0 -0
  193. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/changes.xml +0 -0
  194. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/clients.xml +0 -0
  195. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/creatives.xml +0 -0
  196. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/dictionaries.xml +0 -0
  197. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  198. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  199. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/feeds.xml +0 -0
  200. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  201. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/imports/general.xsd +0 -0
  202. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  203. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/keywordbids.xml +0 -0
  204. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/keywords.xml +0 -0
  205. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  206. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/leads.xml +0 -0
  207. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  208. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/retargetinglists.xml +0 -0
  209. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/sitelinks.xml +0 -0
  210. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/smartadtargets.xml +0 -0
  211. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/strategies.xml +0 -0
  212. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/turbopages.xml +0 -0
  213. {direct_cli-0.3.2 → direct_cli-0.3.4}/tests/wsdl_cache/vcards.xml +0 -0
@@ -0,0 +1,33 @@
1
+ name: Quality
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ quality:
10
+ name: quality
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.11"
20
+
21
+ - name: Install package with dev dependencies
22
+ run: |
23
+ python -m pip install --upgrade pip
24
+ pip install -e ".[dev]"
25
+
26
+ - name: Lint
27
+ run: ruff check .
28
+
29
+ - name: Type check
30
+ run: mypy .
31
+
32
+ - name: Run offline tests
33
+ run: pytest -m "not integration"
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.3.3
4
+
5
+ **BREAKING CHANGE:** OAuth profiles created before 0.3.3 (without `refresh_token` and `expires_at`) are no longer accepted. Any such profile will fail immediately with an "incomplete profile" error. Run `direct auth login --profile <name>` to re-authenticate and create a valid 0.3.3 profile.
6
+
7
+ - Added refresh token persistence for OAuth profiles.
8
+ - Added automatic OAuth access token refresh before expiry.
9
+ - Added `expires_in` details to `direct auth status`.
10
+ - Added JSON output for `direct auth status`.
11
+ - Kept `direct auth login --oauth-token` as a manual access-token import without auto-refresh.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Command-line interface for Yandex Direct API
5
5
  Author: axisrow
6
6
  License: MIT
@@ -33,9 +33,13 @@ Requires-Dist: pytest-recording>=0.13; extra == "dev"
33
33
  Requires-Dist: vcrpy>=6.0; extra == "dev"
34
34
  Requires-Dist: black>=22.0; extra == "dev"
35
35
  Requires-Dist: flake8>=4.0; extra == "dev"
36
+ Requires-Dist: mypy>=1.8; extra == "dev"
37
+ Requires-Dist: ruff>=0.1; extra == "dev"
36
38
  Requires-Dist: requests>=2.0; extra == "dev"
37
39
  Requires-Dist: beautifulsoup4>=4.12; extra == "dev"
38
40
  Requires-Dist: lxml>=4.9; extra == "dev"
41
+ Requires-Dist: types-setuptools; extra == "dev"
42
+ Requires-Dist: types-tabulate; extra == "dev"
39
43
 
40
44
  # Direct CLI
41
45
 
@@ -83,7 +87,6 @@ OAuth and profile commands:
83
87
  direct auth login
84
88
  direct auth login --profile agency1
85
89
  direct auth login --code abc123 --profile agency1
86
- direct auth login --oauth-token y0_example --profile agency1
87
90
  direct auth list
88
91
  direct auth use --profile agency1
89
92
  direct auth status --profile agency1
@@ -95,6 +98,8 @@ Notes:
95
98
  - Select credentials with `--profile`.
96
99
  - `--login` remains Direct client login.
97
100
  - Authorization is performed via `direct auth login`.
101
+ - OAuth profiles store refresh tokens and refresh access tokens automatically.
102
+ - `direct auth login --oauth-token TOKEN` is a manual access-token import and does not auto-refresh.
98
103
  - Alias `auth_login` is not supported.
99
104
 
100
105
  Credential resolution priority:
@@ -156,24 +161,35 @@ direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:
156
161
 
157
162
  ### V4 Live Finance
158
163
 
159
- `get-credit-limits` requires a financial token and operation number. Pass them
160
- with `--finance-token` and `--operation-num`, or set
161
- `YANDEX_DIRECT_FINANCE_TOKEN` and `YANDEX_DIRECT_OPERATION_NUM`.
162
- Money mutation commands are dry-run-only in this release and always require
163
- `--dry-run`; dry-run output masks the financial token.
164
+ Finance methods require an extra financial token for money operations. In the
165
+ Yandex Direct web UI, open Tools -> API -> Financial operations, enable the
166
+ financial operations checkbox, click Save, then issue the master token on the
167
+ same Financial operations page and confirm by SMS. Direct CLI can compute the
168
+ per-request token from `--master-token`, `--operation-num`, and
169
+ `--finance-login`; alternatively pass a precomputed token with `--finance-token`.
170
+ Environment variables are
171
+ `YANDEX_DIRECT_MASTER_TOKEN`, `YANDEX_DIRECT_FINANCE_LOGIN`,
172
+ `YANDEX_DIRECT_FINANCE_TOKEN`, and `YANDEX_DIRECT_OPERATION_NUM`.
173
+ `transfer-money` and `pay-campaigns` are dry-run-only in this release and
174
+ always require `--dry-run`; `create-invoice` can be sent live when `--dry-run`
175
+ is omitted. Dry-run output masks the financial token.
164
176
 
165
177
  ```bash
166
- direct v4finance get-credit-limits --logins client-login --finance-token FINANCE_TOKEN --operation-num 123
178
+ direct v4finance get-clients-units --logins client-login,other-client --format table
179
+ direct v4finance get-credit-limits --logins client-login --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login
167
180
  direct v4finance get-credit-limits --logins client-login,other-client --format table
181
+ 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
168
182
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
169
- direct v4finance transfer-money --from-campaign-id 123 --to-campaign-id 456 --amount 100.50 --finance-token FINANCE_TOKEN --operation-num 123 --dry-run
170
- direct v4finance pay-campaigns --campaign-id 123 --amount 100.50 --contract-id CONTRACT_ID --pay-method CREDIT --finance-token FINANCE_TOKEN --operation-num 123 --dry-run
183
+ 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
184
+ direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --currency RUB --contract-id CONTRACT_ID --pay-method Bank --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
171
185
  ```
172
186
 
173
187
  ### V4 Live Shared Account
174
188
 
175
189
  Shared-account mutations are dry-run-only in this release and always require
176
- `--dry-run`.
190
+ `--dry-run`. These commands follow the official v4 Live shared-account method
191
+ shapes: `EnableSharedAccount` accepts one client `Login`, and
192
+ `AccountManagement` updates shared-account settings through `Accounts`.
177
193
 
178
194
  ```bash
179
195
  direct v4account enable-shared-account --client-login client-login --dry-run
@@ -583,7 +599,7 @@ CI runs a scheduled API coverage workflow that:
583
599
 
584
600
  `WRITE_SANDBOX` smoke is a live check against the Yandex Direct **sandbox**.
585
601
  It does not replay stored HTTP traffic and it does not create new recordings.
586
- Run it only when you intentionally want to call `api-sandbox.direct.yandex.com`:
602
+ Run it only when you intentionally want to call `api-sandbox.direct.yandex.ru`:
587
603
 
588
604
  ```bash
589
605
  set -a && source .env && set +a
@@ -725,13 +741,16 @@ OAuth и profile-команды:
725
741
  direct auth login
726
742
  direct auth login --profile agency1
727
743
  direct auth login --code abc123 --profile agency1
728
- direct auth login --oauth-token y0_example --profile agency1
729
744
  direct auth list
730
745
  direct auth use --profile agency1
731
746
  direct auth status --profile agency1
732
747
  direct --profile agency1 campaigns get
733
748
  ```
734
749
 
750
+ Примечания:
751
+ - OAuth profiles сохраняют refresh token и автоматически обновляют access token.
752
+ - `direct auth login --oauth-token TOKEN` импортирует access token вручную и не включает auto-refresh.
753
+
735
754
  Порядок выбора credentials:
736
755
 
737
756
  | Приоритет | Источник | Пример |
@@ -1176,7 +1195,7 @@ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rew
1176
1195
  `WRITE_SANDBOX` smoke — это live-проверка против **sandbox-окружения**
1177
1196
  Яндекс Директа. Она не воспроизводит сохранённый HTTP-трафик и не создаёт
1178
1197
  новые записи. Запускайте её только когда намеренно хотите обратиться к
1179
- `api-sandbox.direct.yandex.com`:
1198
+ `api-sandbox.direct.yandex.ru`:
1180
1199
 
1181
1200
  ```bash
1182
1201
  set -a && source .env && set +a
@@ -44,7 +44,6 @@ OAuth and profile commands:
44
44
  direct auth login
45
45
  direct auth login --profile agency1
46
46
  direct auth login --code abc123 --profile agency1
47
- direct auth login --oauth-token y0_example --profile agency1
48
47
  direct auth list
49
48
  direct auth use --profile agency1
50
49
  direct auth status --profile agency1
@@ -56,6 +55,8 @@ Notes:
56
55
  - Select credentials with `--profile`.
57
56
  - `--login` remains Direct client login.
58
57
  - Authorization is performed via `direct auth login`.
58
+ - OAuth profiles store refresh tokens and refresh access tokens automatically.
59
+ - `direct auth login --oauth-token TOKEN` is a manual access-token import and does not auto-refresh.
59
60
  - Alias `auth_login` is not supported.
60
61
 
61
62
  Credential resolution priority:
@@ -117,24 +118,35 @@ direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:
117
118
 
118
119
  ### V4 Live Finance
119
120
 
120
- `get-credit-limits` requires a financial token and operation number. Pass them
121
- with `--finance-token` and `--operation-num`, or set
122
- `YANDEX_DIRECT_FINANCE_TOKEN` and `YANDEX_DIRECT_OPERATION_NUM`.
123
- Money mutation commands are dry-run-only in this release and always require
124
- `--dry-run`; dry-run output masks the financial token.
121
+ Finance methods require an extra financial token for money operations. In the
122
+ Yandex Direct web UI, open Tools -> API -> Financial operations, enable the
123
+ financial operations checkbox, click Save, then issue the master token on the
124
+ same Financial operations page and confirm by SMS. Direct CLI can compute the
125
+ per-request token from `--master-token`, `--operation-num`, and
126
+ `--finance-login`; alternatively pass a precomputed token with `--finance-token`.
127
+ Environment variables are
128
+ `YANDEX_DIRECT_MASTER_TOKEN`, `YANDEX_DIRECT_FINANCE_LOGIN`,
129
+ `YANDEX_DIRECT_FINANCE_TOKEN`, and `YANDEX_DIRECT_OPERATION_NUM`.
130
+ `transfer-money` and `pay-campaigns` are dry-run-only in this release and
131
+ always require `--dry-run`; `create-invoice` can be sent live when `--dry-run`
132
+ is omitted. Dry-run output masks the financial token.
125
133
 
126
134
  ```bash
127
- direct v4finance get-credit-limits --logins client-login --finance-token FINANCE_TOKEN --operation-num 123
135
+ direct v4finance get-clients-units --logins client-login,other-client --format table
136
+ direct v4finance get-credit-limits --logins client-login --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login
128
137
  direct v4finance get-credit-limits --logins client-login,other-client --format table
138
+ 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
129
139
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
130
- direct v4finance transfer-money --from-campaign-id 123 --to-campaign-id 456 --amount 100.50 --finance-token FINANCE_TOKEN --operation-num 123 --dry-run
131
- direct v4finance pay-campaigns --campaign-id 123 --amount 100.50 --contract-id CONTRACT_ID --pay-method CREDIT --finance-token FINANCE_TOKEN --operation-num 123 --dry-run
140
+ 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
141
+ direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --currency RUB --contract-id CONTRACT_ID --pay-method Bank --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run
132
142
  ```
133
143
 
134
144
  ### V4 Live Shared Account
135
145
 
136
146
  Shared-account mutations are dry-run-only in this release and always require
137
- `--dry-run`.
147
+ `--dry-run`. These commands follow the official v4 Live shared-account method
148
+ shapes: `EnableSharedAccount` accepts one client `Login`, and
149
+ `AccountManagement` updates shared-account settings through `Accounts`.
138
150
 
139
151
  ```bash
140
152
  direct v4account enable-shared-account --client-login client-login --dry-run
@@ -544,7 +556,7 @@ CI runs a scheduled API coverage workflow that:
544
556
 
545
557
  `WRITE_SANDBOX` smoke is a live check against the Yandex Direct **sandbox**.
546
558
  It does not replay stored HTTP traffic and it does not create new recordings.
547
- Run it only when you intentionally want to call `api-sandbox.direct.yandex.com`:
559
+ Run it only when you intentionally want to call `api-sandbox.direct.yandex.ru`:
548
560
 
549
561
  ```bash
550
562
  set -a && source .env && set +a
@@ -686,13 +698,16 @@ OAuth и profile-команды:
686
698
  direct auth login
687
699
  direct auth login --profile agency1
688
700
  direct auth login --code abc123 --profile agency1
689
- direct auth login --oauth-token y0_example --profile agency1
690
701
  direct auth list
691
702
  direct auth use --profile agency1
692
703
  direct auth status --profile agency1
693
704
  direct --profile agency1 campaigns get
694
705
  ```
695
706
 
707
+ Примечания:
708
+ - OAuth profiles сохраняют refresh token и автоматически обновляют access token.
709
+ - `direct auth login --oauth-token TOKEN` импортирует access token вручную и не включает auto-refresh.
710
+
696
711
  Порядок выбора credentials:
697
712
 
698
713
  | Приоритет | Источник | Пример |
@@ -1137,7 +1152,7 @@ YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rew
1137
1152
  `WRITE_SANDBOX` smoke — это live-проверка против **sandbox-окружения**
1138
1153
  Яндекс Директа. Она не воспроизводит сохранённый HTTP-трафик и не создаёт
1139
1154
  новые записи. Запускайте её только когда намеренно хотите обратиться к
1140
- `api-sandbox.direct.yandex.com`:
1155
+ `api-sandbox.direct.yandex.ru`:
1141
1156
 
1142
1157
  ```bash
1143
1158
  set -a && source .env && set +a
@@ -0,0 +1,14 @@
1
+ """Runtime endpoints for Yandex Direct API transports."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ DIRECT_API_PRODUCTION_ROOT = "https://api.direct.yandex.ru/"
6
+ DIRECT_API_SANDBOX_ROOT = "https://api-sandbox.direct.yandex.ru/"
7
+ DIRECT_DEBUG_ROOT = "https://"
8
+
9
+
10
+ def get_direct_api_root(api_params: Dict[str, Any]) -> str:
11
+ """Return the Direct API root for production or sandbox requests."""
12
+ if api_params.get("is_sandbox"):
13
+ return DIRECT_API_SANDBOX_ROOT
14
+ return DIRECT_API_PRODUCTION_ROOT
@@ -1,5 +1,6 @@
1
1
  import io
2
2
  import logging
3
+ import re
3
4
  import time
4
5
  from typing import Union, Optional, Dict, List, Iterator
5
6
 
@@ -9,6 +10,7 @@ from tapi2 import TapiAdapter, generate_wrapper_from_adapter, JSONAdapterMixin
9
10
  from tapi2.exceptions import ResponseProcessException, ClientError, TapiException
10
11
 
11
12
  from . import exceptions
13
+ from .endpoints import DIRECT_DEBUG_ROOT, get_direct_api_root
12
14
  from .resource_mapping import RESOURCE_MAPPING_V5
13
15
 
14
16
  logger = logging.getLogger(__name__)
@@ -62,6 +64,7 @@ RESULT_DICTIONARY_KEYS_OF_API_METHODS = {
62
64
  },
63
65
  }
64
66
  REPORTS_RESOURCE_URL = "/json/v5/reports"
67
+ REPORT_SUMMARY_RE = re.compile(r"^Total rows: \d+$")
65
68
 
66
69
 
67
70
  class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
@@ -72,11 +75,8 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
72
75
 
73
76
  def get_api_root(self, api_params: dict, resource_name: str) -> str:
74
77
  if resource_name == "debugtoken":
75
- return "https://"
76
- elif api_params.get("is_sandbox"):
77
- return "https://api-sandbox.direct.yandex.com/"
78
- else:
79
- return "https://api.direct.yandex.com/"
78
+ return DIRECT_DEBUG_ROOT
79
+ return get_direct_api_root(api_params)
80
80
 
81
81
  def get_request_kwargs(self, api_params: dict, *args, **kwargs) -> dict:
82
82
  """Обогащение запроса, параметрами"""
@@ -154,7 +154,7 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
154
154
  "The report generation time has exceeded the server limit. "
155
155
  "Please try to change the request parameters, "
156
156
  "reduce the period or the amount of requested data.",
157
- **kwargs
157
+ **kwargs,
158
158
  )
159
159
  elif response.status_code == 405:
160
160
  raise exceptions.YandexDirectApiError(
@@ -162,7 +162,7 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
162
162
  "This resource does not support the HTTP method {}\n".format(
163
163
  response.request.method
164
164
  ),
165
- **kwargs
165
+ **kwargs,
166
166
  )
167
167
 
168
168
  data = self.response_to_native(response)
@@ -175,10 +175,15 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
175
175
  data = super().process_response(response, request_kwargs, **kwargs)
176
176
 
177
177
  if response.request.path_url == REPORTS_RESOURCE_URL:
178
- lines = self._iter_lines(data=data, response=response, **kwargs)
179
- kwargs["store"]["columns"] = next(lines).split("\t")
178
+ self._store_report_columns(data, response, request_kwargs, **kwargs)
179
+ kwargs["store"]["skip_report_summary"] = self._is_skip_report_summary(
180
+ request_kwargs
181
+ )
180
182
  else:
181
183
  kwargs["store"].pop("columns", None)
184
+ kwargs["store"].pop("report_data_start_line", None)
185
+ kwargs["store"].pop("report_header_offset", None)
186
+ kwargs["store"].pop("skip_report_summary", None)
182
187
 
183
188
  return data
184
189
 
@@ -190,7 +195,7 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
190
195
  response: Response,
191
196
  request_kwargs: dict,
192
197
  api_params: dict,
193
- **kwargs
198
+ **kwargs,
194
199
  ) -> None:
195
200
  if response.status_code in (201, 202):
196
201
  pass
@@ -230,7 +235,7 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
230
235
  response: Response,
231
236
  request_kwargs: dict,
232
237
  api_params: dict,
233
- **kwargs
238
+ **kwargs,
234
239
  ) -> bool:
235
240
  status_code = response.status_code
236
241
  error_data = error_message.get("error", {})
@@ -283,7 +288,7 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
283
288
  response: Response,
284
289
  request_kwargs: dict,
285
290
  api_params: dict,
286
- **kwargs
291
+ **kwargs,
287
292
  ) -> Optional[dict]:
288
293
  limit = response_data["result"].get("LimitedBy")
289
294
  if limit:
@@ -308,22 +313,109 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
308
313
  raise NotImplementedError("For reports resource only")
309
314
 
310
315
  lines = io.StringIO(data)
311
- iterator = (line.replace("\n", "") for line in lines)
316
+ iterator = (line.rstrip("\r\n") for line in lines)
312
317
 
313
318
  return iterator
314
319
 
320
+ def _store_report_columns(
321
+ self,
322
+ data: str,
323
+ response: Response,
324
+ request_kwargs: dict,
325
+ **kwargs,
326
+ ) -> None:
327
+ field_names = self._get_report_field_names(request_kwargs)
328
+ if self._is_skip_column_header(request_kwargs):
329
+ if not field_names:
330
+ raise ValueError("Report response has no column header or FieldNames")
331
+ data_start_line = self._find_first_report_data_line(
332
+ data, response, **kwargs
333
+ )
334
+ kwargs["store"]["columns"] = field_names
335
+ kwargs["store"]["report_data_start_line"] = data_start_line
336
+ kwargs["store"]["report_header_offset"] = data_start_line
337
+ return
338
+
339
+ for index, line in enumerate(
340
+ self._iter_lines(data=data, response=response, **kwargs)
341
+ ):
342
+ if self._is_report_prelude_line(line):
343
+ continue
344
+ values = line.split("\t")
345
+ if field_names and values == field_names:
346
+ kwargs["store"]["columns"] = values
347
+ kwargs["store"]["report_data_start_line"] = index + 1
348
+ kwargs["store"]["report_header_offset"] = index
349
+ return
350
+ if "\t" in line:
351
+ kwargs["store"]["columns"] = values
352
+ kwargs["store"]["report_data_start_line"] = index + 1
353
+ kwargs["store"]["report_header_offset"] = index
354
+ return
355
+
356
+ raise ValueError("Report response has no column header")
357
+
358
+ def _find_first_report_data_line(
359
+ self, data: str, response: Response, **kwargs
360
+ ) -> int:
361
+ for index, line in enumerate(
362
+ self._iter_lines(data=data, response=response, **kwargs)
363
+ ):
364
+ if self._is_report_prelude_line(line) or self._is_report_summary_line(line):
365
+ continue
366
+ return index
367
+ return index + 1 if "index" in locals() else 0
368
+
369
+ @staticmethod
370
+ def _get_report_field_names(request_kwargs: dict) -> List[str]:
371
+ params = request_kwargs.get("data", {}).get("params", {})
372
+ return list(params.get("FieldNames") or [])
373
+
374
+ @staticmethod
375
+ def _is_skip_column_header(request_kwargs: dict) -> bool:
376
+ value = request_kwargs.get("headers", {}).get("skipColumnHeader")
377
+ return str(value).lower() == "true"
378
+
379
+ @staticmethod
380
+ def _is_skip_report_summary(request_kwargs: dict) -> bool:
381
+ value = request_kwargs.get("headers", {}).get("skipReportSummary", True)
382
+ return str(value).lower() == "true"
383
+
384
+ @staticmethod
385
+ def _is_report_title_line(line: str) -> bool:
386
+ return "\t" not in line and line.strip().startswith('"')
387
+
388
+ @staticmethod
389
+ def _is_report_summary_line(line: str) -> bool:
390
+ return bool(REPORT_SUMMARY_RE.match(line.strip()))
391
+
392
+ def _is_report_prelude_line(self, line: str) -> bool:
393
+ return not line.strip() or self._is_report_title_line(line)
394
+
315
395
  def iter_lines(self, **kwargs) -> Iterator[str]:
316
396
  iterator = self._iter_lines(**kwargs)
317
- next(iterator) # skipping columns
318
- yield from iterator
397
+ data_start_line = kwargs["store"].get("report_data_start_line", 1)
398
+ skip_report_summary = kwargs["store"].get("skip_report_summary", True)
399
+ for index, line in enumerate(iterator):
400
+ if index < data_start_line:
401
+ continue
402
+ if not line.strip():
403
+ continue
404
+ if skip_report_summary and self._is_report_summary_line(line):
405
+ continue
406
+ yield line
319
407
 
320
408
  def iter_values(self, **kwargs) -> Iterator[list]:
321
409
  for line in self.iter_lines(**kwargs):
322
- yield line.split("\t")
410
+ values = line.split("\t")
411
+ if self._is_report_summary_line(line):
412
+ columns_count = len(kwargs["store"]["columns"])
413
+ values.extend([""] * (columns_count - len(values)))
414
+ yield values
323
415
 
324
416
  def iter_dicts(self, **kwargs) -> Iterator[dict]:
325
- for line in self.iter_lines(**kwargs):
326
- yield dict(zip(kwargs["store"]["columns"], line.split("\t")))
417
+ for values in self.iter_values(**kwargs):
418
+ yield dict(zip(kwargs["store"]["columns"], values))
327
419
 
328
420
  def to_values(self, **kwargs) -> List[list]:
329
421
  return list(self.iter_values(**kwargs))
@@ -18,6 +18,7 @@ from tapi2 import JSONAdapterMixin, TapiAdapter, generate_wrapper_from_adapter
18
18
  from tapi2.exceptions import ClientError, ResponseProcessException, TapiException
19
19
 
20
20
  from .. import exceptions
21
+ from ..endpoints import get_direct_api_root
21
22
  from .resource_mapping import (
22
23
  RESOURCE_MAPPING_V4_LIVE,
23
24
  SUPPORTED_V4_METHODS,
@@ -33,9 +34,7 @@ class V4LiveClientAdapter(JSONAdapterMixin, TapiAdapter):
33
34
  super().__init__(*args, **kwargs)
34
35
 
35
36
  def get_api_root(self, api_params: dict, resource_name: str) -> str:
36
- if api_params.get("is_sandbox"):
37
- return "https://api-sandbox.direct.yandex.ru/"
38
- return "https://api.direct.yandex.ru/"
37
+ return get_direct_api_root(api_params)
39
38
 
40
39
  def get_request_kwargs(self, api_params: dict, *args, **kwargs) -> dict:
41
40
  params = super().get_request_kwargs(api_params, *args, **kwargs)
@@ -107,7 +106,9 @@ class V4LiveClientAdapter(JSONAdapterMixin, TapiAdapter):
107
106
  data = None
108
107
  return data
109
108
 
110
- def process_response(self, response: Response, request_kwargs: dict, **kwargs) -> dict:
109
+ def process_response(
110
+ self, response: Response, request_kwargs: dict, **kwargs
111
+ ) -> dict:
111
112
  # Mirror the v5 behaviour: turn the serialised body back into a dict so
112
113
  # downstream hooks (extract, retry) can read it.
113
114
  if isinstance(request_kwargs.get("data"), (bytes, bytearray, str)):
@@ -187,8 +188,13 @@ class V4LiveClientAdapter(JSONAdapterMixin, TapiAdapter):
187
188
 
188
189
  return False
189
190
 
190
- def extract(self, data, response: Optional[Response] = None,
191
- request_kwargs: Optional[dict] = None, **kwargs):
191
+ def extract(
192
+ self,
193
+ data,
194
+ response: Optional[Response] = None,
195
+ request_kwargs: Optional[dict] = None,
196
+ **kwargs,
197
+ ):
192
198
  # v4 Live always nests payload under "data". For methods returning a
193
199
  # bare scalar (TransferMoney → 1), the scalar comes through unchanged.
194
200
  # response / request_kwargs are accepted but unused — they are kept