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.
- {direct_cli-0.4.2 → direct_cli-0.4.3}/CHANGELOG.md +215 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/PKG-INFO +17 -3
- {direct_cli-0.4.2 → direct_cli-0.4.3}/README.md +16 -2
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +12 -4
- direct_cli-0.4.3/direct_cli/commands/_batch.py +181 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/_lifecycle.py +8 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/adgroups.py +1131 -254
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/ads.py +1488 -677
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/agencyclients.py +6 -2
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/audiencetargets.py +17 -5
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/balance.py +2 -10
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/bidmodifiers.py +3 -3
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/bids.py +6 -6
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/campaigns.py +3 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/dynamicads.py +18 -4
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/dynamicfeedadtargets.py +6 -4
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/feeds.py +3 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/keywordbids.py +23 -6
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/keywords.py +36 -121
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/negativekeywordsharedsets.py +3 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/retargeting.py +7 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/smartadtargets.py +20 -5
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/strategies.py +3 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4adimage.py +3 -19
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4events.py +2 -10
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4finance.py +5 -37
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4forecast.py +5 -37
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4goals.py +3 -19
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4keywords.py +2 -9
- direct_cli-0.4.3/direct_cli/commands/v4shells.py +54 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4tags.py +5 -37
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4wordstat.py +5 -37
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/vcards.py +3 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/output.py +8 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/smoke_matrix.py +4 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/adgroups.json +42 -2
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/ads.json +40 -3
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/audiencetargets.json +2 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/common.json +2 -1
- direct_cli-0.4.3/direct_cli/translations/v4shells.json +7 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/utils.py +70 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/PKG-INFO +17 -3
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/SOURCES.txt +8 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/pyproject.toml +1 -1
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/check_all_docs_urls.py +30 -13
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/WSDL_OPTIONAL_FIELD_AUDIT.md +3 -3
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/api_coverage_payloads.py +4 -0
- direct_cli-0.4.3/tests/test_adgroups_build_adgroup_object.py +215 -0
- direct_cli-0.4.3/tests/test_adgroups_build_adgroup_update_object.py +129 -0
- direct_cli-0.4.3/tests/test_ads_build_ad_object.py +201 -0
- direct_cli-0.4.3/tests/test_ads_build_ad_update_object.py +163 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_audit_wire_shape.py +37 -0
- direct_cli-0.4.3/tests/test_check_all_docs_urls.py +16 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_cli.py +2 -2
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_dry_run.py +1735 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_integration.py +75 -0
- direct_cli-0.4.3/tests/test_v4_output_options.py +134 -0
- direct_cli-0.4.3/tests/test_v4meta.py +71 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_wsdl_parity_gate.py +62 -9
- direct_cli-0.4.2/direct_cli/commands/v4shells.py +0 -20
- direct_cli-0.4.2/direct_cli/translations/v4shells.json +0 -3
- {direct_cli-0.4.2 → direct_cli-0.4.3}/.env.example +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/.github/copilot-instructions.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/.github/workflows/api-coverage.yml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/.github/workflows/claude.yml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/.github/workflows/quality.yml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/.gitignore +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/AGENTS.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/CLAUDE.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/MANIFEST.in +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/__init__.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_autotargeting.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_bidding_strategy.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_deprecated.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_flag_validation.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_smoke_probes.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/__init__.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/api.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/auth.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/cli.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/__init__.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/adextensions.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/adimages.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/advideos.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/auth.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/businesses.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/changes.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/clients.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/creatives.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/dictionaries.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/keywordsresearch.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/leads.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/reports.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/sitelinks.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/turbopages.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/commands/v4account.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/i18n.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/reports_coverage.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/adextensions.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/adimages.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/advideos.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/agencyclients.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/auth.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/balance.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/bidmodifiers.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/bids.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/businesses.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/campaigns.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/changes.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/clients.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/creatives.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/dictionaries.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/dynamicads.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/dynamicfeedadtargets.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/feeds.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/keywordbids.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/keywords.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/keywordsresearch.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/leads.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/negativekeywordsharedsets.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/reports.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/retargeting.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/sitelinks.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/smartadtargets.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/strategies.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/turbopages.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4account.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4adimage.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4events.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4finance.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4forecast.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4goals.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4keywords.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4tags.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/v4wordstat.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/translations/vcards.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/v4/__init__.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/v4/emit.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/v4/money.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/v4_contracts.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/wsdl_coverage.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/dependency_links.txt +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/entry_points.txt +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/requires.txt +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli.egg-info/top_level.txt +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/API_COVERAGE.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/PROJECT_WIRE_SHAPE_AUDIT_2026-05-30.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/WIRE_SHAPE_TRIAGE_2026-05-30.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/issue-198-mutating-wsdl-audit.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/audits/wire_shape.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/anonymize_cassettes.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/audit_wire_shape.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/build_api_coverage_checklist.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/build_api_coverage_report.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/build_wsdl_optional_field_audit.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/check_reports_drift.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/check_wsdl_drift.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/patch_vendor_imports.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/preflight_check.sh +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/probe_drift_urls.sh +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/refresh_reports_cache.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/refresh_wsdl_cache.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/release_pypi.sh +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/sandbox_write_audit.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/sandbox_write_live.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/test_dangerous_commands.sh +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/test_safe_commands.sh +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/test_sandbox_write.sh +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/scripts/update_vendor.sh +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/setup.cfg +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/setup.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/API_COVERAGE.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/API_ISSUE_AUDIT.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/MANUAL_COVERAGE.md +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/__init__.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/_orphan_store.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
- {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
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
- {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
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteRetargetingUpdate.test_retargeting_update.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[adextensions_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[adgroups_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[adimages_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[ads_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[advideos_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[audiencetargets_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[bidmodifiers_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[bids_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[businesses_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[campaigns_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[changes_check].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[changes_check_campaigns].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[changes_check_dictionaries].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[clients_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[creatives_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[dictionaries_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[dynamicads_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[dynamicfeedadtargets_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[feeds_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[keywordbids_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[keywords_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[keywordsresearch_deduplicate].yaml +0 -0
- {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
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[leads_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[negativekeywordsharedsets_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[reports_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[retargeting_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[sitelinks_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[smartadtargets_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[strategies_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[turbopages_get].yaml +0 -0
- {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
- {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
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4forecast_list].yaml +0 -0
- {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
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4tags_get_campaigns].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[v4wordstat_list_reports].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_read_command[vcards_get].yaml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/cassettes/test_read_cassettes/test_v4finance_check_payment_unknown_transaction.yaml +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/conftest.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/fixtures/test-video.mp4 +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/fields-list.html +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/headers.html +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/period.html +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/spec.html +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/raw/type.html +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/reports_cache/spec.json +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_api_coverage.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_auth_bw.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_auth_oauth.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_auth_op.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_auth_write_json.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_autotargeting.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_balance.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_bidding_strategy_constants.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_cassette_integrity.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_changes.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_cli_contract.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_comprehensive.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_env_loading.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_field_names_option.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_flag_validation.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_handle_api_errors.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_i18n.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_integration_write.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_low_coverage_payloads.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_read_cassettes.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_reports_drift.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_reports_parsing.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_sandbox_write_audit.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_smoke_matrix.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_transport_contract.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_unknown_option_hints.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_contracts.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_exit_codes.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_foundation.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_live_contracts.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_runtime_shape.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4_safety.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4account.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4adimage.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4events.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4finance_money.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4finance_read.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4forecast.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4goals.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4keywords.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4tags.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v4wordstat.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_v5_live_write.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/test_vendor_imports.py +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/adextensions.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/adgroups.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/adimages.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/ads.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/advideos.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/agencyclients.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/audiencetargets.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/bidmodifiers.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/bids.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/businesses.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/campaigns.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/changes.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/clients.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/creatives.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/dictionaries.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/feeds.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/imports/general.xsd +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/keywordbids.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/keywords.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/keywordsresearch.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/leads.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/retargetinglists.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/sitelinks.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/smartadtargets.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/strategies.xml +0 -0
- {direct_cli-0.4.2 → direct_cli-0.4.3}/tests/wsdl_cache/turbopages.xml +0 -0
- {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.
|
|
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`, `--
|
|
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`, `--
|
|
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`, `--
|
|
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`, `--
|
|
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
|
{direct_cli-0.4.2 → direct_cli-0.4.3}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py
RENAMED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|