Skip to main content

PyReverseETL

Reverse ETL: move data from a source system into the tools your team actually works in, with a real lineage graph and real compliance enforcement.

License: Proprietary Version: v3.0.0 CI

A Rust sync engine with Python bindings and a CLI. pyreverseetl execute opens a real connection to a real source, reads real records, runs them through a real compliance/PII-masking engine, writes them to a real destination, and records a real lineage edge (source, destination, record count, timestamps) that you can query afterward. There is no simulated mode: if a connector isn't wired to a real backend yet, it's not offered as an option, not silently faked.

What's real right now

Connector Direction Status
PostgreSQL source + destination Real, via sqlx. Generic-schema read/write, incremental reads, upsert. Verified with a real Postgres container.
MySQL source + destination Real, via sqlx. Same capabilities as Postgres. Verified with a real MySQL container.
S3 / S3-compatible object storage (MinIO, etc.) source + destination Real, via aws-sdk-s3, with a custom-endpoint / path-style option for MinIO. JSON-lines and CSV formats are implemented; Parquet/Avro/ORC/Iceberg/Delta are declared but return an explicit "not implemented" error rather than silently no-op'ing. Verified with a real MinIO container.
Webhook destination Real HTTP POST/PATCH/DELETE via reqwest, with real auth headers (Bearer/API key/Basic) and real JSON payload construction.
Salesforce destination Real REST API client: OAuth2 token exchange, sobjects create/upsert-by-external-ID/delete/describe against the actual Salesforce endpoint shapes. No live Salesforce account was available to verify against, so this is verified against a local mock HTTP server that asserts the exact request shape (method, path, auth header, body) the real API expects.
HubSpot destination Real CRM v3 API client (create, upsert-by-idProperty, delete, properties/schema). Same caveat: verified against a mock server, not a live account.
Marketo destination Real REST API client (identity token endpoint, createOrUpdate bulk leads, lead delete, describe). Same caveat: verified against a mock server, not a live account.
GCS / Azure Blob source + destination Not implemented. Calling them returns an explicit error instead of a fake success.
Kafka, HDFS, Spark/PySpark transforms, CDC streaming, the CLI dashboard, StatGuardian quality gates — Present in the codebase from earlier work but out of scope for this pass and not wired into execute / run_sync. Treat as experimental; several return fixed/fabricated numbers (documented inline where that's the case, e.g. SparkTransformer::submit).

If you need a connector marked "not implemented" above, that's an honest gap, not a documentation oversight — open an issue rather than assuming it works.

Install

pip install pyreverseetl
# or
uv pip install pyreverseetl

pyreverseetl --version    # prints the installed version, read straight from the compiled Rust core

This installs a compiled Rust extension (built with maturin/PyO3) plus the pyreverseetl console command.

Quick start: a real sync against a local Postgres

This spins up a real Postgres container, seeds a table, and syncs it to a webhook using the real engine end to end.

docker run --rm -d -p 5432:5432 \
  -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=demo \
  --name pyreverseetl-demo-pg postgres:16

docker exec -i pyreverseetl-demo-pg psql -U postgres -d demo -c "
  CREATE TABLE customers (id INT PRIMARY KEY, name TEXT, email TEXT, ltv NUMERIC);
  INSERT INTO customers VALUES
    (1, 'Alice', 'alice@example.com', 4200.50),
    (2, 'Bob',   'bob@example.com',   1800.00);
"

# In another terminal: a throwaway HTTP endpoint to receive the synced rows
python3 -m http.server 8000 &   # or use https://webhook.site for a real inspectable URL

pyreverseetl create-workflow ltv_sync "LTV to webhook" postgres customers \
  --source-config '{"host":"localhost","port":5432,"database":"demo","username":"postgres","password":"postgres"}'

pyreverseetl create-activation ltv_to_hook ltv_sync webhook \
  --dest-config '{"url":"http://localhost:8000/hook","auth":{"type":"bearer","token":"demo"}}'

pyreverseetl execute ltv_to_hook
# {"status": "success", "run_id": "...", "rows_synced": 2, "rows_read": 2,
#  "rows_failed": 0, "compliance_violations": [], "duration_ms": 12,
#  "message": "Activation executed: 2 rows written to webhook"}

pyreverseetl lineage
# {"status": "success", "format": "json",
#  "lineage": {"nodes": {...}, "edges": [{"run_id": "...", "record_count": 2, ...}]}}

Mask PII before it ever leaves the process:

pyreverseetl execute ltv_to_hook --compliance-rules \
  '[{"id":"mask_email","rule_type":"pii_masking","target_fields":["email"],"action":{"type":"mask","pattern":"****"}}]'

Every one of these calls goes through the real Rust engine (pyreverseetl._core.run_sync) — rows_synced is the number of records the destination connector actually wrote, not a placeholder.

Python API

import json
import pyreverseetl

result = pyreverseetl.run_sync(
    source_type="postgres",
    source_config=json.dumps({
        "host": "localhost", "port": 5432, "database": "demo",
        "username": "postgres", "password": "postgres", "table": "customers",
    }),
    destination_type="webhook",
    destination_config=json.dumps({
        "url": "http://localhost:8000/hook",
        "auth": {"type": "bearer", "token": "demo"},
    }),
    limit=None,
    compliance_rules=None,
)
print(result.rows_written, result.duration_ms)

# Real lineage graph accumulated across every run_sync call in this process
print(pyreverseetl.lineage_json())
print(pyreverseetl.lineage_dot())  # Graphviz DOT export

Lineage tracking

Every run_sync call registers real source/destination nodes and appends a real edge — actual record count, actual start/completion timestamps — to an in-process lineage graph (pyreverseetl_core::lineage::LineageGraph). It supports upstream/downstream queries and exports to JSON or Graphviz DOT. This did not exist anywhere in the codebase before this pass; the README previously described "lineage tracking" as a feature with zero backing code.

Compliance & PII handling

DefaultComplianceEngine (pyreverseetl_core::governance::compliance_rules) applies real per-record rules before a write: mask a field, remove it, truncate it, or (for Encrypt) honestly report it as unresolved — there is no real encryption implementation, and the engine says so in check_compliance rather than silently claiming success. A MockComplianceEngine still exists but is #[cfg(test)]-only, so it can never run in a real build; it exists purely as a test double for exercising governance wiring without needing real masking behavior.

Architecture

Python CLI / API  →  pyreverseetl._core (PyO3 bindings)  →  pyreverseetl_core::execute_sync
                                                                  │
                                            ┌─────────────────────┼─────────────────────┐
                                       source read          compliance apply        destination write
                                  (postgres/mysql/s3)      (DefaultComplianceEngine)  (postgres/mysql/s3/
                                                                                       webhook/salesforce/
                                                                                       hubspot/marketo)
                                                                  │
                                                          lineage edge recorded
  • core/ — the Rust engine: connectors, the compliance engine, lineage tracking, the sync executor.
  • python/src/ — PyO3 bindings (run_sync, lineage_json, lineage_dot, plus the lower-level data-model classes PyWorkflow/PyDestination/etc.).
  • python/pyreverseetl/ — the installed Python package: cli.py (the pyreverseetl command), server.py (an optional Flask REST wrapper around the same engine), both backed by the real engine rather than any in-process simulation.

Development

# Rust: build, test, lint
cargo build -p pyreverseetl-core
cargo test -p pyreverseetl-core --lib          # unit tests (hermetic, no external services)
cargo test -p pyreverseetl-core --lib -- --ignored   # real Docker-backed round-trip tests, see below
cargo clippy -p pyreverseetl-core --lib
cargo fmt

# Python bindings
maturin develop --release
pytest tests/ -v

Running the real, Docker-backed connector tests

Unit tests are hermetic by design (no network, no containers). The round-trip tests that prove the Postgres/MySQL/S3 connectors actually talk to a real service are #[ignore]d by default; run them explicitly against real containers:

docker run --rm -d -p 5439:5432 -e POSTGRES_PASSWORD=postgres \
    -e POSTGRES_DB=pyreverseetl_test --name pyreverseetl-pg-test postgres:16
docker run --rm -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=mysql \
    -e MYSQL_DATABASE=pyreverseetl_test --name pyreverseetl-mysql-test mysql:8
docker run --rm -d -p 9000:9000 -e MINIO_ROOT_USER=minioadmin \
    -e MINIO_ROOT_PASSWORD=minioadmin --name pyreverseetl-minio-test minio/minio server /data
docker run --rm --entrypoint sh minio/mc -c \
    "mc alias set local http://host.docker.internal:9000 minioadmin minioadmin && mc mb local/pyreverseetl-test"

PYREVERSEETL_TEST_PG_PORT=5439 PYREVERSEETL_TEST_MYSQL_PORT=3307 \
PYREVERSEETL_TEST_MINIO_ENDPOINT=http://localhost:9000 \
    cargo test -p pyreverseetl-core --lib -- --ignored

The same Docker services also back real, end-to-end Python-level tests in tests/test_real_sync_docker.py (they run the actual pyreverseetl CLI command as a subprocess and check real rows moved through it); those are skipped, not failed, when the containers aren't running.

Contributing

See CONTRIBUTING.md for guidelines.

Known gaps (deliberately out of scope for this pass)

  • GCS / Azure object storage: return an explicit "not implemented" error.
  • Kafka / CDC streaming, HDFS, PySpark transforms, the CLI dashboard, StatGuardian quality-gate integration: present in the codebase but not wired into the real sync path (execute / run_sync); several of these return fixed, non-real numbers if you call their APIs directly (this is documented inline in the affected modules, e.g. SparkTransformer::submit). Treat anything not listed in the connector table above as unverified.
  • Salesforce/HubSpot/Marketo clients are real API implementations but were only verified against mocked HTTP responses (no live account was available in this environment) — please report any request-shape mismatches against a real account as issues.

License

Proprietary License. See LICENSE for details. All rights reserved.

Support

Release files for PyReverseETL 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for PyReverseETL 3.0.0
File Interpreter ABI Platform
pyreverseetl-3.0.0-cp313-cp313-macosx_11_0_arm64.whl CPython 3.13 CPython 3.13 macOS 11.0+ ARM64 Details
pyreverseetl-3.0.0-cp311-cp311-macosx_11_0_arm64.whl CPython 3.11 CPython 3.11 macOS 11.0+ ARM64 Details
pyreverseetl-3.0.0-cp39-cp39-macosx_11_0_arm64.whl CPython 3.9 CPython 3.9 macOS 11.0+ ARM64 Details

Total release size: 16.1 MB

Release files / pyreverseetl-3.0.0-cp313-cp313-macosx_11_0_arm64.whl

Download URL pyreverseetl-3.0.0-cp313-cp313-macosx_11_0_arm64.whl
Size 5.4 MB
Tags CPython 3.13 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
e2aaefd0b727295f952be3708fe995d3deb906a1368ddf75bb2d29b29acaaef5
BLAKE2b-256 checksum
How to use checksums
0a61574c3c3a0996c3cecc4a8dcad984db9895d256b4e77456fac0718bbcce3e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / pyreverseetl-3.0.0-cp311-cp311-macosx_11_0_arm64.whl

Download URL pyreverseetl-3.0.0-cp311-cp311-macosx_11_0_arm64.whl
Size 5.4 MB
Tags CPython 3.11 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
fe9e6ab015d82adf0c6b687e4591e3c9670e5bfc24819342ae77091423c5d8ea
BLAKE2b-256 checksum
How to use checksums
e2398912d6cf96ce314a7a69a2ad71e54f7d539ca1d9857cbf9df44646ff2100
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / pyreverseetl-3.0.0-cp39-cp39-macosx_11_0_arm64.whl

Download URL pyreverseetl-3.0.0-cp39-cp39-macosx_11_0_arm64.whl
Size 5.4 MB
Tags CPython 3.9 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
ecc752b57f76a9f2616d96bbdc7e613763fd28c2cc6f1d75ce28174099743bbf
BLAKE2b-256 checksum
How to use checksums
69fbb8c8d96e4111e040abc8ef1f3f17ee472e5ff6ae3d0440acd0c69075ee1e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release history Release notifications | RSS feed

3.1.0

2 release files

This release

3.0.0 This release

3 release files

2.2.1

1 release file

2.1.2

1 release file

2.1.1

1 release file

2.1.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page