From df1757cae35a24448c37102449cec0784df731be Mon Sep 17 00:00:00 2001 From: Johan Hammarstedt Date: Fri, 28 Oct 2022 10:07:16 +0200 Subject: [PATCH 1/9] draft for fastbill --- .../connectors/source-fastbill/.dockerignore | 6 + .../connectors/source-fastbill/Dockerfile | 38 ++ .../connectors/source-fastbill/README.md | 132 +++++++ .../acceptance-test-config.yml | 30 ++ .../source-fastbill/acceptance-test-docker.sh | 16 + .../connectors/source-fastbill/build.gradle | 9 + .../integration_tests/__init__.py | 3 + .../integration_tests/abnormal_state.json | 5 + .../integration_tests/acceptance.py | 16 + .../integration_tests/catalog.json | 39 +++ .../integration_tests/configured_catalog.json | 22 ++ .../integration_tests/invalid_config.json | 3 + .../integration_tests/sample_config.json | 3 + .../integration_tests/sample_state.json | 5 + .../connectors/source-fastbill/main.py | 13 + .../source-fastbill/requirements.txt | 2 + .../connectors/source-fastbill/setup.py | 29 ++ .../source_fastbill/__init__.py | 8 + .../sample_files/configured_catalog.json | 49 +++ .../source_fastbill/schemas/TODO.md | 25 ++ .../source_fastbill/schemas/customers.json | 161 +++++++++ .../source_fastbill/schemas/invoices.json | 241 +++++++++++++ .../source_fastbill/schemas/products.json | 39 +++ .../schemas/recurring_invoices.json | 215 ++++++++++++ .../source_fastbill/schemas/revenues.json | 271 ++++++++++++++ .../source-fastbill/source_fastbill/source.py | 331 ++++++++++++++++++ .../source-fastbill/source_fastbill/spec.yaml | 12 + .../source-fastbill/unit_tests/__init__.py | 3 + .../unit_tests/test_incremental_streams.py | 59 ++++ .../source-fastbill/unit_tests/test_source.py | 22 ++ .../unit_tests/test_streams.py | 83 +++++ 31 files changed, 1890 insertions(+) create mode 100644 airbyte-integrations/connectors/source-fastbill/.dockerignore create mode 100644 airbyte-integrations/connectors/source-fastbill/Dockerfile create mode 100644 airbyte-integrations/connectors/source-fastbill/README.md create mode 100644 airbyte-integrations/connectors/source-fastbill/acceptance-test-config.yml create mode 100644 airbyte-integrations/connectors/source-fastbill/acceptance-test-docker.sh create mode 100644 airbyte-integrations/connectors/source-fastbill/build.gradle create mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/__init__.py create mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/abnormal_state.json create mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/acceptance.py create mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/catalog.json create mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json create mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json create mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json create mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json create mode 100644 airbyte-integrations/connectors/source-fastbill/main.py create mode 100644 airbyte-integrations/connectors/source-fastbill/requirements.txt create mode 100644 airbyte-integrations/connectors/source-fastbill/setup.py create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/__init__.py create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/sample_files/configured_catalog.json create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/TODO.md create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml create mode 100644 airbyte-integrations/connectors/source-fastbill/unit_tests/__init__.py create mode 100644 airbyte-integrations/connectors/source-fastbill/unit_tests/test_incremental_streams.py create mode 100644 airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py create mode 100644 airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py diff --git a/airbyte-integrations/connectors/source-fastbill/.dockerignore b/airbyte-integrations/connectors/source-fastbill/.dockerignore new file mode 100644 index 0000000000000..501945bc2f715 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/.dockerignore @@ -0,0 +1,6 @@ +* +!Dockerfile +!main.py +!source_fastbill +!setup.py +!secrets diff --git a/airbyte-integrations/connectors/source-fastbill/Dockerfile b/airbyte-integrations/connectors/source-fastbill/Dockerfile new file mode 100644 index 0000000000000..796bd1df115e7 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/Dockerfile @@ -0,0 +1,38 @@ +FROM python:3.9.13-alpine3.15 as base + +# build and load all requirements +FROM base as builder +WORKDIR /airbyte/integration_code + +# upgrade pip to the latest version +RUN apk --no-cache upgrade \ + && pip install --upgrade pip \ + && apk --no-cache add tzdata build-base + + +COPY setup.py ./ +# install necessary packages to a temporary folder +RUN pip install --prefix=/install . + +# build a clean environment +FROM base +WORKDIR /airbyte/integration_code + +# copy all loaded and built libraries to a pure basic image +COPY --from=builder /install /usr/local +# add default timezone settings +COPY --from=builder /usr/share/zoneinfo/Etc/UTC /etc/localtime +RUN echo "Etc/UTC" > /etc/timezone + +# bash is installed for more convenient debugging. +RUN apk --no-cache add bash + +# copy payload code only +COPY main.py ./ +COPY source_fastbill ./source_fastbill + +ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" +ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] + +LABEL io.airbyte.version=0.1.0 +LABEL io.airbyte.name=airbyte/source-fastbill diff --git a/airbyte-integrations/connectors/source-fastbill/README.md b/airbyte-integrations/connectors/source-fastbill/README.md new file mode 100644 index 0000000000000..6f41c83e8f717 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/README.md @@ -0,0 +1,132 @@ +# Fastbill Source + +This is the repository for the Fastbill source connector, written in Python. +For information about how to use this connector within Airbyte, see [the documentation](https://docs.airbyte.io/integrations/sources/fastbill). + +## Local development + +### Prerequisites +**To iterate on this connector, make sure to complete this prerequisites section.** + +#### Minimum Python version required `= 3.9.0` + +#### Build & Activate Virtual Environment and install dependencies +From this connector directory, create a virtual environment: +``` +python -m venv .venv +``` + +This will generate a virtualenv for this module in `.venv/`. Make sure this venv is active in your +development environment of choice. To activate it from the terminal, run: +``` +source .venv/bin/activate +pip install -r requirements.txt +pip install '.[tests]' +``` +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. + +#### Building via Gradle +You can also build the connector in Gradle. This is typically used in CI and not needed for your development workflow. + +To build using Gradle, from the Airbyte repository root, run: +``` +./gradlew :airbyte-integrations:connectors:source-fastbill:build +``` + +#### Create credentials +**If you are a community contributor**, follow the instructions in the [documentation](https://docs.airbyte.io/integrations/sources/fastbill) +to generate the necessary credentials. Then create a file `secrets/config.json` conforming to the `source_fastbill/spec.yaml` file. +Note that any directory named `secrets` is gitignored across the entire Airbyte repo, so there is no danger of accidentally checking in sensitive information. +See `integration_tests/sample_config.json` for a sample config file. + +**If you are an Airbyte core member**, copy the credentials in Lastpass under the secret name `source fastbill test creds` +and place them into `secrets/config.json`. + +### Locally running the connector +``` +python main.py spec +python main.py check --config secrets/config.json +python main.py discover --config secrets/config.json +python main.py read --config secrets/config.json --catalog integration_tests/configured_catalog.json +``` + +### Locally running the connector docker image + +#### Build +First, make sure you build the latest Docker image: +``` +docker build . -t airbyte/source-fastbill:dev +``` + +You can also build the connector image via Gradle: +``` +./gradlew :airbyte-integrations:connectors:source-fastbill:airbyteDocker +``` +When building via Gradle, the docker image name and tag, respectively, are the values of the `io.airbyte.name` and `io.airbyte.version` `LABEL`s in +the Dockerfile. + +#### Run +Then run any of the connector commands as follows: +``` +docker run --rm airbyte/source-fastbill:dev spec +docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-fastbill:dev check --config /secrets/config.json +docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-fastbill:dev discover --config /secrets/config.json +docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/source-fastbill:dev read --config /secrets/config.json --catalog /integration_tests/configured_catalog.json +``` +## Testing +Make sure to familiarize yourself with [pytest test discovery](https://docs.pytest.org/en/latest/goodpractices.html#test-discovery) to know how your test files and methods should be named. +First install test dependencies into your virtual environment: +``` +pip install .[tests] +``` +### Unit Tests +To run unit tests locally, from the connector directory run: +``` +python -m pytest unit_tests +``` + +### Integration Tests +There are two types of integration tests: Acceptance Tests (Airbyte's test suite for all source connectors) and custom integration tests (which are specific to this connector). +#### Custom Integration tests +Place custom tests inside `integration_tests/` folder, then, from the connector root, run +``` +python -m pytest integration_tests +``` +#### Acceptance Tests +Customize `acceptance-test-config.yml` file to configure tests. See [Source Acceptance Tests](https://docs.airbyte.io/connector-development/testing-connectors/source-acceptance-tests-reference) for more information. +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 +``` +To run your integration tests with docker + +### Using gradle to run tests +All commands should be run from airbyte project root. +To run unit tests: +``` +./gradlew :airbyte-integrations:connectors:source-fastbill:unitTest +``` +To run acceptance and custom integration tests: +``` +./gradlew :airbyte-integrations:connectors:source-fastbill:integrationTest +``` + +## Dependency Management +All of your dependencies should go in `setup.py`, NOT `requirements.txt`. The requirements file is only used to connect internal Airbyte dependencies in the monorepo for local development. +We split dependencies between two groups, dependencies that are: +* required for your connector to work need to go to `MAIN_REQUIREMENTS` list. +* required for the testing need to go to `TEST_REQUIREMENTS` list + +### Publishing a new version of the connector +You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what? +1. Make sure your changes are passing unit and integration tests. +1. Bump the connector version in `Dockerfile` -- just increment the value of the `LABEL io.airbyte.version` appropriately (we use [SemVer](https://semver.org/)). +1. Create a Pull Request. +1. Pat yourself on the back for being an awesome contributor. +1. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master. diff --git a/airbyte-integrations/connectors/source-fastbill/acceptance-test-config.yml b/airbyte-integrations/connectors/source-fastbill/acceptance-test-config.yml new file mode 100644 index 0000000000000..0d7d630367c8d --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/acceptance-test-config.yml @@ -0,0 +1,30 @@ +# See [Source Acceptance Tests](https://docs.airbyte.io/connector-development/testing-connectors/source-acceptance-tests-reference) +# for more information about how to configure these tests +connector_image: airbyte/source-fastbill:dev +tests: + spec: + - spec_path: "source_fastbill/spec.yaml" + connection: + - config_path: "secrets/config.json" + status: "succeed" + - config_path: "integration_tests/invalid_config.json" + status: "failed" + discovery: + - config_path: "secrets/config.json" + basic_read: + - config_path: "secrets/config.json" + configured_catalog_path: "integration_tests/configured_catalog.json" + empty_streams: [] + # TODO uncomment this block to specify that the tests should assert the connector outputs the records provided in the input file a file + # expect_records: + # path: "integration_tests/expected_records.txt" + # extra_fields: no + # exact_order: no + # extra_records: yes + incremental: # TODO if your connector does not implement incremental sync, remove this block + - config_path: "secrets/config.json" + configured_catalog_path: "integration_tests/configured_catalog.json" + future_state_path: "integration_tests/abnormal_state.json" + full_refresh: + - config_path: "secrets/config.json" + configured_catalog_path: "integration_tests/configured_catalog.json" diff --git a/airbyte-integrations/connectors/source-fastbill/acceptance-test-docker.sh b/airbyte-integrations/connectors/source-fastbill/acceptance-test-docker.sh new file mode 100644 index 0000000000000..c51577d10690c --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/acceptance-test-docker.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +# Build latest connector image +docker build . -t $(cat acceptance-test-config.yml | grep "connector_image" | head -n 1 | cut -d: -f2-) + +# Pull latest acctest image +docker pull airbyte/source-acceptance-test:latest + +# Run +docker run --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /tmp:/tmp \ + -v $(pwd):/test_input \ + airbyte/source-acceptance-test \ + --acceptance-test-config /test_input + diff --git a/airbyte-integrations/connectors/source-fastbill/build.gradle b/airbyte-integrations/connectors/source-fastbill/build.gradle new file mode 100644 index 0000000000000..a23711c3afee8 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'airbyte-python' + id 'airbyte-docker' + id 'airbyte-source-acceptance-test' +} + +airbytePython { + moduleDirectory 'source_fastbill' +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/__init__.py b/airbyte-integrations/connectors/source-fastbill/integration_tests/__init__.py new file mode 100644 index 0000000000000..1100c1c58cf51 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/__init__.py @@ -0,0 +1,3 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/abnormal_state.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/abnormal_state.json new file mode 100644 index 0000000000000..52b0f2c2118f4 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/abnormal_state.json @@ -0,0 +1,5 @@ +{ + "todo-stream-name": { + "todo-field-name": "todo-abnormal-value" + } +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/acceptance.py b/airbyte-integrations/connectors/source-fastbill/integration_tests/acceptance.py new file mode 100644 index 0000000000000..1302b2f57e10e --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/acceptance.py @@ -0,0 +1,16 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +import pytest + +pytest_plugins = ("source_acceptance_test.plugin",) + + +@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-fastbill/integration_tests/catalog.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/catalog.json new file mode 100644 index 0000000000000..6799946a68514 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/catalog.json @@ -0,0 +1,39 @@ +{ + "streams": [ + { + "name": "TODO fix this file", + "supported_sync_modes": ["full_refresh", "incremental"], + "source_defined_cursor": true, + "default_cursor_field": "column1", + "json_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "column1": { + "type": "string" + }, + "column2": { + "type": "number" + } + } + } + }, + { + "name": "table1", + "supported_sync_modes": ["full_refresh", "incremental"], + "source_defined_cursor": false, + "json_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "column1": { + "type": "string" + }, + "column2": { + "type": "number" + } + } + } + } + ] +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json new file mode 100644 index 0000000000000..36f0468db0d8f --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json @@ -0,0 +1,22 @@ +{ + "streams": [ + { + "stream": { + "name": "customers", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, + { + "stream": { + "name": "employees", + "json_schema": {}, + "supported_sync_modes": ["full_refresh", "incremental"] + }, + "sync_mode": "incremental", + "destination_sync_mode": "append" + } + ] +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json new file mode 100644 index 0000000000000..f3732995784f2 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json @@ -0,0 +1,3 @@ +{ + "todo-wrong-field": "this should be an incomplete config file, used in standard tests" +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json new file mode 100644 index 0000000000000..ecc4913b84c74 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json @@ -0,0 +1,3 @@ +{ + "fix-me": "TODO" +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json new file mode 100644 index 0000000000000..3587e579822d0 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json @@ -0,0 +1,5 @@ +{ + "todo-stream-name": { + "todo-field-name": "value" + } +} diff --git a/airbyte-integrations/connectors/source-fastbill/main.py b/airbyte-integrations/connectors/source-fastbill/main.py new file mode 100644 index 0000000000000..9b1acb2e2026e --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/main.py @@ -0,0 +1,13 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_fastbill import SourceFastbill + +if __name__ == "__main__": + source = SourceFastbill() + launch(source, sys.argv[1:]) diff --git a/airbyte-integrations/connectors/source-fastbill/requirements.txt b/airbyte-integrations/connectors/source-fastbill/requirements.txt new file mode 100644 index 0000000000000..0411042aa0911 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/requirements.txt @@ -0,0 +1,2 @@ +-e ../../bases/source-acceptance-test +-e . diff --git a/airbyte-integrations/connectors/source-fastbill/setup.py b/airbyte-integrations/connectors/source-fastbill/setup.py new file mode 100644 index 0000000000000..b3c25a63759e2 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/setup.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from setuptools import find_packages, setup + +MAIN_REQUIREMENTS = [ + "airbyte-cdk~=0.1.56", +] + +TEST_REQUIREMENTS = [ + "pytest~=6.1", + "pytest-mock~=3.6.1", + "source-acceptance-test", +] + +setup( + name="source_fastbill", + description="Source implementation for Fastbill.", + author="Airbyte", + author_email="contact@airbyte.io", + packages=find_packages(), + install_requires=MAIN_REQUIREMENTS, + package_data={"": ["*.json", "*.yaml", "schemas/*.json", "schemas/shared/*.json"]}, + extras_require={ + "tests": TEST_REQUIREMENTS, + }, +) diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/__init__.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/__init__.py new file mode 100644 index 0000000000000..d1caf1ad30261 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/__init__.py @@ -0,0 +1,8 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from .source import SourceFastbill + +__all__ = ["SourceFastbill"] diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/sample_files/configured_catalog.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/sample_files/configured_catalog.json new file mode 100644 index 0000000000000..3e36914dbc8f2 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/sample_files/configured_catalog.json @@ -0,0 +1,49 @@ +{ + "streams": [ + { + "stream": { + "name": "customers", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, + { + "stream": { + "name": "recurring_invoices", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, + { + "stream": { + "name": "invoices", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, + { + "stream": { + "name": "products", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, + { + "stream": { + "name": "revenues", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + } + ] +} diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/TODO.md b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/TODO.md new file mode 100644 index 0000000000000..cf1efadb3c9c9 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/TODO.md @@ -0,0 +1,25 @@ +# TODO: Define your stream schemas +Your connector must describe the schema of each stream it can output using [JSONSchema](https://json-schema.org). + +The simplest way to do this is to describe the schema of your streams using one `.json` file per stream. You can also dynamically generate the schema of your stream in code, or you can combine both approaches: start with a `.json` file and dynamically add properties to it. + +The schema of a stream is the return value of `Stream.get_json_schema`. + +## Static schemas +By default, `Stream.get_json_schema` reads a `.json` file in the `schemas/` directory whose name is equal to the value of the `Stream.name` property. In turn `Stream.name` by default returns the name of the class in snake case. Therefore, if you have a class `class EmployeeBenefits(HttpStream)` the default behavior will look for a file called `schemas/employee_benefits.json`. You can override any of these behaviors as you need. + +Important note: any objects referenced via `$ref` should be placed in the `shared/` directory in their own `.json` files. + +## Dynamic schemas +If you'd rather define your schema in code, override `Stream.get_json_schema` in your stream class to return a `dict` describing the schema using [JSONSchema](https://json-schema.org). + +## Dynamically modifying static schemas +Override `Stream.get_json_schema` to run the default behavior, edit the returned value, then return the edited value: +``` +def get_json_schema(self): + schema = super().get_json_schema() + schema['dynamically_determined_property'] = "property" + return schema +``` + +Delete this file once you're done. Or don't. Up to you :) diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json new file mode 100644 index 0000000000000..58825ef22cba7 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json @@ -0,0 +1,161 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": ["CUSTOMER_ID"], + "properties": { + "CUSTOMER_ID": { + "type": "string" + }, + "CUSTOMER_NUMBER": { + "type": "string" + }, + "DAYS_FOR_PAYMENT": { + "type": "string", + "empty": true + }, + "CREATED": { + "type": "string", + "format": "date-time" + }, + "PAYMENT_TYPE": { + "type": "string", + "empty": true + }, + "BANK_NAME": { + "type": "string", + "empty": true + }, + "BANK_ACCOUNT_NUMBER": { + "type": "string", + "empty": true + }, + "BANK_CODE": { + "type": "string", + "empty": true + }, + "BANK_ACCOUNT_OWNER": { + "type": "string", + "empty": true + }, + "BANK_IBAN": { + "type": "string", + "empty": true + }, + "BANK_BIC": { + "type": "string", + "empty": true + }, + "BANK_ACCOUNT_MANDATE_REFERENCE": { + "type": "string", + "empty": true + }, + "SHOW_PAYMENT_NOTICE": { + "type": "string", + "empty": true + }, + "CUSTOMER_ACCOUNT": { + "type": "string", + "empty": true + }, + "CUSTOMER_TYPE": { + "type": "string", + "empty": true + }, + "TOP": { + "type": "string", + "empty": true + }, + "NEWSLETTER_OPTIN": { + "type": "string", + "empty": true + }, + "ORGANIZATION": { + "type": "string", + "empty": true + }, + "POSITION": { + "type": "string", + "empty": true + }, + "ACADEMIC_DEGREE": { + "type": "string", + "empty": true + }, + "SALUTATION": { + "type": "string", + "empty": true + }, + "FIRST_NAME": { + "type": "string", + "empty": true + }, + "LAST_NAME": { + "type": "string", + "empty": true + }, + "ADDRESS": { + "type": "string", + "empty": true + }, + "ADDRESS_2": { + "type": "string", + "empty": true + }, + "ZIPCODE": { + "type": "string", + "empty": true + }, + "CITY": { + "type": "string", + "empty": true + }, + "COUNTRY_CODE": { + "type": "string" + }, + "SECONDARY_ADDRESS": { + "type": "string", + "empty": true + }, + "PHONE": { + "type": "string", + "empty": true + }, + "PHONE_2": { + "type": "string", + "empty": true + }, + "FAX": { + "type": "string", + "empty": true + }, + "MOBILE": { + "type": "string", + "empty": true + }, + "EMAIL": { + "type": "string", + "empty": true + }, + "WEBSITE": { + "type": "string", + "empty": true + }, + "VAT_ID": { + "type": "string" + }, + "CURRENCY_CODE": { + "type": "string" + }, + "LASTUPDATE": { + "type": "string" + }, + "TAGS": { + "type": "string", + "empty": true + }, + "DOCUMENT_HISTORY_URL": { + "type": "string", + "empty": true + } + } +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json new file mode 100644 index 0000000000000..24179f41c77ae --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json @@ -0,0 +1,241 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": ["INVOICE_ID"], + "properties": { + "INVOICE_ID": { + "type": "string" + }, + "TYPE": { + "type": "string" + }, + "CUSTOMER_ID": { + "type": "string" + }, + "CUSTOMER_NUMBER": { + "type": "string" + }, + "CUSTOMER_COSTCENTER_ID": { + "type": "string" + }, + "CONTACT_ID": { + "type": "string" + }, + "PROJECT_ID": { + "type": "string" + }, + "CURRENCY_CODE": { + "type": "string" + }, + "DELIVERY_DATE": { + "type": "string" + }, + "INVOICE_TITLE": { + "type": "string", + "empty": true + }, + "CASH_DISCOUNT_PERCENT": { + "type": "string" + }, + "CASH_DISCOUNT_DAYS": { + "type": "string" + }, + "SUB_TOTAL": { + "type": "number" + }, + "VAT_TOTAL": { + "type": "number" + }, + "VAT_CASE": { + "type": "string" + }, + "VAT_ITEMS": { + "type": ["null","array"], + "items": [ + { + "type": "object", + "properties": { + "VAT_PERCENT": { + "type": "integer" + }, + "COMPLETE_NET": { + "type": "number" + }, + "VAT_VALUE": { + "type": "number" + } + }, + "required": [ + "VAT_PERCENT", + "COMPLETE_NET", + "VAT_VALUE" + ] + } + ] + }, + "ITEMS": { + "type": ["null","array"], + "items": [ + { + "type": "object", + "properties": { + "INVOICE_ITEM_ID": { + "type": "integer" + }, + "ARTICLE_NUMBER": { + "type": "string" + }, + "DESCRIPTION": { + "type": "string", + "empty": true + }, + "QUANTITY": { + "type": "integer" + }, + "UNIT_PRICE": { + "type": "number" + }, + "VAT_PERCENT": { + "type": "integer" + }, + "VAT_VALUE": { + "type": "number" + }, + "COMPLETE_NET": { + "type": "number" + }, + "COMPLETE_GROSS": { + "type": "number" + }, + "CATEGORY": { + "type": ["null","array"], + "items": {} + }, + "CATEGORY_ID": { + "type": ["null","array"], + "items": {} + }, + "SORT_ORDER": { + "type": "integer" + } + } + } + ] + }, + "TOTAL": { + "type": "number" + }, + "ORGANIZATION": { + "type": "string", + "empty": true + }, + "NOTE": { + "type": "string", + "empty": true + }, + "SALUTATION": { + "type": "string", + "empty": true + }, + "FIRST_NAME": { + "type": "string", + "empty": true + }, + "LAST_NAME": { + "type": "string", + "empty": true + }, + "ADDRESS": { + "type": "string", + "empty": true + }, + "ADDRESS_2": { + "type": "string", + "empty": true + }, + "ZIPCODE": { + "type": "string", + "empty": true + }, + "CITY": { + "type": "string", + "empty": true + }, + "SERVICE_PERIOD_START": { + "type": "string" + }, + "SERVICE_PERIOD_END": { + "type": "string" + }, + "PAYMENT_TYPE": { + "type": "string", + "empty": true + }, + "BANK_NAME": { + "type": "string", + "empty": true + }, + "BANK_ACCOUNT_NUMBER": { + "type": "string", + "empty": true + }, + "BANK_CODE": { + "type": "string", + "empty": true + }, + "BANK_ACCOUNT_OWNER": { + "type": "string", + "empty": true + }, + "BANK_IBAN": { + "type": "string", + "empty": true + }, + "BANK_BIC": { + "type": "string", + "empty": true + }, + "COUNTRY_CODE": { + "type": "string" + }, + "VAT_ID": { + "type": "string" + }, + "TEMPLATE_ID": { + "type": "string", + "empty": true + }, + "INVOICE_NUMBER": { + "type": "string" + }, + "INTROTEXT": { + "type": "string", + "empty": true + }, + "PAID_DATE": { + "type": "string" + }, + "IS_CANCELED": { + "type": "string" + }, + "INVOICE_DATE": { + "type": "string" + }, + "DUE_DATE": { + "type": "string" + }, + "PAYMENT_INFO": { + "type": "string" + }, + "PAYMENTS": { + "type": ["null","array"], + "items": {} + }, + "LASTUPDATE": { + "type": "string" + }, + "DOCUMENT_URL": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json new file mode 100644 index 0000000000000..6dbda3994c8e4 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": ["ARTICLE_ID"], + "properties": { + "ARTICLE_ID": { + "type": "string" + }, + "ARTICLE_NUMBER": { + "type": "string" + }, + "TITLE": { + "type": "string" + }, + "DESCRIPTION": { + "type": "string", + "empty": true + }, + "UNIT": { + "type": "string" + }, + "UNIT_PRICE": { + "type": "string" + }, + "CURRENCY_CODE": { + "type": "string" + }, + "VAT_PERCENT": { + "type": "string" + }, + "IS_GROSS": { + "type": "number" + }, + "TAGS": { + "type": ["string", "null"], + "empty": true + } + } +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json new file mode 100644 index 0000000000000..0a94100b6bb88 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json @@ -0,0 +1,215 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": ["object"], + "required": ["INVOICE_ID"], + "properties": { + "INVOICE_ID": { + "type": ["string"] + }, + "TYPE": { + "type": ["null", "string"] + }, + "CUSTOMER_ID": { + "type": ["null", "string"] + }, + "CUSTOMER_NUMBER": { + "type": ["null", "string"] + }, + "CUSTOMER_COSTCENTER_ID": { + "type": ["null", "string"] + }, + "CONTACT_ID": { + "type": ["null", "string"] + }, + "PROJECT_ID": { + "type": ["null", "string"] + }, + "CURRENCY_CODE": { + "type": ["null", "string"] + }, + "DELIVERY_DATE": { + "type": ["null", "string"] + }, + "INVOICE_TITLE": { + "type": ["null", "string"] + }, + "CASH_DISCOUNT_PERCENT": { + "type": ["null", "string"] + }, + "CASH_DISCOUNT_DAYS": { + "type": ["null", "string"] + }, + "SUB_TOTAL": { + "type": ["null", "number"] + }, + "VAT_TOTAL": { + "type": ["null", "number"] + }, + "VAT_CASE": { + "type": ["null", "string"] + }, + "VAT_ITEMS": { + "type": ["null", "array"], + "items": [ + { + "type": ["null", "object"], + "properties": { + "VAT_PERCENT": { + "type": ["null", "number"] + }, + "COMPLETE_NET": { + "type": ["null", "number"] + }, + "VAT_VALUE": { + "type": ["null", "number"] + } + } + } + ] + }, + "ITEMS": { + "type": ["null", "array"], + "items": [ + { + "type": ["null", "object"], + "properties": { + "INVOICE_ITEM_ID": { + "type": ["null", "number"] + }, + "ARTICLE_NUMBER": { + "type": ["null", "string"] + }, + "DESCRIPTION": { + "type": ["null", "string"] + }, + "QUANTITY": { + "type": ["null", "number"] + }, + "UNIT_PRICE": { + "type": ["null", "number"] + }, + "VAT_PERCENT": { + "type": ["null", "number"] + }, + "VAT_VALUE": { + "type": ["null", "number"] + }, + "COMPLETE_NET": { + "type": ["null", "number"] + }, + "COMPLETE_GROSS": { + "type": ["null", "number"] + }, + "CATEGORY": { + "type": ["null","string", "array"], + "empty": true, + "items": {} + }, + "CATEGORY_ID": { + "type": ["null","integer","array"], + "items": {} + }, + "SORT_ORDER": { + "type": ["null", "number"] + } + } + } + ] + }, + "TOTAL": { + "type": ["null", "number"] + }, + "ORGANIZATION": { + "type": ["null", "string"], + "empty": true + }, + "NOTE": { + "type": ["null", "string"], + "empty": true + }, + "SALUTATION": { + "type": ["null", "string"], + "empty": true + }, + "FIRST_NAME": { + "type": ["null", "string"], + "empty": true + }, + "LAST_NAME": { + "type": ["null", "string"], + "empty": true + }, + "ADDRESS": { + "type": ["null", "string"], + "empty": true + }, + "ADDRESS_2": { + "type": ["null", "string"], + "empty": true + }, + "ZIPCODE": { + "type": ["null", "string"], + "empty": true + }, + "CITY": { + "type": ["null", "string"], + "empty": true + }, + "SERVICE_PERIOD_START": { + "type": ["null", "string"] + }, + "SERVICE_PERIOD_END": { + "type": ["null", "string"] + }, + "PAYMENT_TYPE": { + "type": ["null", "string"] + }, + "BANK_NAME": { + "type": ["null", "string"], + "empty": true + }, + "BANK_ACCOUNT_NUMBER": { + "type": ["null", "string"], + "empty": true + }, + "BANK_CODE": { + "type": ["null", "string"], + "empty": true + }, + "BANK_ACCOUNT_OWNER": { + "type": ["null", "string"], + "empty": true + }, + "BANK_IBAN": { + "type": ["null", "string"], + "empty": true + }, + "BANK_BIC": { + "type": ["null", "string"], + "empty": true + }, + "TEMPLATE_ID": { + "type": ["null", "string"], + "empty": true + }, + "OCCURENCES": { + "type": ["null", "string"] + }, + "FREQUENCY": { + "type": ["null", "string"] + }, + "START_DATE": { + "type": ["null", "string"] + }, + "EMAIL_NOTIFY": { + "type": ["null", "string"] + }, + "OUTPUT_TYPE": { + "type": ["null", "string"] + }, + "INTROTEXT": { + "type": ["null", "string"], + "empty": true + } + } +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json new file mode 100644 index 0000000000000..bcf8848117e9d --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json @@ -0,0 +1,271 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": ["INVOICE_ID"], + "properties": { + "INVOICE_ID": { + "type": ["string"] + }, + "TYPE": { + "type": ["null","string"] + }, + "CUSTOMER_ID": { + "type": ["null","string"] + }, + "CUSTOMER_NUMBER": { + "type": ["null","string"] + }, + "CUSTOMER_COSTCENTER_ID": { + "type": ["null","string"] + }, + "CONTACT_ID": { + "type": ["null","string"] + }, + "PROJECT_ID": { + "type": ["null","string"] + }, + "CURRENCY_CODE": { + "type": ["null","string"] + }, + "DELIVERY_DATE": { + "type": ["null","string"] + }, + "INVOICE_TITLE": { + "type": ["null","string"], + "empty": true + }, + "CASH_DISCOUNT_PERCENT": { + "type": ["null","string"] + }, + "CASH_DISCOUNT_DAYS": { + "type": ["null","string"] + }, + "SUB_TOTAL": { + "type": ["null","integer"] + }, + "VAT_TOTAL": { + "type": ["null","number"] + }, + "VAT_CASE": { + "type": ["null","string"] + }, + "VAT_ITEMS": { + "type": ["array", "null"], + "items": [ + { + "type": "object", + "properties": + { + "VAT_PERCENT": { + "type": ["null","integer"] + }, + "COMPLETE_NET": { + "type": ["null","integer"] + }, + "VAT_VALUE": { + "type": ["null","number"] + } + } + } + ] + }, + "ITEMS": { + "type": ["array", "null"], + "items": + [ + { + "type": "object", + "properties": + { + "INVOICE_ITEM_ID": + { + "type": ["null","integer"] + }, + "ARTICLE_NUMBER": + { + "type": ["null","string"] + }, + "DESCRIPTION": + { + "type": ["null","string"] + }, + "QUANTITY": + { + "type": ["null","integer"] + }, + "UNIT_PRICE": + { + "type": ["null","integer"] + }, + "VAT_PERCENT": + { + "type": ["null","integer"] + }, + "VAT_VALUE": + { + "type": ["null","number"] + }, + "COMPLETE_NET": + { + "type": ["null","integer"] + }, + "COMPLETE_GROSS": + { + "type": ["null","number"] + }, + "CATEGORY": + { + "type": ["null","array","string"], + "empty": true + }, + "CATEGORY_ID": + { + "type": ["null","array","integer"] + }, + "SORT_ORDER": + { + "type": ["null","integer"] + } + } + } + ] + }, + "TOTAL": { + "type": ["number", "null"] + }, + "ORGANIZATION": { + "type": ["null","string"], + "empty": true + }, + "NOTE": { + "type": ["null","string"], + "empty": true + }, + "SALUTATION": { + "type": ["null","string"], + "empty": true + }, + "FIRST_NAME": { + "type": ["null","string"], + "empty": true + }, + "LAST_NAME": { + "type": ["null","string"], + "empty": true + }, + "ADDRESS": { + "type": ["null","string"], + "empty": true + }, + "ADDRESS_2": { + "type": ["null","string"], + "empty": true + }, + "ZIPCODE": { + "type": ["null","string"], + "empty": true + }, + "CITY": { + "type": ["null","string"], + "empty": true + }, + "SERVICE_PERIOD_START": { + "type": ["null","string"], + "empty": true + }, + "SERVICE_PERIOD_END": { + "type": ["null","string"], + "empty": true + }, + "PAYMENT_TYPE": { + "type": ["null","string"], + "empty": true + }, + "BANK_NAME": { + "type": ["null","string"], + "empty": true + }, + "BANK_ACCOUNT_NUMBER": { + "type": ["null","string"], + "empty": true + }, + "BANK_CODE": { + "type": ["null","string"], + "empty": true + }, + "BANK_ACCOUNT_OWNER": { + "type": ["null","string"], + "empty": true + }, + "BANK_IBAN": { + "type": ["null","string"], + "empty": true + }, + "BANK_BIC": { + "type": ["null","string"], + "empty": true + }, + "COUNTRY_CODE": { + "type": ["null","string"] + }, + "VAT_ID": { + "type": ["null","string"] + }, + "TEMPLATE_ID": { + "type": ["null","string"], + "empty": true + }, + "INVOICE_NUMBER": { + "type": ["null","string"] + }, + "INTROTEXT": { + "type": ["null","string"], + "empty": true + }, + "PAID_DATE": { + "type": ["null","string"] + }, + "IS_CANCELED": { + "type": ["null","string"] + }, + "INVOICE_DATE": { + "type": ["null","string"] + }, + "DUE_DATE": { + "type": ["null","string"] + }, + "PAYMENT_INFO": { + "type": ["null","string"] + }, + "PAYMENTS": { + "type": ["null","array"], + "items": { + "PAYMENT_ID": { + "type": ["string", "null"] + }, + "DATE": { + "type": ["string", "null"] + }, + "AMOUNT": { + "type": ["string", "null"] + }, + "CURRENCY_CODE": { + "type": ["string", "null"] + }, + "NOTE": { + "type": ["string", "null"], + "empty": true + }, + "TYPE": { + "type": ["string", "null"] + } + } + }, + "LASTUPDATE": { + "type": ["null","string"] + }, + "DOCUMENT_URL": { + "type": ["null","string"] + } + } +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py new file mode 100644 index 0000000000000..d8b47eded6a35 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py @@ -0,0 +1,331 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from abc import ABC +from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union + +import requests +from airbyte_cdk.models import SyncMode +from airbyte_cdk.sources import AbstractSource +from airbyte_cdk.sources.streams import Stream +from airbyte_cdk.sources.streams.http import HttpStream +from airbyte_cdk.sources.streams.http.auth import BasicHttpAuthenticator +from requests.auth import HTTPBasicAuth + + +#TODO: We might need to implement a backoff strategy since Fastbill rate limits differently depending on which license the customer has + +class FastbillStream(HttpStream, ABC): + + def __init__(self, *args, username: str = None, api_key: str = None,**kwargs): + super().__init__(*args, **kwargs) + self._username = username + self._api_key = api_key + + API_OFFSET_LIMIT = 100 + + @property + def http_method(self) -> str: + return "POST" + + # TODO: Fill in the url base. Required. + url_base = " https://my.fastbill.com/api/1.0/api.php" + + def path( + self, + *, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, + ) -> str: + return None + + def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: + return None + + def request_params( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None + ) -> MutableMapping[str, Any]: + """ + TODO: Override this method to define any query parameters to be set. Remove this method if you don't need to define request params. + Usually contains common params e.g. pagination size etc. + """ + return None + + def request_headers( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + ) -> Mapping[str, Any]: + return {'Content-type': 'application/json'} + + def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + """ + TODO: Override this method to define how a response is parsed. + :return an iterable containing each record in the response + """ + + yield response.json() + +class Invoices(FastbillStream): + primary_key = "INVOICE_ID" + + def request_body_json( + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, + ) -> Optional[Union[Mapping, str]]: + + if next_page_token: + return next_page_token + else: + return { + "SERVICE": "invoice.get", + "FILTER": {}, + "OFFSET": 0 + } + + def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: + def req_body(offset): + return { + "SERVICE": "invoice.get", + "FILTER": {}, + "OFFSET": offset + } + + response = response.json() + offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None + if offset is None: + response_request = response["REQUEST"]["OFFSET"] + raise Exception(f"No valid offset value found:{response_request}") + + if len(response["RESPONSE"]["INVOICES"]) == self.API_OFFSET_LIMIT: + return req_body(offset + self.API_OFFSET_LIMIT) + return None + + def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + invoices = response.json().get("RESPONSE", {}).get("INVOICES", []) + yield from invoices + # for invoice in invoices: + # yield self.schema_applier.apply_schema_transformations( + # invoice, + # self.get_json_schema() + # ) + + +class RecurringInvoices(FastbillStream): + primary_key = "INVOICE_ID" + + def request_body_json( + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, + ) -> Optional[Union[Mapping, str]]: + + if next_page_token: + return next_page_token + else: + return { + "SERVICE": "recurring.get", + "FILTER": {}, + "OFFSET": 0 + } + + def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: + def req_body(offset): + return { + "SERVICE": "recurring.get", + "FILTER": {}, + "OFFSET": offset + } + + response = response.json() + offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None + if offset is None: + response_request = response["REQUEST"]["OFFSET"] + raise Exception(f"No valid offset value found:{response_request}") + + if len(response["RESPONSE"]["INVOICES"]) == self.API_OFFSET_LIMIT: + return req_body(offset + self.API_OFFSET_LIMIT) + return None + + def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + invoices = response.json().get("RESPONSE", {}).get("INVOICES", []) + yield from invoices + # for invoice in invoices: + # yield self.schema_applier.apply_schema_transformations( + # invoice, + # self.get_json_schema() + # ) + + +class Products(FastbillStream): + primary_key = "ARTICLE_ID" + + def request_body_json( + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, + ) -> Optional[Union[Mapping, str]]: + + if next_page_token: + return next_page_token + else: + return { + "SERVICE": "article.get", + "FILTER": {}, + "OFFSET": 0 + } + + def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: + def req_body(offset): + return { + "SERVICE": "article.get", + "FILTER": {}, + "OFFSET": offset + } + + response = response.json() + offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None + if offset is None: + response_request = response["REQUEST"]["OFFSET"] + raise Exception(f"No valid offset value found:{response_request}") + + if len(response["RESPONSE"]["ARTICLES"]) == self.API_OFFSET_LIMIT: + return req_body(offset + self.API_OFFSET_LIMIT) + return None + + def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + products = response.json().get("RESPONSE", {}).get("ARTICLES", []) + yield from products + # for product in products: + # yield self.schema_applier.apply_schema_transformations( + # product, + # self.get_json_schema() + # ) + + +class Revenues(FastbillStream): + primary_key = "INVOICE_ID" + + def request_body_json( + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, + ) -> Optional[Union[Mapping, str]]: + + if next_page_token: + return next_page_token + else: + return { + "SERVICE": "revenue.get", + "FILTER": {}, + "OFFSET": 0 + } + + def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: + def req_body(offset): + return { + "SERVICE": "revenue.get", + "FILTER": {}, + "OFFSET": offset + } + + response = response.json() + offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None + if offset is None: + response_request = response["REQUEST"]["OFFSET"] + raise Exception(f"No valid offset value found:{response_request}") + + if len(response["RESPONSE"]["REVENUES"]) == self.API_OFFSET_LIMIT: + return req_body(offset + self.API_OFFSET_LIMIT) + return None + + def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + customers = response.json().get("RESPONSE", {}).get("REVENUES", []) + yield from customers + # for customer in customers: + # yield self.schema_applier.apply_schema_transformations( + # customer, + # self.get_json_schema() + # ) + + +class Customers(FastbillStream): + primary_key = "CUSTOMER_ID" + + def request_body_json( + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, + ) -> Optional[Union[Mapping, str]]: + + if next_page_token: + return next_page_token + else: + return { + "SERVICE": "customer.get", + "FILTER": {}, + "OFFSET": 0 + } + + def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: + def req_body(offset): + return { + "SERVICE": "customer.get", + "FILTER": {}, + "OFFSET": offset + } + + response = response.json() + offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None + if offset is None: + response_request = response["REQUEST"]["OFFSET"] + raise Exception(f"No valid offset value found:{response_request}") + + if len(response["RESPONSE"]["CUSTOMERS"]) == self.API_OFFSET_LIMIT: + return req_body(offset + self.API_OFFSET_LIMIT) + return None + + def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + customers = response.json().get("RESPONSE", {}).get("CUSTOMERS", []) + yield from customers + # for customer in customers: + # yield self.schema_applier.apply_schema_transformations( + # customer, + # self.get_json_schema() + # ) + + +# Source +class SourceFastbill(AbstractSource): + + def get_basic_auth(self, config: Mapping[str, Any]) -> requests.auth.HTTPBasicAuth: + return requests.auth.HTTPBasicAuth( + config["username"], config["api_key"] + ) + + def check_connection(self, logger, config) -> Tuple[bool, any]: + try: + auth = self.get_basic_auth(config) + records = Customers(auth, **config).read_records(sync_mode=SyncMode.full_refresh) + next(records, None) + return True, None + except Exception as error: + return False, f"Unable to connect to Fastbill API with the provided credentials - {repr(error)}" + + def streams(self, config: Mapping[str, Any]) -> List[Stream]: + + auth = self.get_basic_auth(config) + return [ + Customers(auth, **config), + Invoices(auth, **config), + RecurringInvoices(auth, **config), + Products(auth, **config), + Revenues(auth, **config) + ] diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml b/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml new file mode 100644 index 0000000000000..fdc569ba7912c --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml @@ -0,0 +1,12 @@ +documentationUrl: https://docsurl.com +connectionSpecification: + $schema: http://json-schema.org/draft-07/schema# + title: Fastbill Spec + type: object + required: + - TODO + properties: + # 'TODO: This schema defines the configuration required for the source. This usually involves metadata such as database and/or authentication information.': + TODO: + type: string + description: describe me diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/__init__.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/__init__.py new file mode 100644 index 0000000000000..1100c1c58cf51 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/__init__.py @@ -0,0 +1,3 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_incremental_streams.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_incremental_streams.py new file mode 100644 index 0000000000000..2e1bf8fff4eff --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_incremental_streams.py @@ -0,0 +1,59 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from airbyte_cdk.models import SyncMode +from pytest import fixture +from source_fastbill.source import IncrementalFastbillStream + + +@fixture +def patch_incremental_base_class(mocker): + # Mock abstract methods to enable instantiating abstract class + mocker.patch.object(IncrementalFastbillStream, "path", "v0/example_endpoint") + mocker.patch.object(IncrementalFastbillStream, "primary_key", "test_primary_key") + mocker.patch.object(IncrementalFastbillStream, "__abstractmethods__", set()) + + +def test_cursor_field(patch_incremental_base_class): + stream = IncrementalFastbillStream() + # TODO: replace this with your expected cursor field + expected_cursor_field = [] + assert stream.cursor_field == expected_cursor_field + + +def test_get_updated_state(patch_incremental_base_class): + stream = IncrementalFastbillStream() + # TODO: replace this with your input parameters + inputs = {"current_stream_state": None, "latest_record": None} + # TODO: replace this with your expected updated stream state + expected_state = {} + assert stream.get_updated_state(**inputs) == expected_state + + +def test_stream_slices(patch_incremental_base_class): + stream = IncrementalFastbillStream() + # TODO: replace this with your input parameters + inputs = {"sync_mode": SyncMode.incremental, "cursor_field": [], "stream_state": {}} + # TODO: replace this with your expected stream slices list + expected_stream_slice = [None] + assert stream.stream_slices(**inputs) == expected_stream_slice + + +def test_supports_incremental(patch_incremental_base_class, mocker): + mocker.patch.object(IncrementalFastbillStream, "cursor_field", "dummy_field") + stream = IncrementalFastbillStream() + assert stream.supports_incremental + + +def test_source_defined_cursor(patch_incremental_base_class): + stream = IncrementalFastbillStream() + assert stream.source_defined_cursor + + +def test_stream_checkpoint_interval(patch_incremental_base_class): + stream = IncrementalFastbillStream() + # TODO: replace this with your expected checkpoint interval + expected_checkpoint_interval = None + assert stream.state_checkpoint_interval == expected_checkpoint_interval diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py new file mode 100644 index 0000000000000..0f392193b3657 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py @@ -0,0 +1,22 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + +from unittest.mock import MagicMock + +from source_fastbill.source import SourceFastbill + + +def test_check_connection(mocker): + source = SourceFastbill() + logger_mock, config_mock = MagicMock(), MagicMock() + assert source.check_connection(logger_mock, config_mock) == (True, None) + + +def test_streams(mocker): + source = SourceFastbill() + config_mock = MagicMock() + streams = source.streams(config_mock) + # TODO: replace this with your streams number + expected_streams_number = 2 + assert len(streams) == expected_streams_number diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py new file mode 100644 index 0000000000000..d3a42cb666b58 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py @@ -0,0 +1,83 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + +from http import HTTPStatus +from unittest.mock import MagicMock + +import pytest +from source_fastbill.source import FastbillStream + + +@pytest.fixture +def patch_base_class(mocker): + # Mock abstract methods to enable instantiating abstract class + mocker.patch.object(FastbillStream, "path", "v0/example_endpoint") + mocker.patch.object(FastbillStream, "primary_key", "test_primary_key") + mocker.patch.object(FastbillStream, "__abstractmethods__", set()) + + +def test_request_params(patch_base_class): + stream = FastbillStream() + # TODO: replace this with your input parameters + inputs = {"stream_slice": None, "stream_state": None, "next_page_token": None} + # TODO: replace this with your expected request parameters + expected_params = {} + assert stream.request_params(**inputs) == expected_params + + +def test_next_page_token(patch_base_class): + stream = FastbillStream() + # TODO: replace this with your input parameters + inputs = {"response": MagicMock()} + # TODO: replace this with your expected next page token + expected_token = None + assert stream.next_page_token(**inputs) == expected_token + + +def test_parse_response(patch_base_class): + stream = FastbillStream() + # TODO: replace this with your input parameters + inputs = {"response": MagicMock()} + # TODO: replace this with your expected parced object + expected_parsed_object = {} + assert next(stream.parse_response(**inputs)) == expected_parsed_object + + +def test_request_headers(patch_base_class): + stream = FastbillStream() + # TODO: replace this with your input parameters + inputs = {"stream_slice": None, "stream_state": None, "next_page_token": None} + # TODO: replace this with your expected request headers + expected_headers = {} + assert stream.request_headers(**inputs) == expected_headers + + +def test_http_method(patch_base_class): + stream = FastbillStream() + # TODO: replace this with your expected http request method + expected_method = "GET" + assert stream.http_method == expected_method + + +@pytest.mark.parametrize( + ("http_status", "should_retry"), + [ + (HTTPStatus.OK, False), + (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 = FastbillStream() + assert stream.should_retry(response_mock) == should_retry + + +def test_backoff_time(patch_base_class): + response_mock = MagicMock() + stream = FastbillStream() + expected_backoff_time = None + assert stream.backoff_time(response_mock) == expected_backoff_time From 962018775cf8c258bda586972d8fe10483caac68 Mon Sep 17 00:00:00 2001 From: Johan Hammarstedt Date: Fri, 28 Oct 2022 17:09:06 +0200 Subject: [PATCH 2/9] adding documentation --- airbyte-integrations/builds.md | 255 +++++++++--------- .../integration_tests/acceptance.py | 2 - .../integration_tests/configured_catalog.json | 35 ++- .../integration_tests/invalid_config.json | 3 +- .../integration_tests/sample_config.json | 5 +- .../connectors/source-fastbill/setup.py | 1 + .../sample_files/configured_catalog.json | 49 ---- .../source-fastbill/source_fastbill/source.py | 11 - .../source-fastbill/source_fastbill/spec.yaml | 12 +- .../unit_tests/test_incremental_streams.py | 59 ---- .../source-fastbill/unit_tests/test_source.py | 17 +- .../unit_tests/test_streams.py | 15 +- docs/integrations/README.md | 5 +- docs/integrations/sources/fastbill.md | 64 +++++ 14 files changed, 257 insertions(+), 276 deletions(-) delete mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/sample_files/configured_catalog.json delete mode 100644 airbyte-integrations/connectors/source-fastbill/unit_tests/test_incremental_streams.py create mode 100644 docs/integrations/sources/fastbill.md diff --git a/airbyte-integrations/builds.md b/airbyte-integrations/builds.md index 3f58255ffc527..91f9f63827405 100644 --- a/airbyte-integrations/builds.md +++ b/airbyte-integrations/builds.md @@ -2,133 +2,134 @@ # Sources -|name |status | -| :--- | :--- | -| 3PL Central | [![source-amazon-seller-partner](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-tplcentral%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-tplcentral) | -| Adjust | [![source-adjust](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-adjust%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-adjust) | -| Airtable | [![source-airtable](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-airtable%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-airtable) | -| AlloyDB | [![source-alloydb](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-alloydb%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-alloydb) | -| Amazon Seller Partner | [![source-amazon-seller-partner](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-amazon-seller-partner%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-amazon-seller-partner) | -| Amplitude | [![source-amplitude](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-amplitude%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-amplitude) | -| Apify Dataset | [![source-amplitude](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-apify-dataset%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-apify-dataset) | -| AppsFlyer | [![source-appsflyer-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-appsflyer-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-appsflyer-singer) | -| App Store | [![source-appstore-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-appstore-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-appstore-singer) | -| Asana | [![source-asana](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-asana%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-asana) | -| Ashby | [![source-ashby](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-ashby%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-ashby) | -| AWS CloudTrail | [![source-aws-cloudtrail](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-aws-cloudtrail%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-aws-cloudtrail) | -| Azure Table Storage | [![source-azure-table](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-azure-table%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-azure-table) | -| BambooHR | [![source-bamboo-hr](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-bamboo-hr%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-bamboo-hr) | -| Braintree | [![source-braintree](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-braintree%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-braintree) | -| BigCommerce | [![source-bigcommerce](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-bigcommerce%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-bigcommerce) | -| BigQuery | [![source-bigquery](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-bigquery%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-bigquery/) | -| Bing Ads | [![source-bing-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-bing-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-bing-ads) | -| Chargebee | [![source-chargebee](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-chargebee%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-chargebee/) | -| Chargify | [![source-chargify](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-chargify%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-chargify/) | -| Chartmogul | [![source-chartmogul](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-chartmogul%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-chartmogul/) | -| Cart.com | [![source-cart](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-cart%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-cart/) | -| Close.com | [![source-close-com](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-close-com%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-close-com/) | -| Delighted | [![source-delighted](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-delighted%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-delighted) | -| Dixa | [![source-dixa](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-dixa%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-dixa) | -| Dockerhub | [![source-dockerhub](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-dockerhub%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-dockerhub) | -| Drift | [![source-drift](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-drift%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-drift) | -| End-to-End Testing | [![source-e2e-test](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-e2e-test%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-e2e-test) | -| Exchange Rates API | [![source-exchange-rates](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-exchange-rates%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-exchange-rates) | -| Facebook Marketing | [![source-facebook-marketing](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-facebook-marketing%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-facebook-marketing) | -| Fauna | [![source-fauna](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-fauna%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-fauna) | -| Files | [![source-file](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-file%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-file) | -| Flexport | [![source-file](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-flexport%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-flexport) | -| Freshdesk | [![source-freshdesk](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-freshdesk%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-freshdesk) | -| Freshsales | [![source-freshsales](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-freshsales%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-freshsales) | -| Freshservice | [![source-service](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-freshservice%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-freshservice) | -| GitHub | [![source-github](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-github%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-github) | -| GitLab | [![source-gitlab](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-gitlab%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-gitlab) | -| Google Ads | [![source-google-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-ads) | -| Google Analytics v4 | [![source-google-analytics-v4](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-analytics-v4%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-analytics-v4) | -| Google Search Console | [![source-google-search-console](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-search-console%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-search-console) | -| Google Sheets | [![source-google-sheets](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-sheets%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-sheets) | -| Google Directory API | [![source-google-directory](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-directory%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-directory) | -| Google Workspace Admin | [![source-google-workspace-admin-reports](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-workspace-admin-reports%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-workspace-admin-reports) | -| Greenhouse | [![source-greenhouse](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-greenhouse%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-greenhouse) | -| Gutendex | [![source-gutendex](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-gutendex%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-gutendex) | -| HubSpot | [![source-hubspot](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-hubspot%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-hubspot) | -| IBM Db2 | [![source-db2](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-db2%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-db2) | -| Insightly | [![source-insightly](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-insightly%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-insightly) | -| Instagram | [![source-instagram](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-instagram%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-instagram) | -| Intercom | [![source-intercom](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-intercom-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-intercom) | -| Iterable | [![source-iterable](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-iterable%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-iterable) | -| Jira | [![source-jira](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-jira%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-jira) | -| LinkedIn Ads | [![source-linkedin-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-linkedin-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-linkedin-ads) | -| LinkedIn Pages | [![source-linkedin-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-linkedin-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-linkedin-pages) | -| Linnworks | [![source-linnworks](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-linnworks%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-linnworks) | -| Lever Hiring | [![source-lever-hiring](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-lever-hiring%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-lever-hiring) | -| Looker | [![source-looker](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-looker%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-looker) | -| Kafka | [![source-kafka](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-kafka%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-kafka) | -| Klaviyo | [![source-klaviyo](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-klaviyo%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-klaviyo) | -| Kustomer | [![source-kustomer-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-kustomer-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-kustomer-singer) | -| Kyriba | [![source-kyriba](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-kyriba%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-kyriba) | -| Lemlist | [![source-lemlist](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-lemlist%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-lemlist) | -| Mailchimp | [![source-mailchimp](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mailchimp%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mailchimp) | -| Marketo | [![source-marketo](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-marketo%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-marketo) | -| Metabase | [![source-metabase](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-metabase%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-metabase) | -| Microsoft SQL Server \(MSSQL\) | [![source-mssql](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mssql%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mssql) | -| Microsoft Teams | [![source-microsoft-teams](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-microsoft-teams%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-microsoft-teams) | -| Mixpanel | [![source-mixpanel](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mixpanel%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mixpanel) | -| Mongo DB | [![source-mongodb-v2](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mongodb-v2%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mongodb-v2) | -| Monday | [![source-monday](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-monday%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-monday) | -| My Hours | [![source-my-hours](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-my-hours%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-my-hours) | -| MySQL | [![source-mysql](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mysql%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mysql) | -| Netsuite | [![source-netsuite](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-nensuite%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-netsuite) | -| Notion | [![source-notion](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-notion%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-notion) | -| OneSignal | [![source-onesignal](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-onesignal%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-onesignal) | -| OpenWeather | [![source-openweather](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-openweather%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-openweather) | -| Oracle DB | [![source-oracle](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-oracle%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-oracle) | -| Orbit | [![source-orbit](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-orbit%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-orbit) | -| Paypal Transaction | [![paypal-transaction](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-paypal-transaction%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-paypal-transaction) | -| Paystack | [![source-paystack](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-paystack%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-paystack) | -| PersistIq | [![source-persistiq](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-persistiq%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-persistiq/) | -| Pinterest | [![source-pinterest](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-pinterest%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-pinterest) | -| Pipedrive | [![source-pipedrive](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-pipedrive%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-pipedrive) | -| Plaid | [![source-plaid](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-plaid%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-plaid) | -| Postgres | [![source-postgres](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-postgres%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-postgres) | -| Posthog | [![source-posthog](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-posthog%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-posthog) | -| PrestaShop | [![source-prestashop](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-prestashop%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-prestashop) | -| Primetric | [![source-primetric](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-primetric%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-primetric) | -| CockroachDb | [![source-cockroachdb](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-cockroachdb%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-cockroachdb) | -| Confluence | [![source-confluence](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-confluence%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-confluence) | -| Qualaroo | [![source-qualaroo](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-qualaroo%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-qualaroo) | -| QuickBooks | [![source-quickbooks-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-quickbooks-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-quickbooks-singer) | -| Recharge | [![source-recharge](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-recharge%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-recharge) | -| Recurly | [![source-recurly](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-recurly%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-recurly) | -| Redshift | [![source-redshift](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-redshift%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-redshift) | -| S3 | [![source-s3](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-s3%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-s3) | -| Salesforce | [![source-salesforce](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-salesforce%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-salesforce) | -| Salesloft | [![source-salesloft](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-salesloft%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-salesloft) | -| Sendgrid | [![source-sendgrid](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-sendgrid%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-sendgrid) | -| Sentry | [![source-sentry](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-sentry%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-sentry) | -| SFTP Bulk | [![source-sftp-bulk](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-sftp-bulk%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-sftp-bulk) | -| Shopify | [![source-shopify](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-shopify%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-shopify) | -| Slack | [![source-slack](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-slack%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-slack) | -| Smartsheets | [![source-smartsheets](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-smartsheets%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-smartsheets) | -| Snapchat Marketing | [![source-snapchat-marketing](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-snapchat-marketing%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-snapchat-marketing) | -| Snowflake | [![source-snowflake](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-snowflake%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-snowflake) | -| Square | [![source-square](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-square%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-square) | -| Strava | [![source-stava](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-strava%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-strava) | -| Stripe | [![source-stripe](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-stripe%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-stripe) | -| Tempo | [![source-tempo](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-tempo%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-tempo) | -| TikTok Marketing | [![source-tiktok-marketing](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-tiktok-marketing%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-tiktok-marketing) | -| Trello | [![source-trello](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-trello%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-trello) | -| Twilio | [![source-twilio](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-twilio%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-twilio) | -| Typeform | [![source-typeform](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-typeform%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-typeform) | -| US Census | [![source-us-census](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-us-census%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-us-census) | -| Whisky Hunter | [![source-whisky-hunter](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-whisky-hunter%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-whisky-hunter) | -| Wrike | [![source-wrike](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-wrike%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-wrike) | -| YouTube Analytics | [![source-youtube-analytics](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-youtube-analytics%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-youtube-analytics) | -| Xkcd | [![source-xkcd](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-xkcd%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-xkcd) | -| Zendesk Chat | [![source-zendesk-chat](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zendesk-chat%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zendesk-chat) | -| Zendesk Support | [![source-zendesk-support](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zendesk-support%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zendesk-support) | -| Zendesk Talk | [![source-zendesk-talk](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zendesk-talk%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zendesk-talk) | -| Zoom | [![source-zoom-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zoom-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zoom-singer) | -| Zuora | [![source-zuora](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zuora%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zuora) | +| name |status | +|:-------------------------------| :--- | +| 3PL Central | [![source-amazon-seller-partner](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-tplcentral%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-tplcentral) | +| Adjust | [![source-adjust](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-adjust%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-adjust) | +| Airtable | [![source-airtable](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-airtable%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-airtable) | +| AlloyDB | [![source-alloydb](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-alloydb%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-alloydb) | +| Amazon Seller Partner | [![source-amazon-seller-partner](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-amazon-seller-partner%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-amazon-seller-partner) | +| Amplitude | [![source-amplitude](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-amplitude%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-amplitude) | +| Apify Dataset | [![source-amplitude](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-apify-dataset%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-apify-dataset) | +| AppsFlyer | [![source-appsflyer-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-appsflyer-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-appsflyer-singer) | +| App Store | [![source-appstore-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-appstore-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-appstore-singer) | +| Asana | [![source-asana](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-asana%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-asana) | +| Ashby | [![source-ashby](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-ashby%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-ashby) | +| AWS CloudTrail | [![source-aws-cloudtrail](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-aws-cloudtrail%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-aws-cloudtrail) | +| Azure Table Storage | [![source-azure-table](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-azure-table%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-azure-table) | +| BambooHR | [![source-bamboo-hr](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-bamboo-hr%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-bamboo-hr) | +| Braintree | [![source-braintree](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-braintree%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-braintree) | +| BigCommerce | [![source-bigcommerce](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-bigcommerce%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-bigcommerce) | +| BigQuery | [![source-bigquery](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-bigquery%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-bigquery/) | +| Bing Ads | [![source-bing-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-bing-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-bing-ads) | +| Chargebee | [![source-chargebee](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-chargebee%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-chargebee/) | +| Chargify | [![source-chargify](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-chargify%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-chargify/) | +| Chartmogul | [![source-chartmogul](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-chartmogul%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-chartmogul/) | +| Cart.com | [![source-cart](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-cart%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-cart/) | +| Close.com | [![source-close-com](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-close-com%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-close-com/) | +| Delighted | [![source-delighted](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-delighted%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-delighted) | +| Dixa | [![source-dixa](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-dixa%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-dixa) | +| Dockerhub | [![source-dockerhub](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-dockerhub%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-dockerhub) | +| Drift | [![source-drift](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-drift%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-drift) | +| End-to-End Testing | [![source-e2e-test](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-e2e-test%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-e2e-test) | +| Exchange Rates API | [![source-exchange-rates](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-exchange-rates%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-exchange-rates) | +| Facebook Marketing | [![source-facebook-marketing](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-facebook-marketing%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-facebook-marketing) | +| Fauna | [![source-fauna](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-fauna%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-fauna) | +| Fastbill | [![source-fastbill](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-fastbill%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-fastbill) | +| Files | [![source-file](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-file%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-file) | +| Flexport | [![source-file](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-flexport%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-flexport) | +| Freshdesk | [![source-freshdesk](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-freshdesk%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-freshdesk) | +| Freshsales | [![source-freshsales](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-freshsales%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-freshsales) | +| Freshservice | [![source-service](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-freshservice%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-freshservice) | +| GitHub | [![source-github](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-github%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-github) | +| GitLab | [![source-gitlab](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-gitlab%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-gitlab) | +| Google Ads | [![source-google-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-ads) | +| Google Analytics v4 | [![source-google-analytics-v4](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-analytics-v4%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-analytics-v4) | +| Google Search Console | [![source-google-search-console](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-search-console%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-search-console) | +| Google Sheets | [![source-google-sheets](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-sheets%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-sheets) | +| Google Directory API | [![source-google-directory](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-directory%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-directory) | +| Google Workspace Admin | [![source-google-workspace-admin-reports](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-google-workspace-admin-reports%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-google-workspace-admin-reports) | +| Greenhouse | [![source-greenhouse](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-greenhouse%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-greenhouse) | +| Gutendex | [![source-gutendex](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-gutendex%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-gutendex) | +| HubSpot | [![source-hubspot](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-hubspot%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-hubspot) | +| IBM Db2 | [![source-db2](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-db2%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-db2) | +| Insightly | [![source-insightly](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-insightly%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-insightly) | +| Instagram | [![source-instagram](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-instagram%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-instagram) | +| Intercom | [![source-intercom](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-intercom-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-intercom) | +| Iterable | [![source-iterable](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-iterable%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-iterable) | +| Jira | [![source-jira](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-jira%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-jira) | +| LinkedIn Ads | [![source-linkedin-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-linkedin-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-linkedin-ads) | +| LinkedIn Pages | [![source-linkedin-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-linkedin-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-linkedin-pages) | +| Linnworks | [![source-linnworks](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-linnworks%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-linnworks) | +| Lever Hiring | [![source-lever-hiring](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-lever-hiring%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-lever-hiring) | +| Looker | [![source-looker](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-looker%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-looker) | +| Kafka | [![source-kafka](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-kafka%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-kafka) | +| Klaviyo | [![source-klaviyo](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-klaviyo%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-klaviyo) | +| Kustomer | [![source-kustomer-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-kustomer-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-kustomer-singer) | +| Kyriba | [![source-kyriba](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-kyriba%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-kyriba) | +| Lemlist | [![source-lemlist](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-lemlist%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-lemlist) | +| Mailchimp | [![source-mailchimp](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mailchimp%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mailchimp) | +| Marketo | [![source-marketo](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-marketo%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-marketo) | +| Metabase | [![source-metabase](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-metabase%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-metabase) | +| Microsoft SQL Server \(MSSQL\) | [![source-mssql](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mssql%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mssql) | +| Microsoft Teams | [![source-microsoft-teams](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-microsoft-teams%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-microsoft-teams) | +| Mixpanel | [![source-mixpanel](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mixpanel%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mixpanel) | +| Mongo DB | [![source-mongodb-v2](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mongodb-v2%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mongodb-v2) | +| Monday | [![source-monday](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-monday%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-monday) | +| My Hours | [![source-my-hours](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-my-hours%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-my-hours) | +| MySQL | [![source-mysql](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-mysql%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-mysql) | +| Netsuite | [![source-netsuite](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-nensuite%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-netsuite) | +| Notion | [![source-notion](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-notion%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-notion) | +| OneSignal | [![source-onesignal](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-onesignal%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-onesignal) | +| OpenWeather | [![source-openweather](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-openweather%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-openweather) | +| Oracle DB | [![source-oracle](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-oracle%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-oracle) | +| Orbit | [![source-orbit](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-orbit%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-orbit) | +| Paypal Transaction | [![paypal-transaction](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-paypal-transaction%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-paypal-transaction) | +| Paystack | [![source-paystack](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-paystack%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-paystack) | +| PersistIq | [![source-persistiq](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-persistiq%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-persistiq/) | +| Pinterest | [![source-pinterest](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-pinterest%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-pinterest) | +| Pipedrive | [![source-pipedrive](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-pipedrive%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-pipedrive) | +| Plaid | [![source-plaid](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-plaid%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-plaid) | +| Postgres | [![source-postgres](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-postgres%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-postgres) | +| Posthog | [![source-posthog](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-posthog%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-posthog) | +| PrestaShop | [![source-prestashop](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-prestashop%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-prestashop) | +| Primetric | [![source-primetric](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-primetric%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-primetric) | +| CockroachDb | [![source-cockroachdb](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-cockroachdb%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-cockroachdb) | +| Confluence | [![source-confluence](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-confluence%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-confluence) | +| Qualaroo | [![source-qualaroo](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-qualaroo%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-qualaroo) | +| QuickBooks | [![source-quickbooks-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-quickbooks-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-quickbooks-singer) | +| Recharge | [![source-recharge](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-recharge%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-recharge) | +| Recurly | [![source-recurly](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-recurly%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-recurly) | +| Redshift | [![source-redshift](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-redshift%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-redshift) | +| S3 | [![source-s3](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-s3%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-s3) | +| Salesforce | [![source-salesforce](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-salesforce%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-salesforce) | +| Salesloft | [![source-salesloft](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-salesloft%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-salesloft) | +| Sendgrid | [![source-sendgrid](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-sendgrid%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-sendgrid) | +| Sentry | [![source-sentry](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-sentry%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-sentry) | +| SFTP Bulk | [![source-sftp-bulk](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-sftp-bulk%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-sftp-bulk) | +| Shopify | [![source-shopify](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-shopify%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-shopify) | +| Slack | [![source-slack](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-slack%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-slack) | +| Smartsheets | [![source-smartsheets](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-smartsheets%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-smartsheets) | +| Snapchat Marketing | [![source-snapchat-marketing](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-snapchat-marketing%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-snapchat-marketing) | +| Snowflake | [![source-snowflake](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-snowflake%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-snowflake) | +| Square | [![source-square](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-square%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-square) | +| Strava | [![source-stava](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-strava%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-strava) | +| Stripe | [![source-stripe](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-stripe%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-stripe) | +| Tempo | [![source-tempo](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-tempo%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-tempo) | +| TikTok Marketing | [![source-tiktok-marketing](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-tiktok-marketing%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-tiktok-marketing) | +| Trello | [![source-trello](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-trello%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-trello) | +| Twilio | [![source-twilio](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-twilio%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-twilio) | +| Typeform | [![source-typeform](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-typeform%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-typeform) | +| US Census | [![source-us-census](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-us-census%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-us-census) | +| Whisky Hunter | [![source-whisky-hunter](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-whisky-hunter%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-whisky-hunter) | +| Wrike | [![source-wrike](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-wrike%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-wrike) | +| YouTube Analytics | [![source-youtube-analytics](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-youtube-analytics%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-youtube-analytics) | +| Xkcd | [![source-xkcd](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-xkcd%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-xkcd) | +| Zendesk Chat | [![source-zendesk-chat](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zendesk-chat%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zendesk-chat) | +| Zendesk Support | [![source-zendesk-support](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zendesk-support%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zendesk-support) | +| Zendesk Talk | [![source-zendesk-talk](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zendesk-talk%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zendesk-talk) | +| Zoom | [![source-zoom-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zoom-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zoom-singer) | +| Zuora | [![source-zuora](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-zuora%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/source-zuora) | # Destinations diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/acceptance.py b/airbyte-integrations/connectors/source-fastbill/integration_tests/acceptance.py index 1302b2f57e10e..950b53b59d416 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/acceptance.py +++ b/airbyte-integrations/connectors/source-fastbill/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-fastbill/integration_tests/configured_catalog.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json index 36f0468db0d8f..3e36914dbc8f2 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json @@ -11,12 +11,39 @@ }, { "stream": { - "name": "employees", + "name": "recurring_invoices", "json_schema": {}, - "supported_sync_modes": ["full_refresh", "incremental"] + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, + { + "stream": { + "name": "invoices", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] }, - "sync_mode": "incremental", - "destination_sync_mode": "append" + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, + { + "stream": { + "name": "products", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, + { + "stream": { + "name": "revenues", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" } ] } diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json index f3732995784f2..03f9d38cdd3e7 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json @@ -1,3 +1,4 @@ { - "todo-wrong-field": "this should be an incomplete config file, used in standard tests" + "api_key": "", + "username": "" } diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json index ecc4913b84c74..73771f4e5c128 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json @@ -1,3 +1,4 @@ { - "fix-me": "TODO" -} + "username": "", + "api_key": "" +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/setup.py b/airbyte-integrations/connectors/source-fastbill/setup.py index b3c25a63759e2..0cf372e3eb766 100644 --- a/airbyte-integrations/connectors/source-fastbill/setup.py +++ b/airbyte-integrations/connectors/source-fastbill/setup.py @@ -13,6 +13,7 @@ "pytest~=6.1", "pytest-mock~=3.6.1", "source-acceptance-test", + "responses~=0.21.0" ] setup( diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/sample_files/configured_catalog.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/sample_files/configured_catalog.json deleted file mode 100644 index 3e36914dbc8f2..0000000000000 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/sample_files/configured_catalog.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "streams": [ - { - "stream": { - "name": "customers", - "json_schema": {}, - "supported_sync_modes": ["full_refresh"] - }, - "sync_mode": "full_refresh", - "destination_sync_mode": "overwrite" - }, - { - "stream": { - "name": "recurring_invoices", - "json_schema": {}, - "supported_sync_modes": ["full_refresh"] - }, - "sync_mode": "full_refresh", - "destination_sync_mode": "overwrite" - }, - { - "stream": { - "name": "invoices", - "json_schema": {}, - "supported_sync_modes": ["full_refresh"] - }, - "sync_mode": "full_refresh", - "destination_sync_mode": "overwrite" - }, - { - "stream": { - "name": "products", - "json_schema": {}, - "supported_sync_modes": ["full_refresh"] - }, - "sync_mode": "full_refresh", - "destination_sync_mode": "overwrite" - }, - { - "stream": { - "name": "revenues", - "json_schema": {}, - "supported_sync_modes": ["full_refresh"] - }, - "sync_mode": "full_refresh", - "destination_sync_mode": "overwrite" - } - ] -} diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py index d8b47eded6a35..137b30c0ff06c 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py @@ -13,10 +13,6 @@ from airbyte_cdk.sources.streams.http import HttpStream from airbyte_cdk.sources.streams.http.auth import BasicHttpAuthenticator from requests.auth import HTTPBasicAuth - - -#TODO: We might need to implement a backoff strategy since Fastbill rate limits differently depending on which license the customer has - class FastbillStream(HttpStream, ABC): def __init__(self, *args, username: str = None, api_key: str = None,**kwargs): @@ -29,8 +25,6 @@ def __init__(self, *args, username: str = None, api_key: str = None,**kwargs): @property def http_method(self) -> str: return "POST" - - # TODO: Fill in the url base. Required. url_base = " https://my.fastbill.com/api/1.0/api.php" def path( @@ -49,7 +43,6 @@ def request_params( self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None ) -> MutableMapping[str, Any]: """ - TODO: Override this method to define any query parameters to be set. Remove this method if you don't need to define request params. Usually contains common params e.g. pagination size etc. """ return None @@ -60,10 +53,6 @@ def request_headers( return {'Content-type': 'application/json'} def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - """ - TODO: Override this method to define how a response is parsed. - :return an iterable containing each record in the response - """ yield response.json() diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml b/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml index fdc569ba7912c..7580e9d3cbaae 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml @@ -4,9 +4,13 @@ connectionSpecification: title: Fastbill Spec type: object required: - - TODO + - username + - api_key properties: - # 'TODO: This schema defines the configuration required for the source. This usually involves metadata such as database and/or authentication information.': - TODO: + username: type: string - description: describe me + description: Username for Fastbill account + api_key: + type: string + description: Fastbill API key + airbyte_secret: true diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_incremental_streams.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_incremental_streams.py deleted file mode 100644 index 2e1bf8fff4eff..0000000000000 --- a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_incremental_streams.py +++ /dev/null @@ -1,59 +0,0 @@ -# -# Copyright (c) 2022 Airbyte, Inc., all rights reserved. -# - - -from airbyte_cdk.models import SyncMode -from pytest import fixture -from source_fastbill.source import IncrementalFastbillStream - - -@fixture -def patch_incremental_base_class(mocker): - # Mock abstract methods to enable instantiating abstract class - mocker.patch.object(IncrementalFastbillStream, "path", "v0/example_endpoint") - mocker.patch.object(IncrementalFastbillStream, "primary_key", "test_primary_key") - mocker.patch.object(IncrementalFastbillStream, "__abstractmethods__", set()) - - -def test_cursor_field(patch_incremental_base_class): - stream = IncrementalFastbillStream() - # TODO: replace this with your expected cursor field - expected_cursor_field = [] - assert stream.cursor_field == expected_cursor_field - - -def test_get_updated_state(patch_incremental_base_class): - stream = IncrementalFastbillStream() - # TODO: replace this with your input parameters - inputs = {"current_stream_state": None, "latest_record": None} - # TODO: replace this with your expected updated stream state - expected_state = {} - assert stream.get_updated_state(**inputs) == expected_state - - -def test_stream_slices(patch_incremental_base_class): - stream = IncrementalFastbillStream() - # TODO: replace this with your input parameters - inputs = {"sync_mode": SyncMode.incremental, "cursor_field": [], "stream_state": {}} - # TODO: replace this with your expected stream slices list - expected_stream_slice = [None] - assert stream.stream_slices(**inputs) == expected_stream_slice - - -def test_supports_incremental(patch_incremental_base_class, mocker): - mocker.patch.object(IncrementalFastbillStream, "cursor_field", "dummy_field") - stream = IncrementalFastbillStream() - assert stream.supports_incremental - - -def test_source_defined_cursor(patch_incremental_base_class): - stream = IncrementalFastbillStream() - assert stream.source_defined_cursor - - -def test_stream_checkpoint_interval(patch_incremental_base_class): - stream = IncrementalFastbillStream() - # TODO: replace this with your expected checkpoint interval - expected_checkpoint_interval = None - assert stream.state_checkpoint_interval == expected_checkpoint_interval diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py index 0f392193b3657..cb09f74eb46bd 100644 --- a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py @@ -1,15 +1,26 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # - +import responses from unittest.mock import MagicMock - from source_fastbill.source import SourceFastbill +@responses.activate def test_check_connection(mocker): + url = "https://my.fastbill.com/api/1.0/api.php" source = SourceFastbill() logger_mock, config_mock = MagicMock(), MagicMock() + responses.add(responses.POST, url, json={ + "REQUEST": { + "OFFSET": 0, + "FILTER": [], + "LIMIT": 0, + }, + "RESPONSE": { + "CUSTOMERS": "", + } + }) assert source.check_connection(logger_mock, config_mock) == (True, None) @@ -18,5 +29,5 @@ def test_streams(mocker): config_mock = MagicMock() streams = source.streams(config_mock) # TODO: replace this with your streams number - expected_streams_number = 2 + expected_streams_number = 5 assert len(streams) == expected_streams_number diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py index d3a42cb666b58..deedb8a50a0d4 100644 --- a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py @@ -22,7 +22,7 @@ def test_request_params(patch_base_class): # TODO: replace this with your input parameters inputs = {"stream_slice": None, "stream_state": None, "next_page_token": None} # TODO: replace this with your expected request parameters - expected_params = {} + expected_params = None assert stream.request_params(**inputs) == expected_params @@ -35,28 +35,19 @@ def test_next_page_token(patch_base_class): assert stream.next_page_token(**inputs) == expected_token -def test_parse_response(patch_base_class): - stream = FastbillStream() - # TODO: replace this with your input parameters - inputs = {"response": MagicMock()} - # TODO: replace this with your expected parced object - expected_parsed_object = {} - assert next(stream.parse_response(**inputs)) == expected_parsed_object - - def test_request_headers(patch_base_class): stream = FastbillStream() # TODO: replace this with your input parameters inputs = {"stream_slice": None, "stream_state": None, "next_page_token": None} # TODO: replace this with your expected request headers - expected_headers = {} + expected_headers = {'Content-type': 'application/json'} assert stream.request_headers(**inputs) == expected_headers def test_http_method(patch_base_class): stream = FastbillStream() # TODO: replace this with your expected http request method - expected_method = "GET" + expected_method = "POST" assert stream.http_method == expected_method diff --git a/docs/integrations/README.md b/docs/integrations/README.md index b46b57ab0c42e..fa5565b307129 100644 --- a/docs/integrations/README.md +++ b/docs/integrations/README.md @@ -15,7 +15,7 @@ For more information about the grading system, see [Product Release Stages](http ## Sources | Connector | Product Release Stage| Available in Cloud? | -|:--------------------------------------------------------------------------------------------| :------------------- | :------------------ | +|:--------------------------------------------------------------------------------------------| :------------------- |:--------------------| | [3PL Central](sources/tplcentral.md) | Alpha | No | | [ActiveCampaign](sources/activecampaign.md) | Alpha | No | | [Adjust](sources/adjust.md) | Alpha | No | @@ -60,6 +60,7 @@ For more information about the grading system, see [Product Release Stages](http | [Facebook Marketing](sources/facebook-marketing.md) | Generally Available | Yes | | [Facebook Pages](sources/facebook-pages.md) | Alpha | No | | [Faker](sources/faker.md) | Alpha | Yes | +| [Fastbill ](sources/fastbill.md) | Alpha | No | | [Fauna](sources/fauna.md) | Beta | No | | [File](sources/file.md) | Beta | Yes | | [Firebolt](sources/firebolt.md) | Alpha | Yes | @@ -182,7 +183,7 @@ For more information about the grading system, see [Product Release Stages](http | [Whisky Hunter](sources/whisky-hunter.md ) | Alpha | No | | [WooCommerce](sources/woocommerce.md) | Alpha | No | | [Wordpress](sources/wordpress.md) | Alpha | No | -| [Workable](sources/workable.md) | Alpha | No | +| [Workable](sources/workable.md) | Alpha | No | | [Wrike](sources/wrike.md) | Alpha | No | | [YouTube Analytics](sources/youtube-analytics.md) | Beta | Yes | | [Xkcd](sources/xkcd.md) | Alpha | No | diff --git a/docs/integrations/sources/fastbill.md b/docs/integrations/sources/fastbill.md new file mode 100644 index 0000000000000..6979873f96bfc --- /dev/null +++ b/docs/integrations/sources/fastbill.md @@ -0,0 +1,64 @@ +# Fastbill + +This page contains the setup guide and reference information for the [Fastbill](https://www.fastbill.com/) source connector. + +You can find more information about the Fastbill REST API [here](https://apidocs.fastbill.com/). + +## Prerequisites + +You can find your Project ID and find or create an API key within [Fastbill](https://my.fastbill.com/index.php?s=D7GCLx0WuylFq3nl4gAvRQMwS8RDyb3sCe_bEoXoU_w). + +## Setup guide + +## Step 1: Set up the Fastbill 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 Set up the source page, enter the name for the Fastbill connector and select **Fastbill** from the Source type dropdown. +4. Enter your `username` - Fastbill username/email. +5. Enter your `api_key` - Fastbill API key with read permissions. +6. Click **Set up source**. + +### For Airbyte OSS: + +1. Navigate to the Airbyte Open Source dashboard. +2. Set the name for your source. +3. Enter your `project_id` - Fastbill Project ID. +4. Enter your `api_key` - Fastbill API key with read permissions. +5. Click **Set up source**. + +## Supported sync modes + +The Fastbill source connector supports the following [sync modes](https://docs.airbyte.com/cloud/core-concepts#connection-sync-modes): + +| Feature | Supported? | +| :---------------- |:-----------| +| Full Refresh Sync | Yes | +| Incremental Sync | No | +| SSL connection | No | +| Namespaces | No | + +## Supported Streams + +* [Customers](https://apidocs.fastbill.com/fastbill/de/customer.html#customer.get) +* [Invoices](https://apidocs.fastbill.com/fastbill/de/invoice.html#invoice.get) +* [Products](https://apidocs.fastbill.com/fastbill/de/recurring.html#recurring.get) +* [Recurring_invoices](https://apidocs.fastbill.com/fastbill/de/recurring.html#recurring.get) +* [Revenues](https://apidocs.fastbill.com/fastbill/de/revenue.html#revenue.get) + +## Data type map + +| Integration Type | Airbyte Type | +| :------------------ | :----------- | +| `string` | `string` | +| `integer`, `number` | `number` | +| `array` | `array` | +| `object` | `object` | + +## Changelog + +| Version | Date | Pull Request | Subject | +|:--------|:------------|:---------------------------------------------------------|:--------------------------------------------------| +| 0.1.0 | 2022-10-TBA | [18522](https://github.com/airbytehq/airbyte/pull/18593) | New Source: Fastbill | \ No newline at end of file From 22d46ad2bd1afbf75c309dc2f4d6c3c61ee862b9 Mon Sep 17 00:00:00 2001 From: Johan Hammarstedt Date: Mon, 31 Oct 2022 15:38:15 +0100 Subject: [PATCH 3/9] fixing common errors --- .../resources/seed/source_definitions.yaml | 7 ++++ .../src/main/resources/seed/source_specs.yaml | 20 ++++++++++ .../acceptance-test-config.yml | 12 +----- .../source-fastbill/acceptance-test-docker.sh | 0 .../integration_tests/abnormal_state.json | 4 +- .../integration_tests/catalog.json | 39 ------------------- .../integration_tests/configured_catalog.json | 2 +- .../integration_tests/invalid_config.json | 6 +-- .../integration_tests/sample_state.json | 6 +-- .../source-fastbill/requirements.txt | 2 +- .../source_fastbill/schemas/customers.json | 1 - .../source_fastbill/schemas/invoices.json | 14 ++----- .../source_fastbill/schemas/products.json | 1 - .../schemas/recurring_invoices.json | 7 +--- .../source_fastbill/schemas/revenues.json | 6 +-- .../source-fastbill/source_fastbill/spec.yaml | 6 ++- docs/integrations/README.md | 2 +- 17 files changed, 49 insertions(+), 86 deletions(-) mode change 100644 => 100755 airbyte-integrations/connectors/source-fastbill/acceptance-test-docker.sh delete mode 100644 airbyte-integrations/connectors/source-fastbill/integration_tests/catalog.json 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 693c626ff4de7..886004d0e7bf5 100644 --- a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml @@ -342,6 +342,13 @@ documentationUrl: https://docs.airbyte.com/integrations/sources/faker sourceType: api releaseStage: alpha +- name: Faker + sourceDefinitionId: eb3e9c1c-0467-4eb7-a172-5265e04ccd0a + dockerRepository: airbyte/source-fastbill + dockerImageTag: 0.1.0 + documentationUrl: https://docs.airbyte.com/integrations/sources/fastbill + sourceType: api + releaseStage: alpha - name: Fauna sourceDefinitionId: 3825db3e-c94b-42ac-bd53-b5a9507ace2b dockerRepository: airbyte/source-fauna 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 6d77f9af2a468..39b0e98a02924 100644 --- a/airbyte-config/init/src/main/resources/seed/source_specs.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_specs.yaml @@ -3251,6 +3251,26 @@ supportsNormalization: false supportsDBT: false supported_destination_sync_modes: [] +- dockerImage: "airbyte/source-fastbill:dev" + spec: + documentationUrl: "https://docs.airbyte.com/integrations/sources/fastbill" + connectionSpecification: + $schema: http://json-schema.org/draft-07/schema# + title: Fastbill Spec + type: object + required: + - username + - api_key + properties: + username: + title: Username + type: string + description: Username for Fastbill account + api_key: + title: API Key + type: string + description: Fastbill API key + airbyte_secret: true - dockerImage: "airbyte/source-fauna:dev" spec: documentationUrl: "https://github.com/fauna/airbyte/blob/source-fauna/docs/integrations/sources/fauna.md" diff --git a/airbyte-integrations/connectors/source-fastbill/acceptance-test-config.yml b/airbyte-integrations/connectors/source-fastbill/acceptance-test-config.yml index 0d7d630367c8d..f49f10d2c8c5c 100644 --- a/airbyte-integrations/connectors/source-fastbill/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-fastbill/acceptance-test-config.yml @@ -14,17 +14,7 @@ tests: basic_read: - config_path: "secrets/config.json" configured_catalog_path: "integration_tests/configured_catalog.json" - empty_streams: [] - # TODO uncomment this block to specify that the tests should assert the connector outputs the records provided in the input file a file - # expect_records: - # path: "integration_tests/expected_records.txt" - # extra_fields: no - # exact_order: no - # extra_records: yes - incremental: # TODO if your connector does not implement incremental sync, remove this block - - config_path: "secrets/config.json" - configured_catalog_path: "integration_tests/configured_catalog.json" - future_state_path: "integration_tests/abnormal_state.json" + empty_streams: ["products","recurring_invoices"] full_refresh: - config_path: "secrets/config.json" configured_catalog_path: "integration_tests/configured_catalog.json" diff --git a/airbyte-integrations/connectors/source-fastbill/acceptance-test-docker.sh b/airbyte-integrations/connectors/source-fastbill/acceptance-test-docker.sh old mode 100644 new mode 100755 diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/abnormal_state.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/abnormal_state.json index 52b0f2c2118f4..8890ca08332df 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/abnormal_state.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/abnormal_state.json @@ -1,5 +1,5 @@ { - "todo-stream-name": { - "todo-field-name": "todo-abnormal-value" + "customers": { + "CUSTOMER_TYPE": 12 } } diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/catalog.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/catalog.json deleted file mode 100644 index 6799946a68514..0000000000000 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/catalog.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "streams": [ - { - "name": "TODO fix this file", - "supported_sync_modes": ["full_refresh", "incremental"], - "source_defined_cursor": true, - "default_cursor_field": "column1", - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "column1": { - "type": "string" - }, - "column2": { - "type": "number" - } - } - } - }, - { - "name": "table1", - "supported_sync_modes": ["full_refresh", "incremental"], - "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "column1": { - "type": "string" - }, - "column2": { - "type": "number" - } - } - } - } - ] -} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json index 3e36914dbc8f2..aca0e401e4605 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json @@ -46,4 +46,4 @@ "destination_sync_mode": "overwrite" } ] -} +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json index 03f9d38cdd3e7..1eafc78beb01f 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json @@ -1,4 +1,4 @@ { - "api_key": "", - "username": "" -} + "api_key": "badkeeeey", + "username": "wrong_username" +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json index 3587e579822d0..6c02d8f9f5283 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json @@ -1,5 +1,5 @@ { - "todo-stream-name": { - "todo-field-name": "value" + "customers": { + "CUSTOMER_TYPE": "strings" } -} +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/requirements.txt b/airbyte-integrations/connectors/source-fastbill/requirements.txt index 0411042aa0911..78140e52009f5 100644 --- a/airbyte-integrations/connectors/source-fastbill/requirements.txt +++ b/airbyte-integrations/connectors/source-fastbill/requirements.txt @@ -1,2 +1,2 @@ -e ../../bases/source-acceptance-test --e . +-e . \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json index 58825ef22cba7..35188addfcfff 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json @@ -1,7 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "required": ["CUSTOMER_ID"], "properties": { "CUSTOMER_ID": { "type": "string" diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json index 24179f41c77ae..3f6550d5046d8 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json @@ -1,7 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "required": ["INVOICE_ID"], "properties": { "INVOICE_ID": { "type": "string" @@ -51,7 +50,7 @@ }, "VAT_ITEMS": { "type": ["null","array"], - "items": [ + "items": { "type": "object", "properties": { @@ -64,18 +63,12 @@ "VAT_VALUE": { "type": "number" } - }, - "required": [ - "VAT_PERCENT", - "COMPLETE_NET", - "VAT_VALUE" - ] + } } - ] }, "ITEMS": { "type": ["null","array"], - "items": [ + "items": { "type": "object", "properties": { @@ -120,7 +113,6 @@ } } } - ] }, "TOTAL": { "type": "number" diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json index 6dbda3994c8e4..3603b23a2f55d 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json @@ -1,7 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "required": ["ARTICLE_ID"], "properties": { "ARTICLE_ID": { "type": "string" diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json index 0a94100b6bb88..03d42d301510b 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json @@ -1,7 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": ["object"], - "required": ["INVOICE_ID"], "properties": { "INVOICE_ID": { "type": ["string"] @@ -50,7 +49,7 @@ }, "VAT_ITEMS": { "type": ["null", "array"], - "items": [ + "items": { "type": ["null", "object"], "properties": { @@ -65,11 +64,10 @@ } } } - ] }, "ITEMS": { "type": ["null", "array"], - "items": [ + "items": { "type": ["null", "object"], "properties": { @@ -114,7 +112,6 @@ } } } - ] }, "TOTAL": { "type": ["null", "number"] diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json index bcf8848117e9d..93c51d7b5c1d4 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json @@ -1,7 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "required": ["INVOICE_ID"], "properties": { "INVOICE_ID": { "type": ["string"] @@ -51,7 +50,7 @@ }, "VAT_ITEMS": { "type": ["array", "null"], - "items": [ + "items": { "type": "object", "properties": @@ -67,12 +66,10 @@ } } } - ] }, "ITEMS": { "type": ["array", "null"], "items": - [ { "type": "object", "properties": @@ -128,7 +125,6 @@ } } } - ] }, "TOTAL": { "type": ["number", "null"] diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml b/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml index 7580e9d3cbaae..a811223172acb 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml @@ -1,4 +1,4 @@ -documentationUrl: https://docsurl.com +documentationUrl: "https://docs.airbyte.com/integrations/sources/fastbill" connectionSpecification: $schema: http://json-schema.org/draft-07/schema# title: Fastbill Spec @@ -8,9 +8,11 @@ connectionSpecification: - api_key properties: username: + title: Username type: string description: Username for Fastbill account api_key: + title: API Key type: string description: Fastbill API key - airbyte_secret: true + airbyte_secret: true \ No newline at end of file diff --git a/docs/integrations/README.md b/docs/integrations/README.md index fa5565b307129..9889a93759c15 100644 --- a/docs/integrations/README.md +++ b/docs/integrations/README.md @@ -60,7 +60,7 @@ For more information about the grading system, see [Product Release Stages](http | [Facebook Marketing](sources/facebook-marketing.md) | Generally Available | Yes | | [Facebook Pages](sources/facebook-pages.md) | Alpha | No | | [Faker](sources/faker.md) | Alpha | Yes | -| [Fastbill ](sources/fastbill.md) | Alpha | No | +| [Fastbill ](sources/fastbill.md) | Alpha | Yes | | [Fauna](sources/fauna.md) | Beta | No | | [File](sources/file.md) | Beta | Yes | | [Firebolt](sources/firebolt.md) | Alpha | Yes | From d1cc8c8745ca8f8c21f376ab907791080e7d6e0a Mon Sep 17 00:00:00 2001 From: Johan Hammarstedt Date: Mon, 31 Oct 2022 21:41:04 +0100 Subject: [PATCH 4/9] make requested changes according to review --- .../resources/seed/source_definitions.yaml | 2 +- .../source_fastbill/helpers.py | 29 +++ .../source_fastbill/schemas/TODO.md | 25 -- .../source-fastbill/source_fastbill/source.py | 218 +++--------------- .../source-fastbill/source_fastbill/spec.yaml | 2 +- 5 files changed, 68 insertions(+), 208 deletions(-) create mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/helpers.py delete mode 100644 airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/TODO.md 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 51b0b23670438..ec9ef217cea39 100644 --- a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml @@ -349,7 +349,7 @@ documentationUrl: https://docs.airbyte.com/integrations/sources/faker sourceType: api releaseStage: alpha -- name: Faker +- name: Fastbill sourceDefinitionId: eb3e9c1c-0467-4eb7-a172-5265e04ccd0a dockerRepository: airbyte/source-fastbill dockerImageTag: 0.1.0 diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/helpers.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/helpers.py new file mode 100644 index 0000000000000..9f6c6aff20865 --- /dev/null +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/helpers.py @@ -0,0 +1,29 @@ +def req_body(offset, endpoint: str): + return { + "SERVICE": f"{endpoint}.get", + "FILTER": {}, + "OFFSET": offset + } + + +def get_next_page_token(response, response_key: str, API_OFFSET_LIMIT: int, endpoint: str): + response = response.json() + offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None + if offset is None: + response_request = response["REQUEST"]["OFFSET"] + raise Exception(f"No valid offset value found:{response_request}") + + if len(response["RESPONSE"][response_key]) == API_OFFSET_LIMIT: + return req_body(offset + API_OFFSET_LIMIT, endpoint) + return None + + +def get_request_body_json(next_page_token, endpoint): + if next_page_token: + return next_page_token + else: + return { + "SERVICE": f"{endpoint}.get", + "FILTER": {}, + "OFFSET": 0 + } diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/TODO.md b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/TODO.md deleted file mode 100644 index cf1efadb3c9c9..0000000000000 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/TODO.md +++ /dev/null @@ -1,25 +0,0 @@ -# TODO: Define your stream schemas -Your connector must describe the schema of each stream it can output using [JSONSchema](https://json-schema.org). - -The simplest way to do this is to describe the schema of your streams using one `.json` file per stream. You can also dynamically generate the schema of your stream in code, or you can combine both approaches: start with a `.json` file and dynamically add properties to it. - -The schema of a stream is the return value of `Stream.get_json_schema`. - -## Static schemas -By default, `Stream.get_json_schema` reads a `.json` file in the `schemas/` directory whose name is equal to the value of the `Stream.name` property. In turn `Stream.name` by default returns the name of the class in snake case. Therefore, if you have a class `class EmployeeBenefits(HttpStream)` the default behavior will look for a file called `schemas/employee_benefits.json`. You can override any of these behaviors as you need. - -Important note: any objects referenced via `$ref` should be placed in the `shared/` directory in their own `.json` files. - -## Dynamic schemas -If you'd rather define your schema in code, override `Stream.get_json_schema` in your stream class to return a `dict` describing the schema using [JSONSchema](https://json-schema.org). - -## Dynamically modifying static schemas -Override `Stream.get_json_schema` to run the default behavior, edit the returned value, then return the edited value: -``` -def get_json_schema(self): - schema = super().get_json_schema() - schema['dynamically_determined_property'] = "property" - return schema -``` - -Delete this file once you're done. Or don't. Up to you :) diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py index 137b30c0ff06c..3886b80af6e52 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py @@ -4,47 +4,41 @@ from abc import ABC -from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union - +from typing import Any, Iterable, List, Mapping, Optional, Tuple, Union, MutableMapping +from source_fastbill.helpers import get_request_body_json, get_next_page_token import requests from airbyte_cdk.models import SyncMode from airbyte_cdk.sources import AbstractSource from airbyte_cdk.sources.streams import Stream from airbyte_cdk.sources.streams.http import HttpStream -from airbyte_cdk.sources.streams.http.auth import BasicHttpAuthenticator from requests.auth import HTTPBasicAuth + + class FastbillStream(HttpStream, ABC): + url_base = " https://my.fastbill.com/api/1.0/api.php" + API_OFFSET_LIMIT = 100 - def __init__(self, *args, username: str = None, api_key: str = None,**kwargs): + def __init__(self, *args, username: str = None, api_key: str = None, **kwargs): super().__init__(*args, **kwargs) self._username = username self._api_key = api_key - API_OFFSET_LIMIT = 100 - @property def http_method(self) -> str: return "POST" - url_base = " https://my.fastbill.com/api/1.0/api.php" def path( - self, - *, - stream_state: Mapping[str, Any] = None, - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, + self, + *, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, ) -> str: return None - def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return None - def request_params( self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None ) -> MutableMapping[str, Any]: - """ - Usually contains common params e.g. pagination size etc. - """ return None def request_headers( @@ -52,9 +46,6 @@ def request_headers( ) -> Mapping[str, Any]: return {'Content-type': 'application/json'} - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - - yield response.json() class Invoices(FastbillStream): primary_key = "INVOICE_ID" @@ -65,42 +56,15 @@ def request_body_json( stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, ) -> Optional[Union[Mapping, str]]: - - if next_page_token: - return next_page_token - else: - return { - "SERVICE": "invoice.get", - "FILTER": {}, - "OFFSET": 0 - } + return get_request_body_json(next_page_token, endpoint="invoice") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - def req_body(offset): - return { - "SERVICE": "invoice.get", - "FILTER": {}, - "OFFSET": offset - } - - response = response.json() - offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None - if offset is None: - response_request = response["REQUEST"]["OFFSET"] - raise Exception(f"No valid offset value found:{response_request}") - - if len(response["RESPONSE"]["INVOICES"]) == self.API_OFFSET_LIMIT: - return req_body(offset + self.API_OFFSET_LIMIT) - return None + return get_next_page_token(response=response, response_key="INVOICES", + API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, + endpoint="invoice") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - invoices = response.json().get("RESPONSE", {}).get("INVOICES", []) - yield from invoices - # for invoice in invoices: - # yield self.schema_applier.apply_schema_transformations( - # invoice, - # self.get_json_schema() - # ) + yield from response.json().get("RESPONSE", {}).get("INVOICES", []) class RecurringInvoices(FastbillStream): @@ -112,42 +76,15 @@ def request_body_json( stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, ) -> Optional[Union[Mapping, str]]: - - if next_page_token: - return next_page_token - else: - return { - "SERVICE": "recurring.get", - "FILTER": {}, - "OFFSET": 0 - } + return get_request_body_json(next_page_token, endpoint="recurring") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - def req_body(offset): - return { - "SERVICE": "recurring.get", - "FILTER": {}, - "OFFSET": offset - } - - response = response.json() - offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None - if offset is None: - response_request = response["REQUEST"]["OFFSET"] - raise Exception(f"No valid offset value found:{response_request}") - - if len(response["RESPONSE"]["INVOICES"]) == self.API_OFFSET_LIMIT: - return req_body(offset + self.API_OFFSET_LIMIT) - return None + return get_next_page_token(response=response, response_key="INVOICES", + API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, + endpoint="recurring") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - invoices = response.json().get("RESPONSE", {}).get("INVOICES", []) - yield from invoices - # for invoice in invoices: - # yield self.schema_applier.apply_schema_transformations( - # invoice, - # self.get_json_schema() - # ) + yield from response.json().get("RESPONSE", {}).get("INVOICES", []) class Products(FastbillStream): @@ -159,42 +96,15 @@ def request_body_json( stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, ) -> Optional[Union[Mapping, str]]: - - if next_page_token: - return next_page_token - else: - return { - "SERVICE": "article.get", - "FILTER": {}, - "OFFSET": 0 - } + return get_request_body_json(next_page_token, endpoint="article") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - def req_body(offset): - return { - "SERVICE": "article.get", - "FILTER": {}, - "OFFSET": offset - } - - response = response.json() - offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None - if offset is None: - response_request = response["REQUEST"]["OFFSET"] - raise Exception(f"No valid offset value found:{response_request}") - - if len(response["RESPONSE"]["ARTICLES"]) == self.API_OFFSET_LIMIT: - return req_body(offset + self.API_OFFSET_LIMIT) - return None + return get_next_page_token(response=response, response_key="ARTICLES", + API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, + endpoint="article") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - products = response.json().get("RESPONSE", {}).get("ARTICLES", []) - yield from products - # for product in products: - # yield self.schema_applier.apply_schema_transformations( - # product, - # self.get_json_schema() - # ) + yield from response.json().get("RESPONSE", {}).get("ARTICLES", []) class Revenues(FastbillStream): @@ -206,42 +116,15 @@ def request_body_json( stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, ) -> Optional[Union[Mapping, str]]: - - if next_page_token: - return next_page_token - else: - return { - "SERVICE": "revenue.get", - "FILTER": {}, - "OFFSET": 0 - } + return get_request_body_json(next_page_token, endpoint="revenue") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - def req_body(offset): - return { - "SERVICE": "revenue.get", - "FILTER": {}, - "OFFSET": offset - } - - response = response.json() - offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None - if offset is None: - response_request = response["REQUEST"]["OFFSET"] - raise Exception(f"No valid offset value found:{response_request}") - - if len(response["RESPONSE"]["REVENUES"]) == self.API_OFFSET_LIMIT: - return req_body(offset + self.API_OFFSET_LIMIT) - return None + return get_next_page_token(response=response, response_key="REVENUES", + API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, + endpoint="revenue") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - customers = response.json().get("RESPONSE", {}).get("REVENUES", []) - yield from customers - # for customer in customers: - # yield self.schema_applier.apply_schema_transformations( - # customer, - # self.get_json_schema() - # ) + yield from response.json().get("RESPONSE", {}).get("REVENUES", []) class Customers(FastbillStream): @@ -253,42 +136,15 @@ def request_body_json( stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, ) -> Optional[Union[Mapping, str]]: - - if next_page_token: - return next_page_token - else: - return { - "SERVICE": "customer.get", - "FILTER": {}, - "OFFSET": 0 - } + return get_request_body_json(next_page_token, endpoint="customer") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - def req_body(offset): - return { - "SERVICE": "customer.get", - "FILTER": {}, - "OFFSET": offset - } - - response = response.json() - offset = response["REQUEST"]["OFFSET"] if response["REQUEST"]["OFFSET"] >= 0 else None - if offset is None: - response_request = response["REQUEST"]["OFFSET"] - raise Exception(f"No valid offset value found:{response_request}") - - if len(response["RESPONSE"]["CUSTOMERS"]) == self.API_OFFSET_LIMIT: - return req_body(offset + self.API_OFFSET_LIMIT) - return None + return get_next_page_token(response=response, response_key="CUSTOMERS", + API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, + endpoint="customer") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - customers = response.json().get("RESPONSE", {}).get("CUSTOMERS", []) - yield from customers - # for customer in customers: - # yield self.schema_applier.apply_schema_transformations( - # customer, - # self.get_json_schema() - # ) + yield from response.json().get("RESPONSE", {}).get("CUSTOMERS", []) # Source diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml b/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml index a811223172acb..7352591fdcf46 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/spec.yaml @@ -15,4 +15,4 @@ connectionSpecification: title: API Key type: string description: Fastbill API key - airbyte_secret: true \ No newline at end of file + airbyte_secret: true From fc795cb4b1b4eb348e37e034042f373fe4c52c36 Mon Sep 17 00:00:00 2001 From: Johan Hammarstedt Date: Wed, 2 Nov 2022 08:56:25 +0100 Subject: [PATCH 5/9] requested changes according to review --- .../source-fastbill/source_fastbill/source.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py index 3886b80af6e52..68ab7a1cea5b6 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py @@ -49,7 +49,7 @@ def request_headers( class Invoices(FastbillStream): primary_key = "INVOICE_ID" - + data = "INVOICES" def request_body_json( self, stream_state: Mapping[str, Any], @@ -59,17 +59,17 @@ def request_body_json( return get_request_body_json(next_page_token, endpoint="invoice") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key="INVOICES", + return get_next_page_token(response=response, response_key=self.data, API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, endpoint="invoice") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get("INVOICES", []) + yield from response.json().get("RESPONSE", {}).get(self.data, []) class RecurringInvoices(FastbillStream): primary_key = "INVOICE_ID" - + data = "INVOICES" def request_body_json( self, stream_state: Mapping[str, Any], @@ -79,17 +79,17 @@ def request_body_json( return get_request_body_json(next_page_token, endpoint="recurring") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key="INVOICES", + return get_next_page_token(response=response, response_key=self.data, API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, endpoint="recurring") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get("INVOICES", []) + yield from response.json().get("RESPONSE", {}).get(self.data, []) class Products(FastbillStream): primary_key = "ARTICLE_ID" - + data = "ARTICLES" def request_body_json( self, stream_state: Mapping[str, Any], @@ -99,17 +99,17 @@ def request_body_json( return get_request_body_json(next_page_token, endpoint="article") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key="ARTICLES", + return get_next_page_token(response=response, response_key=self.data, API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, endpoint="article") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get("ARTICLES", []) + yield from response.json().get("RESPONSE", {}).get(self.data, []) class Revenues(FastbillStream): primary_key = "INVOICE_ID" - + data = "REVENUES" def request_body_json( self, stream_state: Mapping[str, Any], @@ -119,17 +119,17 @@ def request_body_json( return get_request_body_json(next_page_token, endpoint="revenue") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key="REVENUES", + return get_next_page_token(response=response, response_key=self.data, API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, endpoint="revenue") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get("REVENUES", []) + yield from response.json().get("RESPONSE", {}).get(self.data, []) class Customers(FastbillStream): primary_key = "CUSTOMER_ID" - + data = "CUSTOMERS" def request_body_json( self, stream_state: Mapping[str, Any], @@ -139,12 +139,12 @@ def request_body_json( return get_request_body_json(next_page_token, endpoint="customer") def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key="CUSTOMERS", + return get_next_page_token(response=response, response_key=self.data, API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, endpoint="customer") def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get("CUSTOMERS", []) + yield from response.json().get("RESPONSE", {}).get(self.data, []) # Source From 464f07ed52e32a584dd55881a750adb26904b9c3 Mon Sep 17 00:00:00 2001 From: Johan Hammarstedt Date: Thu, 3 Nov 2022 23:41:18 +0100 Subject: [PATCH 6/9] compressing code even more --- .../source-fastbill/source_fastbill/source.py | 78 ++++--------------- .../unit_tests/test_streams.py | 9 +-- 2 files changed, 17 insertions(+), 70 deletions(-) diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py index 68ab7a1cea5b6..57cd38b15f4da 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py @@ -20,8 +20,10 @@ class FastbillStream(HttpStream, ABC): def __init__(self, *args, username: str = None, api_key: str = None, **kwargs): super().__init__(*args, **kwargs) + # self.endpoint = None self._username = username self._api_key = api_key + # self.data = None @property def http_method(self) -> str: @@ -46,105 +48,51 @@ def request_headers( ) -> Mapping[str, Any]: return {'Content-type': 'application/json'} - -class Invoices(FastbillStream): - primary_key = "INVOICE_ID" - data = "INVOICES" def request_body_json( self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, ) -> Optional[Union[Mapping, str]]: - return get_request_body_json(next_page_token, endpoint="invoice") + return get_request_body_json(next_page_token, endpoint=self.endpoint) def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: return get_next_page_token(response=response, response_key=self.data, API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, - endpoint="invoice") + endpoint=self.endpoint) def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: yield from response.json().get("RESPONSE", {}).get(self.data, []) -class RecurringInvoices(FastbillStream): +class Invoices(FastbillStream): primary_key = "INVOICE_ID" data = "INVOICES" - def request_body_json( - self, - stream_state: Mapping[str, Any], - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, - ) -> Optional[Union[Mapping, str]]: - return get_request_body_json(next_page_token, endpoint="recurring") + endpoint = "invoice" - def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key=self.data, - API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, - endpoint="recurring") - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get(self.data, []) +class RecurringInvoices(FastbillStream): + primary_key = "INVOICE_ID" + data = "INVOICES" + endpoint = "recurring" class Products(FastbillStream): primary_key = "ARTICLE_ID" data = "ARTICLES" - def request_body_json( - self, - stream_state: Mapping[str, Any], - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, - ) -> Optional[Union[Mapping, str]]: - return get_request_body_json(next_page_token, endpoint="article") - - def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key=self.data, - API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, - endpoint="article") - - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get(self.data, []) + endpoint = "article" class Revenues(FastbillStream): primary_key = "INVOICE_ID" data = "REVENUES" - def request_body_json( - self, - stream_state: Mapping[str, Any], - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, - ) -> Optional[Union[Mapping, str]]: - return get_request_body_json(next_page_token, endpoint="revenue") - - def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key=self.data, - API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, - endpoint="revenue") - - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get(self.data, []) + endpoint = "revenue" class Customers(FastbillStream): primary_key = "CUSTOMER_ID" data = "CUSTOMERS" - def request_body_json( - self, - stream_state: Mapping[str, Any], - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, - ) -> Optional[Union[Mapping, str]]: - return get_request_body_json(next_page_token, endpoint="customer") - - def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key=self.data, - API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, - endpoint="customer") - - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json().get("RESPONSE", {}).get(self.data, []) + endpoint = "customer" # Source diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py index deedb8a50a0d4..3ba4e347fe06b 100644 --- a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py @@ -28,25 +28,24 @@ def test_request_params(patch_base_class): def test_next_page_token(patch_base_class): stream = FastbillStream() - # TODO: replace this with your input parameters + stream.endpoint = "test_endpoint" + stream.data = "test_data" inputs = {"response": MagicMock()} - # TODO: replace this with your expected next page token + + inputs["response"].json.return_value = {"REQUEST": {"OFFSET": 0}, "RESPONSE": {"test_data": []}} expected_token = None assert stream.next_page_token(**inputs) == expected_token def test_request_headers(patch_base_class): stream = FastbillStream() - # TODO: replace this with your input parameters inputs = {"stream_slice": None, "stream_state": None, "next_page_token": None} - # TODO: replace this with your expected request headers expected_headers = {'Content-type': 'application/json'} assert stream.request_headers(**inputs) == expected_headers def test_http_method(patch_base_class): stream = FastbillStream() - # TODO: replace this with your expected http request method expected_method = "POST" assert stream.http_method == expected_method From ac5b56465ff95d611a54fa5ad081955437f4f9ec Mon Sep 17 00:00:00 2001 From: marcosmarxm Date: Mon, 7 Nov 2022 18:47:08 -0300 Subject: [PATCH 7/9] format --- .../connectors/source-fastbill/setup.py | 9 +--- .../source_fastbill/helpers.py | 17 +++----- .../source-fastbill/source_fastbill/source.py | 43 +++++++++---------- .../source-fastbill/unit_tests/test_source.py | 26 ++++++----- .../unit_tests/test_streams.py | 2 +- 5 files changed, 46 insertions(+), 51 deletions(-) diff --git a/airbyte-integrations/connectors/source-fastbill/setup.py b/airbyte-integrations/connectors/source-fastbill/setup.py index 0cf372e3eb766..15c8044979eac 100644 --- a/airbyte-integrations/connectors/source-fastbill/setup.py +++ b/airbyte-integrations/connectors/source-fastbill/setup.py @@ -6,15 +6,10 @@ from setuptools import find_packages, setup MAIN_REQUIREMENTS = [ - "airbyte-cdk~=0.1.56", + "airbyte-cdk~=0.4", ] -TEST_REQUIREMENTS = [ - "pytest~=6.1", - "pytest-mock~=3.6.1", - "source-acceptance-test", - "responses~=0.21.0" -] +TEST_REQUIREMENTS = ["pytest~=6.1", "pytest-mock~=3.6.1", "source-acceptance-test", "responses~=0.21.0"] setup( name="source_fastbill", diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/helpers.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/helpers.py index 9f6c6aff20865..b3527c0e254e0 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/helpers.py +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/helpers.py @@ -1,9 +1,10 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + def req_body(offset, endpoint: str): - return { - "SERVICE": f"{endpoint}.get", - "FILTER": {}, - "OFFSET": offset - } + return {"SERVICE": f"{endpoint}.get", "FILTER": {}, "OFFSET": offset} def get_next_page_token(response, response_key: str, API_OFFSET_LIMIT: int, endpoint: str): @@ -22,8 +23,4 @@ def get_request_body_json(next_page_token, endpoint): if next_page_token: return next_page_token else: - return { - "SERVICE": f"{endpoint}.get", - "FILTER": {}, - "OFFSET": 0 - } + return {"SERVICE": f"{endpoint}.get", "FILTER": {}, "OFFSET": 0} diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py index 57cd38b15f4da..d1ca03432506f 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/source.py @@ -4,14 +4,14 @@ from abc import ABC -from typing import Any, Iterable, List, Mapping, Optional, Tuple, Union, MutableMapping -from source_fastbill.helpers import get_request_body_json, get_next_page_token +from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union + import requests from airbyte_cdk.models import SyncMode from airbyte_cdk.sources import AbstractSource from airbyte_cdk.sources.streams import Stream from airbyte_cdk.sources.streams.http import HttpStream -from requests.auth import HTTPBasicAuth +from source_fastbill.helpers import get_next_page_token, get_request_body_json class FastbillStream(HttpStream, ABC): @@ -30,36 +30,36 @@ def http_method(self) -> str: return "POST" def path( - self, - *, - stream_state: Mapping[str, Any] = None, - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, + self, + *, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, ) -> str: return None def request_params( - self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None ) -> MutableMapping[str, Any]: return None def request_headers( - self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None ) -> Mapping[str, Any]: - return {'Content-type': 'application/json'} + return {"Content-type": "application/json"} def request_body_json( - self, - stream_state: Mapping[str, Any], - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, ) -> Optional[Union[Mapping, str]]: return get_request_body_json(next_page_token, endpoint=self.endpoint) def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - return get_next_page_token(response=response, response_key=self.data, - API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, - endpoint=self.endpoint) + return get_next_page_token( + response=response, response_key=self.data, API_OFFSET_LIMIT=self.API_OFFSET_LIMIT, endpoint=self.endpoint + ) def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: yield from response.json().get("RESPONSE", {}).get(self.data, []) @@ -97,11 +97,8 @@ class Customers(FastbillStream): # Source class SourceFastbill(AbstractSource): - def get_basic_auth(self, config: Mapping[str, Any]) -> requests.auth.HTTPBasicAuth: - return requests.auth.HTTPBasicAuth( - config["username"], config["api_key"] - ) + return requests.auth.HTTPBasicAuth(config["username"], config["api_key"]) def check_connection(self, logger, config) -> Tuple[bool, any]: try: @@ -120,5 +117,5 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]: Invoices(auth, **config), RecurringInvoices(auth, **config), Products(auth, **config), - Revenues(auth, **config) + Revenues(auth, **config), ] diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py index cb09f74eb46bd..1c71b6b390551 100644 --- a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_source.py @@ -1,8 +1,10 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # -import responses + from unittest.mock import MagicMock + +import responses from source_fastbill.source import SourceFastbill @@ -11,16 +13,20 @@ def test_check_connection(mocker): url = "https://my.fastbill.com/api/1.0/api.php" source = SourceFastbill() logger_mock, config_mock = MagicMock(), MagicMock() - responses.add(responses.POST, url, json={ - "REQUEST": { - "OFFSET": 0, - "FILTER": [], - "LIMIT": 0, + responses.add( + responses.POST, + url, + json={ + "REQUEST": { + "OFFSET": 0, + "FILTER": [], + "LIMIT": 0, + }, + "RESPONSE": { + "CUSTOMERS": "", + }, }, - "RESPONSE": { - "CUSTOMERS": "", - } - }) + ) assert source.check_connection(logger_mock, config_mock) == (True, None) diff --git a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py index 3ba4e347fe06b..c992d8f11167e 100644 --- a/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-fastbill/unit_tests/test_streams.py @@ -40,7 +40,7 @@ def test_next_page_token(patch_base_class): def test_request_headers(patch_base_class): stream = FastbillStream() inputs = {"stream_slice": None, "stream_state": None, "next_page_token": None} - expected_headers = {'Content-type': 'application/json'} + expected_headers = {"Content-type": "application/json"} assert stream.request_headers(**inputs) == expected_headers From 68a9852540b433388b09b848f05926a6b042a85d Mon Sep 17 00:00:00 2001 From: marcosmarxm Date: Mon, 7 Nov 2022 18:53:34 -0300 Subject: [PATCH 8/9] run format --- .../integration_tests/configured_catalog.json | 2 +- .../integration_tests/invalid_config.json | 2 +- .../integration_tests/sample_config.json | 2 +- .../integration_tests/sample_state.json | 2 +- .../source_fastbill/schemas/customers.json | 2 +- .../source_fastbill/schemas/invoices.json | 118 ++-- .../source_fastbill/schemas/products.json | 72 +-- .../schemas/recurring_invoices.json | 112 ++-- .../source_fastbill/schemas/revenues.json | 502 +++++++++--------- 9 files changed, 397 insertions(+), 417 deletions(-) diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json index aca0e401e4605..3e36914dbc8f2 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/configured_catalog.json @@ -46,4 +46,4 @@ "destination_sync_mode": "overwrite" } ] -} \ No newline at end of file +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json index 1eafc78beb01f..27bdf1f2dc26c 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/invalid_config.json @@ -1,4 +1,4 @@ { "api_key": "badkeeeey", "username": "wrong_username" -} \ No newline at end of file +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json index 73771f4e5c128..ae6a028d17145 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_config.json @@ -1,4 +1,4 @@ { "username": "", "api_key": "" -} \ No newline at end of file +} diff --git a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json index 6c02d8f9f5283..7a9ca5814ba87 100644 --- a/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json +++ b/airbyte-integrations/connectors/source-fastbill/integration_tests/sample_state.json @@ -2,4 +2,4 @@ "customers": { "CUSTOMER_TYPE": "strings" } -} \ No newline at end of file +} diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json index 35188addfcfff..c3c846c5a517a 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/customers.json @@ -157,4 +157,4 @@ "empty": true } } -} \ No newline at end of file +} diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json index 3f6550d5046d8..4ff47f38dc9e9 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/invoices.json @@ -49,70 +49,68 @@ "type": "string" }, "VAT_ITEMS": { - "type": ["null","array"], - "items": - { - "type": "object", - "properties": { - "VAT_PERCENT": { - "type": "integer" - }, - "COMPLETE_NET": { - "type": "number" - }, - "VAT_VALUE": { - "type": "number" - } + "type": ["null", "array"], + "items": { + "type": "object", + "properties": { + "VAT_PERCENT": { + "type": "integer" + }, + "COMPLETE_NET": { + "type": "number" + }, + "VAT_VALUE": { + "type": "number" } } + } }, "ITEMS": { - "type": ["null","array"], - "items": - { - "type": "object", - "properties": { - "INVOICE_ITEM_ID": { - "type": "integer" - }, - "ARTICLE_NUMBER": { - "type": "string" - }, - "DESCRIPTION": { - "type": "string", - "empty": true - }, - "QUANTITY": { - "type": "integer" - }, - "UNIT_PRICE": { - "type": "number" - }, - "VAT_PERCENT": { - "type": "integer" - }, - "VAT_VALUE": { - "type": "number" - }, - "COMPLETE_NET": { - "type": "number" - }, - "COMPLETE_GROSS": { - "type": "number" - }, - "CATEGORY": { - "type": ["null","array"], - "items": {} - }, - "CATEGORY_ID": { - "type": ["null","array"], - "items": {} - }, - "SORT_ORDER": { - "type": "integer" - } + "type": ["null", "array"], + "items": { + "type": "object", + "properties": { + "INVOICE_ITEM_ID": { + "type": "integer" + }, + "ARTICLE_NUMBER": { + "type": "string" + }, + "DESCRIPTION": { + "type": "string", + "empty": true + }, + "QUANTITY": { + "type": "integer" + }, + "UNIT_PRICE": { + "type": "number" + }, + "VAT_PERCENT": { + "type": "integer" + }, + "VAT_VALUE": { + "type": "number" + }, + "COMPLETE_NET": { + "type": "number" + }, + "COMPLETE_GROSS": { + "type": "number" + }, + "CATEGORY": { + "type": ["null", "array"], + "items": {} + }, + "CATEGORY_ID": { + "type": ["null", "array"], + "items": {} + }, + "SORT_ORDER": { + "type": "integer" } } + } }, "TOTAL": { "type": "number" @@ -220,7 +218,7 @@ "type": "string" }, "PAYMENTS": { - "type": ["null","array"], + "type": ["null", "array"], "items": {} }, "LASTUPDATE": { @@ -230,4 +228,4 @@ "type": "string" } } -} \ No newline at end of file +} diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json index 3603b23a2f55d..41103e22ab2bc 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/products.json @@ -1,38 +1,38 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": { - "ARTICLE_ID": { - "type": "string" - }, - "ARTICLE_NUMBER": { - "type": "string" - }, - "TITLE": { - "type": "string" - }, - "DESCRIPTION": { - "type": "string", - "empty": true - }, - "UNIT": { - "type": "string" - }, - "UNIT_PRICE": { - "type": "string" - }, - "CURRENCY_CODE": { - "type": "string" - }, - "VAT_PERCENT": { - "type": "string" - }, - "IS_GROSS": { - "type": "number" - }, - "TAGS": { - "type": ["string", "null"], - "empty": true - } + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "ARTICLE_ID": { + "type": "string" + }, + "ARTICLE_NUMBER": { + "type": "string" + }, + "TITLE": { + "type": "string" + }, + "DESCRIPTION": { + "type": "string", + "empty": true + }, + "UNIT": { + "type": "string" + }, + "UNIT_PRICE": { + "type": "string" + }, + "CURRENCY_CODE": { + "type": "string" + }, + "VAT_PERCENT": { + "type": "string" + }, + "IS_GROSS": { + "type": "number" + }, + "TAGS": { + "type": ["string", "null"], + "empty": true } -} \ No newline at end of file + } +} diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json index 03d42d301510b..36bdc11c09e71 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/recurring_invoices.json @@ -49,69 +49,67 @@ }, "VAT_ITEMS": { "type": ["null", "array"], - "items": - { - "type": ["null", "object"], - "properties": { - "VAT_PERCENT": { - "type": ["null", "number"] - }, - "COMPLETE_NET": { - "type": ["null", "number"] - }, - "VAT_VALUE": { - "type": ["null", "number"] - } + "items": { + "type": ["null", "object"], + "properties": { + "VAT_PERCENT": { + "type": ["null", "number"] + }, + "COMPLETE_NET": { + "type": ["null", "number"] + }, + "VAT_VALUE": { + "type": ["null", "number"] } } + } }, "ITEMS": { "type": ["null", "array"], - "items": - { - "type": ["null", "object"], - "properties": { - "INVOICE_ITEM_ID": { - "type": ["null", "number"] - }, - "ARTICLE_NUMBER": { - "type": ["null", "string"] - }, - "DESCRIPTION": { - "type": ["null", "string"] - }, - "QUANTITY": { - "type": ["null", "number"] - }, - "UNIT_PRICE": { - "type": ["null", "number"] - }, - "VAT_PERCENT": { - "type": ["null", "number"] - }, - "VAT_VALUE": { - "type": ["null", "number"] - }, - "COMPLETE_NET": { - "type": ["null", "number"] - }, - "COMPLETE_GROSS": { - "type": ["null", "number"] - }, - "CATEGORY": { - "type": ["null","string", "array"], - "empty": true, - "items": {} - }, - "CATEGORY_ID": { - "type": ["null","integer","array"], - "items": {} - }, - "SORT_ORDER": { - "type": ["null", "number"] - } + "items": { + "type": ["null", "object"], + "properties": { + "INVOICE_ITEM_ID": { + "type": ["null", "number"] + }, + "ARTICLE_NUMBER": { + "type": ["null", "string"] + }, + "DESCRIPTION": { + "type": ["null", "string"] + }, + "QUANTITY": { + "type": ["null", "number"] + }, + "UNIT_PRICE": { + "type": ["null", "number"] + }, + "VAT_PERCENT": { + "type": ["null", "number"] + }, + "VAT_VALUE": { + "type": ["null", "number"] + }, + "COMPLETE_NET": { + "type": ["null", "number"] + }, + "COMPLETE_GROSS": { + "type": ["null", "number"] + }, + "CATEGORY": { + "type": ["null", "string", "array"], + "empty": true, + "items": {} + }, + "CATEGORY_ID": { + "type": ["null", "integer", "array"], + "items": {} + }, + "SORT_ORDER": { + "type": ["null", "number"] } } + } }, "TOTAL": { "type": ["null", "number"] @@ -209,4 +207,4 @@ "empty": true } } -} \ No newline at end of file +} diff --git a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json index 93c51d7b5c1d4..b3c9bbae07f71 100644 --- a/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json +++ b/airbyte-integrations/connectors/source-fastbill/source_fastbill/schemas/revenues.json @@ -1,267 +1,251 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": { - "INVOICE_ID": { - "type": ["string"] - }, - "TYPE": { - "type": ["null","string"] - }, - "CUSTOMER_ID": { - "type": ["null","string"] - }, - "CUSTOMER_NUMBER": { - "type": ["null","string"] - }, - "CUSTOMER_COSTCENTER_ID": { - "type": ["null","string"] - }, - "CONTACT_ID": { - "type": ["null","string"] - }, - "PROJECT_ID": { - "type": ["null","string"] + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "INVOICE_ID": { + "type": ["string"] + }, + "TYPE": { + "type": ["null", "string"] + }, + "CUSTOMER_ID": { + "type": ["null", "string"] + }, + "CUSTOMER_NUMBER": { + "type": ["null", "string"] + }, + "CUSTOMER_COSTCENTER_ID": { + "type": ["null", "string"] + }, + "CONTACT_ID": { + "type": ["null", "string"] + }, + "PROJECT_ID": { + "type": ["null", "string"] + }, + "CURRENCY_CODE": { + "type": ["null", "string"] + }, + "DELIVERY_DATE": { + "type": ["null", "string"] + }, + "INVOICE_TITLE": { + "type": ["null", "string"], + "empty": true + }, + "CASH_DISCOUNT_PERCENT": { + "type": ["null", "string"] + }, + "CASH_DISCOUNT_DAYS": { + "type": ["null", "string"] + }, + "SUB_TOTAL": { + "type": ["null", "integer"] + }, + "VAT_TOTAL": { + "type": ["null", "number"] + }, + "VAT_CASE": { + "type": ["null", "string"] + }, + "VAT_ITEMS": { + "type": ["array", "null"], + "items": { + "type": "object", + "properties": { + "VAT_PERCENT": { + "type": ["null", "integer"] + }, + "COMPLETE_NET": { + "type": ["null", "integer"] + }, + "VAT_VALUE": { + "type": ["null", "number"] + } + } + } + }, + "ITEMS": { + "type": ["array", "null"], + "items": { + "type": "object", + "properties": { + "INVOICE_ITEM_ID": { + "type": ["null", "integer"] + }, + "ARTICLE_NUMBER": { + "type": ["null", "string"] + }, + "DESCRIPTION": { + "type": ["null", "string"] + }, + "QUANTITY": { + "type": ["null", "integer"] + }, + "UNIT_PRICE": { + "type": ["null", "integer"] + }, + "VAT_PERCENT": { + "type": ["null", "integer"] + }, + "VAT_VALUE": { + "type": ["null", "number"] + }, + "COMPLETE_NET": { + "type": ["null", "integer"] + }, + "COMPLETE_GROSS": { + "type": ["null", "number"] + }, + "CATEGORY": { + "type": ["null", "array", "string"], + "empty": true + }, + "CATEGORY_ID": { + "type": ["null", "array", "integer"] + }, + "SORT_ORDER": { + "type": ["null", "integer"] + } + } + } + }, + "TOTAL": { + "type": ["number", "null"] + }, + "ORGANIZATION": { + "type": ["null", "string"], + "empty": true + }, + "NOTE": { + "type": ["null", "string"], + "empty": true + }, + "SALUTATION": { + "type": ["null", "string"], + "empty": true + }, + "FIRST_NAME": { + "type": ["null", "string"], + "empty": true + }, + "LAST_NAME": { + "type": ["null", "string"], + "empty": true + }, + "ADDRESS": { + "type": ["null", "string"], + "empty": true + }, + "ADDRESS_2": { + "type": ["null", "string"], + "empty": true + }, + "ZIPCODE": { + "type": ["null", "string"], + "empty": true + }, + "CITY": { + "type": ["null", "string"], + "empty": true + }, + "SERVICE_PERIOD_START": { + "type": ["null", "string"], + "empty": true + }, + "SERVICE_PERIOD_END": { + "type": ["null", "string"], + "empty": true + }, + "PAYMENT_TYPE": { + "type": ["null", "string"], + "empty": true + }, + "BANK_NAME": { + "type": ["null", "string"], + "empty": true + }, + "BANK_ACCOUNT_NUMBER": { + "type": ["null", "string"], + "empty": true + }, + "BANK_CODE": { + "type": ["null", "string"], + "empty": true + }, + "BANK_ACCOUNT_OWNER": { + "type": ["null", "string"], + "empty": true + }, + "BANK_IBAN": { + "type": ["null", "string"], + "empty": true + }, + "BANK_BIC": { + "type": ["null", "string"], + "empty": true + }, + "COUNTRY_CODE": { + "type": ["null", "string"] + }, + "VAT_ID": { + "type": ["null", "string"] + }, + "TEMPLATE_ID": { + "type": ["null", "string"], + "empty": true + }, + "INVOICE_NUMBER": { + "type": ["null", "string"] + }, + "INTROTEXT": { + "type": ["null", "string"], + "empty": true + }, + "PAID_DATE": { + "type": ["null", "string"] + }, + "IS_CANCELED": { + "type": ["null", "string"] + }, + "INVOICE_DATE": { + "type": ["null", "string"] + }, + "DUE_DATE": { + "type": ["null", "string"] + }, + "PAYMENT_INFO": { + "type": ["null", "string"] + }, + "PAYMENTS": { + "type": ["null", "array"], + "items": { + "PAYMENT_ID": { + "type": ["string", "null"] + }, + "DATE": { + "type": ["string", "null"] + }, + "AMOUNT": { + "type": ["string", "null"] }, "CURRENCY_CODE": { - "type": ["null","string"] - }, - "DELIVERY_DATE": { - "type": ["null","string"] - }, - "INVOICE_TITLE": { - "type": ["null","string"], - "empty": true - }, - "CASH_DISCOUNT_PERCENT": { - "type": ["null","string"] - }, - "CASH_DISCOUNT_DAYS": { - "type": ["null","string"] - }, - "SUB_TOTAL": { - "type": ["null","integer"] - }, - "VAT_TOTAL": { - "type": ["null","number"] - }, - "VAT_CASE": { - "type": ["null","string"] - }, - "VAT_ITEMS": { - "type": ["array", "null"], - "items": - { - "type": "object", - "properties": - { - "VAT_PERCENT": { - "type": ["null","integer"] - }, - "COMPLETE_NET": { - "type": ["null","integer"] - }, - "VAT_VALUE": { - "type": ["null","number"] - } - } - } - }, - "ITEMS": { - "type": ["array", "null"], - "items": - { - "type": "object", - "properties": - { - "INVOICE_ITEM_ID": - { - "type": ["null","integer"] - }, - "ARTICLE_NUMBER": - { - "type": ["null","string"] - }, - "DESCRIPTION": - { - "type": ["null","string"] - }, - "QUANTITY": - { - "type": ["null","integer"] - }, - "UNIT_PRICE": - { - "type": ["null","integer"] - }, - "VAT_PERCENT": - { - "type": ["null","integer"] - }, - "VAT_VALUE": - { - "type": ["null","number"] - }, - "COMPLETE_NET": - { - "type": ["null","integer"] - }, - "COMPLETE_GROSS": - { - "type": ["null","number"] - }, - "CATEGORY": - { - "type": ["null","array","string"], - "empty": true - }, - "CATEGORY_ID": - { - "type": ["null","array","integer"] - }, - "SORT_ORDER": - { - "type": ["null","integer"] - } - } - } - }, - "TOTAL": { - "type": ["number", "null"] - }, - "ORGANIZATION": { - "type": ["null","string"], - "empty": true + "type": ["string", "null"] }, "NOTE": { - "type": ["null","string"], - "empty": true - }, - "SALUTATION": { - "type": ["null","string"], - "empty": true - }, - "FIRST_NAME": { - "type": ["null","string"], - "empty": true - }, - "LAST_NAME": { - "type": ["null","string"], - "empty": true - }, - "ADDRESS": { - "type": ["null","string"], - "empty": true - }, - "ADDRESS_2": { - "type": ["null","string"], - "empty": true - }, - "ZIPCODE": { - "type": ["null","string"], - "empty": true + "type": ["string", "null"], + "empty": true }, - "CITY": { - "type": ["null","string"], - "empty": true - }, - "SERVICE_PERIOD_START": { - "type": ["null","string"], - "empty": true - }, - "SERVICE_PERIOD_END": { - "type": ["null","string"], - "empty": true - }, - "PAYMENT_TYPE": { - "type": ["null","string"], - "empty": true - }, - "BANK_NAME": { - "type": ["null","string"], - "empty": true - }, - "BANK_ACCOUNT_NUMBER": { - "type": ["null","string"], - "empty": true - }, - "BANK_CODE": { - "type": ["null","string"], - "empty": true - }, - "BANK_ACCOUNT_OWNER": { - "type": ["null","string"], - "empty": true - }, - "BANK_IBAN": { - "type": ["null","string"], - "empty": true - }, - "BANK_BIC": { - "type": ["null","string"], - "empty": true - }, - "COUNTRY_CODE": { - "type": ["null","string"] - }, - "VAT_ID": { - "type": ["null","string"] - }, - "TEMPLATE_ID": { - "type": ["null","string"], - "empty": true - }, - "INVOICE_NUMBER": { - "type": ["null","string"] - }, - "INTROTEXT": { - "type": ["null","string"], - "empty": true - }, - "PAID_DATE": { - "type": ["null","string"] - }, - "IS_CANCELED": { - "type": ["null","string"] - }, - "INVOICE_DATE": { - "type": ["null","string"] - }, - "DUE_DATE": { - "type": ["null","string"] - }, - "PAYMENT_INFO": { - "type": ["null","string"] - }, - "PAYMENTS": { - "type": ["null","array"], - "items": { - "PAYMENT_ID": { - "type": ["string", "null"] - }, - "DATE": { - "type": ["string", "null"] - }, - "AMOUNT": { - "type": ["string", "null"] - }, - "CURRENCY_CODE": { - "type": ["string", "null"] - }, - "NOTE": { - "type": ["string", "null"], - "empty": true - }, - "TYPE": { - "type": ["string", "null"] - } - } - }, - "LASTUPDATE": { - "type": ["null","string"] - }, - "DOCUMENT_URL": { - "type": ["null","string"] + "TYPE": { + "type": ["string", "null"] } + } + }, + "LASTUPDATE": { + "type": ["null", "string"] + }, + "DOCUMENT_URL": { + "type": ["null", "string"] } -} \ No newline at end of file + } +} From d74609b2474256d821d2d1a05bb21800b7d2631d Mon Sep 17 00:00:00 2001 From: Octavia Squidington III Date: Mon, 7 Nov 2022 22:34:40 +0000 Subject: [PATCH 9/9] auto-bump connector version --- .../src/main/resources/seed/source_specs.yaml | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) 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 9fca5c9b07cbf..acba1b9f0e5aa 100644 --- a/airbyte-config/init/src/main/resources/seed/source_specs.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_specs.yaml @@ -3429,26 +3429,29 @@ supportsNormalization: false supportsDBT: false supported_destination_sync_modes: [] -- dockerImage: "airbyte/source-fastbill:dev" +- dockerImage: "airbyte/source-fastbill:0.1.0" spec: documentationUrl: "https://docs.airbyte.com/integrations/sources/fastbill" connectionSpecification: - $schema: http://json-schema.org/draft-07/schema# - title: Fastbill Spec - type: object + $schema: "http://json-schema.org/draft-07/schema#" + title: "Fastbill Spec" + type: "object" required: - - username - - api_key + - "username" + - "api_key" properties: username: - title: Username - type: string - description: Username for Fastbill account + title: "Username" + type: "string" + description: "Username for Fastbill account" api_key: - title: API Key - type: string - description: Fastbill API key + title: "API Key" + type: "string" + description: "Fastbill API key" airbyte_secret: true + supportsNormalization: false + supportsDBT: false + supported_destination_sync_modes: [] - dockerImage: "airbyte/source-fauna:dev" spec: documentationUrl: "https://github.com/fauna/airbyte/blob/source-fauna/docs/integrations/sources/fauna.md"