Skip to content

fix(declarative): support gzip-compressed XML decoding - #1132

Draft
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1787677661-declarative-xml-parser
Draft

fix(declarative): support gzip-compressed XML decoding#1132
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1787677661-declarative-xml-parser

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

Makes GzipDecoder { decoder: XmlDecoder } expressible in manifest-only / Connector Builder connectors, so gzip-compressed XML payloads (e.g. a .xml.gz fetched from a presigned S3 URL) can be read without a custom Python component.

Two independent blockers existed on main:

  1. Schema: GzipDecoder.decoder (and ZipfileDecoder.decoder) anyOf omitted XmlDecoder, so the model failed validation:
    ValidationError: ... decoder -> type: unexpected value; permitted: 'CsvDecoder' ... (given=XmlDecoder)
  2. Runtime: ModelToComponentFactory._get_parser raised
    ValueError: Decoder type type='XmlDecoder' does not have parser associated to it,
    because XmlDecoder is a legacy whole-requests.Response Decoder, not a streaming Parser, so it could not be nested inside GzipParser.

The fix adds a streaming XmlParser(Parser) next to CsvParser/JsonParser, registers it in the factory, and adds XmlDecoder to both nested-decoder unions:

@dataclass
class XmlParser(Parser):
    encoding: Optional[str] = None

    def parse(self, data: BufferedIOBase) -> PARSER_OUTPUT_TYPE:
        try:
            yield xmltodict.parse(data, encoding=self.encoding)
        except ExpatError as exc:
            logger.warning(f"Response cannot be parsed from XML: {exc}")
            yield {}

xmltodict.parse() accepts a file-like object, so records keep the same shape as the existing XmlDecoder (@ attribute prefix, #text for text content of elements carrying attributes) while reading from a byte stream. Malformed-XML behaviour (warn + yield {}) matches XmlDecoder for parity.

Because the Builder form is generated from declarative_component_schema.yaml, the union addition is what surfaces "XML" as a nested parser option in Connector Builder; no frontend change is needed. This builds on #1124, which made GzipParser sniff the 1f 8b magic bytes, so a single GzipDecoder { decoder: XmlDecoder } now works in both the Builder test read and real syncs.

Note on the generated model: declarative_component_schema.py is edited only where the schema change lands (the two Union[...] members). A full poe assemble on unmodified main currently produces an unrelated ~650-line diff, so the generated file was kept minimal to keep the diff reviewable.

Reproduction

On main, constructing the model raised ValidationError (permitted values exclude XmlDecoder) and, once bypassed, the factory raised
ValueError: Decoder type type='XmlDecoder' does not have parser associated to it.

With this branch, an end-to-end read through ConcurrentDeclarativeSource with a gzipped XML response (no Content-Encoding header) and
decoder: {type: GzipDecoder, decoder: {type: XmlDecoder}} on SimpleRetriever yields:

[{'@id': '1', 'units': '42'}, {'@id': '2', 'units': '7'}]

Declarative-First Evaluation

Declarative approach — no custom Python component added. The requested behaviour is a nested decoder composition (GzipDecoderXmlDecoder), which no existing declarative feature (RecordFilter, AddFields/RemoveFields, transformations, $ref overrides, paginators, error handlers) can express: the payload must be decompressed and XML-parsed before records exist. The only ways forward were a CustomDecoder (unavailable in Cloud Builder — the whole point of the request; source-amazon-seller-partner ships exactly such a GzipXmlDecoder component today) or a built-in streaming parser in the CDK. This PR takes the latter, so the capability is available to every manifest-only connector.

Breaking Change Evaluation

Non-breaking, additive: no stream schema, primary key, cursor, state format, or spec field changes; no stream removed; no existing behaviour altered. Previously-invalid manifests become valid, and existing XmlDecoder (top-level) usage still goes through the unchanged XmlDecoder path. No connector version bump applies — this is a CDK library change, released via the repo's normal release-drafter flow.

Test Plan

  • poetry run pytest unit_tests/sources/declarative/decoders/ unit_tests/sources/declarative/parsers/test_model_to_component_factory.py -q — 237 passed
  • poetry run pytest unit_tests/ -x -q — 4376 passed, 2 skipped
  • poetry run ruff check ., poetry run ruff format --check ., poetry run mypy --config-file mypy.ini airbyte_cdk — clean

New tests:

  • test_xml_parser (parametrized: simple element, attributes + #text, malformed document), test_xml_parser_honors_encoding, test_gzip_parser_composes_with_xml_parser (gzipped and non-gzipped payload) in unit_tests/sources/declarative/decoders/test_composite_decoder.py
  • test_create_gzip_decoder_handles_compressed_xml_response in unit_tests/sources/declarative/parsers/test_model_to_component_factory.py, parametrized over emit_connector_builder_messages in [False, True] (Builder test read and sync path) and over headers including a case with no Content-Encoding header. This test fails on main at model construction (ValidationError).

Requested by Devin Bot via the /ai-fix workflow on the oncall issue.

Resolves https://github.com/airbytehq/oncall/issues/13387:

Link to Devin session: https://app.devin.ai/sessions/86a19a76f8494c46b2f68bca517a2894

Add a streaming XmlParser, register it in ModelToComponentFactory._get_parser, and allow XmlDecoder inside the GzipDecoder and ZipfileDecoder nested-decoder unions.

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This CDK Version

You 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/1787677661-declarative-xml-parser#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/1787677661-declarative-xml-parser

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /poetry-lock - Updates poetry.lock file
  • /test - Runs connector tests with the updated CDK
  • /prerelease - Triggers a prerelease publish with default arguments
  • /poe build - Regenerate git-committed build artifacts, such as the pydantic models which are generated from the manifest JSON schema in YAML.
  • /poe <command> - Runs any poe command in the CDK environment
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for composing gzip decoding with XML parsing in declarative (manifest-only / Connector Builder) connectors by introducing a stream-based XML parser and updating the schema + factory wiring so GzipDecoder { decoder: XmlDecoder } validates and runs.

Changes:

  • Added XmlParser (byte-stream compatible) and registered it in ModelToComponentFactory._get_parser for nested XmlDecoder usage.
  • Extended the declarative schema (YAML + generated model) so XmlDecoder is allowed as a nested decoder inside GzipDecoder and ZipfileDecoder.
  • Added unit tests covering GzipDecoder(XmlDecoder) behavior across header variants and XmlParser parsing behavior (including encoding + malformed XML).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
unit_tests/sources/declarative/parsers/test_model_to_component_factory.py Adds coverage for create_gzip_decoder composing with XmlDecoder in both sync and Builder modes.
unit_tests/sources/declarative/decoders/test_composite_decoder.py Adds unit tests for XmlParser and gzip composition.
airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py Registers XmlDecoderModel -> XmlParser for nested decoder composition.
airbyte_cdk/sources/declarative/models/declarative_component_schema.py Updates generated nested-decoder unions to include XmlDecoder for gzip/zipfile decoders.
airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py Introduces XmlParser that parses XML from a byte stream (enabling nesting under gzip/zipfile).
airbyte_cdk/sources/declarative/decoders/init.py Re-exports XmlParser.
airbyte_cdk/sources/declarative/declarative_component_schema.yaml Updates schema unions to allow XmlDecoder nested inside gzip/zipfile decoders.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py
Co-Authored-By: bot_apk <apk@cognition.ai>
@github-actions

Copy link
Copy Markdown

PyTest Results (Fast)

4 375 tests  +12   4 364 ✅ +12   10m 26s ⏱️ + 1m 39s
    1 suites ± 0      11 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 34a9539. ± Comparison against base commit 4855c2d.

@github-actions

Copy link
Copy Markdown

PyTest Results (Full)

4 378 tests  +12   4 366 ✅ +12   14m 8s ⏱️ +8s
    1 suites ± 0      12 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 34a9539. ± Comparison against base commit 4855c2d.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant