direct-cli 0.3.7__tar.gz → 0.3.8__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.3.7 → direct_cli-0.3.8}/.github/workflows/claude.yml +1 -1
- direct_cli-0.3.8/CHANGELOG.md +43 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/CLAUDE.md +13 -1
- {direct_cli-0.3.7 → direct_cli-0.3.8}/PKG-INFO +21 -7
- {direct_cli-0.3.7 → direct_cli-0.3.8}/README.md +20 -6
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/ads.py +145 -26
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/feeds.py +11 -1
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/PKG-INFO +21 -7
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/SOURCES.txt +24 -19
- {direct_cli-0.3.7 → direct_cli-0.3.8}/pyproject.toml +1 -1
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/build_api_coverage_report.py +85 -34
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/sandbox_write_live.py +18 -7
- direct_cli-0.3.8/scripts/test_sandbox_write.sh +38 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/API_COVERAGE.md +1 -1
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/API_ISSUE_AUDIT.md +1 -1
- direct_cli-0.3.8/tests/_orphan_store.py +124 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/api_coverage_payloads.py +416 -31
- direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_get.yaml +437 -0
- direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteBidsRead.test_bids_set_auto.yaml +437 -0
- direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml +58 -0
- direct_cli-0.3.7/tests/cassettes/test_integration_write/TestWriteFeeds.test_add_update_delete.yaml → direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteRetargetingUpdate.test_retargeting_update.yaml +29 -29
- direct_cli-0.3.8/tests/cassettes/test_integration_write/TestWriteStrategies.test_strategies_lifecycle.yaml +69 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/conftest.py +9 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_api_coverage.py +353 -3
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_dry_run.py +55 -12
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_integration.py +117 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_integration_write.py +256 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_low_coverage_payloads.py +213 -8
- direct_cli-0.3.8/tests/test_v4_live_contracts.py +341 -0
- direct_cli-0.3.7/tests/test_integration_live_write.py → direct_cli-0.3.8/tests/test_v5_live_write.py +27 -28
- direct_cli-0.3.7/CHANGELOG.md +0 -11
- direct_cli-0.3.7/scripts/test_sandbox_write.sh +0 -23
- direct_cli-0.3.7/tests/test_v4_live_contracts.py +0 -190
- {direct_cli-0.3.7 → direct_cli-0.3.8}/.env.example +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/.github/copilot-instructions.md +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/.github/workflows/api-coverage.yml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/.github/workflows/quality.yml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/.gitignore +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/AGENTS.md +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/MANIFEST.in +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/__init__.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_deprecated.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_smoke_probes.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/__init__.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/__init__.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/endpoints.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/exceptions.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/resource_mapping.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/tapi_yandex_direct.pyi +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/__init__.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/adapter.pyi +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/_vendor/tapi_yandex_direct/v4/resource_mapping.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/api.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/auth.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/cli.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/__init__.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/adextensions.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/adgroups.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/adimages.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/advideos.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/agencyclients.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/audiencetargets.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/auth.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/balance.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/bidmodifiers.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/bids.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/businesses.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/campaigns.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/changes.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/clients.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/creatives.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/dictionaries.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/dynamicads.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/dynamicfeedadtargets.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/keywordbids.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/keywords.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/keywordsresearch.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/leads.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/negativekeywordsharedsets.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/reports.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/retargeting.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/sitelinks.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/smartadtargets.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/strategies.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/turbopages.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4account.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4events.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4finance.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4forecast.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4goals.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4shells.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4tags.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/v4wordstat.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/commands/vcards.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/output.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/reports_coverage.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/smoke_matrix.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/utils.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/v4/__init__.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/v4/money.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/v4_contracts.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli/wsdl_coverage.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/dependency_links.txt +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/entry_points.txt +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/requires.txt +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/direct_cli.egg-info/top_level.txt +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/docs/superpowers/plans/2026-04-12-issue-32-completion.md +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/docs/superpowers/specs/2026-04-23-vendor-tapi-yandex-direct-design.md +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/anonymize_cassettes.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/build_api_coverage_checklist.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/check_reports_drift.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/check_wsdl_drift.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/patch_vendor_imports.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/refresh_reports_cache.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/refresh_wsdl_cache.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/release_pypi.sh +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/test_dangerous_commands.sh +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/test_safe_commands.sh +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/scripts/update_vendor.sh +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/setup.cfg +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/setup.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/MANUAL_COVERAGE.md +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/__init__.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdExtensions.test_add_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdGroups.test_add_update_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAdImages.test_add_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAds.test_add_text_ad_update_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteAudienceTargets.test_add_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBidModifiersAdd.test_add_delete_mobile.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBidModifiersSet.test_set_without_id_is_rejected.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteBids.test_set_bid.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteCampaignDraftLifecycle.test_draft_create_get_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteCampaigns.test_campaign_lifecycle.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteDynamicAds.test_add_update_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteKeywordBids.test_set_keyword_bid.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteKeywords.test_add_update_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteNegativeKeywordSharedSets.test_add_update_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteRetargeting.test_add_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteSitelinks.test_add_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteSmartAdTargets.test_add_update_delete.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/cassettes/test_integration_write/TestWriteVCards.test_add_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_adgroups_add_update_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_adgroups_add_update_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_adimages_add_get_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_adimages_add_get_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_ads_add_update_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_add_update_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_ads_suspend_resume_archive_unarchive.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_ads_suspend_resume_archive_unarchive.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_advideos_add_get.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_advideos_add_get.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_add_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_add_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_audiencetargets_suspend_resume.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_audiencetargets_suspend_resume.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_bids_set.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_bids_set.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_campaign_create_get_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_campaign_create_get_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_creatives_chain_advideo_to_creative.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_creatives_chain_advideo_to_creative.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_add_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_add_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_dynamicads_suspend_resume.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_dynamicads_suspend_resume.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_keywordbids_set.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywordbids_set.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_keywords_add_update_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_add_update_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_keywords_suspend_resume.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_keywords_suspend_resume.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_sitelinks_add_get_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_sitelinks_add_get_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_add_update_delete.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_add_update_delete.yaml +0 -0
- /direct_cli-0.3.7/tests/cassettes/test_integration_live_write/test_live_draft_smartadtargets_suspend_resume.yaml → /direct_cli-0.3.8/tests/cassettes/test_v5_live_write/test_v5_live_draft_smartadtargets_suspend_resume.yaml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/fixtures/test-video.mp4 +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/fields-list.html +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/headers.html +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/period.html +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/spec.html +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/raw/type.html +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/reports_cache/spec.json +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_auth_bw.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_auth_oauth.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_auth_op.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_balance.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_cli.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_cli_contract.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_comprehensive.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_reports_drift.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_reports_parsing.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_smoke_matrix.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_transport_contract.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4_contracts.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4_foundation.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4_safety.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4account.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4events.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4finance_money.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4finance_read.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4forecast.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4goals.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4tags.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_v4wordstat.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/test_vendor_imports.py +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/adextensions.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/adgroups.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/adimages.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/ads.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/advideos.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/agencyclients.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/audiencetargets.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/bidmodifiers.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/bids.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/businesses.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/campaigns.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/changes.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/clients.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/creatives.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/dictionaries.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/dynamicfeedadtargets.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/dynamictextadtargets.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/feeds.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/imports/adextensiontypes.xsd +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/imports/general.xsd +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/imports/generalclients.xsd +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/keywordbids.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/keywords.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/keywordsresearch.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/leads.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/negativekeywordsharedsets.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/retargetinglists.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/sitelinks.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/smartadtargets.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/strategies.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/turbopages.xml +0 -0
- {direct_cli-0.3.7 → direct_cli-0.3.8}/tests/wsdl_cache/vcards.xml +0 -0
|
@@ -46,5 +46,5 @@ jobs:
|
|
|
46
46
|
# Optional: Add claude_args to customize behavior and configuration
|
|
47
47
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
48
48
|
# or https://code.claude.com/docs/en/cli-reference for available options
|
|
49
|
-
|
|
49
|
+
claude_args: "--model claude-opus-4-7"
|
|
50
50
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.8
|
|
4
|
+
|
|
5
|
+
**BREAKING CHANGES:**
|
|
6
|
+
|
|
7
|
+
- `direct ads update` now requires `--type {TEXT_AD,TEXT_IMAGE_AD,MOBILE_APP_AD}`. Scripts that called `ads update` with only field flags will fail with `Missing option '--type'`. Mirrors the WSDL one-of choice between TextAd/TextImageAd/MobileAppAd update subtypes (PR #197).
|
|
8
|
+
- `direct ads add --type TEXT_IMAGE_AD` rejects `--title/--text` (TEXT_IMAGE_AD has no such WSDL fields). `direct ads update --status` rejected — use `ads suspend/resume/archive/unarchive` for status changes (PR #190).
|
|
9
|
+
- `direct ads add --type MOBILE_APP_AD --href` rejected — MobileAppAd uses `--tracking-url`, not `--href` (PR #196).
|
|
10
|
+
- `direct feeds add` now requires `--business-type {RETAIL,HOTELS,REALTY,AUTOMOBILES,FLIGHTS,OTHER}`. Mirrors WSDL FeedAddItem.BusinessType (minOccurs=1) (PR #201).
|
|
11
|
+
|
|
12
|
+
**Schema gate — mutating ops parity:**
|
|
13
|
+
|
|
14
|
+
- Extended the WSDL `*FieldNames` schema gate (introduced for `get` in 0.3.7) to mutating operations (`add/update/set/setBids/lifecycle`). Added per-operation waiver granularity via `SCHEMA_GATE_OPERATION_WAIVERS` (PR #181).
|
|
15
|
+
- Promoted dynamicads, bidmodifiers add/set, adimages/advideos/vcards add (media payloads), adextensions/retargeting/feeds.add typed fixtures to `PAYLOAD_CASES` (PRs #184, #185, #187, #188).
|
|
16
|
+
- Added MOBILE_APP_AD branch to `ads add` mirroring WSDL `MobileAppAdAdd` (PR #190).
|
|
17
|
+
- `bidmodifiers.delete` correctly classified as a real destructive WSDL operation and added to schema gate (PR #194); the earlier "Helper/legacy surface" rationale was a mis-classification — see post-mortem in #199 / PR #200.
|
|
18
|
+
|
|
19
|
+
**Strict WSDL parity policy:**
|
|
20
|
+
|
|
21
|
+
- Documented "Strict WSDL parity" principle in `CLAUDE.md`: `DRY_RUN_PAYLOAD_EXCLUSIONS` may only contain entries from five legitimate categories (read-path `*.get`, runtime-deprecated, v4-not-in-v5-wsdl, custom non-RPC endpoints, methods covered by `tests/test_dry_run.py`). New guard test `test_dry_run_exclusions_have_no_helper_or_legacy_rationale` fails CI if any rationale uses banned phrases (PR #200).
|
|
22
|
+
|
|
23
|
+
**Integration test coverage:**
|
|
24
|
+
|
|
25
|
+
- Added read-only sandbox integration tests for `changes`, `keywordsresearch`, `balance` (PR #186).
|
|
26
|
+
- Added v5 write integration coverage for `strategies` lifecycle, `retargeting update`, `bids get/set-auto`, plus `auth status/list` read-only tests (PR #189).
|
|
27
|
+
- Re-recorded TestWriteBidsRead cassettes against live API and rewrote host to sandbox so the bids endpoints get real coverage in replay mode (PR #193).
|
|
28
|
+
|
|
29
|
+
**CI infrastructure:**
|
|
30
|
+
|
|
31
|
+
- Switched Claude code-review GitHub Action from default (Sonnet 4.5) to Claude Opus 4.7 for deeper PR review (PR #192).
|
|
32
|
+
|
|
33
|
+
**Refs:** Closes issues #118, #136, #137, #175, #176, #180, #183, #191, #199.
|
|
34
|
+
|
|
35
|
+
## 0.3.3
|
|
36
|
+
|
|
37
|
+
**BREAKING CHANGE:** OAuth profiles created before 0.3.3 (without `refresh_token` and `expires_at`) are no longer accepted. Any such profile will fail immediately with an "incomplete profile" error. Run `direct auth login --profile <name>` to re-authenticate and create a valid 0.3.3 profile.
|
|
38
|
+
|
|
39
|
+
- Added refresh token persistence for OAuth profiles.
|
|
40
|
+
- Added automatic OAuth access token refresh before expiry.
|
|
41
|
+
- Added `expires_in` details to `direct auth status`.
|
|
42
|
+
- Added JSON output for `direct auth status`.
|
|
43
|
+
- Kept `direct auth login --oauth-token` as a manual access-token import without auto-refresh.
|
|
@@ -24,7 +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 flags (`--token`, `--login`) > env vars (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_LOGIN`) > `.env` file. `load_dotenv()` runs at `cli.py` import time.
|
|
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.
|
|
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`).
|
|
28
30
|
|
|
29
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.
|
|
30
32
|
|
|
@@ -41,6 +43,15 @@ Click group-of-groups. Each Yandex Direct API resource = one file in `direct_cli
|
|
|
41
43
|
|
|
42
44
|
**Runtime-deprecated methods:** WSDL-visible methods that Yandex rejects at runtime belong in `RUNTIME_DEPRECATED_METHODS` (`direct_cli/wsdl_coverage.py`) and must fail with `click.UsageError` before request construction. `agencyclients add` is blocked this way; use `agencyclients add-passport-organization`.
|
|
43
45
|
|
|
46
|
+
**Strict WSDL parity:** `DRY_RUN_PAYLOAD_EXCLUSIONS` in `tests/api_coverage_payloads.py` must NOT contain any entry whose rationale claims the CLI surface is a «helper», «legacy», or «not part of strict WSDL parity». If the WSDL declares the operation, the CLI mirrors it 1:1 with a `PAYLOAD_CASES` fixture. Legitimate permanent exclusions are limited to:
|
|
47
|
+
- read-path `*.get` (covered by SelectionCriteria tests);
|
|
48
|
+
- runtime-deprecated methods (see `RUNTIME_DEPRECATED_METHODS`);
|
|
49
|
+
- v4 methods that have no v5 WSDL (covered by `direct_cli/v4_contracts.py`);
|
|
50
|
+
- custom non-RPC endpoints (e.g. `reports.get` — TSV stream);
|
|
51
|
+
- methods explicitly covered by `tests/test_dry_run.py::test_<service>_<op>_payload`.
|
|
52
|
+
|
|
53
|
+
A guard in `tests/test_api_coverage.py::test_dry_run_exclusions_have_no_helper_or_legacy_rationale` enforces this — any rationale outside those five categories that uses the banned phrasing is a mis-classification: write a `PAYLOAD_CASES` fixture instead. See post-mortem in issue #199.
|
|
54
|
+
|
|
44
55
|
**SelectionCriteria:** Resources like `adgroups`, `ads`, `keywords` require at least one of `Ids`, `CampaignIds`, or `AdGroupIds` — otherwise API error 4001.
|
|
45
56
|
|
|
46
57
|
**Error handling:** All commands wrap API calls in `try/except Exception` → `print_error(str(e))` + `raise click.Abort()`.
|
|
@@ -55,6 +66,7 @@ Click group-of-groups. Each Yandex Direct API resource = one file in `direct_cli
|
|
|
55
66
|
|
|
56
67
|
- **Unit** (`test_cli.py`, `test_comprehensive.py`) — no API calls, no token needed.
|
|
57
68
|
- **Integration** (`test_integration.py`, `@pytest.mark.integration`) — require `.env` with `YANDEX_DIRECT_TOKEN` and `YANDEX_DIRECT_LOGIN`. Auto-skip if absent.
|
|
69
|
+
- **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`.
|
|
58
70
|
|
|
59
71
|
## Dangerous Commands — Never Auto-Test
|
|
60
72
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: direct-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Command-line interface for Yandex Direct API
|
|
5
5
|
Author: axisrow
|
|
6
6
|
License: MIT
|
|
@@ -125,6 +125,8 @@ fall back to base `YANDEX_DIRECT_LOGIN`; this prevents mixing a profile token
|
|
|
125
125
|
with a login from the project `.env`. For multi-account setups, prefer OAuth
|
|
126
126
|
profiles or profile-specific env vars instead of base credentials.
|
|
127
127
|
|
|
128
|
+
> **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.
|
|
129
|
+
|
|
128
130
|
Install with `pip install direct-cli`, then run commands with `direct`.
|
|
129
131
|
Invoking the deprecated `direct-cli` entrypoint exits with
|
|
130
132
|
`use direct instead of direct-cli`.
|
|
@@ -493,7 +495,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
|
|
|
493
495
|
direct adextensions add --callout-text "Free shipping" --dry-run
|
|
494
496
|
direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
|
|
495
497
|
direct creatives add --video-id video-id --dry-run
|
|
496
|
-
direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --dry-run
|
|
498
|
+
direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
|
|
497
499
|
direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
|
|
498
500
|
direct clients update --client-info "Priority client" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
|
|
499
501
|
direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
|
|
@@ -565,17 +567,22 @@ Four tiers of tests live under `tests/`:
|
|
|
565
567
|
| Unit / CLI wiring / dry-run | *(none)* | No | No |
|
|
566
568
|
| Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
|
|
567
569
|
| Write integration | `-m integration_write` | No (replays VCR cassettes) | No |
|
|
568
|
-
| Live draft write integration | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
570
|
+
| Live draft write integration (v5) | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
571
|
+
| v4 live read | `-m v4_live_read` | Yes (production v4 JSON API, read-only) | Yes |
|
|
572
|
+
| v4 live account-level report write (opt-in) | `-k _opt_in_write` in `tests/test_v4_live_contracts.py` | Yes (production v4) | Yes + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
|
|
569
573
|
|
|
570
574
|
```bash
|
|
571
575
|
pip install -e ".[dev]"
|
|
572
576
|
pytest # fast tier — no token
|
|
573
577
|
pytest -m integration -v # read-only integration tests (needs token)
|
|
574
578
|
pytest -m integration_write -v # write cassette replay (no token needed)
|
|
575
|
-
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay
|
|
579
|
+
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5)
|
|
576
580
|
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # re-record live draft cassette
|
|
581
|
+
YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # v4 wordstat/forecast account-level lifecycle
|
|
577
582
|
```
|
|
578
583
|
|
|
584
|
+
The v4 account-level write tier (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) creates real Wordstat and forecast reports in the production account and deletes them in the same run. There are **no cassettes** — these tests run against live API only. Created IDs are tracked in `~/.direct-cli/test-orphans.json` so that if the run is interrupted between create and delete, the next invocation will retry the cleanup automatically (see `tests/_orphan_store.py`).
|
|
585
|
+
|
|
579
586
|
#### Smoke command scripts
|
|
580
587
|
|
|
581
588
|
Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
|
|
@@ -813,6 +820,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
|
|
|
813
820
|
логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
|
|
814
821
|
или профильные env vars, а не базовые credentials.
|
|
815
822
|
|
|
823
|
+
> **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
|
|
824
|
+
|
|
816
825
|
Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
|
|
817
826
|
через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
|
|
818
827
|
подсказкой `use direct instead of direct-cli`.
|
|
@@ -1113,7 +1122,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
|
|
|
1113
1122
|
direct adextensions add --callout-text "Free shipping" --dry-run
|
|
1114
1123
|
direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
|
|
1115
1124
|
direct creatives add --video-id video-id --dry-run
|
|
1116
|
-
direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --dry-run
|
|
1125
|
+
direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
|
|
1117
1126
|
direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
|
|
1118
1127
|
direct clients update --client-info "Приоритетный клиент" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
|
|
1119
1128
|
direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
|
|
@@ -1186,17 +1195,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
|
|
|
1186
1195
|
| Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
|
|
1187
1196
|
| Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
|
|
1188
1197
|
| Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
|
|
1189
|
-
| Live draft write интеграция | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
1198
|
+
| Live draft write интеграция (v5) | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
1199
|
+
| v4 live read | `-m v4_live_read` | Да (prod v4 JSON API, только чтение) | Да |
|
|
1200
|
+
| v4 live запись отчётов на уровне аккаунта (opt-in) | `-k _opt_in_write` в `tests/test_v4_live_contracts.py` | Да (prod v4) | Да + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
|
|
1190
1201
|
|
|
1191
1202
|
```bash
|
|
1192
1203
|
pip install -e ".[dev]"
|
|
1193
1204
|
pytest # быстрый уровень — без токена
|
|
1194
1205
|
pytest -m integration -v # read-only интеграция (нужен токен)
|
|
1195
1206
|
pytest -m integration_write -v # replay write-кассет (токен не нужен)
|
|
1196
|
-
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты
|
|
1207
|
+
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты (v5)
|
|
1197
1208
|
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # перезапись live draft-кассеты
|
|
1209
|
+
YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # жизненный цикл v4 wordstat/forecast
|
|
1198
1210
|
```
|
|
1199
1211
|
|
|
1212
|
+
Уровень v4 account-level write (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) создаёт настоящие Wordstat-отчёты и прогнозы в боевом аккаунте и удаляет их в том же запуске. **Кассет нет** — эти тесты идут только в живой API. Созданные ID пишутся в `~/.direct-cli/test-orphans.json`: если запуск оборвался между create и delete, при следующем вызове осиротевшие ID будут удалены автоматически (см. `tests/_orphan_store.py`).
|
|
1213
|
+
|
|
1200
1214
|
#### Smoke-скрипты команд
|
|
1201
1215
|
|
|
1202
1216
|
Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.
|
|
@@ -82,6 +82,8 @@ fall back to base `YANDEX_DIRECT_LOGIN`; this prevents mixing a profile token
|
|
|
82
82
|
with a login from the project `.env`. For multi-account setups, prefer OAuth
|
|
83
83
|
profiles or profile-specific env vars instead of base credentials.
|
|
84
84
|
|
|
85
|
+
> **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.
|
|
86
|
+
|
|
85
87
|
Install with `pip install direct-cli`, then run commands with `direct`.
|
|
86
88
|
Invoking the deprecated `direct-cli` entrypoint exits with
|
|
87
89
|
`use direct instead of direct-cli`.
|
|
@@ -450,7 +452,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
|
|
|
450
452
|
direct adextensions add --callout-text "Free shipping" --dry-run
|
|
451
453
|
direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
|
|
452
454
|
direct creatives add --video-id video-id --dry-run
|
|
453
|
-
direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --dry-run
|
|
455
|
+
direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
|
|
454
456
|
direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
|
|
455
457
|
direct clients update --client-info "Priority client" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
|
|
456
458
|
direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
|
|
@@ -522,17 +524,22 @@ Four tiers of tests live under `tests/`:
|
|
|
522
524
|
| Unit / CLI wiring / dry-run | *(none)* | No | No |
|
|
523
525
|
| Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
|
|
524
526
|
| Write integration | `-m integration_write` | No (replays VCR cassettes) | No |
|
|
525
|
-
| Live draft write integration | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
527
|
+
| Live draft write integration (v5) | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
528
|
+
| v4 live read | `-m v4_live_read` | Yes (production v4 JSON API, read-only) | Yes |
|
|
529
|
+
| v4 live account-level report write (opt-in) | `-k _opt_in_write` in `tests/test_v4_live_contracts.py` | Yes (production v4) | Yes + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
|
|
526
530
|
|
|
527
531
|
```bash
|
|
528
532
|
pip install -e ".[dev]"
|
|
529
533
|
pytest # fast tier — no token
|
|
530
534
|
pytest -m integration -v # read-only integration tests (needs token)
|
|
531
535
|
pytest -m integration_write -v # write cassette replay (no token needed)
|
|
532
|
-
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay
|
|
536
|
+
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5)
|
|
533
537
|
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # re-record live draft cassette
|
|
538
|
+
YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # v4 wordstat/forecast account-level lifecycle
|
|
534
539
|
```
|
|
535
540
|
|
|
541
|
+
The v4 account-level write tier (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) creates real Wordstat and forecast reports in the production account and deletes them in the same run. There are **no cassettes** — these tests run against live API only. Created IDs are tracked in `~/.direct-cli/test-orphans.json` so that if the run is interrupted between create and delete, the next invocation will retry the cleanup automatically (see `tests/_orphan_store.py`).
|
|
542
|
+
|
|
536
543
|
#### Smoke command scripts
|
|
537
544
|
|
|
538
545
|
Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
|
|
@@ -770,6 +777,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
|
|
|
770
777
|
логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
|
|
771
778
|
или профильные env vars, а не базовые credentials.
|
|
772
779
|
|
|
780
|
+
> **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
|
|
781
|
+
|
|
773
782
|
Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
|
|
774
783
|
через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
|
|
775
784
|
подсказкой `use direct instead of direct-cli`.
|
|
@@ -1070,7 +1079,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
|
|
|
1070
1079
|
direct adextensions add --callout-text "Free shipping" --dry-run
|
|
1071
1080
|
direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
|
|
1072
1081
|
direct creatives add --video-id video-id --dry-run
|
|
1073
|
-
direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --dry-run
|
|
1082
|
+
direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
|
|
1074
1083
|
direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
|
|
1075
1084
|
direct clients update --client-info "Приоритетный клиент" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
|
|
1076
1085
|
direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
|
|
@@ -1143,17 +1152,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
|
|
|
1143
1152
|
| Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
|
|
1144
1153
|
| Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
|
|
1145
1154
|
| Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
|
|
1146
|
-
| Live draft write интеграция | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
1155
|
+
| Live draft write интеграция (v5) | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
1156
|
+
| v4 live read | `-m v4_live_read` | Да (prod v4 JSON API, только чтение) | Да |
|
|
1157
|
+
| v4 live запись отчётов на уровне аккаунта (opt-in) | `-k _opt_in_write` в `tests/test_v4_live_contracts.py` | Да (prod v4) | Да + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
|
|
1147
1158
|
|
|
1148
1159
|
```bash
|
|
1149
1160
|
pip install -e ".[dev]"
|
|
1150
1161
|
pytest # быстрый уровень — без токена
|
|
1151
1162
|
pytest -m integration -v # read-only интеграция (нужен токен)
|
|
1152
1163
|
pytest -m integration_write -v # replay write-кассет (токен не нужен)
|
|
1153
|
-
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты
|
|
1164
|
+
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты (v5)
|
|
1154
1165
|
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # перезапись live draft-кассеты
|
|
1166
|
+
YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # жизненный цикл v4 wordstat/forecast
|
|
1155
1167
|
```
|
|
1156
1168
|
|
|
1169
|
+
Уровень v4 account-level write (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) создаёт настоящие Wordstat-отчёты и прогнозы в боевом аккаунте и удаляет их в том же запуске. **Кассет нет** — эти тесты идут только в живой API. Созданные ID пишутся в `~/.direct-cli/test-orphans.json`: если запуск оборвался между create и delete, при следующем вызове осиротевшие ID будут удалены автоматически (см. `tests/_orphan_store.py`).
|
|
1170
|
+
|
|
1157
1171
|
#### Smoke-скрипты команд
|
|
1158
1172
|
|
|
1159
1173
|
Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.
|
|
@@ -163,21 +163,40 @@ def get(
|
|
|
163
163
|
default="TEXT_AD",
|
|
164
164
|
help="Ad type",
|
|
165
165
|
)
|
|
166
|
-
@click.option("--title", help="Ad title (TEXT_AD
|
|
167
|
-
@click.option("--text", help="Ad text (TEXT_AD
|
|
168
|
-
@click.option("--href", help="Ad URL (TEXT_AD
|
|
169
|
-
@click.option("--image-hash", help="Ad image hash
|
|
166
|
+
@click.option("--title", help="Ad title (TEXT_AD / MOBILE_APP_AD)")
|
|
167
|
+
@click.option("--text", help="Ad text (TEXT_AD / MOBILE_APP_AD)")
|
|
168
|
+
@click.option("--href", help="Ad URL (TEXT_AD / TEXT_IMAGE_AD)")
|
|
169
|
+
@click.option("--image-hash", help="Ad image hash (TEXT_IMAGE_AD / MOBILE_APP_AD)")
|
|
170
|
+
@click.option(
|
|
171
|
+
"--action",
|
|
172
|
+
help="MOBILE_APP_AD call-to-action (MobileAppAdActionEnum, e.g. INSTALL)",
|
|
173
|
+
)
|
|
174
|
+
@click.option("--tracking-url", help="MOBILE_APP_AD tracking URL")
|
|
175
|
+
@click.option("--age-label", help="MOBILE_APP_AD age label (MobAppAgeLabelEnum)")
|
|
170
176
|
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
171
177
|
@click.pass_context
|
|
172
|
-
def add(
|
|
178
|
+
def add(
|
|
179
|
+
ctx,
|
|
180
|
+
adgroup_id,
|
|
181
|
+
ad_type,
|
|
182
|
+
title,
|
|
183
|
+
text,
|
|
184
|
+
href,
|
|
185
|
+
image_hash,
|
|
186
|
+
action,
|
|
187
|
+
tracking_url,
|
|
188
|
+
age_label,
|
|
189
|
+
dry_run,
|
|
190
|
+
):
|
|
173
191
|
"""Add new ad"""
|
|
174
192
|
try:
|
|
175
193
|
ad_type_norm = (ad_type or "TEXT_AD").upper().replace("-", "_")
|
|
176
|
-
supported_types = {"TEXT_AD", "TEXT_IMAGE_AD"}
|
|
194
|
+
supported_types = {"TEXT_AD", "TEXT_IMAGE_AD", "MOBILE_APP_AD"}
|
|
177
195
|
if ad_type_norm not in supported_types:
|
|
178
196
|
raise click.UsageError(
|
|
179
197
|
"Invalid value for '--type': "
|
|
180
|
-
f"{ad_type!r} is not one of
|
|
198
|
+
f"{ad_type!r} is not one of "
|
|
199
|
+
"'TEXT_AD', 'TEXT_IMAGE_AD', 'MOBILE_APP_AD'."
|
|
181
200
|
)
|
|
182
201
|
|
|
183
202
|
ad_data = {"AdGroupId": adgroup_id}
|
|
@@ -200,6 +219,11 @@ def add(ctx, adgroup_id, ad_type, title, text, href, image_hash, dry_run):
|
|
|
200
219
|
"Href": href,
|
|
201
220
|
}
|
|
202
221
|
elif ad_type_norm == "TEXT_IMAGE_AD":
|
|
222
|
+
if title or text:
|
|
223
|
+
raise click.UsageError(
|
|
224
|
+
"--title/--text are only valid for TEXT_AD. "
|
|
225
|
+
"For TEXT_IMAGE_AD, use --image-hash and --href."
|
|
226
|
+
)
|
|
203
227
|
if not image_hash or not href:
|
|
204
228
|
raise click.UsageError(
|
|
205
229
|
"TEXT_IMAGE_AD requires both --image-hash and --href"
|
|
@@ -208,10 +232,37 @@ def add(ctx, adgroup_id, ad_type, title, text, href, image_hash, dry_run):
|
|
|
208
232
|
"AdImageHash": image_hash,
|
|
209
233
|
"Href": href,
|
|
210
234
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
elif ad_type_norm == "MOBILE_APP_AD":
|
|
236
|
+
if href:
|
|
237
|
+
raise click.UsageError(
|
|
238
|
+
"--href does not apply to MOBILE_APP_AD. "
|
|
239
|
+
"Use --tracking-url instead."
|
|
240
|
+
)
|
|
241
|
+
missing_fields = [
|
|
242
|
+
option_name
|
|
243
|
+
for option_name, value in (
|
|
244
|
+
("--title", title),
|
|
245
|
+
("--text", text),
|
|
246
|
+
("--action", action),
|
|
247
|
+
)
|
|
248
|
+
if not value
|
|
249
|
+
]
|
|
250
|
+
if missing_fields:
|
|
251
|
+
raise click.UsageError(
|
|
252
|
+
"MOBILE_APP_AD requires " + ", ".join(missing_fields)
|
|
253
|
+
)
|
|
254
|
+
mobile_app_ad = {
|
|
255
|
+
"Title": title,
|
|
256
|
+
"Text": text,
|
|
257
|
+
"Action": action.upper(),
|
|
258
|
+
}
|
|
259
|
+
if image_hash:
|
|
260
|
+
mobile_app_ad["AdImageHash"] = image_hash
|
|
261
|
+
if tracking_url:
|
|
262
|
+
mobile_app_ad["TrackingUrl"] = tracking_url
|
|
263
|
+
if age_label:
|
|
264
|
+
mobile_app_ad["AgeLabel"] = age_label.upper()
|
|
265
|
+
ad_data["MobileAppAd"] = mobile_app_ad
|
|
215
266
|
|
|
216
267
|
body = {"method": "add", "params": {"Ads": [ad_data]}}
|
|
217
268
|
|
|
@@ -237,30 +288,98 @@ def add(ctx, adgroup_id, ad_type, title, text, href, image_hash, dry_run):
|
|
|
237
288
|
|
|
238
289
|
@ads.command()
|
|
239
290
|
@click.option("--id", "ad_id", required=True, type=int, help="Ad ID")
|
|
240
|
-
@click.option(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
291
|
+
@click.option(
|
|
292
|
+
"--type",
|
|
293
|
+
"ad_type",
|
|
294
|
+
required=True,
|
|
295
|
+
help="Ad subtype: TEXT_AD | TEXT_IMAGE_AD | MOBILE_APP_AD",
|
|
296
|
+
)
|
|
297
|
+
@click.option(
|
|
298
|
+
"--status",
|
|
299
|
+
help=(
|
|
300
|
+
"Deprecated: not part of WSDL AdUpdateItem. "
|
|
301
|
+
"Use 'direct ads suspend/resume/archive/unarchive' instead."
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
@click.option("--title", help="Title (TEXT_AD / MOBILE_APP_AD)")
|
|
305
|
+
@click.option("--text", help="Text (TEXT_AD / MOBILE_APP_AD)")
|
|
306
|
+
@click.option("--href", help="URL (TEXT_AD / TEXT_IMAGE_AD)")
|
|
307
|
+
@click.option("--image-hash", help="Image hash (TEXT_IMAGE_AD / MOBILE_APP_AD)")
|
|
308
|
+
@click.option(
|
|
309
|
+
"--action",
|
|
310
|
+
help="MOBILE_APP_AD call-to-action (MobileAppAdActionEnum, e.g. INSTALL)",
|
|
311
|
+
)
|
|
312
|
+
@click.option("--tracking-url", help="MOBILE_APP_AD tracking URL")
|
|
313
|
+
@click.option("--age-label", help="MOBILE_APP_AD age label (MobAppAgeLabelEnum)")
|
|
245
314
|
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
246
315
|
@click.pass_context
|
|
247
|
-
def update(
|
|
316
|
+
def update(
|
|
317
|
+
ctx,
|
|
318
|
+
ad_id,
|
|
319
|
+
ad_type,
|
|
320
|
+
status,
|
|
321
|
+
title,
|
|
322
|
+
text,
|
|
323
|
+
href,
|
|
324
|
+
image_hash,
|
|
325
|
+
action,
|
|
326
|
+
tracking_url,
|
|
327
|
+
age_label,
|
|
328
|
+
dry_run,
|
|
329
|
+
):
|
|
248
330
|
"""Update ad"""
|
|
331
|
+
if status:
|
|
332
|
+
raise click.UsageError(
|
|
333
|
+
"Use 'direct ads suspend/resume/archive/unarchive' to change status. "
|
|
334
|
+
"The --status flag is not supported by WSDL AdUpdateItem."
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
ad_type_norm = ad_type.upper().replace("-", "_")
|
|
338
|
+
supported_types = {"TEXT_AD", "TEXT_IMAGE_AD", "MOBILE_APP_AD"}
|
|
339
|
+
if ad_type_norm not in supported_types:
|
|
340
|
+
raise click.UsageError(
|
|
341
|
+
"Invalid value for '--type': "
|
|
342
|
+
f"{ad_type!r} is not one of "
|
|
343
|
+
"'TEXT_AD', 'TEXT_IMAGE_AD', 'MOBILE_APP_AD'."
|
|
344
|
+
)
|
|
345
|
+
|
|
249
346
|
try:
|
|
250
347
|
ad_data = {"Id": ad_id}
|
|
251
348
|
|
|
252
|
-
if
|
|
253
|
-
|
|
254
|
-
if any([title, text, href]):
|
|
255
|
-
ad_data["TextAd"] = {}
|
|
349
|
+
if ad_type_norm == "TEXT_AD":
|
|
350
|
+
text_ad = {}
|
|
256
351
|
if title:
|
|
257
|
-
|
|
352
|
+
text_ad["Title"] = title
|
|
258
353
|
if text:
|
|
259
|
-
|
|
354
|
+
text_ad["Text"] = text
|
|
355
|
+
if href:
|
|
356
|
+
text_ad["Href"] = href
|
|
357
|
+
if text_ad:
|
|
358
|
+
ad_data["TextAd"] = text_ad
|
|
359
|
+
elif ad_type_norm == "TEXT_IMAGE_AD":
|
|
360
|
+
text_image_ad = {}
|
|
361
|
+
if image_hash:
|
|
362
|
+
text_image_ad["AdImageHash"] = image_hash
|
|
260
363
|
if href:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
364
|
+
text_image_ad["Href"] = href
|
|
365
|
+
if text_image_ad:
|
|
366
|
+
ad_data["TextImageAd"] = text_image_ad
|
|
367
|
+
elif ad_type_norm == "MOBILE_APP_AD":
|
|
368
|
+
mobile_app_ad = {}
|
|
369
|
+
if title:
|
|
370
|
+
mobile_app_ad["Title"] = title
|
|
371
|
+
if text:
|
|
372
|
+
mobile_app_ad["Text"] = text
|
|
373
|
+
if image_hash:
|
|
374
|
+
mobile_app_ad["AdImageHash"] = image_hash
|
|
375
|
+
if action:
|
|
376
|
+
mobile_app_ad["Action"] = action.upper()
|
|
377
|
+
if tracking_url:
|
|
378
|
+
mobile_app_ad["TrackingUrl"] = tracking_url
|
|
379
|
+
if age_label:
|
|
380
|
+
mobile_app_ad["AgeLabel"] = age_label.upper()
|
|
381
|
+
if mobile_app_ad:
|
|
382
|
+
ad_data["MobileAppAd"] = mobile_app_ad
|
|
264
383
|
|
|
265
384
|
body = {"method": "update", "params": {"Ads": [ad_data]}}
|
|
266
385
|
|
|
@@ -70,13 +70,23 @@ def get(ctx, ids, limit, fetch_all, output_format, output, fields, dry_run):
|
|
|
70
70
|
@feeds.command()
|
|
71
71
|
@click.option("--name", required=True, help="Feed name")
|
|
72
72
|
@click.option("--url", required=True, help="Feed URL")
|
|
73
|
+
@click.option(
|
|
74
|
+
"--business-type",
|
|
75
|
+
required=True,
|
|
76
|
+
type=click.Choice(
|
|
77
|
+
["RETAIL", "HOTELS", "REALTY", "AUTOMOBILES", "FLIGHTS", "OTHER"],
|
|
78
|
+
case_sensitive=False,
|
|
79
|
+
),
|
|
80
|
+
help="Business type (BusinessTypeEnum)",
|
|
81
|
+
)
|
|
73
82
|
@click.option("--dry-run", is_flag=True, help="Show request without sending")
|
|
74
83
|
@click.pass_context
|
|
75
|
-
def add(ctx, name, url, dry_run):
|
|
84
|
+
def add(ctx, name, url, business_type, dry_run):
|
|
76
85
|
"""Add feed"""
|
|
77
86
|
try:
|
|
78
87
|
feed_data = {
|
|
79
88
|
"Name": name,
|
|
89
|
+
"BusinessType": business_type.upper(),
|
|
80
90
|
"SourceType": "URL",
|
|
81
91
|
"UrlFeed": {"Url": url},
|
|
82
92
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: direct-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Command-line interface for Yandex Direct API
|
|
5
5
|
Author: axisrow
|
|
6
6
|
License: MIT
|
|
@@ -125,6 +125,8 @@ fall back to base `YANDEX_DIRECT_LOGIN`; this prevents mixing a profile token
|
|
|
125
125
|
with a login from the project `.env`. For multi-account setups, prefer OAuth
|
|
126
126
|
profiles or profile-specific env vars instead of base credentials.
|
|
127
127
|
|
|
128
|
+
> **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.
|
|
129
|
+
|
|
128
130
|
Install with `pip install direct-cli`, then run commands with `direct`.
|
|
129
131
|
Invoking the deprecated `direct-cli` entrypoint exits with
|
|
130
132
|
`use direct instead of direct-cli`.
|
|
@@ -493,7 +495,7 @@ direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company
|
|
|
493
495
|
direct adextensions add --callout-text "Free shipping" --dry-run
|
|
494
496
|
direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
|
|
495
497
|
direct creatives add --video-id video-id --dry-run
|
|
496
|
-
direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --dry-run
|
|
498
|
+
direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
|
|
497
499
|
direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --dry-run
|
|
498
500
|
direct clients update --client-info "Priority client" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
|
|
499
501
|
direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
|
|
@@ -565,17 +567,22 @@ Four tiers of tests live under `tests/`:
|
|
|
565
567
|
| Unit / CLI wiring / dry-run | *(none)* | No | No |
|
|
566
568
|
| Read-only integration | `-m integration` | Yes (production API, read-only) | Yes |
|
|
567
569
|
| Write integration | `-m integration_write` | No (replays VCR cassettes) | No |
|
|
568
|
-
| Live draft write integration | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
570
|
+
| Live draft write integration (v5) | `-m integration_live_write` | Yes when recording, otherwise VCR replay | Yes + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
571
|
+
| v4 live read | `-m v4_live_read` | Yes (production v4 JSON API, read-only) | Yes |
|
|
572
|
+
| v4 live account-level report write (opt-in) | `-k _opt_in_write` in `tests/test_v4_live_contracts.py` | Yes (production v4) | Yes + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
|
|
569
573
|
|
|
570
574
|
```bash
|
|
571
575
|
pip install -e ".[dev]"
|
|
572
576
|
pytest # fast tier — no token
|
|
573
577
|
pytest -m integration -v # read-only integration tests (needs token)
|
|
574
578
|
pytest -m integration_write -v # write cassette replay (no token needed)
|
|
575
|
-
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay
|
|
579
|
+
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5)
|
|
576
580
|
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # re-record live draft cassette
|
|
581
|
+
YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # v4 wordstat/forecast account-level lifecycle
|
|
577
582
|
```
|
|
578
583
|
|
|
584
|
+
The v4 account-level write tier (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) creates real Wordstat and forecast reports in the production account and deletes them in the same run. There are **no cassettes** — these tests run against live API only. Created IDs are tracked in `~/.direct-cli/test-orphans.json` so that if the run is interrupted between create and delete, the next invocation will retry the cleanup automatically (see `tests/_orphan_store.py`).
|
|
585
|
+
|
|
579
586
|
#### Smoke command scripts
|
|
580
587
|
|
|
581
588
|
Every CLI subcommand is classified in `direct_cli/smoke_matrix.py`.
|
|
@@ -813,6 +820,8 @@ base `YANDEX_DIRECT_LOGIN`; это защищает от смешивания т
|
|
|
813
820
|
логином из project `.env`. Для нескольких аккаунтов используйте OAuth profiles
|
|
814
821
|
или профильные env vars, а не базовые credentials.
|
|
815
822
|
|
|
823
|
+
> **В тестах порядок инвертирован.** Live-API тесты (например `tests/test_v4_live_contracts.py`) сначала читают `YANDEX_DIRECT_TOKEN` / `YANDEX_DIRECT_LOGIN` из окружения, затем падают на активный профиль `direct auth`, и скипают тест если ни того ни другого нет. Это сделано специально: на машине разработчика с активным профилем обычный `pytest` не должен молча идти в боевой API. Контракт зафиксирован в `CLAUDE.md`.
|
|
824
|
+
|
|
816
825
|
Установка остаётся через `pip install direct-cli`, а запуск команд теперь идет
|
|
817
826
|
через `direct`. Вызов deprecated entrypoint `direct-cli` завершается ошибкой с
|
|
818
827
|
подсказкой `use direct instead of direct-cli`.
|
|
@@ -1113,7 +1122,7 @@ direct vcards add --campaign-id 555 --country "Россия" --city "Москв
|
|
|
1113
1122
|
direct adextensions add --callout-text "Free shipping" --dry-run
|
|
1114
1123
|
direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run
|
|
1115
1124
|
direct creatives add --video-id video-id --dry-run
|
|
1116
|
-
direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --dry-run
|
|
1125
|
+
direct feeds add --name "Фид A" --url "https://example.com/feed.xml" --business-type RETAIL --dry-run
|
|
1117
1126
|
direct feeds update --id 18 --name "Фид A v2" --url "https://example.com/feed-v2.xml" --dry-run
|
|
1118
1127
|
direct clients update --client-info "Приоритетный клиент" --phone +70000000000 --notification-email user@example.com --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run
|
|
1119
1128
|
direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email user@example.com --dry-run
|
|
@@ -1186,17 +1195,22 @@ direct campaigns add --name "Тест" --start-date 2024-01-01 --dry-run
|
|
|
1186
1195
|
| Юнит / CLI / dry-run | *(без маркера)* | Нет | Нет |
|
|
1187
1196
|
| Read-only интеграция | `-m integration` | Да (prod API, только чтение) | Да |
|
|
1188
1197
|
| Write интеграция | `-m integration_write` | Нет (replay VCR-кассет) | Нет |
|
|
1189
|
-
| Live draft write интеграция | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
1198
|
+
| Live draft write интеграция (v5) | `-m integration_live_write` | Да при записи, иначе VCR replay | Да + `YANDEX_DIRECT_LIVE_WRITE=1` |
|
|
1199
|
+
| v4 live read | `-m v4_live_read` | Да (prod v4 JSON API, только чтение) | Да |
|
|
1200
|
+
| v4 live запись отчётов на уровне аккаунта (opt-in) | `-k _opt_in_write` в `tests/test_v4_live_contracts.py` | Да (prod v4) | Да + `YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1` |
|
|
1190
1201
|
|
|
1191
1202
|
```bash
|
|
1192
1203
|
pip install -e ".[dev]"
|
|
1193
1204
|
pytest # быстрый уровень — без токена
|
|
1194
1205
|
pytest -m integration -v # read-only интеграция (нужен токен)
|
|
1195
1206
|
pytest -m integration_write -v # replay write-кассет (токен не нужен)
|
|
1196
|
-
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты
|
|
1207
|
+
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # replay live draft-кассеты (v5)
|
|
1197
1208
|
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # перезапись live draft-кассеты
|
|
1209
|
+
YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # жизненный цикл v4 wordstat/forecast
|
|
1198
1210
|
```
|
|
1199
1211
|
|
|
1212
|
+
Уровень v4 account-level write (`YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1`) создаёт настоящие Wordstat-отчёты и прогнозы в боевом аккаунте и удаляет их в том же запуске. **Кассет нет** — эти тесты идут только в живой API. Созданные ID пишутся в `~/.direct-cli/test-orphans.json`: если запуск оборвался между create и delete, при следующем вызове осиротевшие ID будут удалены автоматически (см. `tests/_orphan_store.py`).
|
|
1213
|
+
|
|
1200
1214
|
#### Smoke-скрипты команд
|
|
1201
1215
|
|
|
1202
1216
|
Каждая CLI-подкоманда классифицирована в `direct_cli/smoke_matrix.py`.
|