direct-cli 0.3.3__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 (208) hide show
  1. direct_cli-0.3.4/.github/workflows/quality.yml +33 -0
  2. {direct_cli-0.3.3 → direct_cli-0.3.4}/PKG-INFO +14 -5
  3. {direct_cli-0.3.3 → direct_cli-0.3.4}/README.md +9 -4
  4. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +102 -8
  5. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/reports.py +6 -8
  6. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/v4finance.py +169 -0
  7. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/smoke_matrix.py +2 -0
  8. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/v4_contracts.py +15 -4
  9. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli.egg-info/PKG-INFO +14 -5
  10. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli.egg-info/SOURCES.txt +2 -0
  11. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli.egg-info/requires.txt +4 -0
  12. {direct_cli-0.3.3 → direct_cli-0.3.4}/pyproject.toml +27 -1
  13. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/test_safe_commands.sh +12 -0
  14. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/API_COVERAGE.md +4 -0
  15. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/MANUAL_COVERAGE.md +4 -3
  16. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/api_coverage_payloads.py +2 -0
  17. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_api_coverage.py +115 -0
  18. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_auth_oauth.py +46 -0
  19. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_dry_run.py +29 -2
  20. direct_cli-0.3.4/tests/test_reports_parsing.py +245 -0
  21. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_smoke_matrix.py +3 -2
  22. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4_contracts.py +13 -0
  23. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4_live_contracts.py +36 -0
  24. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4_safety.py +3 -1
  25. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4finance_money.py +178 -0
  26. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4finance_read.py +59 -4
  27. {direct_cli-0.3.3 → direct_cli-0.3.4}/.env.example +0 -0
  28. {direct_cli-0.3.3 → direct_cli-0.3.4}/.github/copilot-instructions.md +0 -0
  29. {direct_cli-0.3.3 → direct_cli-0.3.4}/.github/workflows/api-coverage.yml +0 -0
  30. {direct_cli-0.3.3 → direct_cli-0.3.4}/.github/workflows/claude-code-review.yml +0 -0
  31. {direct_cli-0.3.3 → direct_cli-0.3.4}/.github/workflows/claude.yml +0 -0
  32. {direct_cli-0.3.3 → direct_cli-0.3.4}/.gitignore +0 -0
  33. {direct_cli-0.3.3 → direct_cli-0.3.4}/AGENTS.md +0 -0
  34. {direct_cli-0.3.3 → direct_cli-0.3.4}/CHANGELOG.md +0 -0
  35. {direct_cli-0.3.3 → direct_cli-0.3.4}/CLAUDE.md +0 -0
  36. {direct_cli-0.3.3 → direct_cli-0.3.4}/MANIFEST.in +0 -0
  37. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/__init__.py +0 -0
  38. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_deprecated.py +0 -0
  39. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_smoke_probes.py +0 -0
  40. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/__init__.py +0 -0
  41. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  42. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
  43. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  44. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  45. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
  46. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  47. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
  48. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  49. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  50. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/api.py +0 -0
  51. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/auth.py +0 -0
  52. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/cli.py +0 -0
  53. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/__init__.py +0 -0
  54. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/adextensions.py +0 -0
  55. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/adgroups.py +0 -0
  56. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/adimages.py +0 -0
  57. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/ads.py +0 -0
  58. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/advideos.py +0 -0
  59. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/agencyclients.py +0 -0
  60. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/audiencetargets.py +0 -0
  61. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/auth.py +0 -0
  62. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/balance.py +0 -0
  63. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/bidmodifiers.py +0 -0
  64. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/bids.py +0 -0
  65. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/businesses.py +0 -0
  66. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/campaigns.py +0 -0
  67. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/changes.py +0 -0
  68. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/clients.py +0 -0
  69. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/creatives.py +0 -0
  70. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/dictionaries.py +0 -0
  71. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/dynamicads.py +0 -0
  72. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/dynamicfeedadtargets.py +0 -0
  73. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/feeds.py +0 -0
  74. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/keywordbids.py +0 -0
  75. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/keywords.py +0 -0
  76. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/keywordsresearch.py +0 -0
  77. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/leads.py +0 -0
  78. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
  79. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/retargeting.py +0 -0
  80. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/sitelinks.py +0 -0
  81. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/smartadtargets.py +0 -0
  82. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/strategies.py +0 -0
  83. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/turbopages.py +0 -0
  84. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/v4account.py +0 -0
  85. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/v4events.py +0 -0
  86. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/v4goals.py +0 -0
  87. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/v4shells.py +0 -0
  88. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/commands/vcards.py +0 -0
  89. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/output.py +0 -0
  90. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/reports_coverage.py +0 -0
  91. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/utils.py +0 -0
  92. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/v4/__init__.py +0 -0
  93. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/v4/money.py +0 -0
  94. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli/wsdl_coverage.py +0 -0
  95. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli.egg-info/dependency_links.txt +0 -0
  96. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli.egg-info/entry_points.txt +0 -0
  97. {direct_cli-0.3.3 → direct_cli-0.3.4}/direct_cli.egg-info/top_level.txt +0 -0
  98. {direct_cli-0.3.3 → direct_cli-0.3.4}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  99. {direct_cli-0.3.3 → direct_cli-0.3.4}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  100. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/anonymize_cassettes.py +0 -0
  101. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/build_api_coverage_checklist.py +0 -0
  102. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/build_api_coverage_report.py +0 -0
  103. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/check_reports_drift.py +0 -0
  104. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/check_wsdl_drift.py +0 -0
  105. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/patch_vendor_imports.py +0 -0
  106. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/refresh_reports_cache.py +0 -0
  107. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/refresh_wsdl_cache.py +0 -0
  108. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/release_pypi.sh +0 -0
  109. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/sandbox_write_live.py +0 -0
  110. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/test_dangerous_commands.sh +0 -0
  111. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/test_sandbox_write.sh +0 -0
  112. {direct_cli-0.3.3 → direct_cli-0.3.4}/scripts/update_vendor.sh +0 -0
  113. {direct_cli-0.3.3 → direct_cli-0.3.4}/setup.cfg +0 -0
  114. {direct_cli-0.3.3 → direct_cli-0.3.4}/setup.py +0 -0
  115. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/API_ISSUE_AUDIT.md +0 -0
  116. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/__init__.py +0 -0
  117. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_adgroups_add_update_delete.yaml +0 -0
  118. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_adimages_add_get_delete.yaml +0 -0
  119. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_ads_add_update_delete.yaml +0 -0
  120. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
  121. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_advideos_add_get.yaml +0 -0
  122. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_add_delete.yaml +0 -0
  123. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_suspend_resume.yaml +0 -0
  124. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_bids_set.yaml +0 -0
  125. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_campaign_create_get_delete.yaml +0 -0
  126. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
  127. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_add_delete.yaml +0 -0
  128. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_suspend_resume.yaml +0 -0
  129. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_keywordbids_set.yaml +0 -0
  130. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_keywords_add_update_delete.yaml +0 -0
  131. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_keywords_suspend_resume.yaml +0 -0
  132. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_sitelinks_add_get_delete.yaml +0 -0
  133. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_add_update_delete.yaml +0 -0
  134. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_suspend_resume.yaml +0 -0
  135. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  136. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  137. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  138. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  139. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  140. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  141. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  142. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  143. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  144. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  145. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  146. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +0 -0
  147. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  148. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  149. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  150. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  151. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  152. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
  153. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  154. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/conftest.py +0 -0
  155. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/fixtures/test-video.mp4 +0 -0
  156. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/reports_cache/raw/fields-list.html +0 -0
  157. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/reports_cache/raw/headers.html +0 -0
  158. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/reports_cache/raw/period.html +0 -0
  159. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/reports_cache/raw/spec.html +0 -0
  160. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/reports_cache/raw/type.html +0 -0
  161. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/reports_cache/spec.json +0 -0
  162. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_auth_bw.py +0 -0
  163. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_auth_op.py +0 -0
  164. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_balance.py +0 -0
  165. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_cli.py +0 -0
  166. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_comprehensive.py +0 -0
  167. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_integration.py +0 -0
  168. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_integration_live_write.py +0 -0
  169. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_integration_write.py +0 -0
  170. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_reports_drift.py +0 -0
  171. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_transport_contract.py +0 -0
  172. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4_foundation.py +0 -0
  173. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4account.py +0 -0
  174. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4events.py +0 -0
  175. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_v4goals.py +0 -0
  176. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/test_vendor_imports.py +0 -0
  177. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/adextensions.xml +0 -0
  178. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/adgroups.xml +0 -0
  179. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/adimages.xml +0 -0
  180. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/ads.xml +0 -0
  181. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/advideos.xml +0 -0
  182. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/agencyclients.xml +0 -0
  183. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/audiencetargets.xml +0 -0
  184. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  185. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/bids.xml +0 -0
  186. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/businesses.xml +0 -0
  187. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/campaigns.xml +0 -0
  188. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/changes.xml +0 -0
  189. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/clients.xml +0 -0
  190. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/creatives.xml +0 -0
  191. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/dictionaries.xml +0 -0
  192. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  193. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  194. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/feeds.xml +0 -0
  195. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  196. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/imports/general.xsd +0 -0
  197. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  198. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/keywordbids.xml +0 -0
  199. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/keywords.xml +0 -0
  200. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  201. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/leads.xml +0 -0
  202. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  203. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/retargetinglists.xml +0 -0
  204. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/sitelinks.xml +0 -0
  205. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/smartadtargets.xml +0 -0
  206. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/strategies.xml +0 -0
  207. {direct_cli-0.3.3 → direct_cli-0.3.4}/tests/wsdl_cache/turbopages.xml +0 -0
  208. {direct_cli-0.3.3 → 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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.3.3
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
 
@@ -165,13 +169,16 @@ per-request token from `--master-token`, `--operation-num`, and
165
169
  `--finance-login`; alternatively pass a precomputed token with `--finance-token`.
166
170
  Environment variables are
167
171
  `YANDEX_DIRECT_MASTER_TOKEN`, `YANDEX_DIRECT_FINANCE_LOGIN`,
168
- `YANDEX_DIRECT_FINANCE_TOKEN`, and `YANDEX_DIRECT_OPERATION_NUM`. Money mutation
169
- commands are dry-run-only in this release and always require `--dry-run`; dry-run
170
- output masks the financial token.
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.
171
176
 
172
177
  ```bash
178
+ direct v4finance get-clients-units --logins client-login,other-client --format table
173
179
  direct v4finance get-credit-limits --logins client-login --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login
174
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
175
182
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
176
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
177
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
@@ -180,7 +187,9 @@ direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --currency
180
187
  ### V4 Live Shared Account
181
188
 
182
189
  Shared-account mutations are dry-run-only in this release and always require
183
- `--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`.
184
193
 
185
194
  ```bash
186
195
  direct v4account enable-shared-account --client-login client-login --dry-run
@@ -126,13 +126,16 @@ per-request token from `--master-token`, `--operation-num`, and
126
126
  `--finance-login`; alternatively pass a precomputed token with `--finance-token`.
127
127
  Environment variables are
128
128
  `YANDEX_DIRECT_MASTER_TOKEN`, `YANDEX_DIRECT_FINANCE_LOGIN`,
129
- `YANDEX_DIRECT_FINANCE_TOKEN`, and `YANDEX_DIRECT_OPERATION_NUM`. Money mutation
130
- commands are dry-run-only in this release and always require `--dry-run`; dry-run
131
- output masks the financial token.
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.
132
133
 
133
134
  ```bash
135
+ direct v4finance get-clients-units --logins client-login,other-client --format table
134
136
  direct v4finance get-credit-limits --logins client-login --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login
135
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
136
139
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
137
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
138
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
@@ -141,7 +144,9 @@ direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --currency
141
144
  ### V4 Live Shared Account
142
145
 
143
146
  Shared-account mutations are dry-run-only in this release and always require
144
- `--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`.
145
150
 
146
151
  ```bash
147
152
  direct v4account enable-shared-account --client-login client-login --dry-run
@@ -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
 
@@ -63,6 +64,7 @@ RESULT_DICTIONARY_KEYS_OF_API_METHODS = {
63
64
  },
64
65
  }
65
66
  REPORTS_RESOURCE_URL = "/json/v5/reports"
67
+ REPORT_SUMMARY_RE = re.compile(r"^Total rows: \d+$")
66
68
 
67
69
 
68
70
  class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
@@ -173,10 +175,15 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
173
175
  data = super().process_response(response, request_kwargs, **kwargs)
174
176
 
175
177
  if response.request.path_url == REPORTS_RESOURCE_URL:
176
- lines = self._iter_lines(data=data, response=response, **kwargs)
177
- 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
+ )
178
182
  else:
179
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)
180
187
 
181
188
  return data
182
189
 
@@ -306,22 +313,109 @@ class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter):
306
313
  raise NotImplementedError("For reports resource only")
307
314
 
308
315
  lines = io.StringIO(data)
309
- iterator = (line.replace("\n", "") for line in lines)
316
+ iterator = (line.rstrip("\r\n") for line in lines)
310
317
 
311
318
  return iterator
312
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
+
313
395
  def iter_lines(self, **kwargs) -> Iterator[str]:
314
396
  iterator = self._iter_lines(**kwargs)
315
- next(iterator) # skipping columns
316
- 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
317
407
 
318
408
  def iter_values(self, **kwargs) -> Iterator[list]:
319
409
  for line in self.iter_lines(**kwargs):
320
- 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
321
415
 
322
416
  def iter_dicts(self, **kwargs) -> Iterator[dict]:
323
- for line in self.iter_lines(**kwargs):
324
- 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))
325
419
 
326
420
  def to_values(self, **kwargs) -> List[list]:
327
421
  return list(self.iter_values(**kwargs))
@@ -323,10 +323,9 @@ def reports():
323
323
  help="Processing mode: auto, online, offline",
324
324
  )
325
325
  @click.option(
326
- "--skip-report-header",
327
- is_flag=True,
328
- default=False,
329
- help="Omit report header row",
326
+ "--skip-report-header/--no-skip-report-header",
327
+ default=True,
328
+ help="Omit report header row (default: yes)",
330
329
  )
331
330
  @click.option(
332
331
  "--skip-column-header",
@@ -335,10 +334,9 @@ def reports():
335
334
  help="Omit column header row",
336
335
  )
337
336
  @click.option(
338
- "--skip-report-summary",
339
- is_flag=True,
340
- default=False,
341
- help="Omit report summary row",
337
+ "--skip-report-summary/--no-skip-report-summary",
338
+ default=True,
339
+ help="Omit report summary row (default: yes)",
342
340
  )
343
341
  @click.option(
344
342
  "--return-money-in-micros",
@@ -34,6 +34,43 @@ def _logins_param(logins: str) -> list[str]:
34
34
  return login_list
35
35
 
36
36
 
37
+ def _invoice_payments_param(payments: tuple[str, ...], currency: str) -> dict:
38
+ """Build the v4 Live CreateInvoice payment object parameter."""
39
+ if not payments:
40
+ raise click.UsageError("--payment is required")
41
+
42
+ parsed_payments = []
43
+ seen_campaign_ids = set()
44
+ normalized_currency = currency.upper()
45
+ for payment in payments:
46
+ spec = (payment or "").strip()
47
+ if "=" not in spec:
48
+ raise click.UsageError("--payment must use CAMPAIGN_ID=AMOUNT")
49
+ campaign_id_text, amount_text = spec.split("=", 1)
50
+ campaign_id_text = campaign_id_text.strip()
51
+ amount_text = amount_text.strip()
52
+ try:
53
+ campaign_id = int(campaign_id_text)
54
+ except ValueError as exc:
55
+ raise click.UsageError(
56
+ "--payment campaign ID must be a positive integer"
57
+ ) from exc
58
+ if campaign_id <= 0:
59
+ raise click.UsageError("--payment campaign ID must be a positive integer")
60
+ if campaign_id in seen_campaign_ids:
61
+ raise click.UsageError("--payment campaign IDs must be unique")
62
+ seen_campaign_ids.add(campaign_id)
63
+ parsed_payments.append(
64
+ {
65
+ "CampaignID": campaign_id,
66
+ "Sum": parse_v4_money_sum(amount_text),
67
+ "Currency": normalized_currency,
68
+ }
69
+ )
70
+
71
+ return {"Payments": parsed_payments}
72
+
73
+
37
74
  def _finance_credentials(
38
75
  finance_token: Optional[str],
39
76
  master_token: Optional[str],
@@ -122,6 +159,41 @@ def v4finance():
122
159
  """Yandex Direct v4 Live finance commands."""
123
160
 
124
161
 
162
+ @v4_method_contract("GetClientsUnits")
163
+ @v4finance.command(name="get-clients-units")
164
+ @click.option("--logins", required=True, help="Comma-separated client logins")
165
+ @click.option(
166
+ "--format",
167
+ "output_format",
168
+ default="json",
169
+ type=click.Choice(["json", "table", "csv", "tsv"]),
170
+ help="Output format",
171
+ )
172
+ @click.option("--output", help="Output file")
173
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
174
+ @click.pass_context
175
+ def get_clients_units(ctx, logins, output_format, output, dry_run):
176
+ """Get available API units for clients."""
177
+ login_list = _logins_param(logins)
178
+
179
+ if dry_run:
180
+ format_output(build_v4_body("GetClientsUnits", login_list), "json", None)
181
+ return
182
+
183
+ try:
184
+ client = create_v4_client(
185
+ token=ctx.obj.get("token"),
186
+ login=ctx.obj.get("login"),
187
+ profile=ctx.obj.get("profile"),
188
+ sandbox=ctx.obj.get("sandbox"),
189
+ )
190
+ data = call_v4(client, "GetClientsUnits", login_list)
191
+ format_output(data, output_format, output)
192
+ except Exception as e:
193
+ print_error(str(e))
194
+ raise click.Abort()
195
+
196
+
125
197
  @v4_method_contract("GetCreditLimits")
126
198
  @v4finance.command(name="get-credit-limits")
127
199
  @click.option("--logins", required=True, help="Comma-separated client logins")
@@ -249,6 +321,103 @@ def check_payment(
249
321
  raise click.Abort()
250
322
 
251
323
 
324
+ @v4_method_contract("CreateInvoice")
325
+ @v4finance.command(name="create-invoice")
326
+ @click.option(
327
+ "--payment",
328
+ "payments",
329
+ multiple=True,
330
+ required=True,
331
+ help="Invoice payment as CAMPAIGN_ID=AMOUNT; repeat for multiple campaigns",
332
+ )
333
+ @click.option(
334
+ "--currency",
335
+ default="RUB",
336
+ show_default=True,
337
+ type=click.Choice(V4_FINANCE_CURRENCIES, case_sensitive=False),
338
+ help="Payment currency",
339
+ )
340
+ @click.option(
341
+ "--finance-token",
342
+ envvar="YANDEX_DIRECT_FINANCE_TOKEN",
343
+ help="Precomputed financial token for this method",
344
+ )
345
+ @click.option(
346
+ "--master-token",
347
+ envvar="YANDEX_DIRECT_MASTER_TOKEN",
348
+ help=(
349
+ "Financial master token issued after enabling and saving financial "
350
+ "operations in Tools -> API -> Financial operations"
351
+ ),
352
+ )
353
+ @click.option(
354
+ "--operation-num",
355
+ type=click.IntRange(min=1, max=9223372036854775807),
356
+ envvar="YANDEX_DIRECT_OPERATION_NUM",
357
+ help="Financial operation number",
358
+ )
359
+ @click.option(
360
+ "--finance-login",
361
+ envvar="YANDEX_DIRECT_FINANCE_LOGIN",
362
+ help="Login used in financial token generation",
363
+ )
364
+ @click.option(
365
+ "--format",
366
+ "output_format",
367
+ default="json",
368
+ type=click.Choice(["json", "table", "csv", "tsv"]),
369
+ help="Output format",
370
+ )
371
+ @click.option("--output", help="Output file")
372
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
373
+ @click.pass_context
374
+ def create_invoice(
375
+ ctx,
376
+ payments,
377
+ currency,
378
+ finance_token,
379
+ master_token,
380
+ operation_num,
381
+ finance_login,
382
+ output_format,
383
+ output,
384
+ dry_run,
385
+ ):
386
+ """Create a payment invoice for campaigns."""
387
+ finance_token, operation_num = _finance_credentials(
388
+ finance_token,
389
+ master_token,
390
+ operation_num,
391
+ finance_login,
392
+ "CreateInvoice",
393
+ ctx.obj.get("login"),
394
+ )
395
+ param = _invoice_payments_param(payments, currency)
396
+
397
+ if dry_run:
398
+ format_output(
399
+ _masked_finance_body("CreateInvoice", param, operation_num),
400
+ "json",
401
+ None,
402
+ )
403
+ return
404
+
405
+ try:
406
+ client = create_v4_client(
407
+ token=ctx.obj.get("token"),
408
+ login=ctx.obj.get("login"),
409
+ profile=ctx.obj.get("profile"),
410
+ sandbox=ctx.obj.get("sandbox"),
411
+ finance_token=finance_token,
412
+ operation_num=operation_num,
413
+ )
414
+ data = call_v4(client, "CreateInvoice", param)
415
+ format_output(data, output_format, output)
416
+ except Exception as e:
417
+ print_error(str(e))
418
+ raise click.Abort()
419
+
420
+
252
421
  @v4_method_contract("TransferMoney")
253
422
  @v4finance.command(name="transfer-money")
254
423
  @click.option(
@@ -59,6 +59,7 @@ SMOKE_MATRIX = {
59
59
  "turbopages.get",
60
60
  "v4events.get-events-log",
61
61
  "v4finance.check-payment",
62
+ "v4finance.get-clients-units",
62
63
  "v4finance.get-credit-limits",
63
64
  "v4goals.get-retargeting-goals",
64
65
  "v4goals.get-stat-goals",
@@ -154,6 +155,7 @@ SMOKE_MATRIX = {
154
155
  "auth.use",
155
156
  "v4account.account-management",
156
157
  "v4account.enable-shared-account",
158
+ "v4finance.create-invoice",
157
159
  "v4finance.pay-campaigns",
158
160
  "v4finance.transfer-money",
159
161
  ],
@@ -142,11 +142,22 @@ V4_METHOD_CONTRACTS: dict[str, V4MethodContract] = {
142
142
  "CreateInvoice": V4MethodContract(
143
143
  method="CreateInvoice",
144
144
  group="finance",
145
- param_shape=PARAM_UNDOCUMENTED,
146
- login_placement="unknown",
147
- safety=SAFETY_WRITE,
148
- source_status=SOURCE_UNDOCUMENTED,
145
+ param_shape=PARAM_OBJECT,
146
+ login_placement=(
147
+ "param contains Payments; finance_token and operation_num are "
148
+ "top-level v4 Live body fields"
149
+ ),
150
+ safety=SAFETY_DANGEROUS,
151
+ source_status=SOURCE_DOCS,
149
152
  live_probe_allowed=False,
153
+ example_param={
154
+ "Payments": [{"CampaignID": 123, "Sum": 100.5, "Currency": "RUB"}],
155
+ },
156
+ notes=(
157
+ "Official v4 docs define invoice creation with Payments[]."
158
+ "CampaignID/Sum/Currency and a URL response. The method has no "
159
+ "v5 equivalent and requires finance_token plus operation_num."
160
+ ),
150
161
  ),
151
162
  "AccountManagement": V4MethodContract(
152
163
  method="AccountManagement",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.3.3
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
 
@@ -165,13 +169,16 @@ per-request token from `--master-token`, `--operation-num`, and
165
169
  `--finance-login`; alternatively pass a precomputed token with `--finance-token`.
166
170
  Environment variables are
167
171
  `YANDEX_DIRECT_MASTER_TOKEN`, `YANDEX_DIRECT_FINANCE_LOGIN`,
168
- `YANDEX_DIRECT_FINANCE_TOKEN`, and `YANDEX_DIRECT_OPERATION_NUM`. Money mutation
169
- commands are dry-run-only in this release and always require `--dry-run`; dry-run
170
- output masks the financial token.
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.
171
176
 
172
177
  ```bash
178
+ direct v4finance get-clients-units --logins client-login,other-client --format table
173
179
  direct v4finance get-credit-limits --logins client-login --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login
174
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
175
182
  direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B
176
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
177
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
@@ -180,7 +187,9 @@ direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --currency
180
187
  ### V4 Live Shared Account
181
188
 
182
189
  Shared-account mutations are dry-run-only in this release and always require
183
- `--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`.
184
193
 
185
194
  ```bash
186
195
  direct v4account enable-shared-account --client-login client-login --dry-run
@@ -12,6 +12,7 @@ setup.py
12
12
  .github/workflows/api-coverage.yml
13
13
  .github/workflows/claude-code-review.yml
14
14
  .github/workflows/claude.yml
15
+ .github/workflows/quality.yml
15
16
  direct_cli/__init__.py
16
17
  direct_cli/_deprecated.py
17
18
  direct_cli/_smoke_probes.py
@@ -115,6 +116,7 @@ tests/test_integration.py
115
116
  tests/test_integration_live_write.py
116
117
  tests/test_integration_write.py
117
118
  tests/test_reports_drift.py
119
+ tests/test_reports_parsing.py
118
120
  tests/test_smoke_matrix.py
119
121
  tests/test_transport_contract.py
120
122
  tests/test_v4_contracts.py
@@ -14,6 +14,10 @@ pytest-recording>=0.13
14
14
  vcrpy>=6.0
15
15
  black>=22.0
16
16
  flake8>=4.0
17
+ mypy>=1.8
18
+ ruff>=0.1
17
19
  requests>=2.0
18
20
  beautifulsoup4>=4.12
19
21
  lxml>=4.9
22
+ types-setuptools
23
+ types-tabulate
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "direct-cli"
7
- version = "0.3.3"
7
+ version = "0.3.4"
8
8
  description = "Command-line interface for Yandex Direct API"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -43,9 +43,13 @@ dev = [
43
43
  "vcrpy>=6.0",
44
44
  "black>=22.0",
45
45
  "flake8>=4.0",
46
+ "mypy>=1.8",
47
+ "ruff>=0.1",
46
48
  "requests>=2.0",
47
49
  "beautifulsoup4>=4.12",
48
50
  "lxml>=4.9",
51
+ "types-setuptools",
52
+ "types-tabulate",
49
53
  ]
50
54
 
51
55
  [project.scripts]
@@ -82,6 +86,28 @@ exclude = '''
82
86
  [tool.ruff]
83
87
  exclude = ["direct_cli/_vendor"]
84
88
 
89
+ [tool.mypy]
90
+ python_version = "3.9"
91
+ ignore_missing_imports = true
92
+ no_implicit_optional = false
93
+ exclude = [
94
+ "direct_cli/_vendor/",
95
+ "scripts/",
96
+ "tests/",
97
+ ]
98
+ disable_error_code = [
99
+ "assignment",
100
+ "arg-type",
101
+ "attr-defined",
102
+ "import-untyped",
103
+ "index",
104
+ "list-item",
105
+ "misc",
106
+ "no-redef",
107
+ "truthy-function",
108
+ "var-annotated",
109
+ ]
110
+
85
111
  [tool.pytest.ini_options]
86
112
  testpaths = ["tests"]
87
113
  python_files = "test_*.py"