Skip to content

Commit 1285668

Browse files
committed
Add unit tests
1 parent 15f830c commit 1285668

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

unit_tests/sources/declarative/parsers/test_model_to_component_factory.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
from airbyte_cdk.sources.declarative.transformations import AddFields, RemoveFields
143143
from airbyte_cdk.sources.declarative.transformations.add_fields import AddedFieldDefinition
144144
from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource
145+
from airbyte_cdk.sources.streams.call_rate import MovingWindowCallRatePolicy
145146
from airbyte_cdk.sources.streams.concurrent.clamping import (
146147
ClampingEndProvider,
147148
DayClampingStrategy,
@@ -3564,3 +3565,82 @@ def test_create_async_retriever():
35643565
assert isinstance(selector, RecordSelector)
35653566
assert isinstance(extractor, DpathExtractor)
35663567
assert extractor.field_path == ["data"]
3568+
3569+
3570+
def test_api_budget():
3571+
manifest = {
3572+
"type": "DeclarativeSource",
3573+
"api_budget": {
3574+
"type": "HTTPAPIBudget",
3575+
"ratelimit_reset_header": "X-RateLimit-Reset",
3576+
"ratelimit_remaining_header": "X-RateLimit-Remaining",
3577+
"status_codes_for_ratelimit_hit": [429, 503],
3578+
"policies": [
3579+
{
3580+
"type": "MovingWindowCallRatePolicy",
3581+
"rates": [
3582+
{
3583+
"type": "Rate",
3584+
"limit": 3,
3585+
"interval": "PT0.1S", # 0.1 seconds
3586+
}
3587+
],
3588+
"matchers": [
3589+
{
3590+
"type": "HttpRequestRegexMatcher",
3591+
"method": "GET",
3592+
"url_base": "https://api.sendgrid.com",
3593+
"url_path_pattern": "/v3/marketing/lists",
3594+
}
3595+
],
3596+
}
3597+
],
3598+
},
3599+
"my_requester": {
3600+
"type": "HttpRequester",
3601+
"path": "/v3/marketing/lists",
3602+
"url_base": "https://api.sendgrid.com",
3603+
"http_method": "GET",
3604+
"authenticator": {
3605+
"type": "BasicHttpAuthenticator",
3606+
"username": "admin",
3607+
"password": "{{ config['password'] }}",
3608+
},
3609+
},
3610+
}
3611+
3612+
config = {
3613+
"password": "verysecrettoken",
3614+
}
3615+
3616+
factory = ModelToComponentFactory()
3617+
if "api_budget" in manifest:
3618+
factory.set_api_budget(manifest["api_budget"], config)
3619+
3620+
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
3621+
HttpRequester as HttpRequesterModel,
3622+
)
3623+
3624+
requester_definition = manifest["my_requester"]
3625+
assert requester_definition["type"] == "HttpRequester"
3626+
3627+
http_requester = factory.create_component(
3628+
model_type=HttpRequesterModel,
3629+
component_definition=requester_definition,
3630+
config=config,
3631+
name="lists_stream",
3632+
decoder=None,
3633+
)
3634+
3635+
assert http_requester.api_budget is not None
3636+
assert http_requester.api_budget.ratelimit_reset_header == "X-RateLimit-Reset"
3637+
assert http_requester.api_budget.status_codes_for_ratelimit_hit == [429, 503]
3638+
assert len(http_requester.api_budget.policies) == 1
3639+
3640+
# The single policy is a MovingWindowCallRatePolicy
3641+
policy = http_requester.api_budget.policies[0]
3642+
assert isinstance(policy, MovingWindowCallRatePolicy)
3643+
assert policy._bucket.rates[0].limit == 3
3644+
# The 0.1s from 'PT0.1S' is stored in ms by PyRateLimiter internally
3645+
# but here just check that the limit and interval exist
3646+
assert policy._bucket.rates[0].interval == 100 # 100 ms

unit_tests/sources/declarative/requesters/test_http_requester.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
33
#
44

5+
from datetime import timedelta
56
from typing import Any, Mapping, Optional
67
from unittest import mock
78
from unittest.mock import MagicMock
89
from urllib.parse import parse_qs, urlparse
910

1011
import pytest as pytest
1112
import requests
13+
import requests.sessions
1214
from requests import PreparedRequest
1315

1416
from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator
@@ -27,6 +29,12 @@
2729
InterpolatedRequestOptionsProvider,
2830
)
2931
from airbyte_cdk.sources.message import MessageRepository
32+
from airbyte_cdk.sources.streams.call_rate import (
33+
AbstractAPIBudget,
34+
HttpAPIBudget,
35+
MovingWindowCallRatePolicy,
36+
Rate,
37+
)
3038
from airbyte_cdk.sources.streams.http.error_handlers.response_models import ResponseAction
3139
from airbyte_cdk.sources.streams.http.exceptions import (
3240
RequestBodyException,
@@ -45,6 +53,7 @@ def factory(
4553
request_options_provider: Optional[InterpolatedRequestOptionsProvider] = None,
4654
authenticator: Optional[DeclarativeAuthenticator] = None,
4755
error_handler: Optional[ErrorHandler] = None,
56+
api_budget: Optional[HttpAPIBudget] = None,
4857
config: Optional[Config] = None,
4958
parameters: Mapping[str, Any] = None,
5059
disable_retries: bool = False,
@@ -61,6 +70,7 @@ def factory(
6170
http_method=http_method,
6271
request_options_provider=request_options_provider,
6372
error_handler=error_handler,
73+
api_budget=api_budget,
6474
disable_retries=disable_retries,
6575
message_repository=message_repository or MagicMock(),
6676
use_cache=use_cache,
@@ -934,3 +944,25 @@ def test_backoff_strategy_from_manifest_is_respected(http_requester_factory: Any
934944
http_requester._http_client._request_attempt_count.get(request_mock)
935945
== http_requester._http_client._max_retries + 1
936946
)
947+
948+
949+
def test_http_requester_with_mock_apibudget(http_requester_factory, monkeypatch):
950+
mock_budget = MagicMock(spec=HttpAPIBudget)
951+
952+
requester = http_requester_factory(
953+
url_base="https://example.com",
954+
path="test",
955+
api_budget=mock_budget,
956+
)
957+
958+
dummy_response = requests.Response()
959+
dummy_response.status_code = 200
960+
send_mock = MagicMock(return_value=dummy_response)
961+
monkeypatch.setattr(requests.Session, "send", send_mock)
962+
963+
response = requester.send_request()
964+
965+
assert send_mock.call_count == 1
966+
assert response.status_code == 200
967+
968+
assert mock_budget.acquire_call.call_count == 1

0 commit comments

Comments
 (0)