fix(changes): mutex ID filters + FieldNames enum validation (#228)#229
Conversation
Yandex Direct Changes.check accepts three mutually-exclusive ID
filters — CampaignIds (up to 3000), AdGroupIds (up to 10 000), AdIds
(up to 50 000). The CLI previously only exposed --campaign-ids (with
required=True), making it impossible to ask the API «did ad 17722
change after X?» without re-fetching the parent campaign. Likewise
--fields accepted any string and forwarded typos to the API as
opaque server-side errors.
This change reshapes `direct changes check`:
* Three options --campaign-ids / --ad-group-ids / --ad-ids; exactly
one is required. Mutex enforced via click.UsageError BEFORE the
try/except wrapper so users get a real Usage hint and exit code 2.
* --fields validated against the WSDL CheckFieldEnum
{CampaignIds, AdGroupIds, AdIds, CampaignsStat}; default behaviour
(all four) is unchanged when --fields is omitted.
* Empty / comma-only inputs that would otherwise produce empty arrays
(FieldNames=[] violates WSDL minOccurs=1) or a confusing Abort exit
code are caught as UsageErrors.
tests/test_changes.py (new) — 12 unit tests via CliRunner with
patched create_client: missing-all / two-at-once / three-at-once
mutex, valid each ID flag (assert correct WSDL field name in body),
default FieldNames coverage, valid passthrough, unknown enum value,
mixed valid+invalid, comma-only --fields, comma-only --campaign-ids.
tests/test_low_coverage_payloads.py — existing
test_changes_check_builds_canonical_payload used
FieldNames=["CampaignId", "ChangesIn"], which are NOT in the
CheckFieldEnum per WSDL (they're response-side fields). Replaced
with the valid pair ["CampaignIds", "CampaignsStat"]. The old
fixture would fail under the new enum gate — and rightly so.
Scope deliberately narrow: numeric size limits (3000/10000/50000),
timestamp normalisation (already handled by parse_datetime), and the
broader UsageError→Abort wrapper refactor (#227) are out of scope.
The MAX_BATCH_SIZE=10 / SKILL.md / timestamp claims in #228 belong
to the MCP plugin and were redirected to
axisrow/yandex-direct-mcp-plugin#115.
Verification:
pytest -m "not integration" -q # 901 passed, 44 skipped
black --check direct_cli tests # clean on touched files
flake8 direct_cli tests # clean on touched files
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production. |
|
Claude finished @axisrow's task in 2m 24s —— View job Review — no critical issues found ✅Scope per request: bugs, security, logic errors, data-loss risk, performance. Style / naming / nits skipped intentionally.
What I checked1. Mutex enforcement ( 2. FieldNames enum ( 3. Order of validation 4. Edge cases
5. Fixture correction ( 6. Integration test ( 7. Backward compatibility Things that are not bugs but worth noting
SummaryClean fix. The WSDL-derived enum is correctly extracted, the mutex matches the documented Yandex Direct contract, and the order of No blocking issues. Safe to merge. --- · |
There was a problem hiding this comment.
Pull request overview
This PR fixes and tightens the direct changes check CLI command to better match the Yandex Direct Changes.check contract by supporting all mutually-exclusive ID filters and validating FieldNames before sending the request to the API.
Changes:
- Replace the single required
--campaign-idsfilter with mutually-exclusive--campaign-ids/--ad-group-ids/--ad-idsand enforce “exactly one of”. - Validate
--fieldsagainst the WSDL enum (CampaignIds,AdGroupIds,AdIds,CampaignsStat) and reject typos / empty lists early. - Add focused unit tests for mutex routing +
FieldNamesvalidation, and update an existing canonical payload fixture to use valid enum values.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
direct_cli/commands/changes.py |
Adds mutex ID filters and pre-flight FieldNames validation for changes check, with improved error reporting for invalid ID inputs. |
tests/test_changes.py |
New unit tests covering mutex enforcement, ID routing, and --fields validation edge cases. |
tests/test_low_coverage_payloads.py |
Fixes the canonical payload fixture to use valid CheckFieldEnum values. |
Comments suppressed due to low confidence (2)
tests/test_changes.py:47
- Same as above: this test intends to assert Click "usage" semantics for mutually-exclusive flags, but only checks
exit_code != 0. Assertingexit_code == 2would ensure the command keeps raisingclick.UsageError(and not getting wrapped intoclick.Abort()), matching the PR description about correct exit codes.
def test_check_rejects_two_id_filters_together():
"""Mutex enforcement: passing two filters is a UsageError."""
with patch("direct_cli.commands.changes.create_client") as create_client:
result = _invoke("--campaign-ids", "1", "--ad-group-ids", "2")
assert result.exit_code != 0
assert "mutually exclusive" in result.output
create_client.assert_not_called()
tests/test_changes.py:57
- Same as above: asserting
exit_code == 2here would more directly validate that the "all three flags" path is aclick.UsageError(usage exit code) rather than anAbort/runtime failure.
def test_check_rejects_all_three_id_filters_together():
"""Mutex enforcement: even three at once is rejected with the same wording."""
with patch("direct_cli.commands.changes.create_client") as create_client:
result = _invoke("--campaign-ids", "1", "--ad-group-ids", "2", "--ad-ids", "3")
assert result.exit_code != 0
assert "mutually exclusive" in result.output
create_client.assert_not_called()
| """Check for changes""" | ||
|
|
||
|
|
||
| _CHECK_FIELD_NAMES = frozenset({"CampaignIds", "AdGroupIds", "AdIds", "CampaignsStat"}) |
| params = { | ||
| "CampaignIds": parse_ids(campaign_ids), | ||
| id_field: id_value, | ||
| "Timestamp": parse_datetime(timestamp), | ||
| "FieldNames": field_names, | ||
| } |
| def test_check_rejects_missing_all_id_filters(): | ||
| """Mutex enforcement: at least one of three ID flags is required.""" | ||
| with patch("direct_cli.commands.changes.create_client") as create_client: | ||
| result = _invoke() | ||
|
|
||
| assert result.exit_code != 0 | ||
| assert "Provide exactly one of" in result.output | ||
| assert "--campaign-ids" in result.output | ||
| assert "--ad-group-ids" in result.output | ||
| assert "--ad-ids" in result.output | ||
| create_client.assert_not_called() |
Summary
Брал #228 в работу, провёл триаж против кода и официальной документации Yandex Direct: Changes.check. Большая часть исходных пунктов оказалась про MCP-плагин (передал в axisrow/yandex-direct-mcp-plugin#115) либо была опровергнута кодом (
parse_datetimeуже нормализует timestamp к ISO-8601-with-Z; никакогоMAX_BATCH_SIZE = 10в CLI нет). Реальный CLI-скоуп — два пункта:direct changes checkбыла только--campaign-ids. По WSDL и доке APIChanges.checkпринимает три взаимоисключающих ID-фильтра —CampaignIds/AdGroupIds/AdIds.--fieldsне валидировался — тайпо вродеCmapignIdsуходило в API и возвращалось мутной серверной ошибкой.What this PR does
--campaign-ids required=Trueтремя mutually-exclusive флагами:--campaign-ids/--ad-group-ids/--ad-ids. Проверка «exactly one of» черезclick.UsageError, доtry/exceptобёртки — пользователь получает корректный exit code 2 и Usage hint.--fieldsпротив WSDL-энумаCheckFieldEnum = {CampaignIds, AdGroupIds, AdIds, CampaignsStat}. Если значения нет в энуме —UsageErrorс подсказкой допустимых значений. Дефолт (все четыре) остаётся для UX.--fields ,(давалFieldNames=[]в нарушениеminOccurs=1),--campaign-ids ,(раньше падал с exit code 1 черезparse_ids'sValueError→Abort) — оба теперьUsageError.tests/test_low_coverage_payloads.py::test_changes_check_builds_canonical_payload: использовалаFieldNames=[\"CampaignId\", \"ChangesIn\"]— не валидные значенияCheckFieldEnumпо WSDL (это response-поля). Заменено на корректные[\"CampaignIds\", \"CampaignsStat\"].Out of scope
except Exceptionв v4/v5 командных модулях — отдельный follow-up v4 command wrappers should let ClickException propagate #227.Tests
12 новых unit-тестов в
tests/test_changes.py(CliRunner + patchedcreate_client):--fields ,пустой список,--campaign-ids ,ValueError изparse_ids→ UsageErrorVerification
Test plan
--helpдляdirect changes checkпоказывает все три ID-флага и список допустимых FieldNamesCloses #228
🤖 Generated with Claude Code