diff --git a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml index ff8f09ed1be43..148e53a32a971 100644 --- a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml @@ -786,11 +786,11 @@ - name: Recharge sourceDefinitionId: 45d2e135-2ede-49e1-939f-3e3ec357a65e dockerRepository: airbyte/source-recharge - dockerImageTag: 0.1.5 + dockerImageTag: 0.1.6 documentationUrl: https://docs.airbyte.io/integrations/sources/recharge icon: recharge.svg sourceType: api - releaseStage: alpha + releaseStage: beta - name: Recurly sourceDefinitionId: cd42861b-01fc-4658-a8ab-5d11d0510f01 dockerRepository: airbyte/source-recurly diff --git a/airbyte-config/init/src/main/resources/seed/source_specs.yaml b/airbyte-config/init/src/main/resources/seed/source_specs.yaml index e06d4b6e0f04b..f69e3db56f5e0 100644 --- a/airbyte-config/init/src/main/resources/seed/source_specs.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_specs.yaml @@ -7588,7 +7588,7 @@ supportsNormalization: false supportsDBT: false supported_destination_sync_modes: [] -- dockerImage: "airbyte/source-recharge:0.1.5" +- dockerImage: "airbyte/source-recharge:0.1.6" spec: documentationUrl: "https://docs.airbyte.com/integrations/sources/recharge" connectionSpecification: diff --git a/airbyte-integrations/connectors/source-recharge/Dockerfile b/airbyte-integrations/connectors/source-recharge/Dockerfile index 52327e862d226..9fd561a45427f 100644 --- a/airbyte-integrations/connectors/source-recharge/Dockerfile +++ b/airbyte-integrations/connectors/source-recharge/Dockerfile @@ -12,5 +12,5 @@ RUN pip install . ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=0.1.5 +LABEL io.airbyte.version=0.1.6 LABEL io.airbyte.name=airbyte/source-recharge diff --git a/airbyte-integrations/connectors/source-recharge/README.md b/airbyte-integrations/connectors/source-recharge/README.md index 31d452cd66fdb..2e5002b523cb0 100644 --- a/airbyte-integrations/connectors/source-recharge/README.md +++ b/airbyte-integrations/connectors/source-recharge/README.md @@ -26,8 +26,7 @@ If you are in an IDE, follow your IDE's instructions to activate the virtualenv. Note that while we are installing dependencies from `requirements.txt`, you should only edit `setup.py` for your dependencies. `requirements.txt` is used for editable installs (`pip install -e`) to pull in Python dependencies from the monorepo and will call `setup.py`. -If this is mumbo jumbo to you, don't worry about it, just put your deps in `setup.py` but install using `pip install -r requirements.txt` and everything -should work as you expect. +If this is mumbo jumbo to you, don't worry about it, just put your deps in `setup.py` but install using `pip install -r requirements.txt` and everything should work as you expect. #### Building via Gradle You can also build the connector in Gradle. This is typically used in CI and not needed for your development workflow. @@ -101,7 +100,8 @@ Customize `acceptance-test-config.yml` file to configure tests. See [Source Acce If your connector requires to create or destroy resources for use during acceptance tests create fixtures for it and place them inside integration_tests/acceptance.py. To run your integration tests with acceptance tests, from the connector root, run ``` -python -m pytest integration_tests -p integration_tests.acceptance +docker build . --no-cache -t airbyte/source-recharge:dev \ +&& python -m pytest -p integration_tests.acceptance ``` To run your integration tests with docker diff --git a/airbyte-integrations/connectors/source-recharge/acceptance-test-config.yml b/airbyte-integrations/connectors/source-recharge/acceptance-test-config.yml index 8595868f33e24..89a8dd0ce2df3 100644 --- a/airbyte-integrations/connectors/source-recharge/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-recharge/acceptance-test-config.yml @@ -13,6 +13,7 @@ tests: - config_path: "secrets/config.json" configured_catalog_path: "integration_tests/streams_with_output_records_catalog.json" timeout_seconds: 1200 + empty_streams: ["collections", "discounts"] incremental: - config_path: "secrets/config.json" configured_catalog_path: "integration_tests/streams_with_output_records_catalog.json" diff --git a/airbyte-integrations/connectors/source-recharge/integration_tests/acceptance.py b/airbyte-integrations/connectors/source-recharge/integration_tests/acceptance.py index 1302b2f57e10e..950b53b59d416 100644 --- a/airbyte-integrations/connectors/source-recharge/integration_tests/acceptance.py +++ b/airbyte-integrations/connectors/source-recharge/integration_tests/acceptance.py @@ -11,6 +11,4 @@ @pytest.fixture(scope="session", autouse=True) def connector_setup(): """This fixture is a placeholder for external resources that acceptance test might require.""" - # TODO: setup test dependencies if needed. otherwise remove the TODO comments yield - # TODO: clean up test dependencies diff --git a/airbyte-integrations/connectors/source-recharge/setup.py b/airbyte-integrations/connectors/source-recharge/setup.py index b743f6b25339d..a72e19629469a 100644 --- a/airbyte-integrations/connectors/source-recharge/setup.py +++ b/airbyte-integrations/connectors/source-recharge/setup.py @@ -11,6 +11,7 @@ TEST_REQUIREMENTS = [ "pytest~=6.1", + "requests-mock", ] setup( diff --git a/airbyte-integrations/connectors/source-recharge/source_recharge/api.py b/airbyte-integrations/connectors/source-recharge/source_recharge/api.py index aac02158c5bf9..70eaf5c1f159e 100644 --- a/airbyte-integrations/connectors/source-recharge/source_recharge/api.py +++ b/airbyte-integrations/connectors/source-recharge/source_recharge/api.py @@ -74,6 +74,10 @@ def __init__(self, start_date, **kwargs): super().__init__(**kwargs) self._start_date = pendulum.parse(start_date) + @property + def state_checkpoint_interval(self): + return self.limit + def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]: latest_benchmark = latest_record[self.cursor_field] if current_stream_state.get(self.cursor_field): diff --git a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/charges.json b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/charges.json index f85f4ea47c177..13df3e38b1042 100644 --- a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/charges.json +++ b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/charges.json @@ -88,7 +88,7 @@ "discount_codes": { "type": ["null", "array"], "items": { - "type": "object" + "type": ["null", "object"] } }, "email": { @@ -214,7 +214,7 @@ "type": ["null", "string"] }, "shopify_variant_id_not_found": { - "type": ["null", "integer", "string"] + "type": ["null", "integer"] }, "status": { "type": ["null", "string"] @@ -226,13 +226,34 @@ "type": ["null", "string"] }, "tax_lines": { - "type": ["null", "string", "number"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "total_discounts": { - "type": ["null", "string"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "total_line_items_price": { - "type": ["null", "string"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "total_price": { "type": ["null", "string"] @@ -241,7 +262,14 @@ "type": ["null", "string"] }, "total_tax": { - "type": ["null", "string", "number"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "total_weight": { "type": ["null", "integer"] diff --git a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/metafields.json b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/metafields.json index 08914dfb926e0..42c09d4de7b40 100644 --- a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/metafields.json +++ b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/metafields.json @@ -19,7 +19,14 @@ "type": ["null", "string"] }, "owner_id": { - "type": ["null", "integer", "string"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "owner_resource": { "type": ["null", "string"] @@ -29,7 +36,14 @@ "format": "date-time" }, "value": { - "type": ["null", "string", "number", "integer"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "value_type": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/onetimes.json b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/onetimes.json index 533537059d601..e010e4274f356 100644 --- a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/onetimes.json +++ b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/onetimes.json @@ -6,21 +6,42 @@ "type": ["null", "integer"] }, "address_id": { - "type": ["null", "integer", "string"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "created_at": { "type": ["null", "string"], "format": "date-time" }, "customer_id": { - "type": ["null", "integer", "string"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "next_charge_scheduled_at": { "type": ["null", "string"], "format": "date-time" }, "price": { - "type": ["null", "integer", "string", "number"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "product_title": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/orders.json b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/orders.json index 6aeb77e70f41c..5357ba81883f0 100644 --- a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/orders.json +++ b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/orders.json @@ -210,7 +210,7 @@ "type": ["null", "object"] }, "price": { - "type": ["null", "string"] + "type": ["null", "number"] }, "properties": { "type": ["null", "array"] @@ -219,10 +219,24 @@ "type": ["null", "integer"] }, "shopify_product_id": { - "type": ["null", "string", "integer"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "shopify_variant_id": { - "type": ["null", "string", "integer"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "sku": { "type": ["null", "string"] @@ -343,7 +357,7 @@ "type": ["null", "string"] }, "subtotal_price": { - "type": ["null", "number", "string"] + "type": ["null", "number"] }, "tags": { "type": ["null", "string"] @@ -355,19 +369,26 @@ "type": ["null", "string"] }, "total_discounts": { - "type": ["null", "string"] + "type": ["null", "number"] }, "total_line_items_price": { - "type": ["null", "string"] + "type": ["null", "number"] }, "total_price": { - "type": ["null", "string"] + "type": ["null", "number"] }, "total_refunds": { "type": ["null", "string"] }, "total_tax": { - "type": ["null", "number", "string"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] }, "total_weight": { "type": ["null", "integer"] diff --git a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/subscriptions.json b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/subscriptions.json index 603f403f0abd0..7f1b0a9bcd7d2 100644 --- a/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/subscriptions.json +++ b/airbyte-integrations/connectors/source-recharge/source_recharge/schemas/subscriptions.json @@ -85,7 +85,14 @@ "type": "string" }, "value": { - "type": ["string", "integer"] + "oneOf": [ + { + "type": ["null", "number"] + }, + { + "type": ["null", "string"] + } + ] } } } diff --git a/airbyte-integrations/connectors/source-recharge/source_recharge/source.py b/airbyte-integrations/connectors/source-recharge/source_recharge/source.py index e7f9cf7e5979a..5814114b68864 100644 --- a/airbyte-integrations/connectors/source-recharge/source_recharge/source.py +++ b/airbyte-integrations/connectors/source-recharge/source_recharge/source.py @@ -21,10 +21,12 @@ def get_auth_header(self) -> Mapping[str, Any]: class SourceRecharge(AbstractSource): def check_connection(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> Tuple[bool, any]: + auth = RechargeTokenAuthenticator(token=config["access_token"]) + stream = Shop(authenticator=auth) try: - auth = RechargeTokenAuthenticator(token=config["access_token"]) - list(Shop(authenticator=auth).read_records(SyncMode.full_refresh)) - return True, None + result = list(stream.read_records(SyncMode.full_refresh))[0] + if stream.name in result.keys(): + return True, None except Exception as error: return False, f"Unable to connect to Recharge API with the provided credentials - {repr(error)}" diff --git a/airbyte-integrations/connectors/source-recharge/unit_tests/test_api.py b/airbyte-integrations/connectors/source-recharge/unit_tests/test_api.py new file mode 100644 index 0000000000000..8b5a5adaa7387 --- /dev/null +++ b/airbyte-integrations/connectors/source-recharge/unit_tests/test_api.py @@ -0,0 +1,339 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + +from http import HTTPStatus +from unittest.mock import MagicMock, patch + +import pytest +import requests +from source_recharge.api import ( + Addresses, + Charges, + Collections, + Customers, + Discounts, + Metafields, + Onetimes, + Orders, + Products, + RechargeStream, + Shop, + Subscriptions, +) + + +# config +@pytest.fixture(name="config") +def config(): + return { + "authenticator": None, + "access_token": "access_token", + "start_date": "2021-08-15T00:00:00Z", + } + + +class TestCommon: + + main = RechargeStream() + + @pytest.mark.parametrize( + "stream_cls, expected", + [ + (Addresses, "id"), + (Charges, "id"), + (Collections, "id"), + (Customers, "id"), + (Discounts, "id"), + (Metafields, "id"), + (Onetimes, "id"), + (Orders, "id"), + (Products, "id"), + (Shop, ["shop", "store"]), + (Subscriptions, "id"), + ], + ) + def test_primary_key(self, stream_cls, expected): + assert expected == stream_cls.primary_key + + @pytest.mark.parametrize( + "stream_cls", + [ + (Addresses), + (Charges), + (Collections), + (Customers), + (Discounts), + (Metafields), + (Onetimes), + (Orders), + (Products), + (Shop), + (Subscriptions), + ], + ) + def test_url_base(self, stream_cls): + expected = self.main.url_base + result = stream_cls.url_base + assert expected == result + + @pytest.mark.parametrize( + "stream_cls", + [ + (Addresses), + (Charges), + (Collections), + (Customers), + (Discounts), + (Metafields), + (Onetimes), + (Orders), + (Products), + (Shop), + (Subscriptions), + ], + ) + def test_limit(self, stream_cls): + expected = self.main.limit + result = stream_cls.limit + assert expected == result + + @pytest.mark.parametrize( + "stream_cls", + [ + (Addresses), + (Charges), + (Collections), + (Customers), + (Discounts), + (Metafields), + (Onetimes), + (Orders), + (Products), + (Shop), + (Subscriptions), + ], + ) + def test_page_num(self, stream_cls): + expected = self.main.page_num + result = stream_cls.page_num + assert expected == result + + @pytest.mark.parametrize( + "stream_cls, stream_type, expected", + [ + (Addresses, "incremental", "addresses"), + (Charges, "incremental", "charges"), + (Collections, "full-refresh", "collections"), + (Customers, "incremental", "customers"), + (Discounts, "incremental", "discounts"), + (Metafields, "full-refresh", "metafields"), + (Onetimes, "incremental", "onetimes"), + (Orders, "incremental", "orders"), + (Products, "full-refresh", "products"), + (Shop, "full-refresh", None), + (Subscriptions, "incremental", "subscriptions"), + ], + ) + def test_data_path(self, config, stream_cls, stream_type, expected): + if stream_type == "incremental": + result = stream_cls(start_date=config["start_date"]).data_path + else: + result = stream_cls().data_path + assert expected == result + + @pytest.mark.parametrize( + "stream_cls, stream_type, expected", + [ + (Addresses, "incremental", "addresses"), + (Charges, "incremental", "charges"), + (Collections, "full-refresh", "collections"), + (Customers, "incremental", "customers"), + (Discounts, "incremental", "discounts"), + (Metafields, "full-refresh", "metafields"), + (Onetimes, "incremental", "onetimes"), + (Orders, "incremental", "orders"), + (Products, "full-refresh", "products"), + (Shop, "full-refresh", "shop"), + (Subscriptions, "incremental", "subscriptions"), + ], + ) + def test_path(self, config, stream_cls, stream_type, expected): + if stream_type == "incremental": + result = stream_cls(start_date=config["start_date"]).path() + else: + result = stream_cls().path() + assert expected == result + + @pytest.mark.parametrize( + ("http_status", "should_retry"), + [ + (HTTPStatus.OK, True), + (HTTPStatus.BAD_REQUEST, False), + (HTTPStatus.TOO_MANY_REQUESTS, True), + (HTTPStatus.INTERNAL_SERVER_ERROR, True), + ], + ) + def test_should_retry(patch_base_class, http_status, should_retry): + response_mock = MagicMock() + response_mock.status_code = http_status + stream = RechargeStream() + assert stream.should_retry(response_mock) == should_retry + + +class TestFullRefreshStreams: + def generate_records(self, stream_name, count): + result = [] + for i in range(0, count): + result.append({f"record_{i}": f"test_{i}"}) + return {stream_name: result} + + @pytest.mark.parametrize( + "stream_cls, rec_limit, expected", + [ + (Collections, 1, {"page": 2}), + (Metafields, 2, {"page": 2}), + (Products, 1, {"page": 2}), + (Shop, 1, {"page": 2}), + ], + ) + def test_next_page_token(self, stream_cls, rec_limit, requests_mock, expected): + stream = stream_cls() + stream.limit = rec_limit + url = f"{stream.url_base}{stream.path()}" + requests_mock.get(url, json=self.generate_records(stream.name, rec_limit)) + response = requests.get(url) + assert stream.next_page_token(response) == expected + + @pytest.mark.parametrize( + "stream_cls, next_page_token, stream_state, stream_slice, expected", + [ + (Collections, None, {}, {}, {"limit": 250}), + (Metafields, {"page": 2}, {"updated_at": "2030-01-01"}, {}, {"limit": 250, "page": 2}), + (Products, None, {}, {}, {"limit": 250}), + (Shop, None, {}, {}, {"limit": 250}), + ], + ) + def test_request_params(self, stream_cls, next_page_token, stream_state, stream_slice, expected): + stream = stream_cls() + result = stream.request_params(stream_state, stream_slice, next_page_token) + assert result == expected + + @pytest.mark.parametrize( + "stream_cls, data, expected", + [ + (Collections, [{"test": 123}], [{"test": 123}]), + (Metafields, [{"test2": 234}], [{"test2": 234}]), + (Products, [{"test3": 345}], [{"test3": 345}]), + (Shop, {"test4": 456}, [{"test4": 456}]), + ], + ) + def test_parse_response(self, stream_cls, data, requests_mock, expected): + stream = stream_cls() + url = f"{stream.url_base}{stream.path()}" + data = {stream.data_path: data} if stream.data_path else data + requests_mock.get(url, json=data) + response = requests.get(url) + assert list(stream.parse_response(response)) == expected + + @pytest.mark.parametrize( + "stream_cls, data, expected", + [ + (Collections, [{"test": 123}], [{"test": 123}]), + (Metafields, [{"test2": 234}], [{"test2": 234}]), + (Products, [{"test3": 345}], [{"test3": 345}]), + (Shop, {"test4": 456}, [{"test4": 456}]), + ], + ) + def get_stream_data(self, stream_cls, data, requests_mock, expected): + stream = stream_cls() + url = f"{stream.url_base}{stream.path()}" + data = {stream.data_path: data} if stream.data_path else data + requests_mock.get(url, json=data) + response = requests.get(url) + assert list(stream.parse_response(response)) == expected + + @pytest.mark.parametrize("owner_resource, expected", [({"customer": {"id": 123}}, {"customer": {"id": 123}})]) + def test_metafields_read_records(self, owner_resource, expected): + with patch.object(Metafields, "read_records", return_value=owner_resource): + result = Metafields().read_records(stream_slice={"owner_resource": owner_resource}) + assert result == expected + + +class TestIncrementalStreams: + def generate_records(self, stream_name, count): + result = [] + for i in range(0, count): + result.append({f"record_{i}": f"test_{i}"}) + return {stream_name: result} + + @pytest.mark.parametrize( + "stream_cls, expected", + [ + (Addresses, "updated_at"), + (Charges, "updated_at"), + (Customers, "updated_at"), + (Discounts, "updated_at"), + (Onetimes, "updated_at"), + (Orders, "updated_at"), + (Subscriptions, "updated_at"), + ], + ) + def test_cursor_field(self, config, stream_cls, expected): + stream = stream_cls(start_date=config["start_date"]) + result = stream.cursor_field + assert result == expected + + @pytest.mark.parametrize( + "stream_cls, rec_limit, expected", + [ + (Addresses, 1, {"page": 2}), + (Charges, 2, {"page": 2}), + (Customers, 1, {"page": 2}), + (Discounts, 1, {"page": 2}), + (Onetimes, 1, {"page": 2}), + (Orders, 1, {"page": 2}), + (Subscriptions, 1, {"page": 2}), + ], + ) + def test_next_page_token(self, config, stream_cls, rec_limit, requests_mock, expected): + stream = stream_cls(start_date=config["start_date"]) + stream.limit = rec_limit + url = f"{stream.url_base}{stream.path()}" + requests_mock.get(url, json=self.generate_records(stream.name, rec_limit)) + response = requests.get(url) + assert stream.next_page_token(response) == expected + + @pytest.mark.parametrize( + "stream_cls, next_page_token, stream_state, stream_slice, expected", + [ + (Addresses, None, {}, {}, {"limit": 250, "updated_at_min": "2021-08-15 00:00:00"}), + (Charges, {"page": 2}, {"updated_at": "2030-01-01"}, {}, {"limit": 250, "page": 2, "updated_at_min": "2030-01-01 00:00:00"}), + (Customers, None, {}, {}, {"limit": 250, "updated_at_min": "2021-08-15 00:00:00"}), + (Discounts, None, {}, {}, {"limit": 250, "updated_at_min": "2021-08-15 00:00:00"}), + (Onetimes, {"page": 2}, {"updated_at": "2030-01-01"}, {}, {"limit": 250, "page": 2, "updated_at_min": "2030-01-01 00:00:00"}), + (Orders, None, {}, {}, {"limit": 250, "updated_at_min": "2021-08-15 00:00:00"}), + (Subscriptions, None, {}, {}, {"limit": 250, "updated_at_min": "2021-08-15 00:00:00"}), + ], + ) + def test_request_params(self, config, stream_cls, next_page_token, stream_state, stream_slice, expected): + stream = stream_cls(start_date=config["start_date"]) + result = stream.request_params(stream_state, stream_slice, next_page_token) + assert result == expected + + @pytest.mark.parametrize( + "stream_cls, current_state, latest_record, expected", + [ + (Addresses, {}, {"updated_at": 2}, {"updated_at": 2}), + (Charges, {"updated_at": 2}, {"updated_at": 3}, {"updated_at": 3}), + (Customers, {"updated_at": 3}, {"updated_at": 4}, {"updated_at": 4}), + (Discounts, {}, {"updated_at": 2}, {"updated_at": 2}), + (Onetimes, {}, {"updated_at": 2}, {"updated_at": 2}), + (Orders, {"updated_at": 5}, {"updated_at": 5}, {"updated_at": 5}), + (Subscriptions, {"updated_at": 6}, {"updated_at": 7}, {"updated_at": 7}), + ], + ) + def test_get_updated_state(self, config, stream_cls, current_state, latest_record, expected): + stream = stream_cls(start_date=config["start_date"]) + result = stream.get_updated_state(current_state, latest_record) + assert result == expected diff --git a/airbyte-integrations/connectors/source-recharge/unit_tests/test_source.py b/airbyte-integrations/connectors/source-recharge/unit_tests/test_source.py new file mode 100644 index 0000000000000..b3dd4d62d2602 --- /dev/null +++ b/airbyte-integrations/connectors/source-recharge/unit_tests/test_source.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from unittest.mock import patch + +import pytest +from requests.exceptions import HTTPError +from source_recharge.api import Shop +from source_recharge.source import RechargeTokenAuthenticator, SourceRecharge + + +# config +@pytest.fixture(name="config") +def config(): + return { + "authenticator": None, + "access_token": "access_token", + "start_date": "2021-08-15T00:00:00Z", + } + + +# logger +@pytest.fixture(name="logger_mock") +def logger_mock_fixture(): + return patch("source_recharge.source.AirbyteLogger") + + +def test_get_auth_header(config): + expected = {"X-Recharge-Access-Token": config.get("access_token")} + actual = RechargeTokenAuthenticator(token=config["access_token"]).get_auth_header() + assert actual == expected + + +@pytest.mark.parametrize( + "patch, expected", + [ + ( + patch.object(Shop, "read_records", return_value=[{"shop": {"id": 123}}]), + (True, None), + ), + ( + patch.object(Shop, "read_records", side_effect=HTTPError(403)), + (False, "Unable to connect to Recharge API with the provided credentials - HTTPError(403)"), + ), + ], + ids=["success", "fail"], +) +def test_check_connection(logger_mock, config, patch, expected): + with patch: + result = SourceRecharge().check_connection(logger_mock, config=config) + assert result == expected + + +def test_streams(config): + streams = SourceRecharge().streams(config) + assert len(streams) == 11 diff --git a/airbyte-integrations/connectors/source-recharge/unit_tests/unit_test.py b/airbyte-integrations/connectors/source-recharge/unit_tests/unit_test.py deleted file mode 100644 index dddaea0060fa1..0000000000000 --- a/airbyte-integrations/connectors/source-recharge/unit_tests/unit_test.py +++ /dev/null @@ -1,7 +0,0 @@ -# -# Copyright (c) 2022 Airbyte, Inc., all rights reserved. -# - - -def test_example_method(): - assert True diff --git a/docs/integrations/sources/recharge.md b/docs/integrations/sources/recharge.md index 03495b966a502..0cd85835f0ecd 100644 --- a/docs/integrations/sources/recharge.md +++ b/docs/integrations/sources/recharge.md @@ -1,12 +1,47 @@ # Recharge -## Overview +his page guides you through the process of setting up the Recharge source connector. +This source can sync data for the [Recharge API](https://developer.rechargepayments.com/). + +## Prerequisites (Airbyte Cloud & Airbyte Open Source) +* A Recharge account with permission to access data from accounts you want to sync. +* Recharge API Token + +## Step 1: Set up Recharge + +Please read [How to generate your API token](https://support.rechargepayments.com/hc/en-us/articles/360008829993-ReCharge-API). + +## Step 2: Set up the source connector in Airbyte + +**For Airbyte Cloud:** + +1. [Log into your Airbyte Cloud](https://cloud.airbyte.io/workspaces) account. +2. In the left navigation bar, click **Sources**. In the top-right corner, click **+ new source**. +3. On the source setup page, select **Recharge** from the Source type dropdown and enter a name for this connector. +4. Choose required `Start date` +5. Enter your `Access Token`. +6. click `Set up source`. + +**For Airbyte OSS:** + +1. Go to local Airbyte page. +2. In the left navigation bar, click **Sources**. In the top-right corner, click **+ new source**. +3. On the source setup page, select **Recharge** from the Source type dropdown and enter a name for this connector. +4. Choose required `Start date` +5. Enter your `Access Token` generated from `Step 1`. +6. click `Set up source`. + +## Supported sync modes The Recharge supports full refresh and incremental sync. -This source can sync data for the [Recharge API](https://developer.rechargepayments.com/). +| Feature | Supported? | +| :--- | :--- | +| Full Refresh Sync | Yes | +| Incremental Sync | Yes | +| SSL connection | Yes | -### Output schema +## Supported Streams Several output streams are available from this source: @@ -24,32 +59,15 @@ Several output streams are available from this source: If there are more endpoints you'd like Airbyte to support, please [create an issue.](https://github.com/airbytehq/airbyte/issues/new/choose) -### Features - -| Feature | Supported? | -| :--- | :--- | -| Full Refresh Sync | Yes | -| Incremental Sync | Yes | -| SSL connection | Yes | - ### Performance considerations The Recharge connector should gracefully handle Recharge API limitations under normal usage. Please [create an issue](https://github.com/airbytehq/airbyte/issues) if you see any rate limit issues that are not automatically retried successfully. -## Getting started - -### Requirements - -* Recharge API Token - -### Setup guide - -Please read [How to generate your API token](https://support.rechargepayments.com/hc/en-us/articles/360008829993-ReCharge-API). - ## Changelog | Version | Date | Pull Request | Subject | | :--- | :--- | :--- | :--- | +| 0.1.6 | 2022-07-21 | [14902](https://github.com/airbytehq/airbyte/pull/14902) | Increased test coverage, fixed broken `charges`, `orders` schemas, added state checkpoint | | 0.1.5 | 2022-01-26 | [9808](https://github.com/airbytehq/airbyte/pull/9808) | Update connector fields title/description | | 0.1.4 | 2021-11-05 | [7626](https://github.com/airbytehq/airbyte/pull/7626) | Improve 'backoff' for HTTP requests | | 0.1.3 | 2021-09-17 | [6149](https://github.com/airbytehq/airbyte/pull/6149) | Update `discount` and `order` schema |