fix(declarative): support gzip-compressed XML decoding - #1132
fix(declarative): support gzip-compressed XML decoding#1132devin-ai-integration[bot] wants to merge 2 commits into
Conversation
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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
👋 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/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-parserPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
There was a problem hiding this comment.
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 inModelToComponentFactory._get_parserfor nestedXmlDecoderusage. - Extended the declarative schema (YAML + generated model) so
XmlDecoderis allowed as a nested decoder insideGzipDecoderandZipfileDecoder. - Added unit tests covering
GzipDecoder(XmlDecoder)behavior across header variants andXmlParserparsing 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.
Co-Authored-By: bot_apk <apk@cognition.ai>
Summary
Makes
GzipDecoder { decoder: XmlDecoder }expressible in manifest-only / Connector Builder connectors, so gzip-compressed XML payloads (e.g. a.xml.gzfetched from a presigned S3 URL) can be read without a custom Python component.Two independent blockers existed on
main:GzipDecoder.decoder(andZipfileDecoder.decoder)anyOfomittedXmlDecoder, so the model failed validation:ValidationError: ... decoder -> type: unexpected value; permitted: 'CsvDecoder' ... (given=XmlDecoder)ModelToComponentFactory._get_parserraisedValueError: Decoder type type='XmlDecoder' does not have parser associated to it,because
XmlDecoderis a legacy whole-requests.ResponseDecoder, not a streamingParser, so it could not be nested insideGzipParser.The fix adds a streaming
XmlParser(Parser)next toCsvParser/JsonParser, registers it in the factory, and addsXmlDecoderto both nested-decoder unions:xmltodict.parse()accepts a file-like object, so records keep the same shape as the existingXmlDecoder(@attribute prefix,#textfor text content of elements carrying attributes) while reading from a byte stream. Malformed-XML behaviour (warn + yield{}) matchesXmlDecoderfor 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 madeGzipParsersniff the1f 8bmagic bytes, so a singleGzipDecoder { decoder: XmlDecoder }now works in both the Builder test read and real syncs.Note on the generated model:
declarative_component_schema.pyis edited only where the schema change lands (the twoUnion[...]members). A fullpoe assembleon unmodifiedmaincurrently produces an unrelated ~650-line diff, so the generated file was kept minimal to keep the diff reviewable.Reproduction
On
main, constructing the model raisedValidationError(permitted values excludeXmlDecoder) and, once bypassed, the factory raisedValueError: Decoder type type='XmlDecoder' does not have parser associated to it.With this branch, an end-to-end read through
ConcurrentDeclarativeSourcewith a gzipped XML response (noContent-Encodingheader) anddecoder: {type: GzipDecoder, decoder: {type: XmlDecoder}}onSimpleRetrieveryields:Declarative-First Evaluation
Declarative approach — no custom Python component added. The requested behaviour is a nested decoder composition (
GzipDecoder→XmlDecoder), which no existing declarative feature (RecordFilter,AddFields/RemoveFields, transformations,$refoverrides, paginators, error handlers) can express: the payload must be decompressed and XML-parsed before records exist. The only ways forward were aCustomDecoder(unavailable in Cloud Builder — the whole point of the request;source-amazon-seller-partnerships exactly such aGzipXmlDecodercomponent 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 unchangedXmlDecoderpath. 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 passedpoetry run pytest unit_tests/ -x -q— 4376 passed, 2 skippedpoetry run ruff check .,poetry run ruff format --check .,poetry run mypy --config-file mypy.ini airbyte_cdk— cleanNew 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) inunit_tests/sources/declarative/decoders/test_composite_decoder.pytest_create_gzip_decoder_handles_compressed_xml_responseinunit_tests/sources/declarative/parsers/test_model_to_component_factory.py, parametrized overemit_connector_builder_messagesin[False, True](Builder test read and sync path) and over headers including a case with noContent-Encodingheader. This test fails onmainat model construction (ValidationError).Requested by Devin Bot via the
/ai-fixworkflow on the oncall issue.Resolves https://github.com/airbytehq/oncall/issues/13387:
Link to Devin session: https://app.devin.ai/sessions/86a19a76f8494c46b2f68bca517a2894