From 50edd2fb6c97bf98bc386bc1557f3e4c7d0d81d9 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Thu, 23 Apr 2026 22:04:32 +0530 Subject: [PATCH 01/23] Config_form example --- .../configuration_form/configuration.json | 7 + .../configuration_form/connector.py | 262 ++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 examples/quickstart_examples/configuration_form/configuration.json create mode 100644 examples/quickstart_examples/configuration_form/connector.py diff --git a/examples/quickstart_examples/configuration_form/configuration.json b/examples/quickstart_examples/configuration_form/configuration.json new file mode 100644 index 000000000..0c3c1ffea --- /dev/null +++ b/examples/quickstart_examples/configuration_form/configuration.json @@ -0,0 +1,7 @@ +{ + "api_base_url": "https://jsonplaceholder.typicode.com", + "api_key": "demo_key", + "batch_size": "10", + "enable_metrics": "true", + "sync_mode": "incremental" +} diff --git a/examples/quickstart_examples/configuration_form/connector.py b/examples/quickstart_examples/configuration_form/connector.py new file mode 100644 index 000000000..1090d93d2 --- /dev/null +++ b/examples/quickstart_examples/configuration_form/connector.py @@ -0,0 +1,262 @@ +# This is a simple example for how to use ConfigurationForm, form_field, and Test +# with the fivetran_connector_sdk module to build a connector with a setup form. +# It shows all available field types (TextField, DropdownField, ToggleField, DescriptiveDropdownField) +# and how to register a setup test that Fivetran runs when the user clicks "Test Connection". +# See the Technical Reference documentation (https://fivetran.com/docs/connectors/connector-sdk/technical-reference#update) +# and the Best Practices documentation (https://fivetran.com/docs/connectors/connector-sdk/best-practices) for details. + +import json + +import requests + +# Import required classes from fivetran_connector_sdk +# For supporting Connector operations like Update() and Schema() +from fivetran_connector_sdk import Connector + +# For enabling Logs in your connector code +from fivetran_connector_sdk import Logging as log + +# For supporting Data operations like Upsert(), Update(), Delete() and checkpoint() +from fivetran_connector_sdk import Operations as op + +# For building the setup form shown to users when configuring the connector in Fivetran +from fivetran_connector_sdk import ConfigurationForm + +# For defining individual form fields (text inputs, dropdowns, toggles, etc.) +from fivetran_connector_sdk import form_field + +# For returning success/failure responses from setup test functions +from fivetran_connector_sdk import Test + + +def configuration_form(): + """ + Define the setup form shown to users when configuring this connector in Fivetran. + Add fields using add_field() and register connection tests using add_test(). + Fivetran calls this method when rendering the connector setup UI. + Returns: + ConfigurationForm: the completed form with fields and tests. + """ + log.info("Building configuration form") + config_form = ConfigurationForm() + + # Plain text field — visible input, suitable for non-sensitive values like URLs + config_form.add_field( + form_field.TextField( + name="api_base_url", + label="API Base URL", + description="The base URL for your REST API endpoint.", + required=True, + placeholder="https://api.example.com/v1", + ) + ) + + # Password field — masked input, suitable for secrets like API keys + config_form.add_field( + form_field.TextField( + name="api_key", + label="API Key", + description="Your API authentication key. This value is stored securely.", + required=True, + field_type=form_field.TextField.Password, + placeholder="your_api_key_here", + ) + ) + + # Dropdown field — lets the user select one option from a fixed list + config_form.add_field( + form_field.DropdownField( + name="batch_size", + label="Batch Size", + description="Number of records to fetch per API request.", + values=[10, 100, 500], + ) + ) + + # Toggle field — boolean on/off switch + config_form.add_field( + form_field.ToggleField( + name="enable_metrics", + label="Enable Metrics", + description="Log extraction volume metrics (record count and bytes) during each sync.", + ) + ) + + # Descriptive dropdown — like a dropdown but each option includes an explanation + config_form.add_field( + form_field.DescriptiveDropdownField( + name="sync_mode", + label="Sync Mode", + values=[ + { + "value": "full", + "label": "Full Sync", + "description": "Re-syncs all records on every run. Use when the source does not expose a modification timestamp.", + }, + { + "value": "incremental", + "label": "Incremental Sync", + "description": "Syncs only records added or modified since the last run. Requires a cursor field in the API response.", + }, + ], + ) + ) + + # Register a setup test — Fivetran calls connection_test() when the user clicks "Test Connection" + config_form.add_test(connection_test, label="Test connection") + + return config_form + + +def connection_test(configuration: dict): + """ + Validate that the API credentials are correct and the endpoint is reachable. + Fivetran calls this function by its __name__ during connector setup. + Args: + configuration: a dictionary that holds the configuration settings for the connector. + Returns: + Test response indicating success or failure. + """ + test = Test() + api_base_url = configuration.get("api_base_url", "").rstrip("/") + api_key = configuration.get("api_key", "") + + if not api_base_url: + return test.failure("api_base_url is required.") + if not api_key: + return test.failure("api_key is required.") + + try: + response = requests.get( + api_base_url, + headers={"Authorization": f"Bearer {api_key}"}, + timeout=10, + ) + if response.status_code == 200: + log.info("Connection test passed: API returned 200") + return test.success() + else: + log.warning(f"Connection test failed: API returned status {response.status_code}") + return test.failure(f"API returned status code: {response.status_code}") + except requests.exceptions.Timeout: + return test.failure("Connection timed out. Verify your api_base_url is reachable.") + except requests.exceptions.ConnectionError: + return test.failure("Could not connect to the API. Check your api_base_url.") + except requests.exceptions.RequestException as e: + return test.failure(f"Request failed: {e}") + + +def schema(configuration: dict): + """ + Define the schema function which lets you configure the schema your connector delivers. + See the technical reference documentation for more details on the schema function: + https://fivetran.com/docs/connectors/connector-sdk/technical-reference#schema + Args: + configuration: a dictionary that holds the configuration settings for the connector. + """ + return [ + { + "table": "post", + "primary_key": ["id"], + } + ] + + +def validate_configuration(configuration: dict): + """ + Validate the configuration dictionary to ensure it contains all required parameters. + Args: + configuration: a dictionary that holds the configuration settings for the connector. + Raises: + ValueError: if any required configuration parameter is missing. + """ + for key in ("api_base_url", "api_key"): + if not configuration.get(key): + raise ValueError(f"Missing required configuration value: '{key}'") + + +def update(configuration: dict, state: dict): + """ + Define the update function, which is a required function, and is called by Fivetran during each sync. + See the technical reference documentation for more details on the update function + https://fivetran.com/docs/connectors/connector-sdk/technical-reference#update + Args: + configuration: a dictionary that holds the configuration settings for the connector. + state: a dictionary that holds the state of the connector. + The state dictionary is empty for the first sync or for any full re-sync. + """ + log.warning("Example: QuickStart Examples - Configuration Form") + + validate_configuration(configuration) + + api_base_url = configuration["api_base_url"].rstrip("/") + api_key = configuration["api_key"] + batch_size = int(configuration.get("batch_size", 100)) + enable_metrics = str(configuration.get("enable_metrics", "false")).lower() == "true" + sync_mode = configuration.get("sync_mode", "full") + + # In incremental mode, resume from where the last sync left off + cursor = state.get("cursor", 0) if sync_mode == "incremental" else 0 + + total_records = 0 + total_bytes = 0 + + while True: + response = requests.get( + f"{api_base_url}/posts", + headers={"Authorization": f"Bearer {api_key}"}, + params={"_start": cursor, "_limit": batch_size}, + timeout=30, + ) + response.raise_for_status() + posts = response.json() + + if not posts: + break + + for post in posts: + op.upsert(table="post", data=post) + if enable_metrics: + total_bytes += len(json.dumps(post).encode("utf-8")) + + total_records += len(posts) + cursor += len(posts) + + # Save the progress by checkpointing the state. This is important for ensuring that the sync + # process can resume from the correct position in case of the next sync or interruptions. + # Learn more about how and where to checkpoint by reading our best practices documentation + # (https://fivetran.com/docs/connectors/connector-sdk/best-practices#largedatasetrecommendation). + op.checkpoint({"cursor": cursor}) + + # JSONPlaceholder returns all results when the offset exceeds total; stop when partial page received + if len(posts) < batch_size: + break + + if enable_metrics: + log.info(f"Sync complete: {total_records} records, {total_bytes} bytes extracted") + + +# This creates the connector object that will use the update, schema, and configuration_form +# functions defined in this connector.py file. +connector = Connector(update=update, schema=schema, configuration_form=configuration_form) + +# Check if the script is being run as the main module. +# This is Python's standard entry method allowing your script to be run directly from the command line or IDE 'run' button. +# This is useful for debugging while you write your code. Note this method is not called by Fivetran when executing your connector in production. +# Please test using the Fivetran debug command prior to finalizing and deploying your connector. +if __name__ == "__main__": + # Open the configuration.json file and load its contents into a dictionary. + with open("configuration.json", "r") as f: + configuration = json.load(f) + # Adding this code to your `connector.py` allows you to test your connector by running your file directly from your IDE. + connector.debug(configuration=configuration) + +# Resulting table: +# ┌─────┬────────┬───────────────────────┬──────────────────────────────────┐ +# │ id │ userId │ title │ body │ +# │ int │ int │ varchar │ varchar │ +# ├─────┼────────┼───────────────────────┼──────────────────────────────────┤ +# │ 1 │ 1 │ sunt aut facere ... │ quia et suscipit suscipit ... │ +# │ 2 │ 1 │ qui est esse │ est rerum tempore vitae ... │ +# │ ... │ ... │ ... │ ... │ +# └─────┴────────┴───────────────────────┴──────────────────────────────────┘ From d9b68a14360b8f7362400daa45a2e9fe5ff1747b Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Thu, 23 Apr 2026 22:26:35 +0530 Subject: [PATCH 02/23] README.md update --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fe13d7912..0ad83927b 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,8 @@ These examples are designed to help you get started with the Connector SDK quick - [configuration](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/configuration) - This example shows how to use secrets. +- [configuration_form](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/configuration_form) - This example shows how to define a connector setup form using `ConfigurationForm`, `form_field`, and `Test`. It demonstrates all available field types (plain text, password, dropdown, toggle, and descriptive dropdown) and how to register a connection test. + - [multiple_code_files_with_sub_directory_structure](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/multiple_code_files_with_sub_directory_structure) - This example shows how you can write a complex connector comprising multiple `.py` files. - [using_pd_dataframes](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/using_pd_dataframes) - This example shows the use of Pandas DataFrames to manipulate data prior to sending to Fivetran. From 237423696fb746f9a4c29cc4f58a9c93af180077 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Thu, 23 Apr 2026 22:52:29 +0530 Subject: [PATCH 03/23] update --- .../configuration_form/README.md | 52 +++++++++++++++++ .../configuration_form/connector.py | 57 ++++++------------- 2 files changed, 68 insertions(+), 41 deletions(-) create mode 100644 examples/quickstart_examples/configuration_form/README.md diff --git a/examples/quickstart_examples/configuration_form/README.md b/examples/quickstart_examples/configuration_form/README.md new file mode 100644 index 000000000..c0798a8a8 --- /dev/null +++ b/examples/quickstart_examples/configuration_form/README.md @@ -0,0 +1,52 @@ +# Configuration form example + +## Connector overview + +This example demonstrates how to define a connector setup form using `ConfigurationForm`, `form_field`, and `Test` from the Fivetran Connector SDK. It covers all available field types — plain text, password, dropdown, toggle, and descriptive dropdown — and shows how to register a connection test that Fivetran runs when the user clicks **Test Connection** during setup. + +The API fields (`api_base_url`, `api_key`) in the configuration form are included to illustrate how a real connector would collect credentials — they are not required for this example to run. The `configuration.json` file provided contains sample values used only to demonstrate the form fields when running locally. + +Refer to `def configuration_form()` and `def connection_test()` in `connector.py` for the main setup form and test implementation. + +## Requirements + +- [Supported Python versions](https://github.com/fivetran/fivetran_connector_sdk/blob/main/README.md#requirements) +- Operating system: + - Windows: 10 or later (64-bit only) + - macOS: 13 (Ventura) or later (Apple Silicon [arm64] or Intel [x86_64]) + - Linux: Distributions such as Ubuntu 20.04 or later, Debian 10 or later, or Amazon Linux 2 or later (arm64 or x86_64) + +## Getting started + +Refer to the [Connector SDK Setup Guide](https://fivetran.com/docs/connector-sdk/setup-guide) to get started. + +To run the connector locally: + +```bash +fivetran debug --configuration configuration.json +``` + +## Features + +- Demonstrates all available form field types: `TextField` (plain text and password variants), `DropdownField`, `ToggleField`, and `DescriptiveDropdownField` +- Registers a connection test function that Fivetran calls by its `__name__` during connector setup +- Shows how to read and use configuration form values inside `update()` +- Supports full and incremental sync modes, controlled by a form field +- Optionally logs extraction volume when the metrics toggle is enabled +- Supports the `fivetran configuration` command, which interactively prompts for each form field value and generates (or overrides) `configuration.json` — the resulting file can then be used with `fivetran debug --configuration configuration.json` to run the connector locally or with `fivetran deploy` to deploy it. Setup tests registered via `add_test()` can be run independently using `fivetran configuration --test`, which is useful for validating credentials, field values, or connection health without running a full sync + +## Requirements file + +This connector has no third-party dependencies. The `requirements.txt` file is present but empty. + +> Note: The `fivetran_connector_sdk:latest` and `requests:latest` packages are pre-installed in the Fivetran environment. To avoid dependency conflicts, do not declare them in your `requirements.txt`. + +## Data handling + +The connector uses hardcoded sample records to illustrate the sync pattern without requiring a live data source. Configuration values from the form are read in `update()` to control sync behavior: `sync_mode` determines whether all records are re-synced or only records added since the last checkpoint, `batch_size` shows how a page limit would be applied, and `enable_metrics` controls whether extraction stats are logged. + +Refer to `def update()` for details. + +## Additional considerations + +The examples provided are intended to help you effectively use Fivetran's Connector SDK. While we've tested the code, Fivetran cannot be held responsible for any unexpected or negative consequences that may arise from using these examples. For inquiries, please reach out to our Support team. diff --git a/examples/quickstart_examples/configuration_form/connector.py b/examples/quickstart_examples/configuration_form/connector.py index 1090d93d2..47aeee150 100644 --- a/examples/quickstart_examples/configuration_form/connector.py +++ b/examples/quickstart_examples/configuration_form/connector.py @@ -5,10 +5,6 @@ # See the Technical Reference documentation (https://fivetran.com/docs/connectors/connector-sdk/technical-reference#update) # and the Best Practices documentation (https://fivetran.com/docs/connectors/connector-sdk/best-practices) for details. -import json - -import requests - # Import required classes from fivetran_connector_sdk # For supporting Connector operations like Update() and Schema() from fivetran_connector_sdk import Connector @@ -28,6 +24,11 @@ # For returning success/failure responses from setup test functions from fivetran_connector_sdk import Test +import json +import os + +import requests + def configuration_form(): """ @@ -162,19 +163,6 @@ def schema(configuration: dict): ] -def validate_configuration(configuration: dict): - """ - Validate the configuration dictionary to ensure it contains all required parameters. - Args: - configuration: a dictionary that holds the configuration settings for the connector. - Raises: - ValueError: if any required configuration parameter is missing. - """ - for key in ("api_base_url", "api_key"): - if not configuration.get(key): - raise ValueError(f"Missing required configuration value: '{key}'") - - def update(configuration: dict, state: dict): """ Define the update function, which is a required function, and is called by Fivetran during each sync. @@ -187,19 +175,15 @@ def update(configuration: dict, state: dict): """ log.warning("Example: QuickStart Examples - Configuration Form") - validate_configuration(configuration) - - api_base_url = configuration["api_base_url"].rstrip("/") - api_key = configuration["api_key"] + api_base_url = configuration.get("api_base_url", "").rstrip("/") + api_key = configuration.get("api_key", "") batch_size = int(configuration.get("batch_size", 100)) - enable_metrics = str(configuration.get("enable_metrics", "false")).lower() == "true" + is_metrics_enabled = str(configuration.get("enable_metrics", "false")).lower() == "true" sync_mode = configuration.get("sync_mode", "full") - # In incremental mode, resume from where the last sync left off cursor = state.get("cursor", 0) if sync_mode == "incremental" else 0 total_records = 0 - total_bytes = 0 while True: response = requests.get( @@ -215,25 +199,25 @@ def update(configuration: dict, state: dict): break for post in posts: + # The 'upsert' operation is used to insert or update data in the destination table. + # The first argument is the name of the destination table. + # The second argument is a dictionary containing the record to be upserted. op.upsert(table="post", data=post) - if enable_metrics: - total_bytes += len(json.dumps(post).encode("utf-8")) total_records += len(posts) cursor += len(posts) # Save the progress by checkpointing the state. This is important for ensuring that the sync - # process can resume from the correct position in case of the next sync or interruptions. + # process can resume from the correct position in case of next sync or interruptions. # Learn more about how and where to checkpoint by reading our best practices documentation # (https://fivetran.com/docs/connectors/connector-sdk/best-practices#largedatasetrecommendation). op.checkpoint({"cursor": cursor}) - # JSONPlaceholder returns all results when the offset exceeds total; stop when partial page received if len(posts) < batch_size: break - if enable_metrics: - log.info(f"Sync complete: {total_records} records, {total_bytes} bytes extracted") + if is_metrics_enabled: + log.info(f"Sync complete: {total_records} records extracted") # This creates the connector object that will use the update, schema, and configuration_form @@ -246,17 +230,8 @@ def update(configuration: dict, state: dict): # Please test using the Fivetran debug command prior to finalizing and deploying your connector. if __name__ == "__main__": # Open the configuration.json file and load its contents into a dictionary. - with open("configuration.json", "r") as f: + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "configuration.json") + with open(config_path, "r") as f: configuration = json.load(f) # Adding this code to your `connector.py` allows you to test your connector by running your file directly from your IDE. connector.debug(configuration=configuration) - -# Resulting table: -# ┌─────┬────────┬───────────────────────┬──────────────────────────────────┐ -# │ id │ userId │ title │ body │ -# │ int │ int │ varchar │ varchar │ -# ├─────┼────────┼───────────────────────┼──────────────────────────────────┤ -# │ 1 │ 1 │ sunt aut facere ... │ quia et suscipit suscipit ... │ -# │ 2 │ 1 │ qui est esse │ est rerum tempore vitae ... │ -# │ ... │ ... │ ... │ ... │ -# └─────┴────────┴───────────────────────┴──────────────────────────────────┘ From 42b606583043e1fd0cb6c930e59f96707fd70c9b Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Thu, 23 Apr 2026 22:55:38 +0530 Subject: [PATCH 04/23] deleted configuration.json --- .../configuration_form/configuration.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 examples/quickstart_examples/configuration_form/configuration.json diff --git a/examples/quickstart_examples/configuration_form/configuration.json b/examples/quickstart_examples/configuration_form/configuration.json deleted file mode 100644 index 0c3c1ffea..000000000 --- a/examples/quickstart_examples/configuration_form/configuration.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "api_base_url": "https://jsonplaceholder.typicode.com", - "api_key": "demo_key", - "batch_size": "10", - "enable_metrics": "true", - "sync_mode": "incremental" -} From 82d57c22351e33018c441d4cf90a5276f6525011 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Thu, 23 Apr 2026 23:00:04 +0530 Subject: [PATCH 05/23] README.md update --- .../configuration_form/README.md | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/quickstart_examples/configuration_form/README.md b/examples/quickstart_examples/configuration_form/README.md index c0798a8a8..245c1045d 100644 --- a/examples/quickstart_examples/configuration_form/README.md +++ b/examples/quickstart_examples/configuration_form/README.md @@ -20,11 +20,23 @@ Refer to `def configuration_form()` and `def connection_test()` in `connector.py Refer to the [Connector SDK Setup Guide](https://fivetran.com/docs/connector-sdk/setup-guide) to get started. -To run the connector locally: +1. Run the `fivetran configuration` command to interactively provide values for each form field. This generates (or overrides) a `configuration.json` file in the project directory. -```bash -fivetran debug --configuration configuration.json -``` + ```bash + fivetran configuration + ``` + +2. Optionally, run setup tests to validate your inputs without running a full sync. + + ```bash + fivetran configuration --test + ``` + +3. Run the connector locally using the generated `configuration.json`. + + ```bash + fivetran debug --configuration configuration.json + ``` ## Features From 668582252c214324a20f6476eb918b1e8d90f660 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Thu, 23 Apr 2026 23:09:38 +0530 Subject: [PATCH 06/23] review comments --- .../configuration_form/connector.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/quickstart_examples/configuration_form/connector.py b/examples/quickstart_examples/configuration_form/connector.py index 47aeee150..fd9da5cb3 100644 --- a/examples/quickstart_examples/configuration_form/connector.py +++ b/examples/quickstart_examples/configuration_form/connector.py @@ -24,9 +24,12 @@ # For returning success/failure responses from setup test functions from fivetran_connector_sdk import Test +# For reading configuration from the local configuration.json file during debug runs import json import os +import re +# For making HTTP requests to the example API in the connection test and sync logic import requests @@ -71,6 +74,7 @@ def configuration_form(): label="Batch Size", description="Number of records to fetch per API request.", values=[10, 100, 500], + required=True, ) ) @@ -124,6 +128,8 @@ def connection_test(configuration: dict): if not api_base_url: return test.failure("api_base_url is required.") + if not re.match(r"^https?://", api_base_url): + return test.failure("api_base_url must start with http:// or https://.") if not api_key: return test.failure("api_key is required.") @@ -151,7 +157,7 @@ def schema(configuration: dict): """ Define the schema function which lets you configure the schema your connector delivers. See the technical reference documentation for more details on the schema function: - https://fivetran.com/docs/connectors/connector-sdk/technical-reference#schema + https://fivetran.com/docs/connector-sdk/technical-reference/connector-sdk-code/connector-sdk-methods#schema Args: configuration: a dictionary that holds the configuration settings for the connector. """ @@ -169,9 +175,9 @@ def update(configuration: dict, state: dict): See the technical reference documentation for more details on the update function https://fivetran.com/docs/connectors/connector-sdk/technical-reference#update Args: - configuration: a dictionary that holds the configuration settings for the connector. - state: a dictionary that holds the state of the connector. - The state dictionary is empty for the first sync or for any full re-sync. + configuration: A dictionary containing connection details + state: A dictionary containing state information from previous runs + The state dictionary is empty for the first sync or for any full re-sync """ log.warning("Example: QuickStart Examples - Configuration Form") @@ -207,10 +213,12 @@ def update(configuration: dict, state: dict): total_records += len(posts) cursor += len(posts) - # Save the progress by checkpointing the state. This is important for ensuring that the sync - # process can resume from the correct position in case of next sync or interruptions. + # Save the progress by checkpointing the state. This is important for ensuring that the sync process can resume + # from the correct position in case of next sync or interruptions. + # You should checkpoint even if you are not using incremental sync, as it tells Fivetran it is safe to write to destination. + # For large datasets, checkpoint regularly (e.g., every N records) not only at the end. # Learn more about how and where to checkpoint by reading our best practices documentation - # (https://fivetran.com/docs/connectors/connector-sdk/best-practices#largedatasetrecommendation). + # (https://fivetran.com/docs/connector-sdk/best-practices#optimizingperformancewhenhandlinglargedatasets). op.checkpoint({"cursor": cursor}) if len(posts) < batch_size: From a596670eaff76fccce71561a47edd1d8afd67357 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Fri, 24 Apr 2026 16:01:57 +0530 Subject: [PATCH 07/23] test fix --- examples/quickstart_examples/configuration_form/connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quickstart_examples/configuration_form/connector.py b/examples/quickstart_examples/configuration_form/connector.py index fd9da5cb3..0379be827 100644 --- a/examples/quickstart_examples/configuration_form/connector.py +++ b/examples/quickstart_examples/configuration_form/connector.py @@ -108,7 +108,7 @@ def configuration_form(): ) # Register a setup test — Fivetran calls connection_test() when the user clicks "Test Connection" - config_form.add_test(connection_test, label="Test connection") + config_form.add_test(label="Test connection", func=connection_test) return config_form From 63cef96ae5bc4bae22378e29534ffd9ad6d171e5 Mon Sep 17 00:00:00 2001 From: Dejan Tucakov Date: Mon, 27 Apr 2026 12:22:22 +0200 Subject: [PATCH 08/23] Apply suggestions from code review Co-authored-by: Dejan Tucakov --- examples/quickstart_examples/configuration_form/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/quickstart_examples/configuration_form/README.md b/examples/quickstart_examples/configuration_form/README.md index 245c1045d..b06ae1c55 100644 --- a/examples/quickstart_examples/configuration_form/README.md +++ b/examples/quickstart_examples/configuration_form/README.md @@ -4,7 +4,7 @@ This example demonstrates how to define a connector setup form using `ConfigurationForm`, `form_field`, and `Test` from the Fivetran Connector SDK. It covers all available field types — plain text, password, dropdown, toggle, and descriptive dropdown — and shows how to register a connection test that Fivetran runs when the user clicks **Test Connection** during setup. -The API fields (`api_base_url`, `api_key`) in the configuration form are included to illustrate how a real connector would collect credentials — they are not required for this example to run. The `configuration.json` file provided contains sample values used only to demonstrate the form fields when running locally. +The API fields (`api_base_url`, `api_key`) in the configuration form are included to illustrate how a real connector would collect credentials. However, they are not required for this example to run. The `configuration.json` file provided contains sample values used only to demonstrate the form fields when running locally. Refer to `def configuration_form()` and `def connection_test()` in `connector.py` for the main setup form and test implementation. @@ -55,7 +55,7 @@ This connector has no third-party dependencies. The `requirements.txt` file is p ## Data handling -The connector uses hardcoded sample records to illustrate the sync pattern without requiring a live data source. Configuration values from the form are read in `update()` to control sync behavior: `sync_mode` determines whether all records are re-synced or only records added since the last checkpoint, `batch_size` shows how a page limit would be applied, and `enable_metrics` controls whether extraction stats are logged. +The example uses hardcoded sample records to illustrate the sync pattern without requiring a live data source. Configuration values from the form are read in `update()` to control sync behavior. `sync_mode` determines whether all records are re-synced or only records added since the last checkpoint, `batch_size` shows how a page limit would be applied, and `enable_metrics` controls whether extraction stats are logged. Refer to `def update()` for details. From aa7643075d7c03fd30e3b549f7d40fdc3cdc93d7 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Mon, 27 Apr 2026 21:05:44 +0530 Subject: [PATCH 09/23] remove --- examples/quickstart_examples/configuration_form/connector.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/quickstart_examples/configuration_form/connector.py b/examples/quickstart_examples/configuration_form/connector.py index 0379be827..056feee47 100644 --- a/examples/quickstart_examples/configuration_form/connector.py +++ b/examples/quickstart_examples/configuration_form/connector.py @@ -41,7 +41,6 @@ def configuration_form(): Returns: ConfigurationForm: the completed form with fields and tests. """ - log.info("Building configuration form") config_form = ConfigurationForm() # Plain text field — visible input, suitable for non-sensitive values like URLs From d803dd7e02fdc7643ea7b05ab4e553914a806178 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Tue, 28 Apr 2026 19:57:49 +0530 Subject: [PATCH 10/23] CLI update --- examples/quickstart_examples/configuration_form/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/quickstart_examples/configuration_form/README.md b/examples/quickstart_examples/configuration_form/README.md index b06ae1c55..b0a20f70b 100644 --- a/examples/quickstart_examples/configuration_form/README.md +++ b/examples/quickstart_examples/configuration_form/README.md @@ -20,6 +20,8 @@ Refer to `def configuration_form()` and `def connection_test()` in `connector.py Refer to the [Connector SDK Setup Guide](https://fivetran.com/docs/connector-sdk/setup-guide) to get started. +For available CLI commands, refer to the [Connector SDK Commands](https://fivetran.com/docs/connector-sdk/connector-development-and-configuration/connector-sdk-commands) reference. + 1. Run the `fivetran configuration` command to interactively provide values for each form field. This generates (or overrides) a `configuration.json` file in the project directory. ```bash From f59e437b10f5382991e98501d1f8156f48a91ec9 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Wed, 3 Jun 2026 18:17:30 +0530 Subject: [PATCH 11/23] example readme update --- examples/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/README.md b/examples/README.md index a6590936c..a9f48bebc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,6 +17,8 @@ These are graded examples designed to help you get started with the Connector SD - [configuration](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/configuration) - This example shows how to use secrets. +- [configuration_form](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/configuration_form) - This example shows how to define a connector setup form using `ConfigurationForm`, `form_field`, and `Test`. It demonstrates all available field types (plain text, password, dropdown, toggle, and descriptive dropdown) and how to register a connection test. + - [multiple_code_files](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/multiple_code_files_with_sub_directory_structure) - This example shows how you can write a complex connector comprising multiple `.py` files. - [using_pd_dataframes](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/using_pd_dataframes) - This example shows the use of Pandas DataFrames to manipulate data prior to sending to Fivetran. From 5ab91ed1ad5049a8d92eee4172fee5a31a466eb3 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Thu, 4 Jun 2026 16:51:41 +0530 Subject: [PATCH 12/23] update --- examples/quickstart_examples/configuration_form/connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quickstart_examples/configuration_form/connector.py b/examples/quickstart_examples/configuration_form/connector.py index 056feee47..81661e148 100644 --- a/examples/quickstart_examples/configuration_form/connector.py +++ b/examples/quickstart_examples/configuration_form/connector.py @@ -229,7 +229,7 @@ def update(configuration: dict, state: dict): # This creates the connector object that will use the update, schema, and configuration_form # functions defined in this connector.py file. -connector = Connector(update=update, schema=schema, configuration_form=configuration_form) +connector = Connector(update=update, schema=schema, configuration_form_fn=configuration_form) # Check if the script is being run as the main module. # This is Python's standard entry method allowing your script to be run directly from the command line or IDE 'run' button. From fc81fc053d174770c90f8cdf00b27be2998807c9 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Thu, 4 Jun 2026 16:57:50 +0530 Subject: [PATCH 13/23] update plain text explicit --- examples/quickstart_examples/configuration_form/connector.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/quickstart_examples/configuration_form/connector.py b/examples/quickstart_examples/configuration_form/connector.py index 81661e148..f31ee996b 100644 --- a/examples/quickstart_examples/configuration_form/connector.py +++ b/examples/quickstart_examples/configuration_form/connector.py @@ -50,6 +50,7 @@ def configuration_form(): label="API Base URL", description="The base URL for your REST API endpoint.", required=True, + field_type=form_field.TextField.PlainText, placeholder="https://api.example.com/v1", ) ) From 770bfdd2790ad1ab118865fb7ed0e74b2aea3898 Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Fri, 5 Jun 2026 20:11:21 +0530 Subject: [PATCH 14/23] update url --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index a9f48bebc..cab58efbf 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,7 +17,7 @@ These are graded examples designed to help you get started with the Connector SD - [configuration](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/configuration) - This example shows how to use secrets. -- [configuration_form](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/configuration_form) - This example shows how to define a connector setup form using `ConfigurationForm`, `form_field`, and `Test`. It demonstrates all available field types (plain text, password, dropdown, toggle, and descriptive dropdown) and how to register a connection test. +- [configuration_form](https://github.com/fivetran/connector_sdk/tree/main/examples/quickstart_examples/configuration_form) - This example shows how to define a connector setup form using `ConfigurationForm`, `form_field`, and `Test`. It demonstrates all available field types (plain text, password, dropdown, toggle, and descriptive dropdown) and how to register a connection test. - [multiple_code_files](https://github.com/fivetran/fivetran_connector_sdk/tree/main/examples/quickstart_examples/multiple_code_files_with_sub_directory_structure) - This example shows how you can write a complex connector comprising multiple `.py` files. From 90286cbd3dd7fcfe6c3570c25935f2133ef5e8ab Mon Sep 17 00:00:00 2001 From: "anushka.parashar" Date: Fri, 5 Jun 2026 22:22:40 +0530 Subject: [PATCH 15/23] Added images --- .../configuration_form/README.md | 10 ++++++++++ .../images/configuration_form.png | Bin 0 -> 855010 bytes .../configuration_form/images/setup_tests.png | Bin 0 -> 158146 bytes 3 files changed, 10 insertions(+) create mode 100644 examples/quickstart_examples/configuration_form/images/configuration_form.png create mode 100644 examples/quickstart_examples/configuration_form/images/setup_tests.png diff --git a/examples/quickstart_examples/configuration_form/README.md b/examples/quickstart_examples/configuration_form/README.md index b0a20f70b..b9c4b87c3 100644 --- a/examples/quickstart_examples/configuration_form/README.md +++ b/examples/quickstart_examples/configuration_form/README.md @@ -8,6 +8,16 @@ The API fields (`api_base_url`, `api_key`) in the configuration form are include Refer to `def configuration_form()` and `def connection_test()` in `connector.py` for the main setup form and test implementation. +## Example UI + +### Configuration form + +![Configuration form UI](./images/configuration_form.png) + +### Setup tests + +![Setup tests UI](./images/setup_tests.png) + ## Requirements - [Supported Python versions](https://github.com/fivetran/fivetran_connector_sdk/blob/main/README.md#requirements) diff --git a/examples/quickstart_examples/configuration_form/images/configuration_form.png b/examples/quickstart_examples/configuration_form/images/configuration_form.png new file mode 100644 index 0000000000000000000000000000000000000000..611d0507578490001d1cb27a558c764111151341 GIT binary patch literal 855010 zcmeFZby!sG+BZz62#SC-N=Zve4=p7vAdS);Lk%!=2uMq(Qqm352r3=YJs_P!56!!{ z@BQrO-uJU#J>LEN_Z{CH$IM!@))njQ-*sN+OsI;I3?2>{4iXX)o}8?d8gMv5Lc+Gf z!T_#}DLa!QA>C23l$2DFla!=Uak4kFv@u0Ok`0a5!hEIPO`M^t_6QA2N=$xTks#*2 zm^>ESQ6vrXBWwcsci7_JB3T>#tx4_`uQN;k7$b`Q7ToZR_}e@QE^|-AXLi>WMXape zmV@TAmij}O&%h;kpdJiQNA{inmM9NaRYW?VJFA>D@)=)Ven7lI+J;QHj)L>_C+@PE zlCv|vuj%J2PY4u`rst$GKy54Y>ZZm5TiOX3NtWm9hD1FL{?AOLHeGZzZ4iMYRR z3!^8v5Z{hVEvHwjwfb7Y>27vmv?g$D(k$oKWB1=4-mRwegK z!v$kx@`y2ZgO}VTy^5o$2{0-ieI<>Z?b>^k`tgoq^f1OMehI7%(!=kW48$u;3EUrv+&vDr69@fSh-Sz>7qu`4jX z#vG)5E`O9d+RkdXTpsb_UeUX6C~uNw?{Ak4clL0bh*wCSxIhjTCo7D#-xw?tw7kzG zqW;)JbRyeEeMJ4;|2UK`E$}sEjF;1?1FM9pSQfKu8fz@w*8+DvNNdf`Hhf59KA4F%`b$d^0O zcwZ@-6`S{z#t6|)#W1-1u=wwAcF@LRc+%~WVd8v&6NLJDnWWtqm0>*)yB>L>*&Qmw zKQ@MJa_5*y!>P^q?V;Git{_C1IDPk9%+Q59K%f>D^k`92jv>HP*QzWnfT zlkBE9UcgZEs)KV>RNMbtW!I$Wfrlefwavk!Qobgl@`v}X5gDgv&T#nOLRulB}in5pf)gpV#Asg5Ob zq@0)w9tK)<3@NNwLU4~#ga@ss7hA5KSWDa-I!{6Uwn3AS2r z@pI-FTUoXPS|==l$TVrenA9$44(1slBwQHs9^Tv$%8zQkc-H|vWC8m#Qf4SeTjFB8JKp@;%�vh<(r{x8L)pbd!X_*uw25 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