direct-cli 0.4.1__tar.gz → 0.4.2__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 (365) hide show
  1. {direct_cli-0.4.1 → direct_cli-0.4.2}/CHANGELOG.md +43 -0
  2. {direct_cli-0.4.1 → direct_cli-0.4.2}/CLAUDE.md +3 -3
  3. {direct_cli-0.4.1 → direct_cli-0.4.2}/PKG-INFO +30 -21
  4. {direct_cli-0.4.1 → direct_cli-0.4.2}/README.md +29 -20
  5. direct_cli-0.4.2/direct_cli/_autotargeting.py +206 -0
  6. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_bidding_strategy.py +876 -882
  7. direct_cli-0.4.2/direct_cli/_flag_validation.py +53 -0
  8. direct_cli-0.4.2/direct_cli/_smoke_probes.py +173 -0
  9. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +1 -1
  10. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +4 -2
  11. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/api.py +23 -0
  12. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/auth.py +179 -68
  13. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/cli.py +10 -1
  14. direct_cli-0.4.2/direct_cli/commands/_lifecycle.py +92 -0
  15. direct_cli-0.4.2/direct_cli/commands/adextensions.py +124 -0
  16. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/adgroups.py +257 -434
  17. direct_cli-0.4.2/direct_cli/commands/adimages.py +122 -0
  18. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/ads.py +497 -712
  19. direct_cli-0.4.2/direct_cli/commands/advideos.py +96 -0
  20. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/agencyclients.py +144 -205
  21. direct_cli-0.4.2/direct_cli/commands/audiencetargets.py +198 -0
  22. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/auth.py +139 -7
  23. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/balance.py +10 -15
  24. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/bidmodifiers.py +166 -245
  25. direct_cli-0.4.2/direct_cli/commands/bids.py +226 -0
  26. direct_cli-0.4.2/direct_cli/commands/businesses.py +57 -0
  27. direct_cli-0.4.2/direct_cli/commands/campaigns.py +7205 -0
  28. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/changes.py +30 -49
  29. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/clients.py +93 -126
  30. direct_cli-0.4.2/direct_cli/commands/creatives.py +150 -0
  31. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/dictionaries.py +26 -42
  32. direct_cli-0.4.2/direct_cli/commands/dynamicads.py +189 -0
  33. direct_cli-0.4.2/direct_cli/commands/dynamicfeedadtargets.py +190 -0
  34. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/feeds.py +132 -187
  35. direct_cli-0.4.2/direct_cli/commands/keywordbids.py +299 -0
  36. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/keywords.py +145 -393
  37. direct_cli-0.4.2/direct_cli/commands/keywordsresearch.py +68 -0
  38. direct_cli-0.4.2/direct_cli/commands/leads.py +80 -0
  39. direct_cli-0.4.2/direct_cli/commands/negativekeywordsharedsets.py +128 -0
  40. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/reports.py +63 -67
  41. direct_cli-0.4.2/direct_cli/commands/retargeting.py +210 -0
  42. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/sitelinks.py +67 -105
  43. direct_cli-0.4.2/direct_cli/commands/smartadtargets.py +281 -0
  44. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/strategies.py +192 -288
  45. direct_cli-0.4.2/direct_cli/commands/turbopages.py +67 -0
  46. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4account.py +6 -77
  47. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4adimage.py +3 -25
  48. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4events.py +4 -21
  49. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4finance.py +33 -99
  50. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4forecast.py +7 -47
  51. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4goals.py +5 -48
  52. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4keywords.py +4 -21
  53. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4tags.py +5 -41
  54. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4wordstat.py +9 -47
  55. direct_cli-0.4.2/direct_cli/commands/vcards.py +245 -0
  56. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/output.py +26 -0
  57. direct_cli-0.4.2/direct_cli/translations/auth.json +25 -0
  58. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/common.json +2 -2
  59. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/utils.py +96 -6
  60. direct_cli-0.4.2/direct_cli/v4/emit.py +79 -0
  61. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/v4/money.py +8 -27
  62. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli.egg-info/PKG-INFO +30 -21
  63. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli.egg-info/SOURCES.txt +13 -0
  64. {direct_cli-0.4.1 → direct_cli-0.4.2}/pyproject.toml +1 -1
  65. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/build_api_coverage_report.py +11 -0
  66. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/patch_vendor_imports.py +109 -2
  67. direct_cli-0.4.2/tests/MANUAL_COVERAGE.md +147 -0
  68. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/api_coverage_payloads.py +1 -0
  69. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[creatives_get].yaml +2 -2
  70. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[retargeting_get].yaml +2 -2
  71. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[strategies_get].yaml +2 -2
  72. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_add_update_delete.yaml +64 -64
  73. direct_cli-0.4.2/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_add_delete.yaml +560 -0
  74. direct_cli-0.4.2/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_suspend_resume.yaml +622 -0
  75. direct_cli-0.4.2/tests/cassettes/test_v5_live_write/test_v5_live_draft_feeds_add_update_delete.yaml +250 -0
  76. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_add_update_delete.yaml +122 -60
  77. direct_cli-0.4.1/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_suspend_resume.yaml → direct_cli-0.4.2/tests/cassettes/test_v5_live_write/test_v5_live_draft_retargeting_add_update_delete.yaml +40 -103
  78. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_add_update_delete.yaml +24 -24
  79. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_suspend_resume.yaml +24 -24
  80. direct_cli-0.4.1/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_add_delete.yaml → direct_cli-0.4.2/tests/cassettes/test_v5_live_write/test_v5_live_draft_strategies_add_update_archive_unarchive.yaml +49 -50
  81. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/conftest.py +77 -6
  82. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_auth_bw.py +30 -0
  83. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_auth_oauth.py +293 -5
  84. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_auth_op.py +30 -0
  85. direct_cli-0.4.2/tests/test_autotargeting.py +228 -0
  86. direct_cli-0.4.2/tests/test_bidding_strategy_constants.py +197 -0
  87. direct_cli-0.4.2/tests/test_cassette_integrity.py +87 -0
  88. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_cli.py +4 -0
  89. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_dry_run.py +123 -20
  90. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_env_loading.py +54 -0
  91. direct_cli-0.4.2/tests/test_field_names_option.py +85 -0
  92. direct_cli-0.4.2/tests/test_flag_validation.py +130 -0
  93. direct_cli-0.4.2/tests/test_handle_api_errors.py +101 -0
  94. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_integration.py +12 -14
  95. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_low_coverage_payloads.py +71 -0
  96. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_read_cassettes.py +6 -2
  97. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_transport_contract.py +20 -10
  98. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4_exit_codes.py +9 -9
  99. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4account.py +26 -26
  100. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4adimage.py +2 -2
  101. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4events.py +4 -4
  102. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4finance_money.py +42 -10
  103. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4finance_read.py +9 -9
  104. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4forecast.py +6 -6
  105. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4goals.py +6 -6
  106. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4keywords.py +2 -2
  107. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4tags.py +4 -4
  108. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4wordstat.py +7 -7
  109. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v5_live_write.py +218 -7
  110. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_vendor_imports.py +119 -0
  111. direct_cli-0.4.1/direct_cli/_smoke_probes.py +0 -94
  112. direct_cli-0.4.1/direct_cli/commands/adextensions.py +0 -164
  113. direct_cli-0.4.1/direct_cli/commands/adimages.py +0 -158
  114. direct_cli-0.4.1/direct_cli/commands/advideos.py +0 -119
  115. direct_cli-0.4.1/direct_cli/commands/audiencetargets.py +0 -306
  116. direct_cli-0.4.1/direct_cli/commands/bids.py +0 -262
  117. direct_cli-0.4.1/direct_cli/commands/businesses.py +0 -67
  118. direct_cli-0.4.1/direct_cli/commands/campaigns.py +0 -7610
  119. direct_cli-0.4.1/direct_cli/commands/creatives.py +0 -173
  120. direct_cli-0.4.1/direct_cli/commands/dynamicads.py +0 -296
  121. direct_cli-0.4.1/direct_cli/commands/dynamicfeedadtargets.py +0 -299
  122. direct_cli-0.4.1/direct_cli/commands/keywordbids.py +0 -331
  123. direct_cli-0.4.1/direct_cli/commands/keywordsresearch.py +0 -88
  124. direct_cli-0.4.1/direct_cli/commands/leads.py +0 -84
  125. direct_cli-0.4.1/direct_cli/commands/negativekeywordsharedsets.py +0 -180
  126. direct_cli-0.4.1/direct_cli/commands/retargeting.py +0 -244
  127. direct_cli-0.4.1/direct_cli/commands/smartadtargets.py +0 -397
  128. direct_cli-0.4.1/direct_cli/commands/turbopages.py +0 -79
  129. direct_cli-0.4.1/direct_cli/commands/vcards.py +0 -289
  130. direct_cli-0.4.1/direct_cli/translations/auth.json +0 -23
  131. direct_cli-0.4.1/tests/MANUAL_COVERAGE.md +0 -92
  132. {direct_cli-0.4.1 → direct_cli-0.4.2}/.env.example +0 -0
  133. {direct_cli-0.4.1 → direct_cli-0.4.2}/.github/copilot-instructions.md +0 -0
  134. {direct_cli-0.4.1 → direct_cli-0.4.2}/.github/workflows/api-coverage.yml +0 -0
  135. {direct_cli-0.4.1 → direct_cli-0.4.2}/.github/workflows/claude.yml +0 -0
  136. {direct_cli-0.4.1 → direct_cli-0.4.2}/.github/workflows/quality.yml +0 -0
  137. {direct_cli-0.4.1 → direct_cli-0.4.2}/.gitignore +0 -0
  138. {direct_cli-0.4.1 → direct_cli-0.4.2}/AGENTS.md +0 -0
  139. {direct_cli-0.4.1 → direct_cli-0.4.2}/MANIFEST.in +0 -0
  140. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/__init__.py +0 -0
  141. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_deprecated.py +0 -0
  142. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/__init__.py +0 -0
  143. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
  144. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
  145. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
  146. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
  147. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
  148. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
  149. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
  150. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
  151. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/__init__.py +0 -0
  152. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/commands/v4shells.py +0 -0
  153. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/i18n.py +0 -0
  154. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/reports_coverage.py +0 -0
  155. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/smoke_matrix.py +0 -0
  156. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/adextensions.json +0 -0
  157. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/adgroups.json +0 -0
  158. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/adimages.json +0 -0
  159. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/ads.json +0 -0
  160. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/advideos.json +0 -0
  161. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/agencyclients.json +0 -0
  162. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/audiencetargets.json +0 -0
  163. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/balance.json +0 -0
  164. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/bidmodifiers.json +0 -0
  165. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/bids.json +0 -0
  166. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/businesses.json +0 -0
  167. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/campaigns.json +0 -0
  168. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/changes.json +0 -0
  169. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/clients.json +0 -0
  170. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/creatives.json +0 -0
  171. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/dictionaries.json +0 -0
  172. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/dynamicads.json +0 -0
  173. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/dynamicfeedadtargets.json +0 -0
  174. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/feeds.json +0 -0
  175. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/keywordbids.json +0 -0
  176. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/keywords.json +0 -0
  177. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/keywordsresearch.json +0 -0
  178. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/leads.json +0 -0
  179. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/negativekeywordsharedsets.json +0 -0
  180. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/reports.json +0 -0
  181. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/retargeting.json +0 -0
  182. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/sitelinks.json +0 -0
  183. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/smartadtargets.json +0 -0
  184. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/strategies.json +0 -0
  185. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/turbopages.json +0 -0
  186. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4account.json +0 -0
  187. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4adimage.json +0 -0
  188. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4events.json +0 -0
  189. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4finance.json +0 -0
  190. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4forecast.json +0 -0
  191. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4goals.json +0 -0
  192. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4keywords.json +0 -0
  193. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4shells.json +0 -0
  194. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4tags.json +0 -0
  195. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/v4wordstat.json +0 -0
  196. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/translations/vcards.json +0 -0
  197. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/v4/__init__.py +0 -0
  198. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/v4_contracts.py +0 -0
  199. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli/wsdl_coverage.py +0 -0
  200. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli.egg-info/dependency_links.txt +0 -0
  201. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli.egg-info/entry_points.txt +0 -0
  202. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli.egg-info/requires.txt +0 -0
  203. {direct_cli-0.4.1 → direct_cli-0.4.2}/direct_cli.egg-info/top_level.txt +0 -0
  204. {direct_cli-0.4.1 → direct_cli-0.4.2}/docs/audits/API_COVERAGE.md +0 -0
  205. {direct_cli-0.4.1 → direct_cli-0.4.2}/docs/audits/PROJECT_WIRE_SHAPE_AUDIT_2026-05-30.md +0 -0
  206. {direct_cli-0.4.1 → direct_cli-0.4.2}/docs/audits/WIRE_SHAPE_TRIAGE_2026-05-30.md +0 -0
  207. {direct_cli-0.4.1 → direct_cli-0.4.2}/docs/audits/issue-198-mutating-wsdl-audit.md +0 -0
  208. {direct_cli-0.4.1 → direct_cli-0.4.2}/docs/audits/wire_shape.json +0 -0
  209. {direct_cli-0.4.1 → direct_cli-0.4.2}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
  210. {direct_cli-0.4.1 → direct_cli-0.4.2}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
  211. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/anonymize_cassettes.py +0 -0
  212. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/audit_wire_shape.py +0 -0
  213. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/build_api_coverage_checklist.py +0 -0
  214. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/build_wsdl_optional_field_audit.py +0 -0
  215. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/check_all_docs_urls.py +0 -0
  216. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/check_reports_drift.py +0 -0
  217. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/check_wsdl_drift.py +0 -0
  218. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/preflight_check.sh +0 -0
  219. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/probe_drift_urls.sh +0 -0
  220. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/refresh_reports_cache.py +0 -0
  221. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/refresh_wsdl_cache.py +0 -0
  222. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/release_pypi.sh +0 -0
  223. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/sandbox_write_audit.py +0 -0
  224. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/sandbox_write_live.py +0 -0
  225. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/test_dangerous_commands.sh +0 -0
  226. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/test_safe_commands.sh +0 -0
  227. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/test_sandbox_write.sh +0 -0
  228. {direct_cli-0.4.1 → direct_cli-0.4.2}/scripts/update_vendor.sh +0 -0
  229. {direct_cli-0.4.1 → direct_cli-0.4.2}/setup.cfg +0 -0
  230. {direct_cli-0.4.1 → direct_cli-0.4.2}/setup.py +0 -0
  231. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/API_COVERAGE.md +0 -0
  232. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/API_ISSUE_AUDIT.md +0 -0
  233. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/WSDL_OPTIONAL_FIELD_AUDIT.md +0 -0
  234. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/__init__.py +0 -0
  235. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/_orphan_store.py +0 -0
  236. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
  237. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
  238. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
  239. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
  240. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
  241. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
  242. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
  243. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
  244. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +0 -0
  245. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +0 -0
  246. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
  247. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
  248. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
  249. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +0 -0
  250. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
  251. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
  252. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
  253. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
  254. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteRetargetingUpdate.test_retargeting_update.yaml +0 -0
  255. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
  256. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
  257. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +0 -0
  258. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
  259. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[adextensions_get].yaml +0 -0
  260. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[adgroups_get].yaml +0 -0
  261. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[adimages_get].yaml +0 -0
  262. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[ads_get].yaml +0 -0
  263. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[advideos_get].yaml +0 -0
  264. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[audiencetargets_get].yaml +0 -0
  265. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[bidmodifiers_get].yaml +0 -0
  266. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[bids_get].yaml +0 -0
  267. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[businesses_get].yaml +0 -0
  268. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[campaigns_get].yaml +0 -0
  269. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[changes_check].yaml +0 -0
  270. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[changes_check_campaigns].yaml +0 -0
  271. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[changes_check_dictionaries].yaml +0 -0
  272. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[clients_get].yaml +0 -0
  273. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[dictionaries_get].yaml +0 -0
  274. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[dynamicads_get].yaml +0 -0
  275. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[dynamicfeedadtargets_get].yaml +0 -0
  276. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[feeds_get].yaml +0 -0
  277. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[keywordbids_get].yaml +0 -0
  278. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[keywords_get].yaml +0 -0
  279. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_deduplicate].yaml +0 -0
  280. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_has_search_volume].yaml +0 -0
  281. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[leads_get].yaml +0 -0
  282. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[negativekeywordsharedsets_get].yaml +0 -0
  283. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[reports_get].yaml +0 -0
  284. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[sitelinks_get].yaml +0 -0
  285. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[smartadtargets_get].yaml +0 -0
  286. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[turbopages_get].yaml +0 -0
  287. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[v4events_get_events_log].yaml +0 -0
  288. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[v4finance_get_clients_units].yaml +0 -0
  289. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[v4forecast_list].yaml +0 -0
  290. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[v4goals_get_stat_goals].yaml +0 -0
  291. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[v4tags_get_campaigns].yaml +0 -0
  292. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[v4wordstat_list_reports].yaml +0 -0
  293. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_read_command[vcards_get].yaml +0 -0
  294. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_read_cassettes/test_v4finance_check_payment_unknown_transaction.yaml +0 -0
  295. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_adgroups_add_update_delete.yaml +0 -0
  296. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_adimages_add_get_delete.yaml +0 -0
  297. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
  298. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_advideos_add_get.yaml +0 -0
  299. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_bids_set.yaml +0 -0
  300. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_campaign_create_get_delete.yaml +0 -0
  301. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
  302. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_add_delete.yaml +0 -0
  303. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_suspend_resume.yaml +0 -0
  304. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywordbids_set.yaml +0 -0
  305. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_suspend_resume.yaml +0 -0
  306. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/cassettes/test_v5_live_write/test_v5_live_draft_sitelinks_add_get_delete.yaml +0 -0
  307. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/fixtures/test-video.mp4 +0 -0
  308. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/reports_cache/raw/fields-list.html +0 -0
  309. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/reports_cache/raw/headers.html +0 -0
  310. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/reports_cache/raw/period.html +0 -0
  311. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/reports_cache/raw/spec.html +0 -0
  312. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/reports_cache/raw/type.html +0 -0
  313. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/reports_cache/spec.json +0 -0
  314. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_api_coverage.py +0 -0
  315. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_audit_wire_shape.py +0 -0
  316. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_auth_write_json.py +0 -0
  317. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_balance.py +0 -0
  318. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_changes.py +0 -0
  319. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_cli_contract.py +0 -0
  320. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_comprehensive.py +0 -0
  321. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_i18n.py +0 -0
  322. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_integration_write.py +0 -0
  323. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_reports_drift.py +0 -0
  324. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_reports_parsing.py +0 -0
  325. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_sandbox_write_audit.py +0 -0
  326. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_smoke_matrix.py +0 -0
  327. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_unknown_option_hints.py +0 -0
  328. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4_contracts.py +0 -0
  329. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4_foundation.py +0 -0
  330. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4_live_contracts.py +0 -0
  331. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4_runtime_shape.py +0 -0
  332. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_v4_safety.py +0 -0
  333. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/test_wsdl_parity_gate.py +0 -0
  334. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/adextensions.xml +0 -0
  335. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/adgroups.xml +0 -0
  336. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/adimages.xml +0 -0
  337. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/ads.xml +0 -0
  338. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/advideos.xml +0 -0
  339. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/agencyclients.xml +0 -0
  340. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/audiencetargets.xml +0 -0
  341. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/bidmodifiers.xml +0 -0
  342. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/bids.xml +0 -0
  343. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/businesses.xml +0 -0
  344. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/campaigns.xml +0 -0
  345. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/changes.xml +0 -0
  346. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/clients.xml +0 -0
  347. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/creatives.xml +0 -0
  348. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/dictionaries.xml +0 -0
  349. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
  350. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
  351. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/feeds.xml +0 -0
  352. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
  353. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/imports/general.xsd +0 -0
  354. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
  355. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/keywordbids.xml +0 -0
  356. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/keywords.xml +0 -0
  357. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/keywordsresearch.xml +0 -0
  358. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/leads.xml +0 -0
  359. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
  360. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/retargetinglists.xml +0 -0
  361. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/sitelinks.xml +0 -0
  362. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/smartadtargets.xml +0 -0
  363. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/strategies.xml +0 -0
  364. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/turbopages.xml +0 -0
  365. {direct_cli-0.4.1 → direct_cli-0.4.2}/tests/wsdl_cache/vcards.xml +0 -0
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2
4
+
5
+ **BREAKING CHANGES - get requires SelectionCriteria (#498):**
6
+
7
+ - `adgroups` / `ads` / `keywords` / `strategies` / `creatives` / `dynamicads` /
8
+ `smartadtargets` / `audiencetargets` `get` now refuse an empty
9
+ `SelectionCriteria` before the API call, raising a `UsageError` that asks for
10
+ at least one filter — instead of sending `{"SelectionCriteria": {}}` (which the
11
+ API rejects with the opaque error 4001 for ad-group/ad/keyword resources).
12
+ Extends the same guard already shipped for `bids` / `keywordbids` in 0.4.1
13
+ (#483). WSDL declares `GetRequest.SelectionCriteria` as `minOccurs=1` for all
14
+ eight resources.
15
+ - `retargeting get` gains `--dry-run` and the shared read/pagination option
16
+ stack; its `SelectionCriteria` stays optional (WSDL `minOccurs=0`), so a
17
+ no-filter call is still valid and now omits the empty criteria from the
18
+ payload.
19
+ - All eight commands and `retargeting get` build their request via the shared
20
+ `build_common_params` helper, completing the dedup epic #491 (B3c).
21
+
22
+ **BREAKING CHANGES - auth precedence (#489):**
23
+
24
+ - Base `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` credentials from the
25
+ environment or current-directory `.env` now win over the active OAuth profile
26
+ selected by `direct auth use` when `--profile` is not passed. Explicit
27
+ `--token`, `--login`, and `--profile` still take priority.
28
+ - `direct auth status` reports the selected effective credentials, including
29
+ base env/`.env` and secret-manager fallbacks, instead of reporting only the
30
+ active OAuth profile.
31
+ - `direct auth login` can now ask interactive users whether to save the OAuth
32
+ access token and resolved login into the current-directory `.env`; the default
33
+ answer is no.
34
+
35
+ **Docs — live-write coverage limitation (#538):**
36
+
37
+ - Documented why the SMART_CAMPAIGN / DYNAMIC_TEXT_CAMPAIGN / `adimages`
38
+ live-write lifecycle (`dynamicads`, `smartadtargets`, `adimages`) stays
39
+ recorded only as 3500/5004 error cassettes. Verified via direct API calls that
40
+ the available sandbox **agency** account has no client accounts under it
41
+ (`agencyclients.get` → empty), cannot create one (3001 "No rights to create
42
+ clients", access by request only), and that without a client login every
43
+ agency-scoped mutation returns 8000. Closed #538 as a documented account-tier
44
+ limitation; no CLI code change. See `tests/MANUAL_COVERAGE.md`.
45
+
3
46
  ## 0.4.1
4
47
 
5
48
  Russian-default CLI localization across all command modules (epic #466).
@@ -24,9 +24,9 @@ Click group-of-groups. Each Yandex Direct API resource = one file in `direct_cli
24
24
 
25
25
  **Request flow:** `cli.py` → `auth.py` (resolves token/login) → `api.py` (`create_client`) → `tapi_yandex_direct.YandexDirect` → Yandex API → `output.py` (format/print).
26
26
 
27
- **Credentials priority (CLI):** CLI flags (`--token`, `--login`) > active profile from `direct auth login` > env vars (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN`) > `.env` file > 1Password/Bitwarden refs. See `direct_cli/auth.py:600` (`get_credentials`) and README table for the full chain. `load_dotenv()` runs at `cli.py` import time.
27
+ **Credentials priority (CLI):** explicit CLI flags (`--token`, `--login`, `--profile`) > base env/current working directory `.env` (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN`) > active profile from `direct auth login` / `direct auth use` > 1Password/Bitwarden refs. Explicit `--profile` is isolated and does not fall back to base `.env` login. See `direct_cli/auth.py` (`get_credentials`) and README table for the full chain.
28
28
 
29
- **Credentials priority (tests):** **inverted** env vars > active profile > skip. Tests must not silently hit production when a developer has an active `direct auth` profile, so env vars take precedence over the profile (see `tests/test_v4_live_contracts.py::_credentials`).
29
+ **Credentials priority (tests):** env vars/current working directory `.env` > active profile > skip. Tests must not silently hit production when a developer has an active `direct auth` profile, so env vars take precedence over the profile (see `tests/test_v4_live_contracts.py::_credentials`).
30
30
 
31
31
  **Shared utilities** (`utils.py`): `parse_ids`, `parse_json`, `build_selection_criteria`, `build_common_params`, `get_default_fields`, `COMMON_FIELDS` dict. All command modules import from here — don't duplicate.
32
32
 
@@ -106,7 +106,7 @@ Builds dist artifacts, runs twine checks, uploads to TestPyPI + PyPI. Does **not
106
106
 
107
107
  - **Unit** (`test_cli.py`, `test_comprehensive.py`) — no API calls, no token needed.
108
108
  - **Integration** (`test_integration.py`, `@pytest.mark.integration`) — require `.env` with `YANDEX_DIRECT_TOKEN` and `YANDEX_DIRECT_LOGIN`. Auto-skip if absent.
109
- - **Credential resolution in tests:** env vars first, then active `direct auth` profile, then skip. This is **inverted** vs. CLI (where the profile wins) on purpose: a developer machine with an active profile must not silently hit production on a plain `pytest`.
109
+ - **Credential resolution in tests:** env vars/current working directory `.env` first, then active `direct auth` profile, then skip. This matches the safe CLI default for base env vs. active profile: a developer machine with an active profile must not silently hit production on a plain `pytest`.
110
110
 
111
111
  ## Dangerous Commands — Never Auto-Test
112
112
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: direct-cli
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Command-line interface for Yandex Direct API
5
5
  Author: axisrow
6
6
  License: MIT
@@ -106,6 +106,9 @@ Notes:
106
106
  - If the first non-interactive step includes `--client-secret`, the secret is remembered for the matching completion step.
107
107
  - If a profile already stores a confidential OAuth client, `direct auth login --code CODE --profile NAME` reuses the saved `client_id` and `client_secret`.
108
108
  - `direct auth login --oauth-token TOKEN` is a manual access-token import and does not auto-refresh.
109
+ - After a successful interactive login, Direct CLI asks whether to save the
110
+ access token and login to the current working directory `.env`; non-interactive
111
+ login flows do not prompt.
109
112
  - Alias `auth_login` is not supported.
110
113
 
111
114
  Credential resolution priority:
@@ -113,22 +116,23 @@ Credential resolution priority:
113
116
  | Priority | Source | Example |
114
117
  |----------|--------|---------|
115
118
  | 1 | Explicit CLI options | `direct --token TOKEN --login LOGIN campaigns get` |
116
- | 2 | OAuth profile storage | `direct --profile agency1 campaigns get` |
117
- | 3 | Profile-specific env vars | `YANDEX_DIRECT_TOKEN_AGENCY1`, `YANDEX_DIRECT_LOGIN_AGENCY1` |
118
- | 4 | Base env vars or current working directory `.env` | `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN` |
119
+ | 2 | Explicit profile credentials | `direct --profile agency1 campaigns get` |
120
+ | 3 | Base env vars or current working directory `.env` | `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN` |
121
+ | 4 | Active profile credentials | `direct auth use --profile agency1` |
119
122
  | 5 | 1Password references | `--op-token-ref`, `YANDEX_DIRECT_OP_TOKEN_REF` |
120
123
  | 6 | Bitwarden references | `--bw-token-ref`, `YANDEX_DIRECT_BW_TOKEN_REF` |
121
124
 
122
125
  Direct CLI automatically loads only the `.env` file from the current working
123
126
  directory, i.e. the directory where you run `direct`. It does not search for
124
- `.env` from the installed package or source-code location. If a profile is
125
- selected with `--profile` or `direct auth use --profile NAME`, Direct CLI does
126
- not fall back to base `YANDEX_DIRECT_LOGIN`; this prevents mixing a profile
127
- token with a login from the current working directory `.env`. For multi-account
128
- setups, prefer OAuth profiles or profile-specific env vars instead of base
129
- credentials.
127
+ `.env` from the installed package or source-code location. Without an explicit
128
+ `--profile`, base `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` from env or cwd
129
+ `.env` win over the active OAuth profile. With explicit `--profile`, Direct CLI
130
+ uses only that profile's OAuth/profile-env credentials and does not fall back to
131
+ base `YANDEX_DIRECT_LOGIN`; this prevents mixing accounts. `direct auth status`
132
+ without `--profile` reports the effective credential source; with `--profile` it
133
+ reports that profile.
130
134
 
131
- > **Tests use the inverted order.** Live-API test suites (e.g. `tests/test_v4_live_contracts.py`) read `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` from the environment first, only then fall back to the active `direct auth` profile, and skip the test if neither is set. This is intentional: a developer machine with an active profile must not silently hit production on a plain `pytest` invocation. See `CLAUDE.md` for the contract.
135
+ > **Tests follow the safe credential order.** Live-API test suites (e.g. `tests/test_v4_live_contracts.py`) read `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` from the environment first, only then fall back to the active `direct auth` profile, and skip the test if neither is set. This prevents a developer machine with an active profile from silently hitting production on a plain `pytest` invocation. See `CLAUDE.md` for the contract.
132
136
 
133
137
  Install with `pip install direct-cli`, then run commands with `direct`.
134
138
  Invoking the deprecated `direct-cli` entrypoint exits with
@@ -1065,27 +1069,32 @@ direct --profile agency1 campaigns get
1065
1069
  - Если первый non-interactive шаг включает `--client-secret`, secret запоминается для последующего completion step.
1066
1070
  - Если profile уже хранит confidential OAuth client, `direct auth login --code CODE --profile NAME` использует сохраненные `client_id` и `client_secret`.
1067
1071
  - `direct auth login --oauth-token TOKEN` импортирует access token вручную и не включает auto-refresh.
1072
+ - После успешного интерактивного входа Direct CLI спрашивает, сохранить ли
1073
+ access token и login в `.env` текущей рабочей папки; non-interactive вход этот
1074
+ вопрос не задаёт.
1068
1075
 
1069
1076
  Порядок выбора credentials:
1070
1077
 
1071
1078
  | Приоритет | Источник | Пример |
1072
1079
  |-----------|----------|--------|
1073
1080
  | 1 | Явные CLI-опции | `direct --token TOKEN --login LOGIN campaigns get` |
1074
- | 2 | OAuth profile storage | `direct --profile agency1 campaigns get` |
1075
- | 3 | Профильные env vars | `YANDEX_DIRECT_TOKEN_AGENCY1`, `YANDEX_DIRECT_LOGIN_AGENCY1` |
1076
- | 4 | Базовые env vars или `.env` из папки запуска | `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN` |
1081
+ | 2 | Явно выбранный profile | `direct --profile agency1 campaigns get` |
1082
+ | 3 | Базовые env vars или `.env` из папки запуска | `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN` |
1083
+ | 4 | Активный profile | `direct auth use --profile agency1` |
1077
1084
  | 5 | 1Password references | `--op-token-ref`, `YANDEX_DIRECT_OP_TOKEN_REF` |
1078
1085
  | 6 | Bitwarden references | `--bw-token-ref`, `YANDEX_DIRECT_BW_TOKEN_REF` |
1079
1086
 
1080
1087
  Direct CLI автоматически читает только `.env` из текущей рабочей папки, то есть
1081
1088
  из папки, где запущена команда `direct`. Он не ищет `.env` от папки
1082
- установленного пакета или исходного кода. Если профиль выбран через `--profile`
1083
- или `direct auth use --profile NAME`, Direct CLI не подставляет base
1084
- `YANDEX_DIRECT_LOGIN`; это защищает от смешивания токена из профиля с логином из
1085
- `.env` текущей рабочей папки. Для нескольких аккаунтов используйте OAuth
1086
- profiles или профильные env vars, а не базовые credentials.
1087
-
1088
- > **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
1089
+ установленного пакета или исходного кода. Без явного `--profile` базовые
1090
+ `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения или cwd `.env`
1091
+ побеждают активный OAuth profile. С явным `--profile` Direct CLI использует
1092
+ только OAuth/profile-env credentials этого профиля и не подставляет base
1093
+ `YANDEX_DIRECT_LOGIN`; это защищает от смешивания аккаунтов. `direct auth status`
1094
+ без `--profile` показывает реально выбранный источник credentials, а с
1095
+ `--profile` показывает этот профиль.
1096
+
1097
+ > **Тесты используют безопасный порядок credentials.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это защищает от случайного обращения к production API на машине разработчика с активным profile. Контракт зафиксирован в `CLAUDE.md`.
1089
1098
 
1090
1099
  Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
1091
1100
  через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
@@ -63,6 +63,9 @@ Notes:
63
63
  - If the first non-interactive step includes `--client-secret`, the secret is remembered for the matching completion step.
64
64
  - If a profile already stores a confidential OAuth client, `direct auth login --code CODE --profile NAME` reuses the saved `client_id` and `client_secret`.
65
65
  - `direct auth login --oauth-token TOKEN` is a manual access-token import and does not auto-refresh.
66
+ - After a successful interactive login, Direct CLI asks whether to save the
67
+ access token and login to the current working directory `.env`; non-interactive
68
+ login flows do not prompt.
66
69
  - Alias `auth_login` is not supported.
67
70
 
68
71
  Credential resolution priority:
@@ -70,22 +73,23 @@ Credential resolution priority:
70
73
  | Priority | Source | Example |
71
74
  |----------|--------|---------|
72
75
  | 1 | Explicit CLI options | `direct --token TOKEN --login LOGIN campaigns get` |
73
- | 2 | OAuth profile storage | `direct --profile agency1 campaigns get` |
74
- | 3 | Profile-specific env vars | `YANDEX_DIRECT_TOKEN_AGENCY1`, `YANDEX_DIRECT_LOGIN_AGENCY1` |
75
- | 4 | Base env vars or current working directory `.env` | `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN` |
76
+ | 2 | Explicit profile credentials | `direct --profile agency1 campaigns get` |
77
+ | 3 | Base env vars or current working directory `.env` | `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN` |
78
+ | 4 | Active profile credentials | `direct auth use --profile agency1` |
76
79
  | 5 | 1Password references | `--op-token-ref`, `YANDEX_DIRECT_OP_TOKEN_REF` |
77
80
  | 6 | Bitwarden references | `--bw-token-ref`, `YANDEX_DIRECT_BW_TOKEN_REF` |
78
81
 
79
82
  Direct CLI automatically loads only the `.env` file from the current working
80
83
  directory, i.e. the directory where you run `direct`. It does not search for
81
- `.env` from the installed package or source-code location. If a profile is
82
- selected with `--profile` or `direct auth use --profile NAME`, Direct CLI does
83
- not fall back to base `YANDEX_DIRECT_LOGIN`; this prevents mixing a profile
84
- token with a login from the current working directory `.env`. For multi-account
85
- setups, prefer OAuth profiles or profile-specific env vars instead of base
86
- credentials.
84
+ `.env` from the installed package or source-code location. Without an explicit
85
+ `--profile`, base `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` from env or cwd
86
+ `.env` win over the active OAuth profile. With explicit `--profile`, Direct CLI
87
+ uses only that profile's OAuth/profile-env credentials and does not fall back to
88
+ base `YANDEX_DIRECT_LOGIN`; this prevents mixing accounts. `direct auth status`
89
+ without `--profile` reports the effective credential source; with `--profile` it
90
+ reports that profile.
87
91
 
88
- > **Tests use the inverted order.** Live-API test suites (e.g. `tests/test_v4_live_contracts.py`) read `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` from the environment first, only then fall back to the active `direct auth` profile, and skip the test if neither is set. This is intentional: a developer machine with an active profile must not silently hit production on a plain `pytest` invocation. See `CLAUDE.md` for the contract.
92
+ > **Tests follow the safe credential order.** Live-API test suites (e.g. `tests/test_v4_live_contracts.py`) read `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` from the environment first, only then fall back to the active `direct auth` profile, and skip the test if neither is set. This prevents a developer machine with an active profile from silently hitting production on a plain `pytest` invocation. See `CLAUDE.md` for the contract.
89
93
 
90
94
  Install with `pip install direct-cli`, then run commands with `direct`.
91
95
  Invoking the deprecated `direct-cli` entrypoint exits with
@@ -1022,27 +1026,32 @@ direct --profile agency1 campaigns get
1022
1026
  - Если первый non-interactive шаг включает `--client-secret`, secret запоминается для последующего completion step.
1023
1027
  - Если profile уже хранит confidential OAuth client, `direct auth login --code CODE --profile NAME` использует сохраненные `client_id` и `client_secret`.
1024
1028
  - `direct auth login --oauth-token TOKEN` импортирует access token вручную и не включает auto-refresh.
1029
+ - После успешного интерактивного входа Direct CLI спрашивает, сохранить ли
1030
+ access token и login в `.env` текущей рабочей папки; non-interactive вход этот
1031
+ вопрос не задаёт.
1025
1032
 
1026
1033
  Порядок выбора credentials:
1027
1034
 
1028
1035
  | Приоритет | Источник | Пример |
1029
1036
  |-----------|----------|--------|
1030
1037
  | 1 | Явные CLI-опции | `direct --token TOKEN --login LOGIN campaigns get` |
1031
- | 2 | OAuth profile storage | `direct --profile agency1 campaigns get` |
1032
- | 3 | Профильные env vars | `YANDEX_DIRECT_TOKEN_AGENCY1`, `YANDEX_DIRECT_LOGIN_AGENCY1` |
1033
- | 4 | Базовые env vars или `.env` из папки запуска | `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN` |
1038
+ | 2 | Явно выбранный profile | `direct --profile agency1 campaigns get` |
1039
+ | 3 | Базовые env vars или `.env` из папки запуска | `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN` |
1040
+ | 4 | Активный profile | `direct auth use --profile agency1` |
1034
1041
  | 5 | 1Password references | `--op-token-ref`, `YANDEX_DIRECT_OP_TOKEN_REF` |
1035
1042
  | 6 | Bitwarden references | `--bw-token-ref`, `YANDEX_DIRECT_BW_TOKEN_REF` |
1036
1043
 
1037
1044
  Direct CLI автоматически читает только `.env` из текущей рабочей папки, то есть
1038
1045
  из папки, где запущена команда `direct`. Он не ищет `.env` от папки
1039
- установленного пакета или исходного кода. Если профиль выбран через `--profile`
1040
- или `direct auth use --profile NAME`, Direct CLI не подставляет base
1041
- `YANDEX_DIRECT_LOGIN`; это защищает от смешивания токена из профиля с логином из
1042
- `.env` текущей рабочей папки. Для нескольких аккаунтов используйте OAuth
1043
- profiles или профильные env vars, а не базовые credentials.
1044
-
1045
- > **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
1046
+ установленного пакета или исходного кода. Без явного `--profile` базовые
1047
+ `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения или cwd `.env`
1048
+ побеждают активный OAuth profile. С явным `--profile` Direct CLI использует
1049
+ только OAuth/profile-env credentials этого профиля и не подставляет base
1050
+ `YANDEX_DIRECT_LOGIN`; это защищает от смешивания аккаунтов. `direct auth status`
1051
+ без `--profile` показывает реально выбранный источник credentials, а с
1052
+ `--profile` показывает этот профиль.
1053
+
1054
+ > **Тесты используют безопасный порядок credentials.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это защищает от случайного обращения к production API на машине разработчика с активным profile. Контракт зафиксирован в `CLAUDE.md`.
1046
1055
 
1047
1056
  Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
1048
1057
  через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
@@ -0,0 +1,206 @@
1
+ """Shared autotargeting CLI helpers for ad groups and keywords.
2
+
3
+ ``adgroups`` (DynamicTextAdGroup/DynamicTextFeedAdGroup) and ``keywords`` both
4
+ parse the same AutotargetingCategories / AutotargetingSettings CLI surface.
5
+ The helpers used to be duplicated in each command module and had drifted
6
+ (category-token normalization, and which legacy flags conflict with the typed
7
+ ``AutotargetingSettings`` flags). This module is the single source of truth.
8
+
9
+ The legacy-mix guard models an **intentional** asymmetry explicitly via
10
+ ``legacy_candidates``: ``adgroups`` treats only ``--autotargeting-category`` as
11
+ legacy (it has no ``--autotargeting-brand-option`` flag at all), while
12
+ ``keywords`` treats both ``--autotargeting-category`` and
13
+ ``--autotargeting-brand-option`` as legacy.
14
+
15
+ Token normalization is unified on :func:`normalize_enum_token` for both
16
+ categories and brand options. For categories this is behavior-preserving (the
17
+ constants are single words with no ``_``, so ``-``->``_`` only ever touches
18
+ already-invalid tokens). For brand options it is a deliberate *loosening*: the
19
+ constants contain underscores (``WITHOUT_BRANDS``), so the hyphenated spelling
20
+ (``WITHOUT-BRANDS``) — rejected by the old flat ``.strip().upper()`` — is now
21
+ accepted, making every autotargeting enum normalize identically.
22
+ """
23
+
24
+ from typing import Dict, List, Optional, Sequence, Tuple
25
+
26
+ import click
27
+
28
+ from .i18n import t
29
+
30
+ AUTOTARGETING_CATEGORIES = (
31
+ "EXACT",
32
+ "ALTERNATIVE",
33
+ "COMPETITOR",
34
+ "BROADER",
35
+ "ACCESSORY",
36
+ )
37
+ AUTOTARGETING_BRAND_OPTIONS = (
38
+ "WITHOUT_BRANDS",
39
+ "WITH_ADVERTISER_BRAND",
40
+ )
41
+
42
+
43
+ def normalize_enum_token(value: str) -> str:
44
+ """Normalize enum-like CLI tokens to Yandex Direct uppercase constants."""
45
+ return value.strip().upper().replace("-", "_")
46
+
47
+
48
+ def parse_autotargeting_categories(
49
+ raw_values: Tuple[str, ...],
50
+ ) -> Optional[List[Dict[str, str]]]:
51
+ """Parse AutotargetingCategories CLI items (CATEGORY=YES|NO)."""
52
+ if not raw_values:
53
+ return None
54
+
55
+ allowed_categories = ", ".join(AUTOTARGETING_CATEGORIES)
56
+ items: List[Dict[str, str]] = []
57
+ for raw_value in raw_values:
58
+ category_raw, separator, value_raw = raw_value.strip().partition("=")
59
+ if not separator:
60
+ raise click.UsageError(
61
+ t(
62
+ "--autotargeting-category expects CATEGORY=YES|NO "
63
+ "(for example EXACT=YES)"
64
+ )
65
+ )
66
+
67
+ category = normalize_enum_token(category_raw)
68
+ value = normalize_enum_token(value_raw)
69
+ if category not in AUTOTARGETING_CATEGORIES:
70
+ raise click.UsageError(
71
+ t(
72
+ "Invalid --autotargeting-category category {category_raw!r}; "
73
+ "allowed: {allowed_categories}"
74
+ ).format(
75
+ category_raw=category_raw, allowed_categories=allowed_categories
76
+ )
77
+ )
78
+ if value not in {"YES", "NO"}:
79
+ raise click.UsageError(
80
+ t(
81
+ "Invalid --autotargeting-category value {value_raw!r}; "
82
+ "expected YES or NO"
83
+ ).format(value_raw=value_raw)
84
+ )
85
+ items.append({"Category": category, "Value": value})
86
+
87
+ return items
88
+
89
+
90
+ def parse_autotargeting_brand_options(
91
+ raw_values: Tuple[str, ...],
92
+ ) -> Optional[List[Dict[str, str]]]:
93
+ """Parse AutotargetingSettings BrandOptions CLI items (OPTION=YES|NO)."""
94
+ if not raw_values:
95
+ return None
96
+
97
+ allowed_options = ", ".join(AUTOTARGETING_BRAND_OPTIONS)
98
+ items: List[Dict[str, str]] = []
99
+ for raw_value in raw_values:
100
+ option_raw, separator, value_raw = raw_value.strip().partition("=")
101
+ if not separator:
102
+ raise click.UsageError(
103
+ t(
104
+ "--autotargeting-brand-option expects OPTION=YES|NO "
105
+ "(for example WITHOUT_BRANDS=YES)"
106
+ )
107
+ )
108
+
109
+ option = normalize_enum_token(option_raw)
110
+ value = normalize_enum_token(value_raw)
111
+ if option not in AUTOTARGETING_BRAND_OPTIONS:
112
+ raise click.UsageError(
113
+ t(
114
+ "Invalid --autotargeting-brand-option option {option_raw!r}; "
115
+ "allowed: {allowed_options}"
116
+ ).format(option_raw=option_raw, allowed_options=allowed_options)
117
+ )
118
+ if value not in {"YES", "NO"}:
119
+ raise click.UsageError(
120
+ t(
121
+ "Invalid --autotargeting-brand-option value {value_raw!r}; "
122
+ "expected YES or NO"
123
+ ).format(value_raw=value_raw)
124
+ )
125
+
126
+ items.append({"Option": option, "Value": value})
127
+
128
+ return items
129
+
130
+
131
+ def build_autotargeting_settings(
132
+ *,
133
+ exact: Optional[str],
134
+ narrow: Optional[str],
135
+ alternative: Optional[str],
136
+ accessory: Optional[str],
137
+ broader: Optional[str],
138
+ without_brands: Optional[str],
139
+ with_advertiser_brand: Optional[str],
140
+ with_competitors_brand: Optional[str],
141
+ ) -> Optional[Dict[str, Dict[str, str]]]:
142
+ """Build AutotargetingSettings (Categories + BrandOptions) from typed flags."""
143
+ categories: Dict[str, str] = {}
144
+ for field_name, value in (
145
+ ("Exact", exact),
146
+ ("Narrow", narrow),
147
+ ("Alternative", alternative),
148
+ ("Accessory", accessory),
149
+ ("Broader", broader),
150
+ ):
151
+ if value is not None:
152
+ categories[field_name] = value.upper()
153
+
154
+ brand_options: Dict[str, str] = {}
155
+ for field_name, value in (
156
+ ("WithoutBrands", without_brands),
157
+ ("WithAdvertiserBrand", with_advertiser_brand),
158
+ ("WithCompetitorsBrand", with_competitors_brand),
159
+ ):
160
+ if value is not None:
161
+ brand_options[field_name] = value.upper()
162
+
163
+ settings: Dict[str, Dict[str, str]] = {}
164
+ if categories:
165
+ settings["Categories"] = categories
166
+ if brand_options:
167
+ settings["BrandOptions"] = brand_options
168
+
169
+ return settings or None
170
+
171
+
172
+ def reject_legacy_autotargeting_mix(
173
+ settings: Optional[Dict[str, Dict[str, str]]],
174
+ *,
175
+ legacy_candidates: Sequence[Tuple[str, bool]],
176
+ ) -> None:
177
+ """Reject typed AutotargetingSettings combined with legacy autotargeting flags.
178
+
179
+ ``legacy_candidates`` is an ordered ``(flag_name, supplied)`` sequence that
180
+ declares which flags count as legacy for the calling resource. Ad groups
181
+ declare only ``--autotargeting-category``; keywords declare both
182
+ ``--autotargeting-category`` and ``--autotargeting-brand-option``. The
183
+ error message preserves both original source strings so every translation
184
+ catalog entry stays referenced and the rendered output is unchanged.
185
+ """
186
+ if settings is None:
187
+ return
188
+
189
+ declared = [flag for flag, _ in legacy_candidates]
190
+ supplied = [flag for flag, present in legacy_candidates if present]
191
+ if not supplied:
192
+ return
193
+
194
+ if declared == ["--autotargeting-category"]:
195
+ raise click.UsageError(
196
+ t(
197
+ "AutotargetingSettings flags cannot be combined with legacy "
198
+ "--autotargeting-category flags."
199
+ )
200
+ )
201
+
202
+ raise click.UsageError(
203
+ t(
204
+ "AutotargetingSettings flags cannot be combined with legacy {arg0} flags."
205
+ ).format(arg0=", ".join(supplied))
206
+ )