Repository navigation
feat(declarative): support RequestPath tokens and first-request checks in SyncTokenCursor - #1238
Conversation
Co-Authored-By: bot_apk <apk@cognition.ai>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
👋 Greetings, Airbyte Team Member!Here are some helpful tips and reminders for your convenience. 💡 Show Tips and TricksTesting This CDK VersionYou can test this version of the CDK using the following: # Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@devin/1792471568-sync-token-request-path#egg=airbyte-python-cdk[dev]' --help
# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch devin/1792471568-sync-token-request-pathPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
|
/prerelease
|
Review of 1. [P2] Compare resolved option destinations, not raw template stringsLocation: The equality checks can reject a working manifest. Give both options this field expression: {{ parameters.get('target', 'nextCursor') }}Then set {"syncToken": "STORED", "nextCursor": "NEXT"}This PR rejects the manifest before the request because the two expression strings are identical. The first regression test below passes on the base and fails on the PR head. The reverse gap exists too: a token option with Code recommendation:
2. [P2] Validate active page-size collisions independently of the ignore flagLocation: The new check treats Reproduced with Code recommendation: separate the two checks. The following is control-flow pseudocode; token_target = resolved_target(token_option, config)
# Only check an active page-size option that actually injects a page size.
if page_size_is_injected and same_known_target(
token_target, resolved_target(page_size_option, config)
):
raise ValueError(
f"Stream {stream_name}: token_option and page_size_option target the "
"same request field on the first request. Configure distinct fields; "
"ignore_stream_slicer_parameters_on_paginated_requests cannot fix this."
)
if same_known_target(token_target, resolved_target(page_token_option, config)):
if page_token_is_injected_on_first_request:
# The ignore flag cannot separate these values on request one either.
raise ValueError(...)
if not ignore_stream_slicer_parameters:
# Here the existing first-request-only recommendation is applicable.
raise ValueError(...)Keep the existing Reproduction and regression testsThese tests use the existing helpers in def _review_retriever(manifest):
source, _ = _source_and_catalog(manifest)
stream = source.streams(config={})[0]
return stream._stream_partition_generator._partition_factory._retriever
def test_distinct_rendered_fields_are_allowed():
from airbyte_cdk.sources.types import StreamSlice
manifest = _manifest()
stream = manifest["streams"][0]
options = (
stream["incremental_sync"]["token_option"],
stream["retriever"]["paginator"]["page_token_option"],
)
for option in options:
option["field_name"] = "{{ parameters.get('target', 'nextCursor') }}"
stream["incremental_sync"]["$parameters"] = {"target": "syncToken"}
retriever = _review_retriever(manifest)
assert retriever._request_body_json(
stream_slice=StreamSlice(partition={}, cursor_slice={"token": "STORED"}),
next_page_token={"next_page_token": "NEXT"},
) == {"syncToken": "STORED", "nextCursor": "NEXT"}
def test_page_size_collision_is_rejected_even_with_ignore_flag():
manifest = _manifest()
retriever_model = manifest["streams"][0]["retriever"]
retriever_model["ignore_stream_slicer_parameters_on_paginated_requests"] = True
paginator = retriever_model["paginator"]
paginator["page_size_option"] = {
"type": "RequestOption",
"inject_into": "body_json",
"field_name": "syncToken",
}
paginator["pagination_strategy"]["page_size"] = 100
with pytest.raises(ValueError):
_review_retriever(manifest)
def test_equivalent_field_name_and_path_are_rejected():
manifest = _manifest()
paginator = manifest["streams"][0]["retriever"]["paginator"]
paginator["page_token_option"] = {
"type": "RequestOption",
"inject_into": "body_json",
"field_path": ["syncToken"],
}
with pytest.raises(ValueError):
_review_retriever(manifest)For the final tests, assert the relevant error text as well as Suggested change scope: No state-format, cursor commit/reset, URL replay, or schema changes are needed for these findings. Validation already performed: 85 focused tests passed across the sync-token cursor, integration, and request-options-provider suites. The three additional reproduction cases fail on the PR head; the valid-template case passes on the base. The MotherDuck CI failure is unrelated: its job cannot retrieve an enabled credential-secret version. |
|
👍 On it. Both findings are valid. Here's the plan:
I'll add your three tests, plus the extra cases you listed, with error-text assertions. Posted by Devin, as requested by Patrick Nilan (@pnilan). |
…fields Co-Authored-By: bot_apk <apk@cognition.ai>
|
☑️ Resolved in 6f91913. Collisions are now checked against the resolved destinations of the constructed options, and the page-size and page-token rules are separate.
Posted by Devin, as requested by Patrick Nilan (@pnilan). |
…set' into devin/1792471568-sync-token-request-path Bring in the current base (#1235, which includes #1234's review fixes) and resolve conflicts: - model_to_component_factory.py: keep both new static methods, _validate_sync_token_option_collisions (this PR) and _flatten_partition_router_models (base). - sync_token_request_options_provider.py: keep this PR's RequestPath support and _get_token helper; take the base's docstring, its removal of the dead `not self.token_option` check, and valid_types=(str,) for initial_token. - sync_token_cursor.py: say "sync token" in the log line of this PR's new first-commit branch, matching the base's rename. test_commit_observability_counts_completed_pages_per_partition fails otherwise.
Overview
👉 TL;DR: Lets the sync-token incremental cursor replay a stored token that is a complete URL (Microsoft Graph delta links), and adds build-time checks so first-request-only token shapes (Box
events) fail with a clear message instead of a request collision on page 2.Specifically,
SyncTokenCursor.token_optionnow acceptsRequestPath,SimpleRetrievergains afirst_request_pathhook with an origin check, the factory validates token/paginator collisions andinject_on_first_request, andSyncTokenCursorcaps stored token state at 4 MB per stream.Resolves https://github.com/airbytehq/airbyte-internal-issues/issues/18023:
Close on merge: yes — internally filed, no customer to confirm with.
This PR is part of a stacked set:
Changes
declarative_component_schema.yaml/ model:SyncTokenCursor.token_optionisanyOf: [RequestOption, RequestPath](Union[RequestOption, RequestPath]).SyncTokenRequestOptionsProvider: returns{}from all request-option methods forRequestPath; newget_request_path(stream_slice)returns the stored token, else renderedinitial_token, elseNone.SimpleRetriever.first_request_path: on the first request of a pass (no paginator path, no page token) the token URL replaces the requester path; paginatorRequestPathlinks still win on later pages. Absolute token URLs whose scheme or host differ from the requester base raise aconfig_errorbefore any request is sent.ModelToComponentFactory:first_request_pathto the innerSyncTokenRequestOptionsProvider(unwrappingPerPartitionRequestOptionsProvider) only forSyncTokenCursor+RequestPath; every other stream getsNone.RequestOption.resolve_target(config), sofield_name: xequalsfield_path: [x]) of theRequestOptiontoken and the paginator options. It rejects an injectedpage_size_optionon the same field even with the ignore flag; apage_token_optionon the same field when the paginator sends a token on the first request; and otherwise apage_token_optionon the same field unlessignore_stream_slicer_parameters_on_paginated_requests: true. If a destination can't be resolved at build time, the runtime collision check still applies.PageIncrement/OffsetIncrementwithinject_on_first_request: truewhen the token goes in the path or on the first request only.SyncTokenCursor:max_state_bytes(default 4 MB) tracked incrementally from partition key + token sizes; stored partitions keep updating, new partitions past the limit are read in full, one WARN per stream.Review Spotlight
Reviewers with limited time, please review first:
simple_retriever.py_paginator_path/_check_token_host: first-request precedence and the origin checkmodel_to_component_factory.pyvalidations: collision and first-request checksReproduction
On the base branch (
4ced6454):SyncTokenRequestOptionsProvider(token_option=RequestPath(...)).get_request_params(...)→AttributeError: 'RequestPath' object has no attribute 'inject_into'.stream_positionas request parameters) → page 2 fails withValueError: Request body collision, duplicate keys detected at key path: stream_position.Risks
RESET_SYNC_TOKEN) with aRequestPathtoken re-reads frominitial_tokenif set, otherwise the requester path — same as theRequestOptionpatterns. A manifest whoseinitial_tokenmeans "sync from now" would skip history on reset; adopters should leaveinitial_tokenunset for Graph.SyncTokenCursorstreams, which are unreleased (this stack).Declarative-First Evaluation
This is a CDK change that adds built-in declarative capability; no connector custom component is written. The adopters (Graph, Box) are manifest-only and could not express this with existing components:
DefaultPaginatorwithRequestPathonly applies after a page token exists (not the first request),RequestOptioncannot carry a full URL, and no existing check catches the token/paginator key collision at build time.Test Coverage
New tests in
test_sync_token_cursor_integration.py(exact URLs/params viaHttpMocker),test_sync_token_cursor.py, andtest_sync_token_request_options_provider.py:T-URL,T-URL-PARAMS,T-URL-INIT,T-URL-RESET,T-URL-PAR,T-ORIGIN,T-POS,T-COLLIDE,T-FIRSTREQ,T-BYTES,T-INERT, resolved-destination collision cases (field name vs path, interpolated and nested paths, page size with and without the ignore flag), plus provider andRequestOption.resolve_targetunit tests.6f91913b: 1058 passed across the cursor, requester, and parser suites). Fullunit_tests/: 5820 passed, 22 skipped, 2 failed —test_parse_start_date[date_only_ab_datetime_parse_fallback]and[with_timezone_offset_converted_to_utc], which fail the same way on the base commit4ced6454.ruff check,ruff format --check,mypyon changed files: clean.Versioning
No breaking change: existing
RequestOptionmanifests and the stored state format are unchanged, andRequestPathis an added union member. The CDK version comes from the release process via the conventionalfeattitle; no version file is edited.Follow-ups
docs/platform/connector-development/config-based/understanding-the-yaml-file/incremental-syncs.mdinairbytehq/airbytehas noSyncTokenCursorsection yet (PRs 1234/1235 did not add one), so the RequestPath / first-request guidance from the issue should land with the stack's docs PR.Link to Devin session: https://app.devin.ai/sessions/ef74cd18b819430c89b904a53a3ed78e
Open in Devin Desktop: https://app.devin.ai/desktop/session/ef74cd18b819430c89b904a53a3ed78e?variant=devin