|
70 | 70 | from airbyte_cdk.sources.declarative.models import ( |
71 | 71 | CustomRecordExtractor as CustomRecordExtractorModel, |
72 | 72 | ) |
| 73 | +from airbyte_cdk.sources.declarative.models import CustomRequester as CustomRequesterModel |
73 | 74 | from airbyte_cdk.sources.declarative.models import CustomSchemaLoader as CustomSchemaLoaderModel |
74 | 75 | from airbyte_cdk.sources.declarative.models import DatetimeBasedCursor as DatetimeBasedCursorModel |
75 | 76 | from airbyte_cdk.sources.declarative.models import DeclarativeStream as DeclarativeStreamModel |
@@ -4312,6 +4313,96 @@ def test_api_budget_fixed_window_policy(): |
4312 | 4313 | assert matcher._url_path_pattern.pattern == "/v2/data" |
4313 | 4314 |
|
4314 | 4315 |
|
| 4316 | +def test_api_budget_propagated_to_custom_requester_subclass_of_http_requester(): |
| 4317 | + """Top-level `api_budget` must be forwarded to custom components that subclass `HttpRequester`. |
| 4318 | +
|
| 4319 | + Without this propagation, connectors using a `CustomRequester` (i.e., a Python subclass of |
| 4320 | + `HttpRequester`) silently lose the manifest-level rate-limit policies because |
| 4321 | + `create_custom_component` does not forward `self._api_budget` the way |
| 4322 | + `create_http_requester` does. See airbytehq/oncall#12011 for the reproducer. |
| 4323 | + """ |
| 4324 | + manifest_api_budget = { |
| 4325 | + "type": "HTTPAPIBudget", |
| 4326 | + "policies": [ |
| 4327 | + { |
| 4328 | + "type": "MovingWindowCallRatePolicy", |
| 4329 | + "rates": [ |
| 4330 | + { |
| 4331 | + "type": "Rate", |
| 4332 | + "limit": 60, |
| 4333 | + "interval": "PT1M", |
| 4334 | + } |
| 4335 | + ], |
| 4336 | + "matchers": [], |
| 4337 | + } |
| 4338 | + ], |
| 4339 | + } |
| 4340 | + |
| 4341 | + custom_requester_definition = { |
| 4342 | + "type": "CustomRequester", |
| 4343 | + "class_name": "unit_tests.sources.declarative.parsers.testing_components.TestingRequester", |
| 4344 | + "url_base": "https://example.org", |
| 4345 | + "path": "/v1/data", |
| 4346 | + "http_method": "GET", |
| 4347 | + } |
| 4348 | + |
| 4349 | + config: Mapping[str, Any] = {} |
| 4350 | + local_factory = ModelToComponentFactory() |
| 4351 | + local_factory.set_api_budget(manifest_api_budget, config) |
| 4352 | + |
| 4353 | + custom_requester = local_factory.create_component( |
| 4354 | + model_type=CustomRequesterModel, |
| 4355 | + component_definition=custom_requester_definition, |
| 4356 | + config=config, |
| 4357 | + name="custom_stream", |
| 4358 | + ) |
| 4359 | + |
| 4360 | + assert isinstance(custom_requester, HttpRequester) |
| 4361 | + assert custom_requester.api_budget is not None, ( |
| 4362 | + "Manifest-level api_budget was not propagated to the CustomRequester instance" |
| 4363 | + ) |
| 4364 | + assert len(custom_requester.api_budget._policies) == 1 |
| 4365 | + policy = custom_requester.api_budget._policies[0] |
| 4366 | + assert isinstance(policy, MovingWindowCallRatePolicy) |
| 4367 | + # Also verify the underlying HttpClient received the same budget |
| 4368 | + assert custom_requester._http_client._api_budget is custom_requester.api_budget |
| 4369 | + |
| 4370 | + |
| 4371 | +def test_api_budget_not_propagated_to_non_http_requester_custom_components(): |
| 4372 | + """Custom components that do NOT subclass `HttpRequester` must not receive `api_budget`. |
| 4373 | +
|
| 4374 | + This guards against accidentally injecting an `api_budget` kwarg into arbitrary custom |
| 4375 | + components (e.g., custom error handlers, partition routers) whose constructors would |
| 4376 | + reject the unexpected keyword. |
| 4377 | + """ |
| 4378 | + manifest_api_budget = { |
| 4379 | + "type": "HTTPAPIBudget", |
| 4380 | + "policies": [ |
| 4381 | + { |
| 4382 | + "type": "MovingWindowCallRatePolicy", |
| 4383 | + "rates": [{"type": "Rate", "limit": 1, "interval": "PT60S"}], |
| 4384 | + "matchers": [], |
| 4385 | + } |
| 4386 | + ], |
| 4387 | + } |
| 4388 | + |
| 4389 | + custom_error_handler_definition = { |
| 4390 | + "type": "CustomErrorHandler", |
| 4391 | + "class_name": "unit_tests.sources.declarative.parsers.testing_components.TestingSomeComponent", |
| 4392 | + "basic_field": "expected", |
| 4393 | + } |
| 4394 | + |
| 4395 | + config: Mapping[str, Any] = {} |
| 4396 | + local_factory = ModelToComponentFactory() |
| 4397 | + local_factory.set_api_budget(manifest_api_budget, config) |
| 4398 | + |
| 4399 | + # Must not raise TypeError about an unexpected "api_budget" kwarg. |
| 4400 | + custom_component = local_factory.create_component( |
| 4401 | + CustomErrorHandlerModel, custom_error_handler_definition, config |
| 4402 | + ) |
| 4403 | + assert custom_component.basic_field == "expected" |
| 4404 | + |
| 4405 | + |
4315 | 4406 | def test_create_grouping_partition_router_with_underlying_router(): |
4316 | 4407 | content = """ |
4317 | 4408 | schema_loader: |
|
0 commit comments