feat(sitelinks): support escaped pipe + --sitelink-json/--sitelinks-from-file (#221) - #223
Conversation
…rom-file (#221) UTM templates in Yandex Direct (e.g. cid|{campaign_id}|gid|{gbid}) contain literal '|' characters in the URL, which broke parse_sitelink_specs's unbounded split('|') and rejected valid sitelinks with "Invalid sitelink". URL-encoding the pipe as %7C is not a workaround because Yandex does not expand macros inside URL-encoded templates. This change introduces two independent, backwards-compatible improvements: 1. `\|` is recognised as a literal pipe inside --sitelink spec strings. The new posix-style scanner _split_sitelink_spec ignores backslash- escaped pipes and unescapes them in the output. Specs without `\|` behave bit-for-bit identically to before, so existing scripts and the existing payload coverage test pass unchanged. 2. Two structural sources are added for callers that don't want to fight the pipe convention at all, mirroring the keywords-add #218 pattern: * --sitelink-json '<JSON-array>' (inline) * --sitelinks-from-file <path.jsonl> (one object per line) Sources are mutually exclusive; exactly one must be provided. Missing Title/Href in a structural row is rejected with the row index. The previously-required Click option --sitelink is now validated manually, so the new INTERNAL_VALIDATION entry in test_wsdl_parity_gate.py keeps the WSDL parity gate honest. api_coverage_payloads.py grows two extra PAYLOAD_CASES (escape variant + JSON variant) for the same reason. ValueError from parse_sitelink_specs is wrapped as click.UsageError so all three input paths return exit code 2 on invalid input. Closes #221 Closes #220 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@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 42s —— View job Claude PR Review — focused on critical issues
Verdict: no critical issues foundI reviewed the change with a focus on bugs, security, logical errors, data-loss risks, and performance. The implementation is sound and the test coverage is broad enough for the scope. What I checked and why it's safe1. 2. Source mutex in 3. JSON / JSONL ingestion (sitelinks.py:16-66) — defensive enough.
4. Dry-run gate (sitelinks.py:199-201) — returns before creating the client. No accidental live calls. 5. Exception envelope (sitelinks.py:212-216) — 6. WSDL parity gate registration (test_wsdl_parity_gate.py) — entry added to both Non-critical observations (not requesting changes)
Security / data-loss / performance
LGTM from a critical-issues standpoint. |
There was a problem hiding this comment.
Pull request overview
This PR improves direct sitelinks add input handling by (1) supporting literal | in the legacy TITLE|HREF[|DESCRIPTION] spec via escaping and (2) adding structured batch inputs (--sitelink-json / --sitelinks-from-file) with consistent UsageError behavior on invalid user input.
Changes:
- Add an escape-aware splitter for
--sitelinkspecs so\|round-trips as a literal pipe inHref. - Extend
sitelinks addwith mutually-exclusive JSON array (--sitelink-json) and JSONL file (--sitelinks-from-file) inputs, plus internal validation andUsageErrorwrapping. - Expand test coverage (dry-run + WSDL parity gate + API coverage payload probes) for the new behaviors.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| direct_cli/utils.py | Adds escape-aware splitting for sitelink spec parsing and improves the invalid-spec error hint. |
| direct_cli/commands/sitelinks.py | Implements new JSON/JSONL input modes and internal validation for sitelinks add. |
| tests/test_dry_run.py | Adds regression and feature tests for escaped pipes and JSON/JSONL input modes. |
| tests/test_wsdl_parity_gate.py | Registers sitelinks.add/Sitelinks as internally validated for the parity gate. |
| tests/api_coverage_payloads.py | Adds payload probes to keep API coverage checks exercising the new input paths. |
| 1 | ||
| for value in ( | ||
| sitelinks_specs or None, | ||
| sitelinks_json, | ||
| sitelinks_from_file, | ||
| ) | ||
| if value |
| item: Dict[str, str] = { | ||
| "Title": str(row["Title"]).strip(), | ||
| "Href": str(row["Href"]).strip(), | ||
| } | ||
| description = row.get("Description") |
#223 review) Copilot review on PR #223 flagged two real risks; both align with the ``keywords.add`` precedent in this codebase: 1. ``_normalize_sitelink_row`` silently dropped unknown keys, so a typo like ``"Decsription"`` would produce a sitelink without a description and no warning to the user — silent data loss. Mirror ``_normalize_keyword_row`` (``direct_cli/commands/keywords.py:80``): compute the unknown-key set, raise ``UsageError`` with the offending key and the allowed list. 2. ``sources_used`` used boolean truthiness, so ``--sitelink-json ""`` was treated as "no source" and produced "Provide exactly one of: ..." instead of "invalid JSON". Match the ``keywords.add`` mutex (``is not None`` for string options, length-based for the ``--sitelink`` tuple) so an explicitly-empty JSON argument falls into the JSON-validation error path. Two regression tests added: typo-key rejection with offending key quoted; ``--sitelink-json ""`` → "invalid JSON". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Triage по двум non-critical observation от @claude:
Copilot inline-замечания #1 и #2 закрыты коммитом 6993009 (unknown-key rejection + source-selection tightening, обе правки выравнивают sitelinks под паттерн |
|
@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 41s —— View job Triage acknowledgedConfirming the decisions on both non-critical observations from my earlier review: 1.
|
|
Claude finished @axisrow's task in 1m 27s —— View job Claude PR Review — critical issues focus
Verdict: no critical issues — Copilot's two flags already resolved in 6993009I re-reviewed the head of the branch (commit Copilot's two inline comments — status1. Truthiness in sources_used = (
(1 if sitelinks_specs else 0)
+ (1 if sitelinks_json is not None else 0)
+ (1 if sitelinks_from_file is not None else 0)
)
2. Silent unknown-key drop in unknown = sorted(set(row) - set(_SITELINK_FIELDS))
if unknown:
allowed = ", ".join(_SITELINK_FIELDS)
raise click.UsageError(
f"Unknown field {unknown[0]!r} in sitelink #{index}; allowed: {allowed}"
)A typo like Re-verified critical paths (still clean)
Security / data-loss / performance
Previously raised non-critical observationsThe author's triage comment closed both prior non-criticals (UnicodeDecodeError exit code; LGTM from a critical-issues standpoint. Safe to merge. · branch |
Summary
direct sitelinks add --sitelink TITLE|HREF[|DESCRIPTION]ломался на URL с литеральным|(UTM-шаблоны Яндекс.Директа видаcid|{campaign_id}|gid|{gbid}). Теперь литеральный pipe экранируется как\|; обычные spec-строки без\|ведут себя bit-for-bit как раньше.keywords add(feat(keywords): batch add via --from-file / --keywords-json (#203) #218):--sitelink-json '<JSON-array>'и--sitelinks-from-file <path.jsonl>. Источники взаимоисключающие; ровно один обязателен; ошибки про отсутствие Title/Href сообщают индекс строки.ValueErrorизparse_sitelink_specsоборачивается вclick.UsageError— все три входа возвращают exit code 2 на невалидный ввод.Verification
pytest— 888 passed, 45 skipped.pytest tests/test_dry_run.py -k sitelinks— 9/9 зелёные (1 страховка backwards-compat + 8 новых).pytest tests/test_wsdl_parity_gate.py—sitelinks.add/Sitelinksзарегистрировано вINTERNAL_VALIDATION(теперь validated в теле команды, а не через Clickrequired=True).black --check— clean.flake8 direct_cli tests— без новых замечаний.Test plan
\|round-trips черезHrefбез изменения payload (UTM-шаблон сохраняется)UsageErrorс hint про\\|--sitelink-jsonс массивом объектов--sitelinks-from-fileс JSONLUsageErrorUsageErrorHref→UsageErrorс индексом строкиUsageErrortest_sitelinks_add_parses_links_arrayпроходит без правок (backwards-compat)Closes #221
Closes #220
🤖 Generated with Claude Code