direct-cli 0.4.2__tar.gz → 0.4.3__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 (354) hide show
  1. {direct_cli-0.4.2 → direct_cli-0.4.3}/CHANGELOG.md +215 -0
  2. {direct_cli-0.4.2 → direct_cli-0.4.3}/PKG-INFO +17 -3
  3. {direct_cli-0.4.2 → direct_cli-0.4.3}/README.md +16 -2
  4. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +12 -4
  5. direct_cli-0.4.3/direct_cli/commands/_batch.py +181 -0
  6. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/_lifecycle.py +8 -1
  7. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/adgroups.py +1131 -254
  8. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/ads.py +1488 -677
  9. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/agencyclients.py +6 -2
  10. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/audiencetargets.py +17 -5
  11. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/balance.py +2 -10
  12. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/bidmodifiers.py +3 -3
  13. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/bids.py +6 -6
  14. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/campaigns.py +3 -1
  15. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/dynamicads.py +18 -4
  16. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/dynamicfeedadtargets.py +6 -4
  17. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/feeds.py +3 -1
  18. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/keywordbids.py +23 -6
  19. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/keywords.py +36 -121
  20. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/negativekeywordsharedsets.py +3 -1
  21. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/retargeting.py +7 -1
  22. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/smartadtargets.py +20 -5
  23. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/strategies.py +3 -1
  24. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4adimage.py +3 -19
  25. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4events.py +2 -10
  26. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4finance.py +5 -37
  27. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4forecast.py +5 -37
  28. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4goals.py +3 -19
  29. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4keywords.py +2 -9
  30. direct_cli-0.4.3/direct_cli/commands/v4shells.py +54 -0
  31. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4tags.py +5 -37
  32. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4wordstat.py +5 -37
  33. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/vcards.py +3 -1
  34. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/output.py +8 -0
  35. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/smoke_matrix.py +4 -0
  36. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/adgroups.json +42 -2
  37. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/ads.json +40 -3
  38. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/audiencetargets.json +2 -1
  39. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/common.json +2 -1
  40. direct_cli-0.4.3/direct_cli/translations/v4shells.json +7 -0
  41. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/utils.py +70 -0
  42. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/PKG-INFO +17 -3
  43. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/SOURCES.txt +8 -0
  44. {direct_cli-0.4.2 → direct_cli-0.4.3}/pyproject.toml +1 -1
  45. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/check_all_docs_urls.py +30 -13
  46. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/WSDL_OPTIONAL_FIELD_AUDIT.md +3 -3
  47. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/api_coverage_payloads.py +4 -0
  48. direct_cli-0.4.3/tests/test_adgroups_build_adgroup_object.py +215 -0
  49. direct_cli-0.4.3/tests/test_adgroups_build_adgroup_update_object.py +129 -0
  50. direct_cli-0.4.3/tests/test_ads_build_ad_object.py +201 -0
  51. direct_cli-0.4.3/tests/test_ads_build_ad_update_object.py +163 -0
  52. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_audit_wire_shape.py +37 -0
  53. direct_cli-0.4.3/tests/test_check_all_docs_urls.py +16 -0
  54. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_cli.py +2 -2
  55. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_dry_run.py +1735 -0
  56. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_integration.py +75 -0
  57. direct_cli-0.4.3/tests/test_v4_output_options.py +134 -0
  58. direct_cli-0.4.3/tests/test_v4meta.py +71 -0
  59. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_wsdl_parity_gate.py +62 -9
  60. direct_cli-0.4.2/direct_cli/commands/v4shells.py +0 -20
  61. direct_cli-0.4.2/direct_cli/translations/v4shells.json +0 -3
  62. {direct_cli-0.4.2 → direct_cli-0.4.3}/.env.example +0 -0
  63. {direct_cli-0.4.2 → direct_cli-0.4.3}/.github/copilot-instructions.md +0 -0
  64. {direct_cli-0.4.2 → direct_cli-0.4.3}/.github/workflows/api-coverage.yml +0 -0
  65. {direct_cli-0.4.2 → direct_cli-0.4.3}/.github/workflows/claude.yml +0 -0
  66. {direct_cli-0.4.2 → direct_cli-0.4.3}/.github/workflows/quality.yml +0 -0
  67. {direct_cli-0.4.2 → direct_cli-0.4.3}/.gitignore +0 -0
  68. {direct_cli-0.4.2 → direct_cli-0.4.3}/AGENTS.md +0 -0
  69. {direct_cli-0.4.2 → direct_cli-0.4.3}/CLAUDE.md +0 -0
  70. {direct_cli-0.4.2 → direct_cli-0.4.3}/MANIFEST.in +0 -0
  71. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/__init__.py +0 -0
  72. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_autotargeting.py +0 -0
  73. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_bidding_strategy.py +0 -0
  74. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_deprecated.py +0 -0
  75. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_flag_validation.py +0 -0
  76. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_smoke_probes.py +0 -0
  77. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/__init__.py +0 -0
  78. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  79. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
  80. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  81. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +0 -0
  82. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
  83. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  84. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
  85. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  86. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  87. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/api.py +0 -0
  88. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/auth.py +0 -0
  89. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/cli.py +0 -0
  90. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/__init__.py +0 -0
  91. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/adextensions.py +0 -0
  92. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/adimages.py +0 -0
  93. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/advideos.py +0 -0
  94. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/auth.py +0 -0
  95. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/businesses.py +0 -0
  96. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/changes.py +0 -0
  97. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/clients.py +0 -0
  98. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/creatives.py +0 -0
  99. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/dictionaries.py +0 -0
  100. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/keywordsresearch.py +0 -0
  101. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/leads.py +0 -0
  102. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/reports.py +0 -0
  103. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/sitelinks.py +0 -0
  104. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/turbopages.py +0 -0
  105. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4account.py +0 -0
  106. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/i18n.py +0 -0
  107. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/reports_coverage.py +0 -0
  108. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/adextensions.json +0 -0
  109. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/adimages.json +0 -0
  110. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/advideos.json +0 -0
  111. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/agencyclients.json +0 -0
  112. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/auth.json +0 -0
  113. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/balance.json +0 -0
  114. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/bidmodifiers.json +0 -0
  115. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/bids.json +0 -0
  116. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/businesses.json +0 -0
  117. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/campaigns.json +0 -0
  118. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/changes.json +0 -0
  119. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/clients.json +0 -0
  120. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/creatives.json +0 -0
  121. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/dictionaries.json +0 -0
  122. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/dynamicads.json +0 -0
  123. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/dynamicfeedadtargets.json +0 -0
  124. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/feeds.json +0 -0
  125. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/keywordbids.json +0 -0
  126. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/keywords.json +0 -0
  127. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/keywordsresearch.json +0 -0
  128. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/leads.json +0 -0
  129. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/negativekeywordsharedsets.json +0 -0
  130. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/reports.json +0 -0
  131. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/retargeting.json +0 -0
  132. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/sitelinks.json +0 -0
  133. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/smartadtargets.json +0 -0
  134. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/strategies.json +0 -0
  135. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/turbopages.json +0 -0
  136. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4account.json +0 -0
  137. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4adimage.json +0 -0
  138. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4events.json +0 -0
  139. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4finance.json +0 -0
  140. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4forecast.json +0 -0
  141. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4goals.json +0 -0
  142. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4keywords.json +0 -0
  143. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4tags.json +0 -0
  144. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4wordstat.json +0 -0
  145. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/vcards.json +0 -0
  146. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/v4/__init__.py +0 -0
  147. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/v4/emit.py +0 -0
  148. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/v4/money.py +0 -0
  149. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/v4_contracts.py +0 -0
  150. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/wsdl_coverage.py +0 -0
  151. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/dependency_links.txt +0 -0
  152. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/entry_points.txt +0 -0
  153. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/requires.txt +0 -0
  154. {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/top_level.txt +0 -0
  155. {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/API_COVERAGE.md +0 -0
  156. {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/PROJECT_WIRE_SHAPE_AUDIT_2026-05-30.md +0 -0
  157. {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/WIRE_SHAPE_TRIAGE_2026-05-30.md +0 -0
  158. {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/issue-198-mutating-wsdl-audit.md +0 -0
  159. {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/wire_shape.json +0 -0
  160. {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  161. {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  162. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/anonymize_cassettes.py +0 -0
  163. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/audit_wire_shape.py +0 -0
  164. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/build_api_coverage_checklist.py +0 -0
  165. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/build_api_coverage_report.py +0 -0
  166. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/build_wsdl_optional_field_audit.py +0 -0
  167. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/check_reports_drift.py +0 -0
  168. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/check_wsdl_drift.py +0 -0
  169. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/patch_vendor_imports.py +0 -0
  170. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/preflight_check.sh +0 -0
  171. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/probe_drift_urls.sh +0 -0
  172. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/refresh_reports_cache.py +0 -0
  173. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/refresh_wsdl_cache.py +0 -0
  174. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/release_pypi.sh +0 -0
  175. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/sandbox_write_audit.py +0 -0
  176. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/sandbox_write_live.py +0 -0
  177. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/test_dangerous_commands.sh +0 -0
  178. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/test_safe_commands.sh +0 -0
  179. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/test_sandbox_write.sh +0 -0
  180. {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/update_vendor.sh +0 -0
  181. {direct_cli-0.4.2 → direct_cli-0.4.3}/setup.cfg +0 -0
  182. {direct_cli-0.4.2 → direct_cli-0.4.3}/setup.py +0 -0
  183. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/API_COVERAGE.md +0 -0
  184. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/API_ISSUE_AUDIT.md +0 -0
  185. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/MANUAL_COVERAGE.md +0 -0
  186. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/__init__.py +0 -0
  187. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/_orphan_store.py +0 -0
  188. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  189. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  190. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  191. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  192. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  193. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  194. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  195. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  196. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +0 -0
  197. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +0 -0
  198. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  199. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  200. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  201. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +0 -0
  202. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  203. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  204. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  205. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  206. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteRetargetingUpdate.test_retargeting_update.yaml +0 -0
  207. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  208. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
  209. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +0 -0
  210. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  211. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[adextensions_get].yaml +0 -0
  212. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[adgroups_get].yaml +0 -0
  213. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[adimages_get].yaml +0 -0
  214. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[ads_get].yaml +0 -0
  215. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[advideos_get].yaml +0 -0
  216. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[audiencetargets_get].yaml +0 -0
  217. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[bidmodifiers_get].yaml +0 -0
  218. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[bids_get].yaml +0 -0
  219. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[businesses_get].yaml +0 -0
  220. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[campaigns_get].yaml +0 -0
  221. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[changes_check].yaml +0 -0
  222. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[changes_check_campaigns].yaml +0 -0
  223. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[changes_check_dictionaries].yaml +0 -0
  224. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[clients_get].yaml +0 -0
  225. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[creatives_get].yaml +0 -0
  226. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[dictionaries_get].yaml +0 -0
  227. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[dynamicads_get].yaml +0 -0
  228. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[dynamicfeedadtargets_get].yaml +0 -0
  229. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[feeds_get].yaml +0 -0
  230. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[keywordbids_get].yaml +0 -0
  231. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[keywords_get].yaml +0 -0
  232. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_deduplicate].yaml +0 -0
  233. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_has_search_volume].yaml +0 -0
  234. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[leads_get].yaml +0 -0
  235. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[negativekeywordsharedsets_get].yaml +0 -0
  236. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[reports_get].yaml +0 -0
  237. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[retargeting_get].yaml +0 -0
  238. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[sitelinks_get].yaml +0 -0
  239. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[smartadtargets_get].yaml +0 -0
  240. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[strategies_get].yaml +0 -0
  241. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[turbopages_get].yaml +0 -0
  242. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4events_get_events_log].yaml +0 -0
  243. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4finance_get_clients_units].yaml +0 -0
  244. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4forecast_list].yaml +0 -0
  245. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4goals_get_stat_goals].yaml +0 -0
  246. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4tags_get_campaigns].yaml +0 -0
  247. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4wordstat_list_reports].yaml +0 -0
  248. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[vcards_get].yaml +0 -0
  249. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_v4finance_check_payment_unknown_transaction.yaml +0 -0
  250. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_adgroups_add_update_delete.yaml +0 -0
  251. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_adimages_add_get_delete.yaml +0 -0
  252. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_add_update_delete.yaml +0 -0
  253. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
  254. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_advideos_add_get.yaml +0 -0
  255. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_add_delete.yaml +0 -0
  256. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_suspend_resume.yaml +0 -0
  257. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_bids_set.yaml +0 -0
  258. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_campaign_create_get_delete.yaml +0 -0
  259. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
  260. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_add_delete.yaml +0 -0
  261. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_suspend_resume.yaml +0 -0
  262. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_feeds_add_update_delete.yaml +0 -0
  263. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywordbids_set.yaml +0 -0
  264. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_add_update_delete.yaml +0 -0
  265. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_suspend_resume.yaml +0 -0
  266. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_retargeting_add_update_delete.yaml +0 -0
  267. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_sitelinks_add_get_delete.yaml +0 -0
  268. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_add_update_delete.yaml +0 -0
  269. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_suspend_resume.yaml +0 -0
  270. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_v5_live_write/test_v5_live_draft_strategies_add_update_archive_unarchive.yaml +0 -0
  271. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/conftest.py +0 -0
  272. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/fixtures/test-video.mp4 +0 -0
  273. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/fields-list.html +0 -0
  274. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/headers.html +0 -0
  275. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/period.html +0 -0
  276. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/spec.html +0 -0
  277. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/type.html +0 -0
  278. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/spec.json +0 -0
  279. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_api_coverage.py +0 -0
  280. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_auth_bw.py +0 -0
  281. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_auth_oauth.py +0 -0
  282. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_auth_op.py +0 -0
  283. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_auth_write_json.py +0 -0
  284. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_autotargeting.py +0 -0
  285. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_balance.py +0 -0
  286. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_bidding_strategy_constants.py +0 -0
  287. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_cassette_integrity.py +0 -0
  288. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_changes.py +0 -0
  289. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_cli_contract.py +0 -0
  290. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_comprehensive.py +0 -0
  291. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_env_loading.py +0 -0
  292. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_field_names_option.py +0 -0
  293. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_flag_validation.py +0 -0
  294. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_handle_api_errors.py +0 -0
  295. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_i18n.py +0 -0
  296. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_integration_write.py +0 -0
  297. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_low_coverage_payloads.py +0 -0
  298. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_read_cassettes.py +0 -0
  299. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_reports_drift.py +0 -0
  300. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_reports_parsing.py +0 -0
  301. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_sandbox_write_audit.py +0 -0
  302. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_smoke_matrix.py +0 -0
  303. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_transport_contract.py +0 -0
  304. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_unknown_option_hints.py +0 -0
  305. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_contracts.py +0 -0
  306. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_exit_codes.py +0 -0
  307. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_foundation.py +0 -0
  308. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_live_contracts.py +0 -0
  309. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_runtime_shape.py +0 -0
  310. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_safety.py +0 -0
  311. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4account.py +0 -0
  312. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4adimage.py +0 -0
  313. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4events.py +0 -0
  314. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4finance_money.py +0 -0
  315. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4finance_read.py +0 -0
  316. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4forecast.py +0 -0
  317. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4goals.py +0 -0
  318. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4keywords.py +0 -0
  319. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4tags.py +0 -0
  320. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4wordstat.py +0 -0
  321. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v5_live_write.py +0 -0
  322. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_vendor_imports.py +0 -0
  323. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/adextensions.xml +0 -0
  324. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/adgroups.xml +0 -0
  325. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/adimages.xml +0 -0
  326. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/ads.xml +0 -0
  327. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/advideos.xml +0 -0
  328. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/agencyclients.xml +0 -0
  329. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/audiencetargets.xml +0 -0
  330. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  331. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/bids.xml +0 -0
  332. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/businesses.xml +0 -0
  333. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/campaigns.xml +0 -0
  334. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/changes.xml +0 -0
  335. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/clients.xml +0 -0
  336. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/creatives.xml +0 -0
  337. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/dictionaries.xml +0 -0
  338. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  339. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  340. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/feeds.xml +0 -0
  341. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  342. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/imports/general.xsd +0 -0
  343. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  344. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/keywordbids.xml +0 -0
  345. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/keywords.xml +0 -0
  346. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  347. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/leads.xml +0 -0
  348. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  349. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/retargetinglists.xml +0 -0
  350. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/sitelinks.xml +0 -0
  351. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/smartadtargets.xml +0 -0
  352. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/strategies.xml +0 -0
  353. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/turbopages.xml +0 -0
  354. {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/vcards.xml +0 -0
@@ -1,5 +1,220 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.3
4
+
5
+ **Features — batch `ads add` via `--from-file` / `--ads-json` (#562, #558 follow-up):**
6
+
7
+ - `ads add` now accepts a batch of flag-form ad rows from a JSONL file
8
+ (`--from-file`) or an inline JSON array (`--ads-json`); each row is the same
9
+ flag set keyed by the kebab flag name without the leading dashes (e.g.
10
+ `{"type":"TEXT_AD","title":"...","text":"...","href":"...","adgroup-id":1}`).
11
+ `--adgroup-id` becomes the batch default and may be overridden per row. Single
12
+ typed-flag mode is unchanged.
13
+ - The ~400-line flag→object logic of `ads add` was extracted into a reusable,
14
+ ctx-free `build_ad_object()` so the single-flag command and the batch
15
+ normalizer emit byte-identical ad objects (golden-tested across every
16
+ subtype).
17
+ - New shared `direct_cli/commands/_batch.py` engine (JSONL/inline loading,
18
+ chunking, per-chunk send with partial-success reporting, dry-run preview,
19
+ `add`/`update`-aware result key). `keywords add` was migrated onto it with no
20
+ behavior change (its existing batch suite is the proof).
21
+ - Chunk size `ADS_ADD_MAX_BATCH = 100` (conservative chunk, not the 1000-object
22
+ API ceiling — a partial failure rolls back at most 100 ads).
23
+
24
+ **Features — batch `ads update` via `--from-file` / `--ads-json` (#563, #558 follow-up):**
25
+
26
+ - `ads update` now accepts a batch of flag-form ad-update rows from a JSONL file
27
+ (`--from-file`) or an inline JSON array (`--ads-json`); each row is the same
28
+ flag set keyed by the kebab flag name without the leading dashes plus its own
29
+ `id` and `type` (e.g. `{"id":5,"type":"TEXT_AD","title":"New"}`). The
30
+ `--clear-image-hash` mechanic works per row as a JSON boolean. Single
31
+ typed-flag mode is unchanged.
32
+ - The subtype-dispatch body of `ads update` (type validation, the
33
+ incompatible-flag / "does not convert between subtypes" guard, per-subtype
34
+ assembly, and the empty-subtype no-op guard) was extracted into a reusable,
35
+ ctx-free `build_ad_update_object()` so the single-flag command and the batch
36
+ normalizer emit byte-identical ad-update objects (golden-tested across every
37
+ subtype). Reuses the shared `_batch.py` engine with `method="update"` /
38
+ `result_key="UpdateResults"`.
39
+ - `--id` and `--type` become per-row in batch mode (each row carries its own);
40
+ single-item mode still requires both. The per-row normalizer reproduces the
41
+ command's `--id`/`--type` required checks, the `--image-hash` /
42
+ `--clear-image-hash` mutex, and the same Click-type coercion as the single
43
+ path (a JSON float `id` is rejected, not truncated).
44
+
45
+ **Features — batch `adgroups add` via `--from-file` / `--adgroups-json` (#564, #558 follow-up):**
46
+
47
+ - `adgroups add` now accepts a batch of flag-form ad-group rows from a JSONL
48
+ file (`--from-file`) or an inline JSON array (`--adgroups-json`); each row is
49
+ the same flag set keyed by the kebab flag name without the leading dashes
50
+ (e.g. `{"name":"G","campaign-id":12,"region-ids":"225","type":"TEXT_AD_GROUP"}`).
51
+ `--campaign-id` becomes the batch default and may be overridden per row.
52
+ Single typed-flag mode is unchanged.
53
+ - The flag→object logic of `adgroups add` (type validation, the
54
+ incompatible-flag guard, the negative-keyword compatibility check, region IDs,
55
+ and per-subtype assembly) was extracted into a reusable, ctx-free
56
+ `build_adgroup_object()` so the single-flag command and the batch normalizer
57
+ emit byte-identical ad-group objects (golden-tested across every subtype).
58
+ `--name` / `--campaign-id` / `--region-ids` become per-row in batch mode;
59
+ single-item mode still requires them (parity-gate `INTERNAL_VALIDATION`
60
+ entries). Per-row coercion runs every typed field through its single-flag
61
+ Click type (a JSON float `campaign-id` is rejected, not truncated).
62
+ - The shared `_batch.send_batch` gained an optional `post` callable so
63
+ `adgroups` keeps its endpoint routing: a `UnifiedAdGroup` payload must use API
64
+ v501 (`_post_adgroups`). Because that routing keys off the whole body, a batch
65
+ may **not** mix `UNIFIED_AD_GROUP` with other ad-group types — the CLI refuses
66
+ the mix up front with a clear `UsageError` rather than send non-unified groups
67
+ to the v501 endpoint.
68
+
69
+ **Features — batch `adgroups update` via `--from-file` / `--adgroups-json` (#565, #558 follow-up):**
70
+
71
+ - `adgroups update` now accepts a batch of flag-form ad-group-update rows from a
72
+ JSONL file (`--from-file`) or an inline JSON array (`--adgroups-json`); each
73
+ row is the same flag set keyed by the kebab flag name without the leading
74
+ dashes plus its own `id` (e.g. `{"id":5,"name":"New"}`). The `--dynamic-feed`
75
+ routing works per row as a JSON boolean. Single typed-flag mode is unchanged.
76
+ - The subtype-dispatch body of `adgroups update` (the mixed-subtype reject
77
+ guard, per-subtype assembly, the `--dynamic-feed` DynamicTextAdGroup ↔
78
+ DynamicTextFeedAdGroup routing, and the empty-payload no-op guard) was
79
+ extracted into a reusable, ctx-free `build_adgroup_update_object()` so the
80
+ single-flag command and the batch normalizer emit byte-identical objects
81
+ (golden-tested across every subtype). `--id` becomes per-row in batch mode;
82
+ single-item mode still requires it (parity-gate `INTERNAL_VALIDATION` entry).
83
+ Per-row coercion runs every typed field through its single-flag Click type (a
84
+ JSON float `id` is rejected, not truncated).
85
+ - Reuses the shared `_batch.send_batch` with `method="update"` /
86
+ `result_key="UpdateResults"` and the `post=_post_adgroups` endpoint routing.
87
+ As with `adgroups add`, a batch may **not** mix `UNIFIED_AD_GROUP` with other
88
+ ad-group types (unified groups use API v501) — the CLI refuses the mix up
89
+ front with a clear `UsageError`.
90
+
91
+ **Fixes — reject non-positive IDs before the request (#558):**
92
+
93
+ - Mutating commands and lifecycle ops took their object-ID selector
94
+ (`--id` / `--adgroup-id` / `--campaign-id` / `--keyword-id` / `--client-id`)
95
+ as a bare `int`, which accepted `0` and negatives and forwarded them to the
96
+ API (opaque rejection). Every such selector now uses `click.IntRange(min=1)`
97
+ and rejects a non-positive id with a clear `UsageError` (exit 2) before any
98
+ request. Coverage is the full mutation surface, not a subset:
99
+ - every `delete` / `suspend` / `resume` / `archive` / `unarchive` /
100
+ `moderate` lifecycle command (via the shared `_lifecycle.py` factory);
101
+ - `ads add` / `ads update`, `adgroups add` / `adgroups update`,
102
+ `keywords add` / `keywords update`;
103
+ - `campaigns update`, `feeds update`, `strategies update`,
104
+ `retargeting update`, `negativekeywordsharedsets update`, `vcards add`;
105
+ - `smartadtargets add` / `update` / `set-bids`,
106
+ `audiencetargets add` / `set-bids`, `dynamicads add` / `set-bids`,
107
+ `dynamicfeedadtargets add` / `set-bids`;
108
+ - the bid setters `bids set` / `set-auto`, `keywordbids set` / `set-auto`
109
+ (the `campaign-id` / `adgroup-id` / `keyword-id` "exactly one of" trios),
110
+ `bidmodifiers add` / `set`;
111
+ - `agencyclients update --client-id`.
112
+
113
+ The ad-image lifecycle (`--hash`, a string) is unchanged. Secondary
114
+ reference-ID flags that point at *other* objects inside a write payload
115
+ (e.g. `--feed-id`, `--counter-id`, `--vcard-id`, `--region-id`,
116
+ `--retargeting-list-id`) are left as-is for now and tracked as follow-up.
117
+ - Batch-size caps (the docs allow up to 1000 objects per add/update and 10000
118
+ ids per delete) are intentionally **not** added: the CLI builds a
119
+ single-item payload for every mutation, so there is no caller-controllable
120
+ array to overflow. Multi-item batch mode (`--from-file`) for ads/adgroups is
121
+ tracked as follow-up work.
122
+ - De-staled the `KEYWORDS_ADD_MAX_BATCH` comment: it claimed the API caps a
123
+ `keywords.add` request at 10 (citing an outdated doc page that states no such
124
+ number). The real documented per-call limit is 1000; the value `10` is a
125
+ conservative chunk size for batch add, not the API ceiling — comment fixed,
126
+ value unchanged.
127
+
128
+ **Fixes — explain Error 8300 on delete/moderate (#548):**
129
+
130
+ - `raise_for_api_result_errors` now appends a hint when the API returns code
131
+ 8300, mirroring the existing 8800 hint: the ad is likely not in `DRAFT`
132
+ status, and `Status=UNKNOWN` is an API fallback value (a status outside the
133
+ v5 enum), not a business status — such ads can only be archived/unarchived,
134
+ not deleted or sent to moderation. Covers `ads delete` / `ads moderate` and
135
+ any command routing through `format_output`. English-only, matching the 8800
136
+ hint (`output.py` does not import i18n).
137
+
138
+ **Docs — audiencetargets get requires a filter (#554):**
139
+
140
+ - Clarified that `audiencetargets get` cannot page the whole account: unlike
141
+ `retargeting get --fetch-all`, the live API hard-rejects an empty
142
+ `SelectionCriteria` (error 8000 with no criteria, 4001 with `{}`). The
143
+ required-filter guard now explains this and recommends the `campaigns get` →
144
+ batched `campaign_ids` sweep instead. No API behavior change; message only.
145
+
146
+ **Fixes — preflight SelectionCriteria array limits on get (#555, P0):**
147
+
148
+ - `keywordbids get` now rejects `--campaign-ids` >10, `--adgroup-ids` >1000,
149
+ `--keyword-ids` >10000; `dynamicads get` / `smartadtargets get` reject
150
+ `--campaign-ids` >2 — before the request, with a clear `UsageError` (exit 2)
151
+ naming the array and ceiling, instead of the opaque API `error_code=4001`.
152
+ These are runtime ceilings (the WSDL declares the arrays `unbounded`), pinned
153
+ next to each command with a doc/live-4001 citation, the same discipline as
154
+ `KEYWORDS_ADD_MAX_BATCH`. Verified live 2026-06-16. Other `get` arrays
155
+ (`AdGroupIds`/`Ids` on dynamic/smart, etc.) are intentionally **not** capped
156
+ because the live API accepts them.
157
+
158
+ **Internal — dedup v4 Live output-option stack (#550):**
159
+
160
+ - Replaced the byte-identical `--format`/`--output`/`--dry-run` trio across the
161
+ standard v4 Live and `balance` commands with a shared `v4_output_options`
162
+ decorator (the v4 analogue of `get_options`, epic #491). The CLI surface is
163
+ unchanged — same option order, names, `click.Choice(["json","table","csv",
164
+ "tsv"])` format, defaults, and help. `v4account enable-shared-account` /
165
+ `account-management` (reversed order, custom `--dry-run` help) and the
166
+ dry-run-only `v4finance transfer-money` / `pay-campaigns` /
167
+ `pay-campaigns-by-card` (no `--format`/`--output`) keep their divergent
168
+ stacks and are intentionally excluded.
169
+
170
+ **Fixes — `ads update` can now clear AdImageHash (#552):**
171
+
172
+ - Added `--clear-image-hash` to `ads update`. The flag sends
173
+ `AdImageHash: null` so an image can be removed from an existing ad — e.g.
174
+ unblocking a `TEXT_AD` whose image was restricted in moderation — without
175
+ recreating the ad. Supported for the three subtypes whose WSDL `AdImageHash`
176
+ is nillable: `TEXT_AD`, `DYNAMIC_TEXT_AD`, `MOBILE_APP_AD`. It is **rejected**
177
+ for `TEXT_IMAGE_AD` and `MOBILE_APP_IMAGE_AD`, which share the non-nillable
178
+ `ImageAdUpdateBase.AdImageHash` — the live API returns error 8000
179
+ (`AdImageHash cannot have the null value`) for those, verified directly.
180
+ `--image-hash` and `--clear-image-hash` are mutually exclusive.
181
+ Previously there was no way to reset the image: `--image-hash ""` was dropped
182
+ by a truthy check, and `--image-hash null` sent the literal string `"null"`.
183
+
184
+ **Fixes — docs-URL drift regression (re-fixes #463):**
185
+
186
+ - Restored the four WSDL `docs` URLs for `dynamicads`,
187
+ `dynamicfeedadtargets`, `smartadtargets` and `vcards` that the
188
+ `tapi-yandex-direct` 2026.5.29 vendor update silently reverted back to the
189
+ removed `…/dev/direct/doc/ru/<service>` HTML pages (which 404 since Yandex
190
+ dropped those pages in September 2025). The fix from #464 was overwritten by
191
+ the `rm -rf` + `cp -R` vendor sync; preflight
192
+ (`scripts/check_all_docs_urls.py`) caught it. URLs now point back at the live
193
+ `https://api.direct.yandex.com/v5/<service>?wsdl` endpoints — the only
194
+ authoritative source still served.
195
+ - Fixed the same URLs at the source in the `axisrow/tapi-yandex-direct` fork so
196
+ the next vendor update no longer re-introduces the dead pages.
197
+ - Added an offline regression guard
198
+ (`tests/test_audit_wire_shape.py::test_removed_doc_services_pin_wsdl_url`):
199
+ the four doc-removed services must keep WSDL `docs` URLs, failing in CI before
200
+ the network preflight ever runs.
201
+
202
+ **Bug Fixes — reject empty-string CSV-ID flags in `adgroups` (#570):**
203
+
204
+ - `adgroups add` and `adgroups update` now reject an explicitly-provided
205
+ empty/whitespace value for `--region-ids`, `--negative-keyword-shared-set-ids`
206
+ and `--feed-category-ids` (e.g. `--region-ids ""`, `--region-ids " "`,
207
+ `--region-ids ","`, or a batch row `{"region-ids":""}`) with a clear
208
+ `UsageError` instead of silently dropping the field. Previously `parse_ids("")`
209
+ returned `None` and the `if region_ids:` guards treated a provided-but-empty
210
+ value identically to an omitted option; for `RegionIds` (WSDL `minOccurs=1` on
211
+ add) that stripped a required field and sent an invalid body to the live API.
212
+ - The fix is centralized in a new `_require_nonempty_ids_option` helper that
213
+ distinguishes `None` (option omitted) from an all-blank value, so single mode
214
+ and `--from-file` / `--adgroups-json` batch mode behave identically for both
215
+ add and update. A genuinely malformed value with real tokens
216
+ (e.g. `225,,226`) still reports the precise `Invalid ID` error, unchanged.
217
+
3
218
  ## 0.4.2
4
219
 
5
220
  **BREAKING CHANGES - get requires SelectionCriteria (#498):**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Command-line interface for Yandex Direct API
5
5
  Author: axisrow
6
6
  License: MIT
@@ -543,6 +543,7 @@ direct ads add --adgroup-id 12345 --type MOBILE_APP_IMAGE_AD --image-hash abcdef
543
543
  direct ads add --adgroup-id 12345 --type SMART_AD_BUILDER_AD --logo-extension-hash logoabcdefghijklmnop --dry-run
544
544
  direct ads update --id 99999 --type TEXT_AD --title "New Title" --text "New text" --href "https://example.com"
545
545
  direct ads update --id 99999 --type TEXT_AD --image-hash abcdefghijklmnopqrst
546
+ direct ads update --id 99999 --type TEXT_AD --clear-image-hash # remove the image (AdImageHash: null; TEXT_AD / DYNAMIC_TEXT_AD / MOBILE_APP_AD only)
546
547
  direct ads update --id 99999 --type TEXT_AD --title2 "New second headline" --vcard-id 222
547
548
  direct ads update --id 99999 --type TEXT_AD --callouts-add "111,222" --callouts-remove "333"
548
549
  direct ads update --id 99999 --type TEXT_AD --callouts-set "444,555"
@@ -561,7 +562,10 @@ direct ads delete --id 99999
561
562
  ```
562
563
 
563
564
  Available TEXT_AD typed flags for `ads add` / `ads update`: `--title`, `--text`,
564
- `--href`, `--image-hash`, `--title2`, `--display-url-path`, `--vcard-id`,
565
+ `--href`, `--image-hash`, `--clear-image-hash` (update only — sets
566
+ `AdImageHash: null`; TEXT_AD / DYNAMIC_TEXT_AD / MOBILE_APP_AD only, since
567
+ TEXT_IMAGE_AD / MOBILE_APP_IMAGE_AD have a non-nillable `AdImageHash`),
568
+ `--title2`, `--display-url-path`, `--vcard-id`,
565
569
  `--sitelink-set-id`, `--turbo-page-id`, `--final-url`,
566
570
  `--video-extension-creative-id`, `--price-extension-*`, `--business-id`,
567
571
  `--prefer-vcard-over-business`, and `--erir-ad-description`. For `ads add`,
@@ -708,6 +712,9 @@ direct bidmodifiers set --id 99 --value 130 --dry-run
708
712
 
709
713
  # Canonical multiword groups
710
714
  direct negativekeywordsharedsets update --id 123 --keywords "foo,bar"
715
+ # audiencetargets get always needs a filter — the API rejects an empty
716
+ # SelectionCriteria, so there is no whole-account paging. To sweep the account,
717
+ # run `campaigns get` first, then page audiencetargets get in batches of campaign ids.
711
718
  direct audiencetargets get --campaign-ids 123 --fields Id,AdGroupId,RetargetingListId,State,ContextBid
712
719
  direct audiencetargets add --adgroup-id 100 --retargeting-list-id 200 --bid 12000000 --priority HIGH --dry-run
713
720
  direct audiencetargets set-bids --id 101 --context-bid 7000000 --priority LOW --dry-run
@@ -1371,6 +1378,7 @@ direct ads add --adgroup-id 12345 --type MOBILE_APP_IMAGE_AD --image-hash abcdef
1371
1378
  direct ads add --adgroup-id 12345 --type SMART_AD_BUILDER_AD --logo-extension-hash logoabcdefghijklmnop --dry-run
1372
1379
  direct ads update --id 99999 --type TEXT_AD --title "Новый заголовок" --text "Новый текст" --href "https://example.com"
1373
1380
  direct ads update --id 99999 --type TEXT_AD --image-hash abcdefghijklmnopqrst
1381
+ direct ads update --id 99999 --type TEXT_AD --clear-image-hash # удалить изображение (AdImageHash: null; только TEXT_AD / DYNAMIC_TEXT_AD / MOBILE_APP_AD)
1374
1382
  direct ads update --id 99999 --type TEXT_AD --title2 "Новый второй заголовок" --vcard-id 222
1375
1383
  direct ads update --id 99999 --type TEXT_AD --callouts-add "111,222" --callouts-remove "333"
1376
1384
  direct ads update --id 99999 --type TEXT_AD --callouts-set "444,555"
@@ -1389,7 +1397,10 @@ direct ads delete --id 99999
1389
1397
  ```
1390
1398
 
1391
1399
  Доступные типизированные флаги TEXT_AD для `ads add` / `ads update`:
1392
- `--title`, `--text`, `--href`, `--image-hash`, `--title2`, `--display-url-path`,
1400
+ `--title`, `--text`, `--href`, `--image-hash`, `--clear-image-hash`
1401
+ (только update — устанавливает `AdImageHash: null`; только TEXT_AD /
1402
+ DYNAMIC_TEXT_AD / MOBILE_APP_AD, так как у TEXT_IMAGE_AD / MOBILE_APP_IMAGE_AD
1403
+ поле `AdImageHash` не nillable), `--title2`, `--display-url-path`,
1393
1404
  `--vcard-id`, `--sitelink-set-id`, `--turbo-page-id`, `--final-url`,
1394
1405
  `--video-extension-creative-id`, `--price-extension-*`, `--business-id`,
1395
1406
  `--prefer-vcard-over-business` и `--erir-ad-description`. Для `ads add`
@@ -1538,6 +1549,9 @@ direct bidmodifiers set --id 99 --value 130 --dry-run
1538
1549
 
1539
1550
  # Канонические многословные группы
1540
1551
  direct negativekeywordsharedsets update --id 123 --keywords "foo,bar"
1552
+ # audiencetargets get всегда требует фильтр — API отклоняет пустой
1553
+ # SelectionCriteria, поэтому обхода всего аккаунта нет. Чтобы собрать аккаунт,
1554
+ # сначала выполните `campaigns get`, затем запрашивайте audiencetargets get батчами campaign id.
1541
1555
  direct audiencetargets get --campaign-ids 123 --fields Id,AdGroupId,RetargetingListId,State,ContextBid
1542
1556
  direct audiencetargets add --adgroup-id 100 --retargeting-list-id 200 --bid 12000000 --priority HIGH --dry-run
1543
1557
  direct audiencetargets set-bids --id 101 --context-bid 7000000 --priority LOW --dry-run
@@ -500,6 +500,7 @@ direct ads add --adgroup-id 12345 --type MOBILE_APP_IMAGE_AD --image-hash abcdef
500
500
  direct ads add --adgroup-id 12345 --type SMART_AD_BUILDER_AD --logo-extension-hash logoabcdefghijklmnop --dry-run
501
501
  direct ads update --id 99999 --type TEXT_AD --title "New Title" --text "New text" --href "https://example.com"
502
502
  direct ads update --id 99999 --type TEXT_AD --image-hash abcdefghijklmnopqrst
503
+ direct ads update --id 99999 --type TEXT_AD --clear-image-hash # remove the image (AdImageHash: null; TEXT_AD / DYNAMIC_TEXT_AD / MOBILE_APP_AD only)
503
504
  direct ads update --id 99999 --type TEXT_AD --title2 "New second headline" --vcard-id 222
504
505
  direct ads update --id 99999 --type TEXT_AD --callouts-add "111,222" --callouts-remove "333"
505
506
  direct ads update --id 99999 --type TEXT_AD --callouts-set "444,555"
@@ -518,7 +519,10 @@ direct ads delete --id 99999
518
519
  ```
519
520
 
520
521
  Available TEXT_AD typed flags for `ads add` / `ads update`: `--title`, `--text`,
521
- `--href`, `--image-hash`, `--title2`, `--display-url-path`, `--vcard-id`,
522
+ `--href`, `--image-hash`, `--clear-image-hash` (update only — sets
523
+ `AdImageHash: null`; TEXT_AD / DYNAMIC_TEXT_AD / MOBILE_APP_AD only, since
524
+ TEXT_IMAGE_AD / MOBILE_APP_IMAGE_AD have a non-nillable `AdImageHash`),
525
+ `--title2`, `--display-url-path`, `--vcard-id`,
522
526
  `--sitelink-set-id`, `--turbo-page-id`, `--final-url`,
523
527
  `--video-extension-creative-id`, `--price-extension-*`, `--business-id`,
524
528
  `--prefer-vcard-over-business`, and `--erir-ad-description`. For `ads add`,
@@ -665,6 +669,9 @@ direct bidmodifiers set --id 99 --value 130 --dry-run
665
669
 
666
670
  # Canonical multiword groups
667
671
  direct negativekeywordsharedsets update --id 123 --keywords "foo,bar"
672
+ # audiencetargets get always needs a filter — the API rejects an empty
673
+ # SelectionCriteria, so there is no whole-account paging. To sweep the account,
674
+ # run `campaigns get` first, then page audiencetargets get in batches of campaign ids.
668
675
  direct audiencetargets get --campaign-ids 123 --fields Id,AdGroupId,RetargetingListId,State,ContextBid
669
676
  direct audiencetargets add --adgroup-id 100 --retargeting-list-id 200 --bid 12000000 --priority HIGH --dry-run
670
677
  direct audiencetargets set-bids --id 101 --context-bid 7000000 --priority LOW --dry-run
@@ -1328,6 +1335,7 @@ direct ads add --adgroup-id 12345 --type MOBILE_APP_IMAGE_AD --image-hash abcdef
1328
1335
  direct ads add --adgroup-id 12345 --type SMART_AD_BUILDER_AD --logo-extension-hash logoabcdefghijklmnop --dry-run
1329
1336
  direct ads update --id 99999 --type TEXT_AD --title "Новый заголовок" --text "Новый текст" --href "https://example.com"
1330
1337
  direct ads update --id 99999 --type TEXT_AD --image-hash abcdefghijklmnopqrst
1338
+ direct ads update --id 99999 --type TEXT_AD --clear-image-hash # удалить изображение (AdImageHash: null; только TEXT_AD / DYNAMIC_TEXT_AD / MOBILE_APP_AD)
1331
1339
  direct ads update --id 99999 --type TEXT_AD --title2 "Новый второй заголовок" --vcard-id 222
1332
1340
  direct ads update --id 99999 --type TEXT_AD --callouts-add "111,222" --callouts-remove "333"
1333
1341
  direct ads update --id 99999 --type TEXT_AD --callouts-set "444,555"
@@ -1346,7 +1354,10 @@ direct ads delete --id 99999
1346
1354
  ```
1347
1355
 
1348
1356
  Доступные типизированные флаги TEXT_AD для `ads add` / `ads update`:
1349
- `--title`, `--text`, `--href`, `--image-hash`, `--title2`, `--display-url-path`,
1357
+ `--title`, `--text`, `--href`, `--image-hash`, `--clear-image-hash`
1358
+ (только update — устанавливает `AdImageHash: null`; только TEXT_AD /
1359
+ DYNAMIC_TEXT_AD / MOBILE_APP_AD, так как у TEXT_IMAGE_AD / MOBILE_APP_IMAGE_AD
1360
+ поле `AdImageHash` не nillable), `--title2`, `--display-url-path`,
1350
1361
  `--vcard-id`, `--sitelink-set-id`, `--turbo-page-id`, `--final-url`,
1351
1362
  `--video-extension-creative-id`, `--price-extension-*`, `--business-id`,
1352
1363
  `--prefer-vcard-over-business` и `--erir-ad-description`. Для `ads add`
@@ -1495,6 +1506,9 @@ direct bidmodifiers set --id 99 --value 130 --dry-run
1495
1506
 
1496
1507
  # Канонические многословные группы
1497
1508
  direct negativekeywordsharedsets update --id 123 --keywords "foo,bar"
1509
+ # audiencetargets get всегда требует фильтр — API отклоняет пустой
1510
+ # SelectionCriteria, поэтому обхода всего аккаунта нет. Чтобы собрать аккаунт,
1511
+ # сначала выполните `campaigns get`, затем запрашивайте audiencetargets get батчами campaign id.
1498
1512
  direct audiencetargets get --campaign-ids 123 --fields Id,AdGroupId,RetargetingListId,State,ContextBid
1499
1513
  direct audiencetargets add --adgroup-id 100 --retargeting-list-id 200 --bid 12000000 --priority HIGH --dry-run
1500
1514
  direct audiencetargets set-bids --id 101 --context-bid 7000000 --priority LOW --dry-run
@@ -76,14 +76,20 @@ RESOURCE_MAPPING_V5 = {
76
76
  "docs": "https://yandex.ru/dev/direct/doc/ru/dictionaries/dictionaries",
77
77
  "methods": ["get"],
78
78
  },
79
+ # Yandex removed the human-readable doc pages for DynamicTextAdTargets,
80
+ # DynamicFeedAdTargets, SmartAdTargets and VCards in September 2025 (the
81
+ # /ru/ and /en/ pages now 404, and the services are gone from the docs
82
+ # navigation). The services themselves remain live, so `docs` points at the
83
+ # WSDL endpoint — the only authoritative source still served for them. See
84
+ # issue #463.
79
85
  "dynamicads": {
80
86
  "resource": "json/v5/dynamictextadtargets",
81
- "docs": "https://yandex.ru/dev/direct/doc/ru/dynamictextadtargets",
87
+ "docs": "https://api.direct.yandex.com/v5/dynamictextadtargets?wsdl",
82
88
  "methods": ["get", "add", "delete", "suspend", "resume", "setBids"],
83
89
  },
84
90
  "dynamicfeedadtargets": {
85
91
  "resource": "json/v5/dynamicfeedadtargets",
86
- "docs": "https://yandex.ru/dev/direct/doc/ru/dynamicfeedadtargets",
92
+ "docs": "https://api.direct.yandex.com/v5/dynamicfeedadtargets?wsdl",
87
93
  "methods": ["get", "add", "delete", "suspend", "resume", "setBids"],
88
94
  },
89
95
  "keywordbids": {
@@ -118,7 +124,8 @@ RESOURCE_MAPPING_V5 = {
118
124
  },
119
125
  "vcards": {
120
126
  "resource": "json/v5/vcards",
121
- "docs": "https://yandex.ru/dev/direct/doc/ru/vcards",
127
+ # Doc page removed Sep 2025 (see comment above); WSDL still live.
128
+ "docs": "https://api.direct.yandex.com/v5/vcards?wsdl",
122
129
  "methods": ["get", "add", "delete"],
123
130
  },
124
131
  "turbopages": {
@@ -154,7 +161,8 @@ RESOURCE_MAPPING_V5 = {
154
161
  },
155
162
  "smartadtargets": {
156
163
  "resource": "json/v5/smartadtargets",
157
- "docs": "https://yandex.ru/dev/direct/doc/ru/smartadtargets",
164
+ # Doc page removed Sep 2025 (see comment above); WSDL still live.
165
+ "docs": "https://api.direct.yandex.com/v5/smartadtargets?wsdl",
158
166
  "methods": ["get", "add", "update", "delete", "suspend", "resume", "setBids"],
159
167
  },
160
168
  "strategies": {
@@ -0,0 +1,181 @@
1
+ """Shared JSONL/inline batch engine for multi-item ``add``/``update`` commands.
2
+
3
+ Extracted from the ``keywords add`` batch machinery (issue #562) so ``ads`` and
4
+ ``adgroups`` reuse one loader/chunker/sender instead of duplicating it. Only the
5
+ resource-specific pieces (the row normalizer and any overflow warning) stay in
6
+ the command module and are passed in.
7
+
8
+ Message strings are NOT hardcoded with a resource name: ``load_inline_rows`` and
9
+ ``send_batch`` take the catalog keys / nouns from the caller, so each command
10
+ keeps its own (already-translated) wording byte-identical.
11
+ """
12
+
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Any, Callable, Iterator, List, Optional
16
+
17
+ import click
18
+
19
+ from ..api import client_from_ctx
20
+ from ..i18n import t
21
+ from ..output import (
22
+ format_json,
23
+ format_output,
24
+ print_error,
25
+ raise_for_api_result_errors,
26
+ )
27
+
28
+
29
+ def load_jsonl_rows(path: str) -> List[Any]:
30
+ """Read a JSONL file into a list of decoded rows (one JSON value per line).
31
+
32
+ Blank lines are skipped. A read error or a malformed line raises a
33
+ ``click.UsageError`` with the same catalog keys ``keywords`` used.
34
+ """
35
+ rows: List[Any] = []
36
+ file_path = Path(path)
37
+ try:
38
+ text = file_path.read_text(encoding="utf-8")
39
+ except OSError as exc:
40
+ raise click.UsageError(
41
+ t("Cannot read --from-file {path!r}: {exc}").format(path=path, exc=exc)
42
+ )
43
+
44
+ for line_number, raw_line in enumerate(text.splitlines(), start=1):
45
+ line = raw_line.strip()
46
+ if not line:
47
+ continue
48
+ try:
49
+ rows.append(json.loads(line))
50
+ except json.JSONDecodeError as exc:
51
+ raise click.UsageError(
52
+ t("Row {line_number}: invalid JSON: {arg0}").format(
53
+ line_number=line_number, arg0=exc.msg
54
+ )
55
+ )
56
+ return rows
57
+
58
+
59
+ def load_inline_rows(
60
+ json_str: str,
61
+ *,
62
+ invalid_json_key: str,
63
+ not_array_key: str,
64
+ ) -> List[Any]:
65
+ """Parse an inline JSON array of rows.
66
+
67
+ ``invalid_json_key`` / ``not_array_key`` are the EN catalog keys for the two
68
+ error cases, so each command keeps its own ``--<resource>-json`` wording
69
+ (and its existing RU translation) unchanged.
70
+ """
71
+ try:
72
+ decoded = json.loads(json_str)
73
+ except json.JSONDecodeError as exc:
74
+ raise click.UsageError(t(invalid_json_key).format(arg0=exc.msg))
75
+ if not isinstance(decoded, list):
76
+ raise click.UsageError(t(not_array_key))
77
+ return decoded
78
+
79
+
80
+ def chunked(items: List[Any], size: int) -> Iterator[List[Any]]:
81
+ for start in range(0, len(items), size):
82
+ yield items[start : start + size]
83
+
84
+
85
+ def normalize_results(raw: Any, result_key: str) -> List[Any]:
86
+ """Unwrap the per-item result list from an ``add``/``update`` response.
87
+
88
+ ``result_key`` is ``"AddResults"`` for ``add`` and ``"UpdateResults"`` for
89
+ ``update`` — Yandex names the list after the method.
90
+ """
91
+ if isinstance(raw, dict):
92
+ results = raw.get(result_key)
93
+ if isinstance(results, list):
94
+ return results
95
+ return [raw]
96
+ if isinstance(raw, list):
97
+ return raw
98
+ return [raw]
99
+
100
+
101
+ def send_batch(
102
+ ctx,
103
+ *,
104
+ resource: str,
105
+ method: str,
106
+ payload_key: str,
107
+ items: List[Any],
108
+ max_batch: int,
109
+ create_client: Callable,
110
+ dry_run: bool,
111
+ noun: str,
112
+ result_key: str = "AddResults",
113
+ on_warn: Optional[Callable[[List[Any]], None]] = None,
114
+ post: Optional[Callable[[Any, dict], Any]] = None,
115
+ ) -> None:
116
+ """Chunk ``items`` and send each chunk through ``client.<resource>()``.
117
+
118
+ ``--dry-run`` prints the chunk preview and returns. Otherwise each chunk is
119
+ posted in a loop; on failure any already-created items are reported (so a
120
+ retry does not silently duplicate them). ``noun`` is the plural object name
121
+ used in that partial-success message (e.g. ``"keywords"``, ``"ads"``).
122
+ ``result_key`` is the response list name (``"AddResults"`` for ``add``,
123
+ ``"UpdateResults"`` for ``update``).
124
+
125
+ ``post`` overrides how a chunk body is sent: a ``(client, body) -> response``
126
+ callable. It defaults to ``client.<resource>().post(data=body)``; ``adgroups``
127
+ passes its endpoint-routing ``_post_adgroups`` (UnifiedAdGroup payloads must
128
+ use API v501).
129
+ """
130
+ chunks = list(chunked(items, max_batch))
131
+
132
+ if on_warn is not None:
133
+ on_warn(items)
134
+
135
+ if dry_run:
136
+ preview = {
137
+ "chunks": len(chunks),
138
+ "totalItems": len(items),
139
+ "chunkSize": max_batch,
140
+ "firstChunk": {"method": method, "params": {payload_key: chunks[0]}},
141
+ }
142
+ print(format_json(preview, indent=2))
143
+ return
144
+
145
+ all_results: List[Any] = []
146
+ try:
147
+ client = client_from_ctx(ctx, create_client)
148
+
149
+ for index, chunk in enumerate(chunks, start=1):
150
+ click.echo(
151
+ f"Sending chunk {index}/{len(chunks)}: {len(chunk)} items",
152
+ err=True,
153
+ )
154
+ body = {"method": method, "params": {payload_key: chunk}}
155
+ if post is not None:
156
+ response = post(client, body)
157
+ else:
158
+ response = getattr(client, resource)().post(data=body)
159
+ chunk_results = normalize_results(response().extract(), result_key)
160
+ # Only items without per-item Errors are "already applied" — the
161
+ # partial-success diagnostic must not lie about failed items.
162
+ all_results.extend(
163
+ item
164
+ for item in chunk_results
165
+ if not (isinstance(item, dict) and item.get("Errors"))
166
+ )
167
+ raise_for_api_result_errors(chunk_results)
168
+
169
+ format_output({result_key: all_results}, "json", None)
170
+ except click.UsageError:
171
+ raise
172
+ except Exception as e:
173
+ if all_results:
174
+ click.echo(
175
+ f"Partial success before failure — these {noun} were already "
176
+ "applied in Yandex Direct (retrying may duplicate them):",
177
+ err=True,
178
+ )
179
+ click.echo(format_json({result_key: all_results}, indent=2), err=True)
180
+ print_error(str(e))
181
+ raise click.Abort()
@@ -68,8 +68,15 @@ def make_lifecycle_command(
68
68
  ``@group.command`` registration sticks).
69
69
  """
70
70
 
71
+ # A lifecycle id is always a positive object id; type=int used to accept 0
72
+ # and negatives and forward them to the API (opaque rejection). For the
73
+ # integer path, validate min=1 before the request (issue #558). The
74
+ # ad-image path keeps id_type=str (a hash is not an integer) and is
75
+ # untouched.
76
+ option_type = click.IntRange(min=1) if id_type is int else id_type
77
+
71
78
  @group.command(name=method, help=help_text)
72
- @click.option(id_option, id_param, required=True, type=id_type, help=id_help)
79
+ @click.option(id_option, id_param, required=True, type=option_type, help=id_help)
73
80
  @click.option("--dry-run", is_flag=True, help="Show request without sending")
74
81
  @click.pass_context
75
82
  @handle_api_errors