Skip to content

feature(connectors/bright_data_scrape): add Bright Data Web Scraper C…#41

Open
adomhamza wants to merge 9 commits into
fivetran:mainfrom
adomhamza:feature/bright_data_scrape
Open

feature(connectors/bright_data_scrape): add Bright Data Web Scraper C…#41
adomhamza wants to merge 9 commits into
fivetran:mainfrom
adomhamza:feature/bright_data_scrape

Conversation

@adomhamza

@adomhamza adomhamza commented Jul 2, 2026

Copy link
Copy Markdown

…onnector.

Description of Change

  • Adds a new Connector SDK example that syncs web scraping data from Bright Data's Web Scraper API to Fivetran.
  • Triggers scrape jobs via /datasets/v3/trigger, polls snapshot completion, flattens nested JSON responses, and upserts results to a scrape_results table with primary key (url, result_index).
  • Follows Connector SDK best practices: explicit PK column types in schema(), scalar op.upsert() calls, retry logic with exponential backoff, configuration validation, URL validation, state checkpointing, and key-presence checks during snapshot/JSONL data extraction. No extra third-party dependencies beyond the pre-installed Fivetran environment.

Features

  • Batch URL scraping with automatic snapshot polling
  • Handles one-to-one and one-to-many URL-to-result mappings
  • Dataset-specific query parameters for supported datasets (e.g. LinkedIn Post By URL)
  • Dynamic field discovery from API responses
  • Invalid URL filtering with log.warning() before sync

Files added

  • connector.pyschema(), update(), URL parsing/validation, result processing, upsert logic, debug entry point
  • helpers/scrape.py — job triggering, snapshot polling, retry logic, response parsing
  • helpers/data_processing.py — flattening and field discovery utilities
  • helpers/validation.py — configuration validation
  • configuration.json — placeholder values (no secrets)
  • README.md — setup, configuration, and usage documentation

Testing

image image image

Checklist

Some tips and links to help validate your PR:

  • Tested the connector with fivetran debug command.
  • Added/Updated example-specific README.md file, see the README template for the required structure and guidelines.
  • Followed Python Coding Standards, refer here

@adomhamza
adomhamza marked this pull request as ready for review July 2, 2026 12:31
@adomhamza

Copy link
Copy Markdown
Author

@fivetran-JenasVimal

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new Connector SDK example (bright_data_scrape) that syncs web scraping results from Bright Data’s Web Scraper API into a Fivetran destination, including helper modules for scraping/polling, validation, and result flattening.

Changes:

  • Introduces a new Bright Data scraper connector (connector.py) with schema, URL parsing/validation, state checkpointing, and upsert logic.
  • Adds helper modules for API job triggering + snapshot polling (helpers/scrape.py), result flattening/field discovery (helpers/data_processing.py), and configuration validation (helpers/validation.py).
  • Adds end-user documentation (README.md) and a placeholder configuration.json.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
bright_data_scrape/connector.py Core Connector SDK implementation (schema/update, URL parsing, sync/upsert, state).
bright_data_scrape/helpers/scrape.py Bright Data API trigger + snapshot polling + response parsing utilities.
bright_data_scrape/helpers/data_processing.py Flattening and row-shaping utilities for scraped results.
bright_data_scrape/helpers/validation.py Configuration validation helper.
bright_data_scrape/helpers/init.py Helper re-exports for simplified imports.
bright_data_scrape/configuration.json Placeholder configuration values (no secrets).
bright_data_scrape/README.md Example documentation (setup/configuration/usage).

Comment thread bright_data_scrape/connector.py Outdated
Comment thread bright_data_scrape/connector.py
Comment thread bright_data_scrape/connector.py
Comment thread bright_data_scrape/connector.py
Comment thread bright_data_scrape/connector.py
Comment thread bright_data_scrape/README.md
Comment thread bright_data_scrape/README.md
Comment thread bright_data_scrape/helpers/__init__.py
Comment thread bright_data_scrape/helpers/scrape.py
Comment thread bright_data_scrape/helpers/scrape.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@fivetran-JenasVimal

Copy link
Copy Markdown
Contributor

Hi @adomhamza ,

Could you please rebase your PR so that the latest checks can run and pass

Thank you!

@adomhamza

Copy link
Copy Markdown
Author

Hi @adomhamza ,

Could you please rebase your PR so that the latest checks can run and pass

Thank you!

Done.

Comment thread bright_data_scrape/README.md Outdated
Comment thread bright_data_scrape/README.md Outdated
Comment thread bright_data_scrape/README.md Outdated
Comment thread bright_data_scrape/README.md Outdated
Comment thread bright_data_scrape/connector.py Outdated
adomhamza and others added 6 commits July 8, 2026 10:21
Co-authored-by: Jenas Anton Vimal <jenas.vimal@fivetran.com>
Co-authored-by: Jenas Anton Vimal <jenas.vimal@fivetran.com>
Co-authored-by: Jenas Anton Vimal <jenas.vimal@fivetran.com>
… on version control for configuration files.
… rearranging imports and enhancing list comprehensions. Added logging for URL parsing errors and streamlined the process of handling scrape results.

@fivetran-anushkaparashar fivetran-anushkaparashar left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a few comment, please have a look!

return results


def _parse_json_response(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response body consumed before fallback parse. In _parse_snapshot_response, when Content-Length exceeds the threshold, _parse_large_json_array_streaming reads the body via response.iter_content(). If it returns None (body is not a JSON array), control falls through to _parse_json_response(response, ...) which calls response.json() on an already-drained stream — this always fails or returns garbage for responses over 100 MB that aren't JSON arrays.
If the chunked path is attempted but returns None, do not call _parse_json_response. Either buffer the full content during chunked reading and pass the string directly, or return None/raise before calling the fallback.

log.info(f"Snapshot {snapshot_id[:8]}... still processing (attempt {attempt})")


def _handle_non_200_response(response: Response, snapshot_id: str, attempt: int) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All non-200, non-404 poll errors are silently retried. _handle_non_200_response calls response.raise_for_status(), which throws requests.exceptions.HTTPError — a subclass of RequestException. The except RequestException handler in _poll_snapshot catches it, logs every 5th attempt, sleeps, and continues. A 401 Unauthorized or 403 Forbidden will therefore be retried up to max_attempts (100) times (~50 minutes at 30s interval) before hitting the timeout error, hiding the actual failure.
In _handle_non_200_response (or inline in the polling loop), re-raise non-transient HTTP status codes (e.g., 401, 403, 400) immediately without letting them reach the except RequestException catch.

Comment on lines +25 to +26
if max_depth <= 0:
return {parent_key: json.dumps(data) if data else None}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix this!

missing_results = []
for url_idx, url in enumerate(urls):
if url_idx < len(scrape_results):
result = scrape_results[url_idx]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Bright Data batch API returns a flat list; if results are reordered, fewer results than URLs are returned, or a URL produces zero items, data is silently misattributed (wrong URL attached to a result) or silently dropped.
Either rely on the URL embedded in the result payload (result.get("input", {}).get("url") or result.get("url")) to attribute results, or add a post-match assertion when the result count diverges from the URL count beyond a warning.

process_scrape_result(result, result_url, result_idx)
)
elif isinstance(result, list):
for item_idx, item in enumerate(result):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate primary key collision when a single URL returns nested-list results. In the single-URL branch, when a top-level result is a list, it enumerates items with item_idx starting from 0. If multiple top-level results are themselves lists, item_idx resets to 0 for each, producing duplicate (url, result_index) pairs — the later upsert silently overwrites earlier rows.
Maintain a monotonically increasing global counter across all nested results instead of resetting item_idx per sub-list.

Comment on lines +17 to +20
required_configs = ["api_token", "dataset_id", "scrape_url"]
for key in required_configs:
if key not in configuration:
raise ValueError(f"Missing required configuration value: {key}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check this!

snapshot_id: Snapshot ID to poll
poll_interval: Interval between polling attempts in seconds
timeout: Request timeout in seconds
max_attempts: Maximum number of polling attempts before raising an error (default: 1000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix this!

process_and_upsert_results(processed_results, all_fields)

# Update state with sync information
state["last_scrape_urls"] = valid_urls

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state["last_scrape_urls"] and state["last_scrape_count"] are checkpointed each sync, but update() never reads them.
Implement actual incremental logic using states.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants