Skip to content

feat: Attempt to sync all streams instead of crashing on the first error - #3545

Merged
edgarrmondragon merged 21 commits into
feat/safely-ignore-errorsfrom
feature/continue-errors
Apr 24, 2026
Merged

edgarrmondragon merged 21 commits into
feat/safely-ignore-errorsfrom
feature/continue-errors

Conversation

@edgarrmondragon

@edgarrmondragon edgarrmondragon commented Mar 6, 2026 •

Copy link
Copy Markdown
Collaborator

Stub

Summary by Sourcery

Track per-stream sync results and allow tap runs to continue syncing remaining streams after individual failures while reporting an aggregate outcome.

New Features:

  • Introduce a SyncResult enum to represent and combine per-stream sync outcomes and map them to process exit codes.
  • Return an aggregate SyncResult from Tap.sync_all and use it to set the tap process exit code instead of always exiting successfully.

Bug Fixes:

  • Ensure errors in child streams mark the parent stream as partially successful instead of aborting remaining children and records.
  • Convert unexpected exceptions during stream sync into lifecycle abort exceptions so they are consistently handled and logged.

Enhancements:

  • Log a one-line sync outcome per stream and improve error log messages during sync to include exception details.
  • Extend parent/child stream typing with Context and Record aliases and apply override annotations for better type safety.
  • Adjust typing-extensions dependency marker for Python 3.13 compatibility.

Tests:

  • Add unit tests and snapshot tests covering SyncResult behavior and tap behavior when some streams fail while others continue syncing, including incremental and parent/child scenarios.

Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
@edgarrmondragon
edgarrmondragon requested a review from a team as a code owner March 6, 2026 19:22
@edgarrmondragon edgarrmondragon self-assigned this Mar 6, 2026
@sourcery-ai

sourcery-ai Bot commented Mar 6, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduce SyncResult-based per-stream sync outcome tracking, adjust tap-level sync_all and CLI exit codes to continue syncing remaining streams while distinguishing lifecycle aborts, and update typing, tests, and dependency constraints accordingly.

Sequence diagram for tap sync_all with per-stream SyncResult and exit codes

sequenceDiagram
    actor User
    participant CLI as tap_entrypoint
    participant Tap as TapBase
    participant Stream1 as Stream_orders
    participant Stream2 as Stream_customers

    User->>CLI: invoke()
    CLI->>Tap: invoke()
    Tap->>Tap: tap = TapClass()
    Tap->>Tap: result = tap.sync_all()

    rect rgb(235, 245, 255)
        Tap->>Stream1: sync()
        Stream1->>Stream1: _run_sync(context=None)
        Note over Stream1: Non-lifecycle error occurs
        Stream1-->>Stream1: _abort_sync(exc)
        Stream1-->>Tap: raise AbortedSyncFailedException
        Tap->>Tap: stream.sync_result already set = FAILED
        Tap->>Tap: result = result.combine(FAILED)
    end

    rect rgb(235, 245, 255)
        Tap->>Stream2: sync()
        Stream2->>Stream2: _run_sync(context=None)
        Stream2-->>Stream2: completes without error
        Stream2->>Stream2: sync_result = SUCCESS.combine(None)
        Stream2-->>Tap: return
        Tap->>Tap: stream.finalize_state_progress_markers()
        Tap->>Tap: result = result.combine(SUCCESS)
    end

    Tap->>Tap: for each stream: log_sync_result(logger, name, sync_result)
    Tap->>Tap: stream.log_sync_costs()

    Tap-->>CLI: return result (FAILED)
    CLI->>CLI: code = result.exit_code()  # 1
    CLI-->>User: process exit with code 1
Loading

Updated class diagram for Stream, TapBase, and SyncResult

classDiagram
    class SyncResult {
        <<enum>>
        SUCCESS
        FAILED
        ABORTED
        PARTIAL
        +SyncResult combine(other SyncResult)
        +int exit_code()
    }

    class Stream {
        -dict _schema
        -dict~str,int~ _sync_costs
        +list~Stream~ child_streams
        +SyncResult sync_result
        +sync(context Context)
        -_run_sync(context Context)
        -_sync_children(child_context Context)
        +finalize_state_progress_markers()
        +log_sync_costs()
    }

    class TapBase {
        -dict streams
        +SyncResult sync_all()
        +invoke()
    }

    class log_sync_result {
        <<function>>
        +log_sync_result(logger Logger, stream_name str, result SyncResult)
    }

    TapBase "1" --> "*" Stream : manages
    Stream --> SyncResult : uses
    TapBase --> SyncResult : returns
    log_sync_result --> SyncResult : logs
    TapBase ..> log_sync_result : calls
Loading

File-Level Changes

Change Details Files
Track per-stream sync outcomes and expose a unified SyncResult API, including logging helpers and tests.
  • Add a SyncResult enum with combine() and exit_code() helpers and a log_sync_result() utility for one-line per-stream outcome logging.
  • Expose SyncResult and log_sync_result via singer_sdk.streams, and add unit tests verifying combination precedence, logging behavior, and exit-code semantics.
singer_sdk/streams/_result.py
singer_sdk/streams/core.py
singer_sdk/tap_base.py
tests/core/streams/test_sync_result.py
Refactor stream sync flow to distinguish lifecycle aborts from generic errors and to record SyncResult per stream, including child streams.
  • Add a sync_result attribute to Stream, wrap the sync body in a new _run_sync() helper that converts unhandled exceptions to lifecycle aborts via _abort_sync(), and set sync_result based on whether sync completes, fails, or is aborted.
  • Update parent-child syncing so that child lifecycle aborts mark the parent stream as PARTIAL without short-circuiting remaining child processing.
  • Improve sync() docstrings to document lifecycle exceptions thrown during sync.
singer_sdk/streams/core.py
Change tap-level sync_all semantics to continue on non-lifecycle stream failures while aggregating SyncResult and returning an overall result used for CLI exit codes.
  • Modify Tap.sync_all() to return a SyncResult representing the aggregate of all stream results instead of raising on the first failure, combining per-stream sync_result via SyncResult.combine().
  • Catch lifecycle abort exceptions per stream to log failures while still allowing other streams to sync, always logging sync results and costs for all streams after the loop.
  • Update Tap.invoke() to call tap.sync_all(), then exit with the combined SyncResult.exit_code().
singer_sdk/tap_base.py
Tighten typing for parent/child stream contexts and use @OverRide annotations in tests.
  • Update test parent/child stream implementations to use Context and Record type aliases instead of raw dicts, and annotate overridden methods with @OverRide from typing or typing_extensions.
  • Adjust test code to align with new context typing expectations and parent-child APIs.
tests/core/test_parent_child.py
Add end-to-end tests and snapshots validating continue-on-error behavior, incremental resumability, and logging output.
  • Introduce a ContinueOnErrorsTap with a mix of normal, failing, incremental, and parent/child streams to exercise partial failures and resumable incremental syncs.
  • Add snapshot tests that capture singer.jsonl output, stderr logs, and state behavior across initial and resumed runs to ensure only resumable streams persist bookmarks after failure.
tests/core/test_continue_on_errors.py
tests/core/snapshots/test_continue_on_errors/test_continue_on_errors/singer.jsonl
tests/core/snapshots/test_continue_on_errors/test_continue_on_errors/singer_incrememtal.jsonl
tests/core/snapshots/test_continue_on_errors/test_continue_on_errors/stderr.log
tests/core/snapshots/test_continue_on_errors/test_continue_on_errors/stderr_incremental.log
Adjust dependency constraints for typing-extensions to reflect full Python version and refresh log snapshots impacted by new behavior.
  • Change the typing-extensions dependency marker to use python_full_version instead of python_version in pyproject.toml.
  • Update various stderr and singer.log snapshot files to match new sync logging behavior and per-stream result output.
pyproject.toml
tests/core/snapshots/test_parent_child/test_child_deselected_parent/stderr.log
tests/core/snapshots/test_parent_child/test_deselected_child/stderr.log
tests/core/snapshots/test_parent_child/test_one_parent_many_children/stderr.log
tests/core/snapshots/test_parent_child/test_parent_context_fields_in_child/stderr.log
tests/core/snapshots/test_parent_child/test_preprocess_context_removes_large_payload/stderr.log
tests/packages/snapshots/test_target_csv/test_countries_to_csv/singer.log
tests/packages/snapshots/test_target_csv/test_countries_to_csv_mapped/activate_version/singer.log
tests/packages/snapshots/test_target_csv/test_countries_to_csv_mapped/no_activate_version/singer.log
tests/packages/snapshots/test_target_csv/test_fake_people_to_csv/singer.log

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@edgarrmondragon
edgarrmondragon marked this pull request as draft March 6, 2026 19:22
@read-the-docs-community

read-the-docs-community Bot commented Mar 6, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 Meltano SDK | 🛠️ Build #32042204 | 📁 Comparing 5395658 against latest (8533f1a)


🔍 Preview build

Show files changed (10 files in total): 📝 10 modified | ➕ 0 added | ➖ 0 deleted
File Status
genindex.html 📝 modified
classes/singer_sdk.GraphQLStream.html 📝 modified
classes/singer_sdk.RESTStream.html 📝 modified
classes/singer_sdk.Stream.html 📝 modified
classes/singer_sdk.Tap.html 📝 modified
classes/singer_sdk.sql.SQLTap.html 📝 modified
implementation/index.html 📝 modified
implementation/errors/design.html 📝 modified
implementation/errors/hierarchy.html 📝 modified
implementation/errors/index.html 📝 modified

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 2 issues, and left some high level feedback:

  • The Context type alias was changed to MutableMapping[str, Any] while Stream.context is now a MappingProxyType | None; this makes the stored context effectively read‑only and inconsistent with the alias, so consider aligning these types (e.g., keep Context as a mapping and introduce a separate alias for mutable contexts where needed).
  • In sync_all, the error log for failed streams uses exc_info=exc.__cause__, which will often be None; if the intention is to capture the full traceback for the failure, you likely want exc_info=exc instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `Context` type alias was changed to `MutableMapping[str, Any]` while `Stream.context` is now a `MappingProxyType | None`; this makes the stored context effectively read‑only and inconsistent with the alias, so consider aligning these types (e.g., keep `Context` as a mapping and introduce a separate alias for mutable contexts where needed).
- In `sync_all`, the error log for failed streams uses `exc_info=exc.__cause__`, which will often be `None`; if the intention is to capture the full traceback for the failure, you likely want `exc_info=exc` instead.

## Individual Comments

### Comment 1
<location path="singer_sdk/tap_base.py" line_range="518-496" />
<code_context>
+                    )
+                    continue
+
+                try:
+                    stream.sync()
+                except Exception as exc:
+                    # stream.sync_result is already FAILED (set inside Stream.sync()).
</code_context>
<issue_to_address>
**issue (bug_risk):** Lifecycle abort exceptions from `stream.sync()` are being caught here despite the docstring stating they should propagate.

`Stream.sync()` now re-raises `AbortedSyncFailedException` / `AbortedSyncPausedException` so they can propagate, and the `sync_all` docstring says these should *not* be caught here. With `except Exception as exc:`, they are still treated as non-fatal and only logged, which breaks the documented lifecycle semantics. Please either re-raise these two exceptions explicitly in this block or narrow the `except` so they bypass the generic handler (e.g., a dedicated `except` for the abort exceptions before `Exception`).
</issue_to_address>

### Comment 2
<location path="tests/core/test_sync_outcomes.py" line_range="171-180" />
<code_context>
+    assert "Stream 'abort_paused' sync result: aborted" in caplog.text
+
+
+def test_summary_not_logged_for_never_synced(
+    caplog: pytest.LogCaptureFixture,
+) -> None:
+    """A stream that is skipped (deselected) must not produce a sync result line."""
+    tap = make_tap(GoodStream)
+    stream = tap.streams["good"]
+    # Patch selected / has_selected_descendents so sync_all skips this stream
+    type(stream).selected = property(lambda _: False)  # type: ignore[assignment]
+    type(stream).has_selected_descendents = property(  # type: ignore[assignment]
+        lambda _: False
+    )
+    with caplog.at_level("INFO", logger="root"):
+        tap.sync_all()
+    assert "sync result" not in caplog.text
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Avoid permanently patching type attributes in the deselected-stream test

In `test_summary_not_logged_for_never_synced`, `selected` and `has_selected_descendents` are reassigned on `type(stream)`, permanently mutating `GoodStream` for all later tests. This risks subtle cross-test interference if other tests rely on the default selection behavior. Instead, either use `monkeypatch.setattr` so the properties are restored automatically, or create a short-lived subclass of `GoodStream` with the overridden properties and pass that into `make_tap`.

Suggested implementation:

```python
def test_summary_not_logged_for_never_synced(
    caplog: pytest.LogCaptureFixture,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """A stream that is skipped (deselected) must not produce a sync result line."""
    tap = make_tap(GoodStream)
    stream = tap.streams["good"]
    stream_type = type(stream)

    # Temporarily patch selected / has_selected_descendents so sync_all skips this stream
    monkeypatch.setattr(
        stream_type,
        "selected",
        property(lambda _: False),
        raising=True,
    )
    monkeypatch.setattr(
        stream_type,
        "has_selected_descendents",
        property(lambda _: False),
        raising=True,
    )

    with caplog.at_level("INFO", logger="root"):
        tap.sync_all()
    assert "sync result" not in caplog.text

```

If `pytest.MonkeyPatch` is not already imported in this file (often it's just used via the fixture name without type annotations), you may need to add or adjust type hints, or remove the explicit `pytest.MonkeyPatch` annotation depending on your existing typing conventions for fixtures.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread singer_sdk/tap_base.py Outdated
Comment thread tests/core/test_sync_outcomes.py Outdated
@codecov

codecov Bot commented Mar 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.12%. Comparing base (111e1a0) to head (5395658).
⚠️ Report is 41 commits behind head on feat/safely-ignore-errors.

Files with missing lines Patch % Lines
singer_sdk/streams/_result.py 83.33% 5 Missing ⚠️
singer_sdk/tap_base.py 91.66% 1 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                      @@
##           feat/safely-ignore-errors    #3545      +/-   ##
=============================================================
+ Coverage                      93.74%   94.12%   +0.37%     
=============================================================
  Files                             73       74       +1     
  Lines                           5897     6617     +720     
  Branches                         724      893     +169     
=============================================================
+ Hits                            5528     6228     +700     
- Misses                           274      292      +18     
- Partials                          95       97       +2     
Flag Coverage Δ
core 83.72% <90.90%> (+1.56%) ⬆️
end-to-end 75.57% <68.18%> (+0.14%) ⬆️
optional-components 42.11% <30.30%> (-0.67%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed

codspeed Bot commented Mar 6, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 8 untouched benchmarks


Comparing feature/continue-errors (5395658) with feat/safely-ignore-errors (111e1a0)

Open in CodSpeed

Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
@edgarrmondragon edgarrmondragon changed the title refactor: Reraise stream errors as sync abortions feat: Attempt to sync all streams instead of crashing on the first error Mar 6, 2026
@edgarrmondragon edgarrmondragon added kind/Feature New feature or request Type/Tap Singer taps labels Mar 6, 2026
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
@edgarrmondragon
edgarrmondragon force-pushed the feature/continue-errors branch from 4b52595 to f23d3e1 Compare March 6, 2026 22:34
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
@edgarrmondragon
edgarrmondragon force-pushed the feature/continue-errors branch from c717d12 to c5240e6 Compare March 6, 2026 23:51
@edgarrmondragon
edgarrmondragon marked this pull request as ready for review March 6, 2026 23:52
@edgarrmondragon edgarrmondragon added this to the v0.54 milestone Mar 6, 2026

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 1 issue, and left some high level feedback:

  • In test_continue_on_errors, the assertion assert "repllication_key_value" not in bookmarks["incremental_with_errors"] has a typo in the key name and will not correctly guard against an incorrectly persisted replication_key_value bookmark.
  • In Tap.sync_all, the error log for failed streams uses exc.__cause__, which will often be None; logging exc (or str(exc)) instead would give a more reliable and informative message.
  • Several new snapshot filenames/labels (e.g. singer_incrememtal.jsonl) appear to be misspelled; if unintentional, correcting these now will avoid confusion for future readers and tooling.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `test_continue_on_errors`, the assertion `assert "repllication_key_value" not in bookmarks["incremental_with_errors"]` has a typo in the key name and will not correctly guard against an incorrectly persisted `replication_key_value` bookmark.
- In `Tap.sync_all`, the error log for failed streams uses `exc.__cause__`, which will often be `None`; logging `exc` (or `str(exc)`) instead would give a more reliable and informative message.
- Several new snapshot filenames/labels (e.g. `singer_incrememtal.jsonl`) appear to be misspelled; if unintentional, correcting these now will avoid confusion for future readers and tooling.

## Individual Comments

### Comment 1
<location path="singer_sdk/streams/core.py" line_range="1383-1392" />
<code_context>
                 for _ in self._sync_records(context=context):
                     pass
-        except Exception:
+        except (AbortedSyncFailedException, AbortedSyncPausedException):
+            raise
+        except Exception as exc:  # noqa: BLE001
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Overwriting `self.sync_result` with `PARTIAL` may accidentally downgrade a more severe result.

If `_sync_children` runs after a more severe outcome has already been set (e.g. `FAILED` or `ABORTED`), assigning `SyncResult.PARTIAL` directly would overwrite that and incorrectly lower the overall severity. To avoid this, use the existing aggregation logic, for example `self.sync_result = SyncResult.PARTIAL.combine(self.sync_result)`, so more severe results are preserved.

Suggested implementation:

```python
        self.sync_result = SyncResult.PARTIAL.combine(self.sync_result)

```

Because I only see a small portion of the file, you should:
1. Search `singer_sdk/streams/core.py` for any other occurrences of `self.sync_result = SyncResult.PARTIAL` and update them in the same way, using `SyncResult.PARTIAL.combine(self.sync_result)`.
2. Confirm that `SyncResult.combine` indeed keeps the highest severity when combining (the suggested order assumes it does). If its contract is different (e.g., argument precedence is reversed), adjust the call order accordingly, for example `self.sync_result = self.sync_result.combine(SyncResult.PARTIAL)`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread singer_sdk/streams/core.py
github-merge-queue Bot pushed a commit that referenced this pull request Mar 11, 2026
…ests (#3556)

https://github.com/meltano/sdk/blob/4a5dd894d69cc068d140acea6e15fa4040831611/singer_sdk/tap_base.py#L284-L286

## Related

- #3029
- #3030
- #3037
- #3545

## Summary by Sourcery

Adjust tap dry-run behavior to cap record counts on all streams while
ensuring parent streams continue emitting records when child streams hit
their record limits.

Bug Fixes:
- Prevent auto-generated dry-run syncs from aborting parent streams when
child streams reach their record limit by catching child abort
exceptions and stopping only remaining child syncs.
- Apply the dry-run record limit consistently to both parent and child
streams so all streams are subject to the same cap.

## Summary by Sourcery

Ensure dry-run syncs apply record limits consistently across parent and
child streams without preventing parent records from being emitted when
children hit their cap.

Bug Fixes:
- Apply the dry-run record limit to all streams, including parents and
children, instead of only non-child streams.
- Prevent child stream aborts due to dry-run record limits from stopping
sibling or parent stream processing by catching abort exceptions during
child sync.

Tests:
- Add a regression test verifying that parent and sibling records are
still emitted, and child records are correctly capped, when a child
stream hits the dry-run record limit.

---------

Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
@edgarrmondragon
edgarrmondragon merged commit 8f1375b into feat/safely-ignore-errors Apr 24, 2026
40 of 41 checks passed
@edgarrmondragon
edgarrmondragon deleted the feature/continue-errors branch April 24, 2026 18:52
@edgarrmondragon
edgarrmondragon restored the feature/continue-errors branch April 24, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/Feature New feature or request Type/Tap Singer taps

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: IgnorableAPIError to compliment RetriableAPIError and FatalAPIError

1 participant