Repository: airbytehq/airbyte-python-cdk
Pull request #455: feat(low-code cdk): add streams limit to full resolve manifest command
Author: login: lazebnyi
Labels: name: enhancement
description: New feature or request
Created: 2025-04-01T12:24:16Z | Merged: 2025-04-01T14:16:21Z

## Details

Fixed: https://github.com/airbytehq/airbyte-internal-issues/issues/12275

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a configurable limit for dynamic stream generation (default value: 100), ensuring that only the allowed number of streams is generated during processing.
  
- **Refactor**
  - Updated the stream resolution logic to produce a cleaner manifest output by removing superfluous entries and aligning the stream handling with the new limit parameter.
  - Renamed the `TestReadLimits` class to `TestLimits` for consistency across the codebase.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/airbyte_cdk/connector_builder/connector_builder_handler.py b/airbyte_cdk/connector_builder/connector_builder_handler.py
index c80884d09..27929dfa2 100644
--- a/airbyte_cdk/connector_builder/connector_builder_handler.py
+++ b/airbyte_cdk/connector_builder/connector_builder_handler.py
@@ -4,7 +4,7 @@
 
 
 from dataclasses import asdict, dataclass, field
-from typing import Any, List, Mapping
+from typing import Any, Dict, List, Mapping
 
 from airbyte_cdk.connector_builder.test_reader import TestReader
 from airbyte_cdk.models import (
@@ -27,30 +27,34 @@
 DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE = 5
 DEFAULT_MAXIMUM_NUMBER_OF_SLICES = 5
 DEFAULT_MAXIMUM_RECORDS = 100
+DEFAULT_MAXIMUM_STREAMS = 100
 
 MAX_PAGES_PER_SLICE_KEY = "max_pages_per_slice"
 MAX_SLICES_KEY = "max_slices"
 MAX_RECORDS_KEY = "max_records"
+MAX_STREAMS_KEY = "max_streams"
 
 
 @dataclass
-class TestReadLimits:
+class TestLimits:
     max_records: int = field(default=DEFAULT_MAXIMUM_RECORDS)
     max_pages_per_slice: int = field(default=DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE)
     max_slices: int = field(default=DEFAULT_MAXIMUM_NUMBER_OF_SLICES)
+    max_streams: int = field(default=DEFAULT_MAXIMUM_STREAMS)
 
 
-def get_limits(config: Mapping[str, Any]) -> TestReadLimits:
+def get_limits(config: Mapping[str, Any]) -> TestLimits:
     command_config = config.get("__test_read_config", {})
     max_pages_per_slice = (
         command_config.get(MAX_PAGES_PER_SLICE_KEY) or DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE
     )
     max_slices = command_config.get(MAX_SLICES_KEY) or DEFAULT_MAXIMUM_NUMBER_OF_SLICES
     max_records = command_config.get(MAX_RECORDS_KEY) or DEFAULT_MAXIMUM_RECORDS
-    return TestReadLimits(max_records, max_pages_per_slice, max_slices)
+    max_streams = command_config.get(MAX_STREAMS_KEY) or DEFAULT_MAXIMUM_STREAMS
+    return TestLimits(max_records, max_pages_per_slice, max_slices, max_streams)
 
 
-def create_source(config: Mapping[str, Any], limits: TestReadLimits) -> ManifestDeclarativeSource:
+def create_source(config: Mapping[str, Any], limits: TestLimits) -> ManifestDeclarativeSource:
     manifest = config["__injected_declarative_manifest"]
     return ManifestDeclarativeSource(
         config=config,
@@ -71,7 +75,7 @@ def read_stream(
     config: Mapping[str, Any],
     configured_catalog: ConfiguredAirbyteCatalog,
     state: List[AirbyteStateMessage],
-    limits: TestReadLimits,
+    limits: TestLimits,
 ) -> AirbyteMessage:
     try:
         test_read_handler = TestReader(
@@ -117,13 +121,23 @@ def resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage:
         return error.as_airbyte_message()
 
 
-def full_resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage:
+def full_resolve_manifest(source: ManifestDeclarativeSource, limits: TestLimits) -> AirbyteMessage:
     try:
         manifest = {**source.resolved_manifest}
         streams = manifest.get("streams", [])
         for stream in streams:
             stream["dynamic_stream_name"] = None
-        streams.extend(source.dynamic_streams)
+
+        mapped_streams: Dict[str, List[Dict[str, Any]]] = {}
+        for stream in source.dynamic_streams:
+            generated_streams = mapped_streams.setdefault(stream["dynamic_stream_name"], [])
+
+            if len(generated_streams) < limits.max_streams:
+                generated_streams += [stream]
+
+        for generated_streams_list in mapped_streams.values():
+            streams.extend(generated_streams_list)
+
         manifest["streams"] = streams
         return AirbyteMessage(
             type=Type.RECORD,
diff --git a/airbyte_cdk/connector_builder/main.py b/airbyte_cdk/connector_builder/main.py
index 525df8752..ad2d6650f 100644
--- a/airbyte_cdk/connector_builder/main.py
+++ b/airbyte_cdk/connector_builder/main.py
@@ -10,7 +10,7 @@
 
 from airbyte_cdk.connector import BaseConnector
 from airbyte_cdk.connector_builder.connector_builder_handler import (
-    TestReadLimits,
+    TestLimits,
     create_source,
     full_resolve_manifest,
     get_limits,
@@ -73,7 +73,7 @@ def handle_connector_builder_request(
     config: Mapping[str, Any],
     catalog: Optional[ConfiguredAirbyteCatalog],
     state: List[AirbyteStateMessage],
-    limits: TestReadLimits,
+    limits: TestLimits,
 ) -> AirbyteMessage:
     if command == "resolve_manifest":
         return resolve_manifest(source)
@@ -83,7 +83,7 @@ def handle_connector_builder_request(
         ), "`test_read` requires a valid `ConfiguredAirbyteCatalog`, got None."
         return read_stream(source, config, catalog, state, limits)
     elif command == "full_resolve_manifest":
-        return full_resolve_manifest(source)
+        return full_resolve_manifest(source, limits)
     else:
         raise ValueError(f"Unrecognized command {command}.")
 
diff --git a/unit_tests/connector_builder/test_connector_builder_handler.py b/unit_tests/connector_builder/test_connector_builder_handler.py
index 4b3d80237..5c537811b 100644
--- a/unit_tests/connector_builder/test_connector_builder_handler.py
+++ b/unit_tests/connector_builder/test_connector_builder_handler.py
@@ -20,7 +20,7 @@
     DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE,
     DEFAULT_MAXIMUM_NUMBER_OF_SLICES,
     DEFAULT_MAXIMUM_RECORDS,
-    TestReadLimits,
+    TestLimits,
     create_source,
     get_limits,
     resolve_manifest,
@@ -384,6 +384,7 @@
 RESOLVE_DYNAMIC_STREAM_MANIFEST_CONFIG = {
     "__injected_declarative_manifest": DYNAMIC_STREAM_MANIFEST,
     "__command": "full_resolve_manifest",
+    "__test_read_config": {"max_streams": 2},
 }
 
 TEST_READ_CONFIG = {
@@ -524,7 +525,7 @@ def test_resolve_manifest(valid_resolve_manifest_config_file):
     command = "resolve_manifest"
     config["__command"] = command
     source = ManifestDeclarativeSource(source_config=MANIFEST)
-    limits = TestReadLimits()
+    limits = TestLimits()
     resolved_manifest = handle_connector_builder_request(
         source, command, config, create_configured_catalog("dummy_stream"), _A_STATE, limits
     )
@@ -728,7 +729,7 @@ def test_read():
             emitted_at=1,
         ),
     )
-    limits = TestReadLimits()
+    limits = TestLimits()
     with patch(
         "airbyte_cdk.connector_builder.test_reader.TestReader.run_test_read",
         return_value=stream_read,
@@ -789,7 +790,7 @@ def test_config_update() -> None:
             config,
             ConfiguredAirbyteCatalogSerializer.load(CONFIGURED_CATALOG),
             _A_PER_PARTITION_STATE,
-            TestReadLimits(),
+            TestLimits(),
         )
         assert output.record.data["latest_config_update"]
 
@@ -825,7 +826,7 @@ def check_config_against_spec(self) -> Literal[False]:
     mock_from_exception.return_value = stack_trace
 
     source = MockManifestDeclarativeSource()
-    limits = TestReadLimits()
+    limits = TestLimits()
     response = read_stream(
         source,
         TEST_READ_CONFIG,
@@ -865,7 +866,7 @@ def test_handle_429_response():
     ] = {"backoff_strategies": [{"type": "ConstantBackoffStrategy", "backoff_time_in_seconds": 5}]}
 
     config = TEST_READ_CONFIG
-    limits = TestReadLimits()
+    limits = TestLimits()
     source = create_source(config, limits)
 
     with patch("requests.Session.send", return_value=response) as mock_send:
@@ -982,7 +983,7 @@ def test_create_source():
     max_records = 3
     max_pages_per_slice = 2
     max_slices = 1
-    limits = TestReadLimits(max_records, max_pages_per_slice, max_slices)
+    limits = TestLimits(max_records, max_pages_per_slice, max_slices)
 
     config = {"__injected_declarative_manifest": MANIFEST}
 
@@ -1064,7 +1065,7 @@ def test_read_source(mock_http_stream):
     max_records = 100
     max_pages_per_slice = 2
     max_slices = 3
-    limits = TestReadLimits(max_records, max_pages_per_slice, max_slices)
+    limits = TestLimits(max_records, max_pages_per_slice, max_slices)
 
     catalog = ConfiguredAirbyteCatalog(
         streams=[
@@ -1111,7 +1112,7 @@ def test_read_source_single_page_single_slice(mock_http_stream):
     max_records = 100
     max_pages_per_slice = 1
     max_slices = 1
-    limits = TestReadLimits(max_records, max_pages_per_slice, max_slices)
+    limits = TestLimits(max_records, max_pages_per_slice, max_slices)
 
     catalog = ConfiguredAirbyteCatalog(
         streams=[
@@ -1195,7 +1196,7 @@ def test_handle_read_external_requests(deployment_mode, url_base, expected_error
     endpoints when running on Cloud or OSS deployments
     """
 
-    limits = TestReadLimits(max_records=100, max_pages_per_slice=1, max_slices=1)
+    limits = TestLimits(max_records=100, max_pages_per_slice=1, max_slices=1)
 
     catalog = ConfiguredAirbyteCatalog(
         streams=[
@@ -1281,7 +1282,7 @@ def test_handle_read_external_oauth_request(deployment_mode, token_url, expected
     endpoints when running on Cloud or OSS deployments
     """
 
-    limits = TestReadLimits(max_records=100, max_pages_per_slice=1, max_slices=1)
+    limits = TestLimits(max_records=100, max_pages_per_slice=1, max_slices=1)
 
     catalog = ConfiguredAirbyteCatalog(
         streams=[
@@ -1339,7 +1340,7 @@ def test_read_stream_exception_with_secrets():
         ]
     )
     state = []
-    limits = TestReadLimits()
+    limits = TestLimits()
 
     # Add the secret to be filtered
     update_secrets([config["api_key"]])
@@ -1367,7 +1368,7 @@ def test_full_resolve_manifest(valid_resolve_manifest_config_file):
     config = copy.deepcopy(RESOLVE_DYNAMIC_STREAM_MANIFEST_CONFIG)
     command = config["__command"]
     source = ManifestDeclarativeSource(source_config=DYNAMIC_STREAM_MANIFEST)
-    limits = TestReadLimits()
+    limits = TestLimits(max_streams=2)
     with HttpMocker() as http_mocker:
         http_mocker.get(
             HttpRequest(url="https://api.test.com/parents"),
@@ -1625,72 +1626,6 @@ def test_full_resolve_manifest(valid_resolve_manifest_config_file):
                 },
                 "dynamic_stream_name": "TestDynamicStream",
             },
-            {
-                "type": "DeclarativeStream",
-                "name": "parent_2_item_1",
-                "primary_key": [],
-                "schema_loader": {
-                    "type": "InlineSchemaLoader",
-                    "schema": {
-                        "$schema": "http://json-schema.org/schema#",
-                        "properties": {"ABC": {"type": "number"}, "AED": {"type": "number"}},
-                        "type": "object",
-                    },
-                },
-                "retriever": {
-                    "type": "SimpleRetriever",
-                    "requester": {
-                        "type": "HttpRequester",
-                        "url_base": "https://api.test.com",
-                        "path": "2/1",
-                        "http_method": "GET",
-                        "authenticator": {
-                            "type": "ApiKeyAuthenticator",
-                            "header": "apikey",
-                            "api_token": "{{ config['api_key'] }}",
-                        },
-                    },
-                    "record_selector": {
-                        "type": "RecordSelector",
-                        "extractor": {"type": "DpathExtractor", "field_path": []},
-                    },
-                    "paginator": {"type": "NoPagination"},
-                },
-                "dynamic_stream_name": "TestDynamicStream",
-            },
-            {
-                "type": "DeclarativeStream",
-                "name": "parent_2_item_2",
-                "primary_key": [],
-                "schema_loader": {
-                    "type": "InlineSchemaLoader",
-                    "schema": {
-                        "$schema": "http://json-schema.org/schema#",
-                        "properties": {"ABC": {"type": "number"}, "AED": {"type": "number"}},
-                        "type": "object",
-                    },
-                },
-                "retriever": {
-                    "type": "SimpleRetriever",
-                    "requester": {
-                        "type": "HttpRequester",
-                        "url_base": "https://api.test.com",
-                        "path": "2/2",
-                        "http_method": "GET",
-                        "authenticator": {
-                            "type": "ApiKeyAuthenticator",
-                            "header": "apikey",
-                            "api_token": "{{ config['api_key'] }}",
-                        },
-                    },
-                    "record_selector": {
-                        "type": "RecordSelector",
-                        "extractor": {"type": "DpathExtractor", "field_path": []},
-                    },
-                    "paginator": {"type": "NoPagination"},
-                },
-                "dynamic_stream_name": "TestDynamicStream",
-            },
         ],
         "check": {"type": "CheckStream", "stream_names": ["lists"]},
         "spec": {


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>airbyte_cdk/connector_builder/connector_builder_handler.py (1)</summary><blockquote>

`1-162`: _⚠️ Potential issue_

**Fix formatting issue flagged by the pipeline**

The pipeline failure indicates a formatting issue with this file that needs to be fixed.


According to the error message, you need to run 'poetry run ruff format' to fix the code style issues in this file.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: Linters</summary>

[error] 1-1: Ruff formatting check failed. 1 file would be reformatted. Please run 'poetry run ruff format' to fix code style issues in this file.

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b9f84d509553d4dccff91284a64642e44fa02e21 and 79631444439258208bf486c99d68cf6e72758f34.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `airbyte_cdk/connector_builder/connector_builder_handler.py` (4 hunks)
* `airbyte_cdk/connector_builder/main.py` (3 hunks)
* `unit_tests/connector_builder/test_connector_builder_handler.py` (14 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>airbyte_cdk/connector_builder/connector_builder_handler.py (4)</summary><blockquote>

<details>
<summary>airbyte_cdk/sources/declarative/manifest_declarative_source.py (4)</summary>

* `ManifestDeclarativeSource` (60-436)
* `resolved_manifest` (113-114)
* `streams` (144-169)
* `dynamic_streams` (121-124)

</details>
<details>
<summary>unit_tests/connector_builder/test_connector_builder_handler.py (2)</summary>

* `resolved_manifest` (678-679)
* `streams` (810-811)

</details>
<details>
<summary>airbyte_cdk/sources/declarative/concurrent_declarative_source.py (1)</summary>

* `streams` (179-188)

</details>
<details>
<summary>unit_tests/sources/mock_server_tests/mock_source_fixture.py (1)</summary>

* `streams` (406-413)

</details>

</blockquote></details>
<details>
<summary>unit_tests/connector_builder/test_connector_builder_handler.py (1)</summary><blockquote>

<details>
<summary>airbyte_cdk/connector_builder/connector_builder_handler.py (1)</summary>

* `TestLimits` (39-43)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: Linters</summary>

<details>
<summary>airbyte_cdk/connector_builder/connector_builder_handler.py</summary>

[error] 1-1: Ruff formatting check failed. 1 file would be reformatted. Please run 'poetry run ruff format' to fix code style issues in this file.

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* GitHub Check: Check: 'source-pokeapi' (skip=false)
* GitHub Check: Check: 'source-amplitude' (skip=false)
* GitHub Check: Check: 'source-shopify' (skip=false)
* GitHub Check: Check: 'source-hardcoded-records' (skip=false)
* GitHub Check: Pytest (All, Python 3.11, Ubuntu)
* GitHub Check: Pytest (All, Python 3.10, Ubuntu)
* GitHub Check: SDM Docker Image Build
* GitHub Check: Pytest (Fast)
* GitHub Check: Analyze (python)

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>airbyte_cdk/connector_builder/main.py (3)</summary>

`13-13`: **Updated import to use `TestLimits` instead of `TestReadLimits`**

The import has been updated to use the new `TestLimits` class instead of the previous `TestReadLimits`. This aligns with the renaming of the class in the connector_builder_handler.py file.

---

`76-76`: **Updated parameter type to `TestLimits`**

The parameter type in the function signature has been updated to use `TestLimits`, which is consistent with the renamed class.

---

`86-86`: **Added `limits` parameter to `full_resolve_manifest` call**

The function call has been updated to pass the `limits` parameter to the `full_resolve_manifest` function, which enables the stream limit functionality.

</details>
<details>
<summary>unit_tests/connector_builder/test_connector_builder_handler.py (4)</summary>

`23-23`: **Updated import to use `TestLimits` instead of `TestReadLimits`**

The import statement has been updated to reference the renamed class.

---

`387-387`: **Added max_streams configuration to test config**

The test configuration now includes the max_streams parameter with a value of 2, which is used to test the new stream limiting functionality.

---

`528-528`: **Updated all instances of TestReadLimits to TestLimits**

All object instantiations have been updated to use the renamed class. This ensures that the tests properly exercise the new functionality.



Also applies to: 732-732, 793-793, 829-829, 869-869, 986-986, 1068-1068, 1115-1115, 1199-1199, 1285-1285, 1343-1343

---

`1371-1371`: **Using TestLimits with max_streams in full_resolve_manifest test**

The test for full_resolve_manifest now explicitly sets max_streams to 2, ensuring that the new stream limiting functionality is tested properly.

</details>
<details>
<summary>airbyte_cdk/connector_builder/connector_builder_handler.py (5)</summary>

`7-7`: **Added Dict to imports**

Added Dict to the imports to properly type-hint the mapped_streams dictionary in the full_resolve_manifest function.

---

`30-30`: **Added constants for maximum streams**

Added constants to define the default maximum number of streams and the corresponding configuration key. This provides consistent values throughout the codebase.



Also applies to: 35-35

---

`39-43`: **Renamed class to TestLimits and added max_streams field**

The TestReadLimits class has been renamed to TestLimits and now includes a max_streams field with a default value of 100. This is a core part of the PR changes.

---

`57-57`: **Updated function signatures to use TestLimits**

The function signatures for create_source and read_stream have been updated to use the TestLimits type, which is consistent with the class rename.



Also applies to: 78-78

---

`124-142`: **Implemented stream limiting in full_resolve_manifest**

The full_resolve_manifest function now includes logic to limit the number of streams generated for each dynamic stream according to the max_streams setting. This is the core functionality being added in this PR.

The implementation:
1. Creates a dictionary to track generated streams by dynamic stream name
2. Restricts the number of generated streams to the configured limit
3. Extends the streams list with the limited number of generated streams

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T12:27:20Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T12:32:03Z
user: login: maxi297
body: Just one question but if this is what we want to achieve, I'm approving this PR
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T13:19:03Z
user: login: lazebnyi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T13:23:47Z

Review comments:
diff_hunk: @@ -27,30 +27,34 @@
 DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE = 5
 DEFAULT_MAXIMUM_NUMBER_OF_SLICES = 5
 DEFAULT_MAXIMUM_RECORDS = 100
+DEFAULT_MAXIMUM_STREAMS = 100
 
 MAX_PAGES_PER_SLICE_KEY = "max_pages_per_slice"
 MAX_SLICES_KEY = "max_slices"
 MAX_RECORDS_KEY = "max_records"
+MAX_STREAMS_KEY = "max_streams"
 
 
 @dataclass
-class TestReadLimits:
+class TestLimits:
     max_records: int = field(default=DEFAULT_MAXIMUM_RECORDS)
     max_pages_per_slice: int = field(default=DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE)
     max_slices: int = field(default=DEFAULT_MAXIMUM_NUMBER_OF_SLICES)
+    max_streams: int = field(default=DEFAULT_MAXIMUM_STREAMS)
 
 
-def get_limits(config: Mapping[str, Any]) -> TestReadLimits:
+def get_limits(config: Mapping[str, Any]) -> TestLimits:
     command_config = config.get("__test_read_config", {})
     max_pages_per_slice = (
         command_config.get(MAX_PAGES_PER_SLICE_KEY) or DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE
     )
     max_slices = command_config.get(MAX_SLICES_KEY) or DEFAULT_MAXIMUM_NUMBER_OF_SLICES
     max_records = command_config.get(MAX_RECORDS_KEY) or DEFAULT_MAXIMUM_RECORDS
-    return TestReadLimits(max_records, max_pages_per_slice, max_slices)
+    max_streams = command_config.get(MAX_STREAMS_KEY) or DEFAULT_MAXIMUM_RECORDS
+    return TestLimits(max_records, max_pages_per_slice, max_slices, max_streams)
path: airbyte_cdk/connector_builder/connector_builder_handler.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Updated get_limits function to support max_streams**

The get_limits function now includes logic to extract the max_streams value from the configuration and returns a TestLimits object with all parameters.

**However, there's a minor issue in the default value for max_streams:**


```diff
-    max_streams = command_config.get(MAX_STREAMS_KEY) or DEFAULT_MAXIMUM_RECORDS
+    max_streams = command_config.get(MAX_STREAMS_KEY) or DEFAULT_MAXIMUM_STREAMS
```

The code currently uses DEFAULT_MAXIMUM_RECORDS (which happens to be 100) as the fallback for max_streams, but for better semantic meaning and future maintenance, it should use DEFAULT_MAXIMUM_STREAMS. In this case, both constants have the same value, but using the correctly named constant would make the code more maintainable, wdyt?


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def get_limits(config: Mapping[str, Any]) -> TestLimits:
    command_config = config.get("__test_read_config", {})
    max_pages_per_slice = (
        command_config.get(MAX_PAGES_PER_SLICE_KEY) or DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE
    )
    max_slices = command_config.get(MAX_SLICES_KEY) or DEFAULT_MAXIMUM_NUMBER_OF_SLICES
    max_records = command_config.get(MAX_RECORDS_KEY) or DEFAULT_MAXIMUM_RECORDS
-    max_streams = command_config.get(MAX_STREAMS_KEY) or DEFAULT_MAXIMUM_RECORDS
+    max_streams = command_config.get(MAX_STREAMS_KEY) or DEFAULT_MAXIMUM_STREAMS
    return TestLimits(max_records, max_pages_per_slice, max_slices, max_streams)
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 6a96feb
created_at: 2025-04-01T12:27:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,13 +121,23 @@ def resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage:
         return error.as_airbyte_message()
 
 
-def full_resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage:
+def full_resolve_manifest(source: ManifestDeclarativeSource, limits: TestLimits) -> AirbyteMessage:
     try:
         manifest = {**source.resolved_manifest}
         streams = manifest.get("streams", [])
         for stream in streams:
             stream["dynamic_stream_name"] = None
-        streams.extend(source.dynamic_streams)
+
+        mapped_streams: Dict[str, List[Dict[str, Any]]] = {}
+        for stream in source.dynamic_streams:
+            generated_streams = mapped_streams.setdefault(stream["dynamic_stream_name"], [])
+
+            if len(generated_streams) < limits.max_streams:
path: airbyte_cdk/connector_builder/connector_builder_handler.py
user: login: maxi297
body: So if we have 3 dynamic streams, we could get 300 streams total, right? So we don't have a hard limit as a user could configure any number of dynamic streams but we assume that in general, they won't configure too many. Does that sound right? 
created_at: 2025-04-01T13:18:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,13 +121,23 @@ def resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage:
         return error.as_airbyte_message()
 
 
-def full_resolve_manifest(source: ManifestDeclarativeSource) -> AirbyteMessage:
+def full_resolve_manifest(source: ManifestDeclarativeSource, limits: TestLimits) -> AirbyteMessage:
     try:
         manifest = {**source.resolved_manifest}
         streams = manifest.get("streams", [])
         for stream in streams:
             stream["dynamic_stream_name"] = None
-        streams.extend(source.dynamic_streams)
+
+        mapped_streams: Dict[str, List[Dict[str, Any]]] = {}
+        for stream in source.dynamic_streams:
+            generated_streams = mapped_streams.setdefault(stream["dynamic_stream_name"], [])
+
+            if len(generated_streams) < limits.max_streams:
path: airbyte_cdk/connector_builder/connector_builder_handler.py
user: login: lazebnyi
body: Yes, we have a client-side limitation on the maximum value, so users cannot send excessively large values to us. Currently, the range is set from 1 to 100, but after testing, we will determine a more suitable value to ensure optimal rendering.
created_at: 2025-04-01T13:23:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: lazebnyi
created_at: 2025-04-01T12:26:10Z
body: /autofix
> **Auto-Fix Job Info**
>
> This job attempts to auto-fix any linting or formating issues. If any fixes are made,
> those changes will be automatically committed and pushed back to the PR.
>
> Note: This job can only be run by maintainers. On PRs from forks, this command requires
> that the PR author has enabled the `Allow edits from maintainers` option.

> PR auto-fix job started... [Check job output.][1]

[1]: https://github.com/airbytehq/airbyte-python-cdk/actions/runs/14195555430
> ✅ Changes applied successfully.
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-04-01T12:27:15Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough
The pull request renames the `TestReadLimits` class to `TestLimits` and introduces a new attribute `max_streams` with a default value of 100. Function signatures in `connector_builder_handler.py` (such as `get_limits`, `create_source`, `read_stream`, and `full_resolve_manifest`) now accept the updated type and enforce stream limits accordingly. The main file updates import statements and function parameters to accommodate `TestLimits`, while unit tests adjust object instantiation and manifest configurations by removing unnecessary entries and including the new limits setting.

## Changes

| File(s)                                                                                   | Change Summary                                                                                                                                                                                                                |
|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `airbyte_cdk/connector_builder/connector_builder_handler.py`                             | Renamed `TestReadLimits` to `TestLimits` and added a new attribute `max_streams` (default=100). Updated functions (`get_limits`, `create_source`, `read_stream`, `full_resolve_manifest`) to use `TestLimits` and apply stream limits. |
| `airbyte_cdk/connector_builder/main.py`                                                    | Updated import and function signature in `handle_connector_builder_request` to use `TestLimits` instead of `TestReadLimits`. Modified the call to `full_resolve_manifest` to pass the new limits parameter.             |
| `unit_tests/connector_builder/test_connector_builder_handler.py`                           | Replaced references to `TestReadLimits` with `TestLimits`. Updated tests to initialize using `TestLimits`, added `__test_read_config: {"max_streams": 2}` to configurations, and removed extraneous dynamic stream entries.     |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant C as ConnectorBuilderHandler
    participant L as TestLimits
    participant S as Source
    participant M as ManifestResolver

    C->>L: get_limits(config)
    L-->>C: return TestLimits(max_streams)
    C->>S: create_source(config, limits)
    S->>S: read_stream(..., limits)
    C->>M: full_resolve_manifest(source, limits)
    M->>L: Validate stream count using max_streams
    M-->>C: return AirbyteMessage with resolved manifest
```

## Possibly related PRs
- **airbytehq/airbyte-python-cdk#305:** The changes in this PR are related to the renaming of the `TestReadLimits` class to `TestLimits`, which is also reflected in the retrieved PR where the same class is renamed and its usage is updated in function signatures.
- **airbytehq/airbyte-python-cdk#293:** This PR involves similar changes as both rename `TestReadLimits` to `TestLimits` and update function signatures to reflect this change, ensuring consistency across the codebase.
- **airbytehq/airbyte-python-cdk#353:** The changes in this PR are related as they involve the renaming of `TestReadLimits` to `TestLimits`, affecting function signatures and parameter types in the same context of handling limits.

## Suggested reviewers
- maxi297
- artem1205

How does this look to you? Any adjustments you'd like to make?

</details>

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 90ca21f326dd40e7e78bba3a5e74213122132b16 and 6a96feb47923d0c50c99d523448e03713c9136d3.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `airbyte_cdk/connector_builder/connector_builder_handler.py` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* airbyte_cdk/connector_builder/connector_builder_handler.py

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* GitHub Check: Check: 'source-pokeapi' (skip=false)
* GitHub Check: Check: 'source-amplitude' (skip=false)
* GitHub Check: Check: 'source-shopify' (skip=false)
* GitHub Check: Check: 'source-hardcoded-records' (skip=false)
* GitHub Check: SDM Docker Image Build
* GitHub Check: Pytest (All, Python 3.11, Ubuntu)
* GitHub Check: Pytest (Fast)
* GitHub Check: Pytest (All, Python 3.10, Ubuntu)
* GitHub Check: Analyze (python)

</details>

</details>
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQ4rjedJAARABmJNQAFN74AO5gTEqMtADWAJRcaLT0iLgBaMzI3vDM6uH4kHE+fgGI+N5SkMyY8AllCs4YtDGQaWjIiNgCdbg09HLhsJ7YiJSQ3mgAXiQCGLI6mPSo8EoY4nHw0ROQGE4C6wAsAKzPGjDL1nYMmJAP6AwGNI1vRqEtPHEOqk0vAMERIABVGwAGS4AG1YHNuIgOAB6XFEdSwaYaJjMXHaChyGiwQIU+BU2Q0MDcJmwfAYTJ5XHcFq4l7PAC6SUxuGxeIJRJJZPpjJpdMp1JILLZHK5uR5fIF+Q07k+SkQDAo8G44g5/DwNXIyFwn3I0QIkGw3Fo1E8ts8AANmr4APptDpSX09DB9UKewYh+iOtCAkim9CQMoVKobWr1CRobwhd5wVBkWCYIFsc7oWo2xq8fA0MToYYoDBxbNkIHgpPwHb8OJtgOdaLJxLMcIkZjcTY0d4AIXktTHI/YsPhttQDELcJIABo23EKBywmR6EkAGIAUXyY3gvj+nhUUQakFI5CoNCT5UHyEhfESq8gtFkGEqeAGFfFN0GQe0lHoT821WdZYRoCg4ljTwkgRABJHUPhWF03RQZB4IPB1GlnXcug9JpsAwMR4A5LN1HkI4nUQFRL3ors2zJKML2XLByOhTJFE8ABhAARABpJoqDYNJ8AoXJdQMCxICElgS1wZBHGcVw9U8Tj2GQc1yN5K8AmCUIeGNHpjW8eQmAwRAkDCfBu1+NA8BYN16DabgOTWSSWBgtY+E2HY9gOfwSCIFxaEXOscAIMBLgAD0gIRBFzT43IS5LUsEPDf2keAiHIaNGmKWg2mQTB5CtcQ4X4PhPx6OZYqQKZpG4hBeM+LISAy3TVPYBsYp+GgbULMJyLSgQxhuWF1HgTy63oKsJFODq0DTDBcnvKa8vwPBeTCaDmgoD0+CUXBtG8RAFI8eLCBy6bZsmbA40QRAfRsyASCS0Q8DoLdGPI1dMFIZAemyWhsGNerl2QKsgQ+sZKBvdzmqArNvs49R5mWrxEGWBZYx2x0jJaCKzLKd40LCVAMGrB0JqWVBnp+LAOW+/4KEov55B6eDtCfRAgfraDjNaIIQjKAzjUJAC6vhHcAs/XIRbbLL2T4ZhVjCQsujIW8mc8GIAEFfHSH6YvU/yhwF84hcoRBRnwU0aKwaC9pmx1EZIOgFIAeSkKhfC3EHBvOZAAibUQwk233oZDp0cJfAJxyI7ohau+b6t6yBgjo3B5FtXdsCIWAHua6JkukLcyCmWGl2Z8mTKl8zimWNpds+KsRDEAByZAslisojmi26jH0YxwCgA92KywhH2UPG9POLheH4YRY8kDrFl6qhVHULQdCnkwoDzfCqqwBfiDIZfolXzh/DQNIkycKz5D3wSD7UTRtF0MAhhp6mAMIqJkJBfQMG5HZcgYhZK+gENgS8SgKC4hgbHeBiDkGUF9GuWgUQKAaFZBwAwMQyGKUsKbNCt8nxLSmFpeQzlGBrnBjpZhYMOrwV3NDVsm1yCvwYJsZGnpoChGROmdSnotwBDHLGWKRkAhrQOsgERoQbCJFoOI2YiAIyCImOrRi5VEz8PQHMY0iCXyeh6ElX0A5Kg6M6omJQSEfBhEzM2diABGAADN4/qFEqJmiwJ6UguBfQ1G0RGQsMg/ZYGYIoPoVxSoRVwDDa+WBYSjyop4JhqiyhaPUA4zJNBijsTybgdRxQCmSK3AzV+WZ2g8Fdj4ag8jPhWLQDYuxVQIzUHKPACxEJdxDnDo2IqMNWnmhGkElwsh/GeiNIkGgtiDoUCBBGZogT3aD3bMVagMNPDROvGQZOro8YxjjAmcp1Sin2RKfQXJoiyiVM0RInR7w2GegqLQWxb5KgbMotRc0RyHgnOdGcjOsYgRXKebgG5vSqo8BcJUEgCF5lfX9NIQMECQxhjKACrZHIdkOT2akgIkAQWxO6Aky4GdYSCOwNkT0ETClcFhfC0WxwbaziiGpSZWBKUnOKEIXWGc8CsU7ORDpXS/k9NMf0wZ7x7DcFEIkn4vhZBA0ztwbgsUmF/gAnUYC3TL4NnKIoN6Gd64HKWGCcidxmAPD4Ewpez5+yyuQLQfAHUGb7iSkCY2kB3EhCTCqhgiToiLGlb8lM7ysIbHwISYC0Ffo0GGG0r0JqIw1AGIKuJNKkn3ihapRQuF4a3BIK/HoOrFx13sjDNpYJObF0yhbNI0QHVOvYia/K5V/ZGCDsoUOHEWEdQLEWd0nxNlAoAjUIuEV07JO6WmbRfMzXcLerFPhlakVSVResRi8SYpxAOPVX6jlYrTqCRWJ04gaiSoQAjZFbAEI/TiAkaiUgbIKSUubBC/Kb3kSUHo587sDLdl+j5M60RZJeAEDUYCC5xDSEnspIRkwioKxteCzyXAFnoZgGojR8KGyQE9GA5ZUCNToLgRQBBSDvAoLQRyWBBA6NYMYzgvBBCiGyAjIAJMIyN6ORuyt5EZYRkYoxAqjzGMCscwQxpjNG2P0ewXR7jlBeOegMFAAAsqi9kpRMP7PJTh+YeHnEPlReEt5SQ7KXCIFwXTaAa1wjRMmLcpt9iCnPIAwjzziNidI+RhkSpIHQJYxg9jinKCyfk9FtTuCjg8dZAJsjlnQk2e0XZjkDmnMud1W5jzkAvOyB87oPQ/m4VBYkyFuU0mItyai6pzjqDlMKcSxpwhqWdOQH07aRQuysOmZTnQCzJBuyLLdCsmGQIctjMc31gri53PlE895rcLL1JsqI1Ut5vnKvOdDP0XAIlRCbFA1Iewqz1nBak+F6jkXaMtaU09lTHGUFJeGClvjkBBOeks1N5Z7RZskHm3lpbrmiCrYoOtsrm23k7fyftirS3juhDOyB1pV2bskHE8E+7Mn2sJda3F5rH2uPJc0z1vTBnBskuG9hCFtBxvdm+TGwcSQNDc/PFtnEVWXkkdq4Txr8WXuxeJ+L9TVPuu/f+5Z9n3Suc85XayqrQuCehfAQ9snz2Kdtbex11rX38HU7471/rhmhsmaZ7h9LE2KJ+l7EGXFJ2kgg7WSQfL6OyiY4u9jkg13QcHZK1rmg+mPpoFIPjyTYeGuPaa3rmLBvE/veTybn7aWAcO4xc7nFvQ3ce6BN7vFp3zvIvEDj0HCPtFI+q9okPps48R+YtHu7cedeS/17rtPnWZdadIeQnTICRcJ7F93+2vGSFkJiBQkr1DXV0PfrM9ioN1yIDYWv8GpGgN9G7LCCQ2KeyGzqPVJhUrYWC6C0XFV95rk1e6l6UfPejdMcn6lpol4+ofFQEetV/L0B31Y4ZBqwK5yJZxZIwhR4aA1J8ZyIr13ZrcyUcluxPQNNIFDcSdPtTJpZcBPR5k+cIxuBn190+AJMpV0Cu908cDwwAkZ18ozMxU/J7Ud179IlwhZBb9ikNEylL9AtIl3hTYSgFpaINUw4eosY79c8sU+xgwC9aDf8C06UqJsxsgpVCDd0UUEItw0gEAfxxhwJqwLISAlFVhvpiCPp/Z418Bg5JC7JzU/AmxLYAh7ZkBAVR1aAtwLE2xuM9Vuw+d8o6l0AahipogYRbQ2wTEb9PBnF5ogkf1KFvB/1r1u4Yjy9QNfJ2JINICYM+BeR4MgIfpzgFoUNetoBODbdzMyM+c68r92CJMEDzQ0CqcMDU9X8cEaD8V296tO9MCpdcR385dqjEd1cH86CgkyNKC+j9dMUqY8Duiwsidpjk8BihYB8oA0JRxIDTk7dvRhkyweiqNSRli1Nji2isDKdvs4ItizoBd+DCkY86tFjRdycVjBis9lYhxR8zjx9k8fjXi+8riyCbiwhRN6jNdDiXik81NVjYQB8Z9J4QFKJ1BfQxp1IX8LjUE0TWjfjATTdZdp9yFf0F8743VShl9XBV9R0N8DB7ot9OFH8LIbD1p6Bj1uxeBagXBLwZwMBD8+xj9DU/DExhMPx9jyk6iHi78wSHidC9CK5UBo4ogxBogJMdYkiTQ7xikJ1xpS5y4DpJpPg0TP8ogMoFST8t0gCxBxpPAtTiiADz9PgAjBA+46YsBMxjRlFhwBhGj7Itx6VVDYpPRsS885Cfc8CtwgzQhMVigpEyNsT7MipfRGDYzGJQCnZ/ETERSyNpTJEGCQRSNZFWxHldtXl2Dwj2Q8BgiEIM1NCX0nYkVLDWSG16pbTxB+U/T01RpG0DSIRAUZk515AXChYh5fJHJ2BBDhD+ybItUTFcgSBZAwBg1PBiCGQyNfRUSoz2cEzFsABvGIaxDnexGILgAAJgAF8okbhQVr4SgM4pUbATx7AA5kQAA1E8X0ESAATQADlTZdM0IhJfR7BoAHy/zfRdNTZvy0JTxgLfQhIA5vyjw0IABxCMaZd2WZfxNk4Ixpf0xlDqciFwmwrMbtEwodP4VIBgNWVfQSBdJaR0T0YggIc4X0E830dQEcX0TxXpesRilwdgVi9imA1ijZfYqVA1QCBgQ8uVNYegh0r0L838/8wC4C0C3TcCyC6Cx86AWM3QoCCuByUcGoWla0zOMMkCN6ZAtdEgFlBWDNPydgY0aQQOWw4dEGakkCQcK0SdAabIRYZiwCOGR0t5RgAjRiIitaeqSiWBYEFfRyq4UUgKciV3cyPSu8e2bOUMIKzwc9MoS9PsjCgc9iciNEieIwOfP9ZeMDVIgqLHFIphbI6DB5PI6YBDIoiIBK1DISAjAKtgFnbMksjXMjZEsJUqjE/o+Mk443LrLTP7Aa5HcE4a0MUa0IRAca7vSa84qXDPM3bTKAF8rko2HYqo5lEKgAXjuL22y25w0HyEeJGo3JlnWpWM2txOmv7w/3+wCIupzMQGV1uvuuWsevRKoJhNeoBPeqBPWMgBEiAhmUpMYP6s9AfKfNfPfMUr/IAqApApPDAogqgpgugDgoQqQtQuCwerGtBtJ3BuhMhvxNmqCLwoNDXOBujJ+W3K4D3IPJNWPMgHPPwMH1n2HyATPiKIeRcnchoXvnoEfi4CoFfnoQ/jXX3hUF/mPgAUMFFpxjCVOEQExTWkrToF+RcDCCngMFFoAHYABOAANgAGZPFHgnbHg7araTzngAAOE87xD2gQOIR4D2m2hgK2q22gG2j2hgOIG2kgC2k8i2z2uIO2x4DW82meSAK27xH4E8zxROk8m2koR4bxGOmO32gQNAO2tAZ4GOx4bOh2k82uk8gQTxG2lO0Wm2tAW2hIAQR4a2k8u22gTO54TOkO2gZ4Pup2j2kgbxO2i2zxO24Oue/Ou2lO1OiASMFE3W/Wq4dtH5OefQIAA=== -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=airbytehq/airbyte-python-cdk&utm_content=455):

> ‼️ **IMPORTANT**
> Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR