Skip to content

feat(aveva_pi): Add AVEVA PI Connector SDK connector#28

Open
fivetran-aishwaryardongal wants to merge 28 commits into
mainfrom
aveva-pi-connector
Open

feat(aveva_pi): Add AVEVA PI Connector SDK connector#28
fivetran-aishwaryardongal wants to merge 28 commits into
mainfrom
aveva-pi-connector

Conversation

@fivetran-aishwaryardongal

@fivetran-aishwaryardongal fivetran-aishwaryardongal commented Jun 18, 2026

Copy link
Copy Markdown

Summary

  • Rewrites the AVEVA PI connector to use PI Web API (REST over HTTPS) instead of ODBC/PI SQL DAS
  • Eliminates the pyodbc dependency and the requirement to install AVEVA's proprietary ODBC driver — the connector now works in Fivetran's managed cloud environment with no additional installation
  • Syncs four fixed tables that map directly to PI AF object types exposed by PI Web API

What this connector does

elements (full reimport): PI AF elements — the asset hierarchy. Uses GET /assetdatabases/{webId}/elements?searchFullHierarchy=true.

attributes (full reimport): PI AF element attributes including PI Point data reference metadata. Uses /elementattributes (PI Web API 2019+) with per-element fallback for older versions. Returns PI Point attribute WebIds for use by the recorded_values sync.

event_frames (cursor-based incremental by start_time): PI AF event frames. Uses GET /assetdatabases/{webId}/eventframes?startTime=...&endTime=.... Adaptive time-window backoff: starts at 30-day windows, halves on request failures, minimum 1-hour window.

recorded_values (cursor-based incremental by timestamp, opt-in): PI archive data for PI Point attributes. Uses GET /streams/{attrWebId}/recorded?startTime=...&endTime=... per attribute. Applies a 2-hour late-arrival rollback on subsequent syncs. Disabled by default (sync_recorded_values = "false") because it can generate very large data volumes.

Design decisions

Decision Rationale
PI Web API instead of ODBC ODBC requires proprietary AVEVA driver; can't install in Fivetran's managed cloud environment
Fixed schema (4 tables) PI Web API exposes well-known object types; dynamic schema discovery not needed
requests (no extra deps) Pre-installed in Fivetran SDK runtime — requirements.txt is empty
Static schema() No connection needed to declare schema; improves setup reliability
sync_recorded_values opt-in Recorded values can be billions of rows on large PI deployments
Shared time cursor across attributes Simpler state management; all attributes advance together per window

Testing

Syntax and import validation:

cd aveva_pi
python -c "import connector; print('OK')"
# → module loads OK

python -c "
import connector
tables = connector.schema({'base_url': 'https://x/piwebapi', 'username': 'u', 'password': 'p'})
for t in tables:
    print(t['table'], t['primary_key'])
"
# → elements ['web_id']
# → attributes ['web_id']
# → event_frames ['web_id']
# → recorded_values ['_fivetran_id']

Local debug run (requires a reachable PI Web API instance):

# Update configuration.json with real PI Web API details
fivetran debug

Validated:

  • python -c "import connector" — no import errors ✓
  • schema() returns all 4 tables with correct PKs and column types ✓
  • No external dependencies beyond pre-installed requests

Test plan

  • Update configuration.json with a real PI Web API endpoint
  • Run fivetran debug and verify elements + attributes are upserted
  • Run fivetran debug a second time and verify incremental cursors advance for event_frames
  • Set sync_recorded_values = "true" and verify recorded_values are synced

Screenshots and testing

fivetran deploy --api-key $FIVETRAN_API_KEY --destination abdul_test_live_2 --connection ad_test3
Screenshot 2026-06-29 at 7 42 11 PM
Screenshot 2026-07-01 at 2 59 24 PM
Screenshot 2026-07-01 at 3 02 36 PM
Screenshot 2026-07-08 at 7 00 13 AM
Screenshot 2026-07-08 at 7 00 24 AM
Screenshot 2026-07-08 at 7 00 34 AM

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

🧹 Python Code Quality Check

✅ No issues found in Python Files.

🔍 See how this check works

This comment is auto-updated with every commit.

@fivetran-aishwaryardongal fivetran-aishwaryardongal marked this pull request as ready for review June 19, 2026 14:30
Copilot AI review requested due to automatic review settings June 19, 2026 14:30
@fivetran-aishwaryardongal fivetran-aishwaryardongal marked this pull request as draft June 19, 2026 14:32

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

Adds a new aveva_pi Connector SDK example that syncs AVEVA PI AF/Archive data via PI Web API (HTTPS/REST) instead of requiring ODBC drivers, aligning the example with Fivetran’s managed cloud environment constraints.

Changes:

  • Introduces a PI Web API–based connector implementation with full-reimport + cursor-based incremental sync strategies.
  • Adds example configuration + documentation describing setup, pagination, and table outputs.
  • Adds connector folder scaffolding (requirements.txt, .gitignore) consistent with a standalone example.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
aveva_pi/connector.py Implements PI Web API session/auth, pagination, incremental cursors, and emits the four target tables.
aveva_pi/README.md Documents connector behavior, configuration, pagination, and tables.
aveva_pi/configuration.json Provides example configuration for PI Web API connectivity.
aveva_pi/requirements.txt Notes that no additional dependencies are required.
aveva_pi/.gitignore Adds local-environment ignores for the example folder.

Comment thread aveva_pi/configuration.json Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/README.md
Comment thread aveva_pi/README.md
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/connector.py Outdated
Comment thread aveva_pi/connector.py Outdated
Comment thread aveva_pi/connector.py Outdated
Comment thread aveva_pi/connector.py Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/.gitignore Outdated

@fivetran-sahilkhirwal fivetran-sahilkhirwal Jun 28, 2026

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.

You should not push this to github when adding it as example

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Acknowledged — the committed configuration.json uses angle-bracket placeholders only (e.g. <PI_WEB_API_BASE_URL>). No real credentials are present in the repository.

Comment thread aveva_pi/connector.py
Comment thread aveva_pi/requirements.txt Outdated
@fivetran-aishwaryardongal fivetran-aishwaryardongal marked this pull request as ready for review June 29, 2026 15:11
Copilot AI review requested due to automatic review settings June 29, 2026 15:11

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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.

Comment thread aveva_pi/connector.py
Comment thread aveva_pi/connector.py Outdated
Comment thread aveva_pi/connector.py Outdated
Comment thread aveva_pi/client.py Outdated
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/README.md
Comment thread aveva_pi/configuration.json Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/models.py Outdated
Comment thread aveva_pi/README.md
Comment thread aveva_pi/README.md
Comment thread aveva_pi/README.md
Comment thread aveva_pi/README.md
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/connector.py
Comment thread aveva_pi/connector.py Outdated
Comment thread aveva_pi/client.py Outdated
Comment thread aveva_pi/client.py
Comment thread aveva_pi/sync.py

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

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

Comment thread aveva_pi/connector.py Outdated
Comment thread aveva_pi/client.py Outdated
Comment thread aveva_pi/client.py
Comment thread aveva_pi/client.py
Comment thread aveva_pi/client.py
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/client.py
Comment thread aveva_pi/client.py
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/sync.py
Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/sync.py Outdated
Copilot AI review requested due to automatic review settings July 1, 2026 10:10
@fivetran-aishwaryardongal fivetran-aishwaryardongal requested a review from a team as a code owner July 1, 2026 10:10

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

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

Comment thread aveva_pi/sync.py Outdated
Comment thread aveva_pi/sync.py Outdated
Comment on lines +421 to +426
except ValueError as exc:
# Surface 401 immediately — session-level auth failure affects all streams
if "Authentication error (401)" in str(exc):
raise
# 404 = PI Point deleted; 403 = no access to this stream — skip
log.warning(f" Skipping stream {attr_web_id}: {exc}")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The per-stream handler now only skips on authentication error 403 (no access) and client error 404 (PI Point deleted). All other client errors — including 400 and 422 — are re-raised to surface real issues rather than silently advancing the cursor.

Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/configuration.json Outdated
Comment thread aveva_pi/README.md Outdated
… case

- Fix flake8 W503/black conflict by restructuring multi-line boolean expressions in sync.py
- Tighten /elementattributes fallback to only trigger on 404/405 errors
- Narrow per-stream skip to 403/404; surface all other errors including 401
- Fix start_date placeholder in configuration.json and README
- Use sentence case for table headings in README per doc standards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 06:20

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

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

Comment thread aveva_pi/sync.py Outdated
Comment on lines +299 to +302
cursors = state.setdefault("cursors", {})
start = parse_pi_timestamp(cursors.get("event_frames", start_date)) or datetime.fromtimestamp(
0, tz=timezone.utc
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The cursor handling now mirrors the sync_recorded_values approach: the stored cursor is parsed separately; if it is present but unparseable, a warning is logged and the fallback chain (start_date → epoch) is used instead of silently jumping to epoch.

Comment thread aveva_pi/sync.py
Comment on lines +195 to +198
log.info("Syncing attributes (full reimport)")
count = 0
pi_point_web_ids = []

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Acknowledged. The list is only materialized when sync_recorded_values = "true", which is opt-in and disabled by default. The README warns that enabling it can generate very large data volumes on large PI deployments. Streaming PI Point WebIds across the two sync functions would require a significant architectural change beyond the scope of this example connector, but the opt-in guard and the README warning together limit the blast radius for typical deployments.

Comment thread aveva_pi/configuration.json Outdated
"password": "<PI_PASSWORD>",
"database_name": "<PI_AF_DATABASE_NAME>",
"verify_ssl": "<TRUE_OR_FALSE_DEFAULT_TRUE>",
"start_date": "<2020-01-01T00:00:00Z>",

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated to <START_DATE_ISO8601_UTC_OPTIONAL> — descriptive, signals the expected format, and makes it clear this is user-supplied and optional.

Comment thread aveva_pi/README.md Outdated
"password": "<PI_PASSWORD>",
"database_name": "<PI_AF_DATABASE_NAME>",
"verify_ssl": "<TRUE_OR_FALSE_DEFAULT_TRUE>",
"start_date": "<2020-01-01T00:00:00Z>",

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated to <START_DATE_ISO8601_UTC_OPTIONAL> in the README config block as well.

Comment thread aveva_pi/sync.py Outdated
Comment on lines +237 to +241
except ValueError as exc:
exc_str = str(exc)
# Surface auth failures immediately.
if "Authentication error" in exc_str:
raise

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Introduced PiApiError(status_code, message) in client.py. api_get() now raises PiApiError for all non-retryable 4xx responses, and all handlers in sync.py now branch on .status_code (e.g., exc.status_code == 401, exc.status_code not in (403, 404)) instead of parsing the error message string.

…, update placeholders

- Add PiApiError(status_code, message) to client.py; api_get() now raises it for 4xx
  responses instead of ValueError, enabling callers to branch on status_code rather
  than parsing error message strings
- Update all except-ValueError handlers in sync.py to except PiApiError and check
  .status_code directly (eliminates brittle substring matching)
- Fix event_frames cursor fallback: malformed cursor now logs a warning and falls back
  to start_date (then epoch), matching the sync_recorded_values approach
- Change start_date placeholder to <START_DATE_ISO8601_UTC_OPTIONAL> in both
  configuration.json and README to clearly signal it is user-supplied and optional

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 10:13

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

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

Comment thread aveva_pi/sync.py Outdated
Comment on lines +213 to +214
if collect_pi_points and item.get("DataReferencePlugIn") == "PI Point":
pi_point_web_ids.append(item["WebId"])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Now uses .get("WebId", "") and only appends non-empty WebIds, matching the defensive style used elsewhere in the file.

Comment thread aveva_pi/sync.py
Comment on lines +251 to +257
elem_web_id = elem_item.get("WebId", "")
try:
for attr_item in paginate(
session,
f"{base}/elements/{elem_web_id}/attributes",
params={"maxCount": __MAX_COUNT},
):

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Elements with a missing or empty WebId are now skipped with a warning before the /elements/{elem_web_id}/attributes URL is constructed.

… fallback fetch

- Use .get("WebId", "") and skip empty WebIds when accumulating PI Point attribute
  WebIds to avoid KeyError when the API omits the field
- Skip elements with a missing WebId in the per-element fallback fetch to avoid
  building an invalid URL and failing the whole sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 15:36

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

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

Comment thread aveva_pi/client.py Outdated
Comment on lines +159 to +173
servers = api_get(session, f"{base}/assetservers").get("Items", [])
if not servers:
raise ValueError("No PI Asset Servers found via PI Web API. Check base_url.")

for server in servers:
databases = api_get(session, f"{base}/assetservers/{server['WebId']}/assetdatabases").get(
"Items", []
)
for db in databases:
if database_name is None or db.get("Name") == database_name:
log.info(f"Connected to database '{db['Name']}' on server '{server.get('Name')}'")
return db["WebId"]

target = f"'{database_name}'" if database_name else "any database"
raise ValueError(f"Could not find {target} on any PI Asset Server. Check database_name.")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. get_database_web_id() now uses paginate() for both /assetservers and /assetdatabases, so all pages are walked. Server items with a missing WebId are skipped safely before the database URL is constructed.

Comment thread aveva_pi/connector.py Outdated
f"Invalid sync_recorded_values value '{sync_rv}'. Expected 'true' or 'false'."
)

# Validate verify_ssl flag if provided — an unrecognised value silently disables TLS

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The comment now reads: build_session() keeps TLS enabled for any value other than "false", which accurately reflects the implementation.

…ading TLS comment

- get_database_web_id() now uses paginate() for both /assetservers and
  /assetdatabases so all pages are walked on large deployments, and server
  items with a missing WebId are skipped safely
- Correct the misleading verify_ssl comment in validate_configuration():
  build_session() keeps TLS enabled for any value other than "false"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 16:32

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

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread aveva_pi/client.py Outdated
Comment on lines +165 to +168
for db in paginate(session, f"{base}/assetservers/{server_web_id}/assetdatabases"):
if database_name is None or db.get("Name") == database_name:
log.info(f"Connected to database '{db['Name']}' on server '{server.get('Name')}'")
return db.get("WebId", "")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Database entries with a missing WebId are now skipped before returning, and db.get("Name", "") is used in both the match condition and the log statement, eliminating the KeyError risk.

…ame in discovery

- Skip database entries that have no WebId to avoid returning an empty string
  and building invalid request URLs downstream
- Use db.get("Name", "") in both the match check and the log statement to
  avoid KeyError if PI Web API omits the Name field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 16:40

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

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

Comment thread aveva_pi/README.md Outdated
Comment thread aveva_pi/sync.py
table="recorded_values",
data=extract_recorded_value(item, attr_web_id),
)
count += 1

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Added a periodic op.checkpoint() every __CHECKPOINT_INTERVAL rows inside fetch_window, consistent with the approach in sync_elements and sync_attributes. This signals liveness for large windows and reduces rework on interruption.

…alueError in README

- Add periodic op.checkpoint() every __CHECKPOINT_INTERVAL rows inside the
  fetch_window closure of sync_recorded_values() to signal liveness and reduce
  rework on large windows, consistent with sync_elements and sync_attributes
- Update README error handling section: api_get() raises PiApiError (not ValueError)
  for non-retryable 4xx responses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 17:04

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

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

Comment thread aveva_pi/client.py
Comment on lines +45 to +50
# Allow users to disable TLS verification for self-signed PI Web API certificates.
# Default to True (verification enabled). Only disable when the user explicitly sets
# verify_ssl to "false"; any other value (including template placeholders) keeps TLS on.
_verify_ssl = str(configuration.get("verify_ssl", "true"))
session.verify = False if _verify_ssl.lower() == "false" else True
return session

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. build_session() now logs a log.warning() whenever TLS verification is disabled, making the security downgrade immediately visible in connector logs.

Comment thread aveva_pi/sync.py
Comment on lines +195 to +217
log.info("Syncing attributes (full reimport)")
count = 0
pi_point_web_ids = []

def _process(item, element_web_id):
"""
Upsert one attribute record and optionally collect PI Point WebIds.

Args:
item: raw attribute dict from PI Web API.
element_web_id: WebId of the parent element, used as a foreign key.
"""
nonlocal count
# 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="attributes", data=extract_attribute(item, element_web_id))
count += 1
if collect_pi_points and item.get("DataReferencePlugIn") == "PI Point":
web_id = item.get("WebId", "")
if web_id:
pi_point_web_ids.append(web_id)
if count % __CHECKPOINT_INTERVAL == 0:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is the same concern raised earlier (and already replied to). The list is only materialized when sync_recorded_values = "true", which is opt-in and disabled by default, and the README warns about large data volumes on large deployments. A full streaming refactor is out of scope for this example connector.

Makes the security downgrade explicit in connector logs when verify_ssl=false
so accidental insecure configuration is immediately visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 11, 2026 03:09

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

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread aveva_pi/connector.py
Args:
configuration: a dictionary that holds the configuration settings for the connector.
"""
validate_configuration(configuration)
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