Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ Thumbs.db
*.log
*.bak
*.orig

# Local measurement artifacts (regenerated via scripts/measure_criteria_limits.py)
scripts/_criteria_limits_results.json
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## Unreleased

**Features — preflight `SelectionCriteria` array caps on remaining read-get commands (#571):**

- `ads`, `adgroups`, `bids`, `bidmodifiers`, `campaigns`, `keywords`,
`dynamicfeedadtargets`, `audiencetargets` `get` now reject over-long
`SelectionCriteria` arrays before the request and surface the array name and
ceiling instead of the opaque API `error_code=4001`. Extends the
`enforce_criteria_array_limits` discipline added in #555 to the rest of the
read-get surface. Per-filter caps were measured live against the Yandex Direct
sandbox on 2026-06-17 (see `scripts/measure_criteria_limits.py` and the
`*_GET_CRITERIA_LIMITS` constant in each command module for the exact
transcript).
- Closes a downstream consistency gap: the MCP plugin
(`axisrow/yandex-direct-mcp-plugin#201`) can now drop its own batch-size
guard and forward the CLI error.

## 0.4.3

**Fixes — known limitation: clearing a carousel AdImageHash (#574):**
Expand Down
22 changes: 19 additions & 3 deletions direct_cli/commands/adgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
from ..utils import (
add_criteria_csv,
build_common_params,
enforce_criteria_array_limits,
get_default_fields,
parse_csv_strings,
parse_ids,
parse_nested_field_names,
)

from .._autotargeting import (
AUTOTARGETING_CATEGORIES,
build_autotargeting_settings,
Expand All @@ -28,6 +30,14 @@
)
from .._flag_validation import reject_incompatible_flags

# Yandex Direct adgroups.get caps SelectionCriteria arrays at runtime (the WSDL
# declares them maxOccurs="unbounded"). Live measurement 2026-06-17 via sandbox:
# --campaign-ids ×11 → 4001 "Exceed the maximum number of IDs per array
# SelectionCriteria.CampaignIds"; --negative-keyword-shared-set-ids ×11 (with
# anchor --campaign-ids 1) → 4001 ".NegativeKeywordSharedSetIds". Ids and TagIds
# accepted at N=10000.
ADGROUPS_GET_CRITERIA_LIMITS = {"CampaignIds": 10, "NegativeKeywordSharedSetIds": 10}

_TRACKING_PARAMS_MAX_LENGTH = 1024
_SUPPORTED_ADGROUP_TYPES = (
"TEXT_AD_GROUP",
Expand Down Expand Up @@ -349,7 +359,7 @@ def _provided_flags(provided_flags: dict[str, object]) -> list[str]:


def _reject_mixed_update_subtype_flags(
subtype_flags: dict[str, dict[str, object]],
subtype_flags: dict[str, dict[str, Any]],
) -> None:
"""Reject update payloads that would target multiple ad group subtypes."""
provided_by_subtype = [
Expand Down Expand Up @@ -555,7 +565,7 @@ def get(
)
parsed_nested = parse_nested_field_names(raw_nested)

criteria = {}
criteria: dict[str, Any] = {}
if ids:
criteria["Ids"] = parse_ids(ids)
if campaign_ids:
Expand All @@ -575,6 +585,10 @@ def get(
integers=True,
)

enforce_criteria_array_limits(
criteria, ADGROUPS_GET_CRITERIA_LIMITS, command_name="adgroups get"
)

if not criteria:
raise click.UsageError(t("Provide at least one typed filter"))

Expand Down Expand Up @@ -1485,7 +1499,9 @@ def build_adgroup_update_object(*, adgroup_id, flags):
"--domain-url": domain_url,
**autotargeting_settings_flags,
}
dynamic_feed_flags = {"--dynamic-feed": True if dynamic_feed else None}
dynamic_feed_flags: dict[str, Any] = {
"--dynamic-feed": True if dynamic_feed else None
}
if dynamic_feed:
dynamic_feed_flags["--autotargeting-category"] = autotargeting_categories
else:
Expand Down
29 changes: 24 additions & 5 deletions direct_cli/commands/ads.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ..utils import (
add_criteria_csv,
build_common_params,
enforce_criteria_array_limits,
get_default_fields,
MICRO_RUBLES,
parse_condition_specs,
Expand All @@ -22,6 +23,20 @@
parse_nested_field_names,
)

# Yandex Direct ads.get caps SelectionCriteria arrays at runtime (the WSDL
# declares them maxOccurs="unbounded"). Live measurement 2026-06-17 via sandbox:
# --campaign-ids ×11 → 4001 "Exceed the maximum number of IDs per array
# SelectionCriteria.CampaignIds"; --adgroup-ids ×10001 → 4001 ".AdGroupIds"
# (N=1000 accepted); --vcard-ids ×11 (with anchor --campaign-ids 1) → 4001
# ".VCardIds"; --sitelink-set-ids ×11 (with anchor) → 4001 ".SitelinkSetIds".
# Ids and AdExtensionIds accepted at N=10000.
ADS_GET_CRITERIA_LIMITS = {
"CampaignIds": 10,
"AdGroupIds": 1000,
"VCardIds": 10,
"SitelinkSetIds": 10,
}


@click.group()
def ads():
Expand Down Expand Up @@ -276,7 +291,7 @@ def _build_text_ad_update_base(
vcard_id: Optional[int],
image_hash: Optional[str],
sitelink_set_id: Optional[int],
callout_setting: Optional[dict[str, object]],
callout_setting: Optional[dict[str, Any]],
clear_image_hash: bool = False,
) -> dict[str, object]:
"""Build fields inherited from WSDL TextAdUpdateBase."""
Expand Down Expand Up @@ -434,7 +449,7 @@ def _build_responsive_ad_update(
image_hashes: Optional[str],
video_extension_ids: Optional[str],
sitelink_set_id: Optional[int],
callout_setting: Optional[dict[str, object]],
callout_setting: Optional[dict[str, Any]],
href: Optional[str],
age_label: Optional[str],
display_url_path: Optional[str],
Expand Down Expand Up @@ -481,7 +496,7 @@ def _build_responsive_ad_update(

def _build_feed_based_ad_update(
sitelink_set_id: Optional[int],
callout_setting: Optional[dict[str, object]],
callout_setting: Optional[dict[str, Any]],
business_id: Optional[int],
feed_filter_conditions: tuple[str, ...],
title_sources: Optional[str],
Expand Down Expand Up @@ -1011,7 +1026,7 @@ def get(
"TextAdFieldNames", get_default_fields("ads", "TextAdFieldNames")
)

criteria = {}
criteria: dict[str, Any] = {}
if ids:
criteria["Ids"] = parse_ids(ids)
if campaign_ids:
Expand Down Expand Up @@ -1042,6 +1057,10 @@ def get(
)
add_criteria_csv(criteria, "AdExtensionIds", adextension_ids, integers=True)

enforce_criteria_array_limits(
criteria, ADS_GET_CRITERIA_LIMITS, command_name="ads get"
)

if not criteria:
raise click.UsageError(t("Provide at least one typed filter"))

Expand Down Expand Up @@ -1368,7 +1387,7 @@ def build_ad_object(*, adgroup_id, ad_type, mobile_provided, flags, flag_for=Non

parsed_texts = _parse_required_csv_strings(texts, "--texts")
parsed_titles = _parse_required_csv_strings(titles, "--titles")
responsive_ad = {
responsive_ad: dict[str, Any] = {
"Texts": parsed_texts,
"Titles": parsed_titles,
}
Expand Down
24 changes: 23 additions & 1 deletion direct_cli/commands/audiencetargets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
AudienceTargets commands
"""

from typing import Any

import click

from ..api import client_from_ctx, create_client
Expand All @@ -12,12 +14,26 @@
MICRO_RUBLES,
add_criteria_csv,
build_common_params,
enforce_criteria_array_limits,
get_default_fields,
get_options,
parse_csv_strings,
parse_ids,
)

# Yandex Direct audiencetargets.get caps SelectionCriteria arrays at runtime
# (the WSDL declares them maxOccurs="unbounded"). Live measurement 2026-06-17
# via sandbox: --campaign-ids ×1001 → 4001 "Exceed the maximum number of IDs
# per array SelectionCriteria.CampaignIds" (N=100 accepted — unique among
# *.get); --adgroup-ids/--retargeting-list-ids/--interest-ids ×10001 → 4001
# (N=1000 accepted). Ids accepted at N=10000.
AUDIENCETARGETS_GET_CRITERIA_LIMITS = {
"CampaignIds": 100,
"AdGroupIds": 1000,
"RetargetingListIds": 1000,
"InterestIds": 1000,
}


@click.group()
def audiencetargets():
Expand Down Expand Up @@ -52,7 +68,7 @@ def get(
"""Get audience targets"""
client = client_from_ctx(ctx, create_client)

criteria = {}
criteria: dict[str, Any] = {}
if ids:
criteria["Ids"] = parse_ids(ids)
if adgroup_ids:
Expand All @@ -65,6 +81,12 @@ def get(
add_criteria_csv(criteria, "InterestIds", interest_ids, integers=True)
add_criteria_csv(criteria, "States", states, upper=True)

enforce_criteria_array_limits(
criteria,
AUDIENCETARGETS_GET_CRITERIA_LIMITS,
command_name="audiencetargets get",
)

if not criteria:
raise click.UsageError(
t(
Expand Down
16 changes: 15 additions & 1 deletion direct_cli/commands/bidmodifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
BidModifiers commands
"""

from typing import Any

import click

from ..api import client_from_ctx, create_client
Expand All @@ -10,6 +12,7 @@
from ._lifecycle import make_lifecycle_command
from ..utils import (
build_common_params,
enforce_criteria_array_limits,
get_default_fields,
parse_csv_strings,
parse_csv_upper,
Expand All @@ -18,6 +21,13 @@
)
from .._flag_validation import reject_incompatible_flags

# Yandex Direct bidmodifiers.get caps SelectionCriteria.CampaignIds at runtime
# (the WSDL declares maxOccurs="unbounded"). Live measurement 2026-06-17 via
# sandbox: --campaign-ids ×10001 → 4001 "Exceed the maximum number of IDs per
# array SelectionCriteria.CampaignIds" (N=1000 accepted). Ids and AdGroupIds
# accepted at N=10000.
BIDMODIFIERS_GET_CRITERIA_LIMITS = {"CampaignIds": 1000}


@click.group()
def bidmodifiers():
Expand Down Expand Up @@ -203,7 +213,7 @@ def get(
)
parsed_nested = parse_nested_field_names(raw_nested)

criteria = {"Levels": [lv.upper() for lv in levels]}
criteria: dict[str, Any] = {"Levels": [lv.upper() for lv in levels]}
if ids:
criteria["Ids"] = parse_ids(ids)
if campaign_ids:
Expand All @@ -213,6 +223,10 @@ def get(
if types:
criteria["Types"] = parse_csv_upper(types) or []

enforce_criteria_array_limits(
criteria, BIDMODIFIERS_GET_CRITERIA_LIMITS, command_name="bidmodifiers get"
)

field_names = parse_csv_strings(fields) or get_default_fields("bidmodifiers")
params = build_common_params(
criteria=criteria, field_names=field_names, limit=limit
Expand Down
16 changes: 15 additions & 1 deletion direct_cli/commands/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Bids commands
"""

from typing import Any

import click

from ..api import client_from_ctx, create_client
Expand All @@ -12,12 +14,20 @@
add_criteria_csv,
add_single_id_selector,
build_common_params,
enforce_criteria_array_limits,
get_default_fields,
get_options,
parse_csv_strings,
parse_ids,
)

# Yandex Direct bids.get caps SelectionCriteria arrays at runtime
# (the WSDL declares them maxOccurs="unbounded"). Live measurement 2026-06-17
# via sandbox: --campaign-ids ×11 → 4001 "Exceed the maximum number of IDs per
# array SelectionCriteria.CampaignIds"; --adgroup-ids ×10001 → 4001 ".AdGroupIds"
# (N=1000 accepted). KeywordIds accepted at N=10000.
BIDS_GET_CRITERIA_LIMITS = {"CampaignIds": 10, "AdGroupIds": 1000}


@click.group()
def bids():
Expand Down Expand Up @@ -48,7 +58,7 @@ def get(
"""Get bids"""
client = client_from_ctx(ctx, create_client)

criteria = {}
criteria: dict[str, Any] = {}
if campaign_ids:
criteria["CampaignIds"] = parse_ids(campaign_ids)
if adgroup_ids:
Expand All @@ -57,6 +67,10 @@ def get(
criteria["KeywordIds"] = parse_ids(keyword_ids)
add_criteria_csv(criteria, "ServingStatuses", serving_statuses, upper=True)

enforce_criteria_array_limits(
criteria, BIDS_GET_CRITERIA_LIMITS, command_name="bids get"
)

if not criteria:
raise click.UsageError(
t(
Expand Down
Loading
Loading