etlpipe 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- etlpipe-2.0.0/.github/workflows/ci.yml +78 -0
- etlpipe-2.0.0/.github/workflows/publish-governance.yml +37 -0
- etlpipe-2.0.0/.github/workflows/publish-main.yml +43 -0
- etlpipe-2.0.0/.gitignore +47 -0
- etlpipe-2.0.0/CHANGELOG.md +74 -0
- etlpipe-2.0.0/CONTRIBUTING.md +170 -0
- etlpipe-2.0.0/DATABRICKS_GUIDE.md +193 -0
- etlpipe-2.0.0/LICENSE +21 -0
- etlpipe-2.0.0/PKG-INFO +526 -0
- etlpipe-2.0.0/README.md +472 -0
- etlpipe-2.0.0/SECURITY.md +63 -0
- etlpipe-2.0.0/USER_GUIDE.md +1944 -0
- etlpipe-2.0.0/demo/demo_customers.csv +13 -0
- etlpipe-2.0.0/demo/demo_pipeline.py +579 -0
- etlpipe-2.0.0/demo/demo_pipeline.yaml +65 -0
- etlpipe-2.0.0/demo/demo_sales.csv +21 -0
- etlpipe-2.0.0/demo/sample_pipeline.yaml +21 -0
- etlpipe-2.0.0/demo/sample_pipeline_pandas.yaml +89 -0
- etlpipe-2.0.0/demo/sample_pipeline_spark.yaml +89 -0
- etlpipe-2.0.0/doc/Developer.md +300 -0
- etlpipe-2.0.0/doc/InOut.md +221 -0
- etlpipe-2.0.0/doc/Join.md +254 -0
- etlpipe-2.0.0/doc/Parse.md +254 -0
- etlpipe-2.0.0/doc/Preparation.md +686 -0
- etlpipe-2.0.0/doc/Transform.md +280 -0
- etlpipe-2.0.0/doc/adr/000-template.md +33 -0
- etlpipe-2.0.0/doc/adr/001-dual-backend-architecture.md +52 -0
- etlpipe-2.0.0/doc/adr/002-security-eval-removal.md +46 -0
- etlpipe-2.0.0/doc/adr/003-governance-spinoff.md +102 -0
- etlpipe-2.0.0/pyproject.toml +77 -0
- etlpipe-2.0.0/src/etlpipe/__init__.py +53 -0
- etlpipe-2.0.0/src/etlpipe/_config.py +70 -0
- etlpipe-2.0.0/src/etlpipe/_contracts.py +29 -0
- etlpipe-2.0.0/src/etlpipe/_pii.py +27 -0
- etlpipe-2.0.0/src/etlpipe/_validators.py +85 -0
- etlpipe-2.0.0/src/etlpipe/_version.py +3 -0
- etlpipe-2.0.0/src/etlpipe/convert.py +1088 -0
- etlpipe-2.0.0/src/etlpipe/developer.py +287 -0
- etlpipe-2.0.0/src/etlpipe/engines/__init__.py +4 -0
- etlpipe-2.0.0/src/etlpipe/engines/base.py +336 -0
- etlpipe-2.0.0/src/etlpipe/engines/pandas_engine.py +1654 -0
- etlpipe-2.0.0/src/etlpipe/engines/spark_engine.py +1359 -0
- etlpipe-2.0.0/src/etlpipe/in_out.py +228 -0
- etlpipe-2.0.0/src/etlpipe/join.py +225 -0
- etlpipe-2.0.0/src/etlpipe/parse.py +230 -0
- etlpipe-2.0.0/src/etlpipe/pipeline.py +326 -0
- etlpipe-2.0.0/src/etlpipe/preparation.py +570 -0
- etlpipe-2.0.0/src/etlpipe/transform.py +255 -0
- etlpipe-2.0.0/src/etlpipe_governance/README_GOVERNANCE.md +99 -0
- etlpipe-2.0.0/src/etlpipe_governance/__init__.py +61 -0
- etlpipe-2.0.0/src/etlpipe_governance/_version.py +1 -0
- etlpipe-2.0.0/src/etlpipe_governance/contracts.py +545 -0
- etlpipe-2.0.0/src/etlpipe_governance/pii.py +398 -0
- etlpipe-2.0.0/src/etlpipe_governance/pyproject.toml +50 -0
- etlpipe-2.0.0/src/etlpipe_governance/tests/__init__.py +1 -0
- etlpipe-2.0.0/src/etlpipe_governance/tests/test_contracts.py +415 -0
- etlpipe-2.0.0/src/etlpipe_governance/tests/test_pii.py +314 -0
- etlpipe-2.0.0/tests/conftest.py +92 -0
- etlpipe-2.0.0/tests/fixtures/sample_workflow.yxmd +140 -0
- etlpipe-2.0.0/tests/test_assessment.py +1197 -0
- etlpipe-2.0.0/tests/test_contracts.py +244 -0
- etlpipe-2.0.0/tests/test_convert.py +393 -0
- etlpipe-2.0.0/tests/test_developer.py +163 -0
- etlpipe-2.0.0/tests/test_engines.py +89 -0
- etlpipe-2.0.0/tests/test_in_out.py +136 -0
- etlpipe-2.0.0/tests/test_join.py +176 -0
- etlpipe-2.0.0/tests/test_parse.py +128 -0
- etlpipe-2.0.0/tests/test_pii.py +268 -0
- etlpipe-2.0.0/tests/test_pipeline.py +147 -0
- etlpipe-2.0.0/tests/test_preparation.py +449 -0
- etlpipe-2.0.0/tests/test_spark_engine.py +142 -0
- etlpipe-2.0.0/tests/test_transform.py +203 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: Lint & Format Check
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- name: Install ruff
|
|
19
|
+
run: pip install ruff
|
|
20
|
+
- name: Check formatting
|
|
21
|
+
run: ruff format --check src/ tests/
|
|
22
|
+
- name: Check linting rules
|
|
23
|
+
run: ruff check src/ tests/
|
|
24
|
+
|
|
25
|
+
security:
|
|
26
|
+
name: Security Scan
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.12"
|
|
33
|
+
- name: Install tools
|
|
34
|
+
run: pip install pip-audit bandit
|
|
35
|
+
- name: Audit dependencies for known vulnerabilities
|
|
36
|
+
run: pip install -e ".[dev]" && pip-audit
|
|
37
|
+
- name: Static security analysis
|
|
38
|
+
run: bandit -r src/etlpipe/ -ll --skip B101
|
|
39
|
+
|
|
40
|
+
test:
|
|
41
|
+
name: Tests (Python ${{ matrix.python-version }})
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
strategy:
|
|
44
|
+
fail-fast: false
|
|
45
|
+
matrix:
|
|
46
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- uses: actions/setup-python@v5
|
|
50
|
+
with:
|
|
51
|
+
python-version: ${{ matrix.python-version }}
|
|
52
|
+
- name: Install package with dev dependencies
|
|
53
|
+
run: python -m pip install -e ".[dev]"
|
|
54
|
+
- name: Run test suite with coverage
|
|
55
|
+
run: |
|
|
56
|
+
pytest tests/ -v --cov=etlpipe --cov-report=term-missing --cov-report=xml \
|
|
57
|
+
-x --tb=short -q
|
|
58
|
+
- name: Upload coverage
|
|
59
|
+
if: matrix.python-version == '3.12'
|
|
60
|
+
uses: actions/upload-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: coverage-report
|
|
63
|
+
path: coverage.xml
|
|
64
|
+
|
|
65
|
+
spark-test:
|
|
66
|
+
name: Spark Integration Tests
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
continue-on-error: true
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
- uses: actions/setup-python@v5
|
|
72
|
+
with:
|
|
73
|
+
python-version: "3.12"
|
|
74
|
+
- name: Install package with Spark dependencies
|
|
75
|
+
run: python -m pip install setuptools && python -m pip install -e ".[dev,spark]"
|
|
76
|
+
- name: Run Spark integration tests
|
|
77
|
+
run: |
|
|
78
|
+
pytest tests/test_spark_engine.py -v --tb=short -q
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Publish Governance to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
name: Build and publish etlpipe-governance ?? to PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Check out repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.11"
|
|
25
|
+
|
|
26
|
+
- name: Install build dependencies
|
|
27
|
+
run: python -m pip install --upgrade pip build
|
|
28
|
+
|
|
29
|
+
- name: Build governance package
|
|
30
|
+
run: |
|
|
31
|
+
cd src/etlpipe_governance
|
|
32
|
+
python -m build --outdir ../../dist_governance
|
|
33
|
+
|
|
34
|
+
- name: Publish governance package
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
36
|
+
with:
|
|
37
|
+
packages-dir: dist_governance/
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Publish Main to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: ["Publish Governance to PyPI"]
|
|
6
|
+
types:
|
|
7
|
+
- completed
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build-and-publish:
|
|
12
|
+
name: Build and publish etlpipe ?? to PyPI
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Check out repository
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.11"
|
|
27
|
+
|
|
28
|
+
- name: Install build dependencies
|
|
29
|
+
run: python -m pip install --upgrade pip build
|
|
30
|
+
|
|
31
|
+
- name: Install package and test dependencies
|
|
32
|
+
run: python -m pip install -e ".[dev]"
|
|
33
|
+
|
|
34
|
+
- name: Run test suite
|
|
35
|
+
run: pytest tests/
|
|
36
|
+
|
|
37
|
+
- name: Build main package
|
|
38
|
+
run: python -m build --outdir dist_main/
|
|
39
|
+
|
|
40
|
+
- name: Publish main package
|
|
41
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
42
|
+
with:
|
|
43
|
+
packages-dir: dist_main/
|
etlpipe-2.0.0/.gitignore
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Environments
|
|
2
|
+
.env
|
|
3
|
+
.venv
|
|
4
|
+
env/
|
|
5
|
+
venv/
|
|
6
|
+
ENV/
|
|
7
|
+
env.bak/
|
|
8
|
+
venv.bak/
|
|
9
|
+
|
|
10
|
+
# Build and Distribution
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# Python and Cache
|
|
28
|
+
__pycache__/
|
|
29
|
+
*.py[cod]
|
|
30
|
+
*$py.class
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.coverage
|
|
33
|
+
htmlcov/
|
|
34
|
+
|
|
35
|
+
# Editor and IDEs
|
|
36
|
+
.vscode/
|
|
37
|
+
.idea/
|
|
38
|
+
*.swp
|
|
39
|
+
*.swo
|
|
40
|
+
|
|
41
|
+
# Mock data generated during E2E test & demo execution
|
|
42
|
+
sales.csv
|
|
43
|
+
regions.csv
|
|
44
|
+
cli_test_output.csv
|
|
45
|
+
e2e_demo.py
|
|
46
|
+
demo/*_output.csv
|
|
47
|
+
demo/pipeline_output_*.csv
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.0.0] — 2026-07-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`.yxmd` Workflow Converter**: New `YxmdConverter` class and `etlpipe-convert` CLI tool to convert proprietary visual ETL workflow XML files into Etlpipe YAML pipelines. Supports 15+ tool mappings with expression translation, topological sorting, and graceful degradation for unparseable tools.
|
|
12
|
+
- **Enterprise Governance Sub-Package (`etlpipe-governance`)**: Extracted and expanded data quality toolkit into a standalone package:
|
|
13
|
+
- `scan_pii()` — Detects PII across 12 international patterns (email, phone, SSN, credit card, Aadhaar, IBAN, passport, etc.) with confidence scoring.
|
|
14
|
+
- `mask_pii()` — Three masking strategies: `redact`, `hash` (SHA-256), and `pseudonymise` (reversible labels with mapping). Competitive differentiator vs. Great Expectations/Pandera.
|
|
15
|
+
- `expect_schema()` / `infer_schema()` — Schema contract validation with dtype aliases, nullability checks, strict/non-strict modes.
|
|
16
|
+
- `profile()` — Statistical profiler with cardinality, null rates, min/max/mean/std, and top-N value distributions.
|
|
17
|
+
- `ContractSuite` — Batch contract runner for pipeline audit checkpoints with PASS/FAIL/SKIPPED/WARN/ERROR reporting.
|
|
18
|
+
- **Structured Logging**: Replaced all `print()` statements in pipeline engine with `logging.getLogger()` using hierarchical names (`etlpipe.pipeline`, `etlpipe.engines.pandas`, etc.). Pipeline metrics emitted as structured JSON.
|
|
19
|
+
- **Pipeline Execution Metrics**: Per-step timing (`duration_s`), row counts, output types, and status tracking. Metrics accessible via `Pipeline.metrics` after execution.
|
|
20
|
+
- **Pipeline Event Hooks**: Four lifecycle callbacks (`on_step_start`, `on_step_complete`, `on_step_error`, `on_pipeline_complete`) for integrating with Slack, PagerDuty, Teams, or any alerting system without adding those as dependencies.
|
|
21
|
+
- **Retry/Backoff for Downloads**: `Developer.download()` now supports configurable `max_retries` (default 3) and `retry_delay` (default 1.0s) with exponential backoff. Handles `URLError`, `TimeoutError`, `OSError`, and HTTP 5xx responses.
|
|
22
|
+
- **Schema Contracts in YAML Pipelines**: Steps can declare `output_schema` to enforce data contracts as part of pipeline execution.
|
|
23
|
+
- **Spark Integration Tests in CI**: Dedicated non-blocking CI job running Spark backend tests on Python 3.12.
|
|
24
|
+
- **Security Policy**: Added `SECURITY.md` with vulnerability reporting process, response timeline, and security track record.
|
|
25
|
+
- **Contributing Guide**: Added `CONTRIBUTING.md` with dev setup, code standards, testing requirements, and PR process.
|
|
26
|
+
- **Architecture Decision Records**: Added `doc/adr/` with template and records for dual-backend architecture and eval() removal.
|
|
27
|
+
- **Enterprise Governance Documentation**: New section in `USER_GUIDE.md` covering PII scanning, data contracts, event hooks, OpenLineage integration patterns, and data catalog recommendations.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **Version bump to 2.0.0** — reflects new sub-package, converter, and breaking packaging changes.
|
|
31
|
+
- **Development Status upgraded from Alpha to Beta** in PyPI classifiers.
|
|
32
|
+
- **Wheel now bundles `etlpipe_governance`** alongside `etlpipe` — governance features are available without separate installation.
|
|
33
|
+
- **Governance tests included in default pytest** via `testpaths` configuration.
|
|
34
|
+
- **Backward-compatible shim modules** (`etlpipe._contracts`, `etlpipe._pii`) re-export from `etlpipe_governance` with deprecation warnings targeting removal in 3.0.
|
|
35
|
+
|
|
36
|
+
### Security
|
|
37
|
+
- **Converter uses `defusedxml`** for safe XML parsing of `.yxmd` files (XXE/SSRF protection). Falls back with a hard error if `defusedxml` is not available (no silent downgrade).
|
|
38
|
+
|
|
39
|
+
## [1.0.0] — 2026-07-04
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- **Automated CI/CD**: Added a GitHub Actions workflow using OIDC (Trusted Publishing) for automated, secure PyPI deployments on new GitHub Releases.
|
|
43
|
+
- **Enterprise Edge Case Protections**: Built a background transpiler to auto-backtick column headers containing spaces to support dirty real-world enterprise datasets without syntax crashes.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- **Memory Optimization (Pandas)**: The Pandas backend now automatically requires the `pyarrow` multi-threaded C++ engine when loading large CSV files, heavily reducing physical RAM usage and preventing `MemoryError` crashes.
|
|
47
|
+
- **Datatype Coercion**: `Join.join` now dynamically casts mismatching datatypes between left and right anchor columns, eliminating strict PySpark `AnalysisException` errors when migrating disparate schemas.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
- **Critical Security Fix**: Eradicated an arbitrary code execution vulnerability (RCE) in `Preparation.formula` by removing the insecure `eval()` fallback. The engine is now entirely zero-trust and sandboxed via `pd.eval` or strict `lambda` callables.
|
|
51
|
+
- **Spark Architecture (Sorting Death Trap)**: Completely refactored `Preparation.record_id` to utilize parallelized `F.monotonically_increasing_id()` instead of `Window.orderBy`, eliminating massive single-node memory bottlenecks when scaling Spark pipelines.
|
|
52
|
+
- **Spark Architecture (Tuple Bombs)**: Injected native `.persist()` boundaries directly into the upstream nodes of `Join.join` to prevent PySpark from redundantly computing the entire historical DAG multiple times during joins.
|
|
53
|
+
- **Regex Edge Cases**: Fixed an engine logic gap where Regex capture groups incorrectly counted non-capturing groups `(?:)` in the Spark backend.
|
|
54
|
+
## [0.2.0] — 2026-07-04
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- **Enterprise Scalability (Dual-Backend Architecture)**: Added native support for distributed execution on big-data clusters using the new `SparkEngine`.
|
|
58
|
+
- **Thread-Safe Context Manager**: Added `Etlpipe.backend()` context manager to allow concurrent multi-threading with different execution engines.
|
|
59
|
+
- **Dynamic Dispatch**: `Etlpipe.set_backend("spark")` now seamlessly reroutes all tool executions down to Spark SQL and Vectorized Pandas UDFs (PyArrow).
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
- All 55 Core tools have been refactored into thin dispatchers to support multiple engine backends without altering user-facing APIs.
|
|
63
|
+
- Updated `README.md` and created `USER_GUIDE.md` with complete documentation, tool reference tables, and declarative YAML pipeline configurations.
|
|
64
|
+
|
|
65
|
+
## [0.1.0] — 2025-06-28
|
|
66
|
+
|
|
67
|
+
- **InOut** class — `input_data`, `output_data`, `text_input`, `browse`, `directory`, `date_time_now`
|
|
68
|
+
- **Preparation** class — `filter`, `formula`, `select`, `data_cleansing`, `sort`, `unique`, `sample`, `record_id`, `generate_rows`, `auto_field`, `multi_field_formula`, `multi_row_formula`, `tile`, `imputation`
|
|
69
|
+
- **Join** class — `join`, `join_multiple`, `union`, `find_replace`, `append_fields`, `fuzzy_match`
|
|
70
|
+
- **Transform** class — `summarize`, `transpose`, `cross_tab`, `running_total`, `count_records`
|
|
71
|
+
- **Parse** class — `date_time`, `regex_match`, `regex_parse`, `regex_replace`, `regex_tokenize`, `text_to_columns`, `xml_parse`
|
|
72
|
+
- **Developer** class — `base64_encode`, `base64_decode`, `download`, `column_info`, `dynamic_rename`
|
|
73
|
+
- Full test suite with pytest
|
|
74
|
+
- Type hints and Google-style docstrings on all public methods
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Contributing to Etlpipe
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to Etlpipe! This guide will help you get started.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Development Setup
|
|
8
|
+
|
|
9
|
+
### Prerequisites
|
|
10
|
+
|
|
11
|
+
- Python 3.10+
|
|
12
|
+
- Git
|
|
13
|
+
|
|
14
|
+
### Getting Started
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Clone the repository
|
|
18
|
+
git clone https://github.com/tonystark7cris/etlpipe.git
|
|
19
|
+
cd etlpipe
|
|
20
|
+
|
|
21
|
+
# Create a virtual environment
|
|
22
|
+
python -m venv venv
|
|
23
|
+
source venv/bin/activate # Linux/Mac
|
|
24
|
+
venv\Scripts\activate # Windows
|
|
25
|
+
|
|
26
|
+
# Install with development dependencies
|
|
27
|
+
pip install -e ".[dev]"
|
|
28
|
+
|
|
29
|
+
# Verify installation
|
|
30
|
+
pytest tests/ -v
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Code Standards
|
|
36
|
+
|
|
37
|
+
### Style & Formatting
|
|
38
|
+
|
|
39
|
+
Etlpipe uses **[Ruff](https://docs.astral.sh/ruff/)** for both linting and formatting. Configuration is in [pyproject.toml](pyproject.toml):
|
|
40
|
+
|
|
41
|
+
- **Target**: Python 3.10
|
|
42
|
+
- **Line length**: 120 characters
|
|
43
|
+
- **Lint rules**: E, F, I, W, UP, B, SIM
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Check formatting
|
|
47
|
+
ruff format --check src/ tests/
|
|
48
|
+
|
|
49
|
+
# Auto-format
|
|
50
|
+
ruff format src/ tests/
|
|
51
|
+
|
|
52
|
+
# Run linter
|
|
53
|
+
ruff check src/ tests/
|
|
54
|
+
|
|
55
|
+
# Auto-fix lint issues
|
|
56
|
+
ruff check --fix src/ tests/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Type Hints
|
|
60
|
+
|
|
61
|
+
All public methods **must** have full type annotations. Use `from __future__ import annotations` at the top of every module.
|
|
62
|
+
|
|
63
|
+
### Docstrings
|
|
64
|
+
|
|
65
|
+
All public methods **must** have Google-style docstrings including:
|
|
66
|
+
- One-line summary
|
|
67
|
+
- `Args:` section with parameter descriptions
|
|
68
|
+
- `Returns:` section
|
|
69
|
+
- `Raises:` section (if applicable)
|
|
70
|
+
- `Example:` section with a short usage snippet
|
|
71
|
+
|
|
72
|
+
### Immutability
|
|
73
|
+
|
|
74
|
+
All Etlpipe tool functions **must** be pure — they return new DataFrames and never mutate inputs. This is a core architectural invariant.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Testing
|
|
79
|
+
|
|
80
|
+
### Running Tests
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Full test suite with coverage
|
|
84
|
+
pytest tests/ -v --cov=etlpipe --cov-report=term-missing
|
|
85
|
+
|
|
86
|
+
# Run specific test file
|
|
87
|
+
pytest tests/test_preparation.py -v
|
|
88
|
+
|
|
89
|
+
# Run a single test
|
|
90
|
+
pytest tests/test_contracts.py::TestExpectSchemaPass::test_round_trip -v
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Writing Tests
|
|
94
|
+
|
|
95
|
+
- Place test files in `tests/` with the naming convention `test_<module>.py`
|
|
96
|
+
- Use shared fixtures from `tests/conftest.py` where possible
|
|
97
|
+
- Test both happy paths and error cases
|
|
98
|
+
- For dual-engine features, ensure Pandas tests exist (Spark tests are optional but encouraged)
|
|
99
|
+
|
|
100
|
+
### Coverage Requirements
|
|
101
|
+
|
|
102
|
+
New code should maintain or improve overall test coverage. Critical paths (security, data contracts, PII scanning) require >90% coverage.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Architecture
|
|
107
|
+
|
|
108
|
+
### Engine Pattern
|
|
109
|
+
|
|
110
|
+
Etlpipe uses an abstract **BackendEngine** pattern:
|
|
111
|
+
|
|
112
|
+
1. **Public API** classes (`Preparation`, `Join`, etc.) are thin dispatchers
|
|
113
|
+
2. They call `get_engine()` to obtain the active backend
|
|
114
|
+
3. The backend (`PandasEngine` or `SparkEngine`) contains the actual implementation
|
|
115
|
+
4. Both backends inherit from `BackendEngine` (ABC)
|
|
116
|
+
|
|
117
|
+
When adding a new tool:
|
|
118
|
+
1. Add the abstract method signature to `engines/base.py`
|
|
119
|
+
2. Implement in `engines/pandas_engine.py`
|
|
120
|
+
3. Implement in `engines/spark_engine.py`
|
|
121
|
+
4. Add the public static method to the appropriate palette class
|
|
122
|
+
5. Add tests for both backends
|
|
123
|
+
|
|
124
|
+
### Architecture Decision Records
|
|
125
|
+
|
|
126
|
+
Significant design decisions are documented as ADRs in `doc/adr/`. When proposing a major change, create a new ADR using the template at `doc/adr/000-template.md`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Pull Request Process
|
|
131
|
+
|
|
132
|
+
1. **Branch**: Create a feature branch from `main` (e.g., `feature/add-pivot-tool`)
|
|
133
|
+
2. **Implement**: Make your changes following the code standards above
|
|
134
|
+
3. **Test**: Ensure all tests pass and add new tests for your changes
|
|
135
|
+
4. **Lint**: Run `ruff format` and `ruff check` before pushing
|
|
136
|
+
5. **PR**: Open a pull request with:
|
|
137
|
+
- Clear description of what changed and why
|
|
138
|
+
- Link to any related issues
|
|
139
|
+
- Screenshots/examples if applicable
|
|
140
|
+
6. **Review**: Address review feedback promptly
|
|
141
|
+
7. **Merge**: Squash-merge after approval
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Release Process
|
|
146
|
+
|
|
147
|
+
Etlpipe follows [Semantic Versioning](https://semver.org/):
|
|
148
|
+
|
|
149
|
+
- **PATCH** (1.0.x): Bug fixes, no API changes
|
|
150
|
+
- **MINOR** (1.x.0): New features, backward-compatible
|
|
151
|
+
- **MAJOR** (x.0.0): Breaking API changes
|
|
152
|
+
|
|
153
|
+
### Release Checklist
|
|
154
|
+
|
|
155
|
+
1. Update `src/etlpipe/_version.py` with the new version
|
|
156
|
+
2. Update `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/) format
|
|
157
|
+
3. Create a GitHub Release with a tag matching the version (e.g., `v1.1.0`)
|
|
158
|
+
4. The CI/CD pipeline will automatically publish to PyPI via OIDC Trusted Publishing
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Security
|
|
163
|
+
|
|
164
|
+
If you discover a security vulnerability, please report it privately. See [SECURITY.md](SECURITY.md) for details.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# End-to-End Etlpipe Implementation on Databricks
|
|
2
|
+
|
|
3
|
+
This guide provides a step-by-step walkthrough for implementing a full, production-ready Etlpipe project on **Databricks**.
|
|
4
|
+
|
|
5
|
+
Because Etlpipe features a dynamic dual-engine architecture, you can write familiar Etlpipe-like logic in Python, and Etlpipe will translate it natively into distributed Spark execution under the hood.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Architecture Overview
|
|
10
|
+
|
|
11
|
+
1. **Ingestion**: Read raw data from Databricks Delta Tables (or DBFS/S3).
|
|
12
|
+
2. **Transformation**: Execute Etlpipe logic utilizing the `spark` backend engine.
|
|
13
|
+
3. **Validation**: Assert data quality before loading.
|
|
14
|
+
4. **Load**: Write the transformed data back to a curated Delta Table.
|
|
15
|
+
5. **Orchestration**: Schedule the notebook/script via Databricks Workflows (Jobs).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Step 1: Cluster Setup
|
|
20
|
+
|
|
21
|
+
To run Etlpipe on Databricks, you just need to install the core package. Databricks already provides the Spark backend natively.
|
|
22
|
+
|
|
23
|
+
1. Navigate to your Databricks Workspace -> **Compute**.
|
|
24
|
+
2. Select your target cluster (e.g., Databricks Runtime 13.3 LTS).
|
|
25
|
+
3. Click the **Libraries** tab -> **Install New**.
|
|
26
|
+
4. Select **PyPI** and enter: `etlpipe`
|
|
27
|
+
5. Click **Install**.
|
|
28
|
+
|
|
29
|
+
Alternatively, if you are using Databricks Repos, add `etlpipe` to your `requirements.txt`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Step 2: Project Structure
|
|
34
|
+
|
|
35
|
+
When using Databricks Repos (Git integration), we recommend structuring your Etlpipe project like a standard software engineering repository:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
/my-databricks-project
|
|
39
|
+
├── notebooks/
|
|
40
|
+
│ └── 01_run_pipeline.py # Main entry point for Databricks Jobs
|
|
41
|
+
├── src/
|
|
42
|
+
│ ├── config.yaml # Pipeline configuration
|
|
43
|
+
│ └── pipeline.py # Core Etlpipe logic
|
|
44
|
+
├── tests/
|
|
45
|
+
│ └── test_pipeline.py # Unit tests (run locally via Pytest)
|
|
46
|
+
└── requirements.txt # Etlpipe[spark], etc.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Step 3: Core Pipeline Implementation (`src/pipeline.py`)
|
|
52
|
+
|
|
53
|
+
Here is the core business logic. We explicitly configure Etlpipe to use the `spark` backend so that operations execute on the cluster rather than the driver node.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import etlpipe
|
|
57
|
+
from etlpipe import InOut, Preparation, Join, Transform, Developer
|
|
58
|
+
|
|
59
|
+
def run_customer_360_pipeline():
|
|
60
|
+
# ==========================================
|
|
61
|
+
# 0. Set Backend to Spark
|
|
62
|
+
# ==========================================
|
|
63
|
+
etlpipe.set_backend("spark")
|
|
64
|
+
print("Executing Etlpipe Pipeline on Apache Spark Backend...")
|
|
65
|
+
|
|
66
|
+
# ==========================================
|
|
67
|
+
# 1. Ingest Data from Delta Tables
|
|
68
|
+
# ==========================================
|
|
69
|
+
# In Databricks, you can query Delta tables directly via Spark SQL syntax
|
|
70
|
+
# Etlpipe's spark backend treats SQL queries natively.
|
|
71
|
+
df_customers = InOut.input_data("dbfs:/mnt/lakehouse/raw/customers")
|
|
72
|
+
df_orders = InOut.input_data("dbfs:/mnt/lakehouse/raw/orders")
|
|
73
|
+
|
|
74
|
+
# ==========================================
|
|
75
|
+
# 2. Data Cleansing & Preparation
|
|
76
|
+
# ==========================================
|
|
77
|
+
# Cleanse Customer data: upper case names, strip whitespace, handle nulls
|
|
78
|
+
df_customers_clean = Preparation.data_cleansing(
|
|
79
|
+
df_customers,
|
|
80
|
+
replace_nulls_with="",
|
|
81
|
+
strip_whitespace=True,
|
|
82
|
+
modify_case="upper"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Filter for completed orders
|
|
86
|
+
df_orders_valid, df_orders_invalid = Preparation.filter(
|
|
87
|
+
df_orders,
|
|
88
|
+
"OrderStatus = 'COMPLETED'"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# ==========================================
|
|
92
|
+
# 3. Join Datasets
|
|
93
|
+
# ==========================================
|
|
94
|
+
# Join (L: Unjoined Customers, J: Joined Data, R: Unjoined Orders)
|
|
95
|
+
left_unjoined, joined_data, right_unjoined = Join.join(
|
|
96
|
+
df_customers_clean,
|
|
97
|
+
df_orders_valid,
|
|
98
|
+
on="CustomerID"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# ==========================================
|
|
102
|
+
# 4. Transform & Aggregate
|
|
103
|
+
# ==========================================
|
|
104
|
+
# Summarize Total Lifetime Value (LTV) and Order Count by Customer
|
|
105
|
+
summary_data = Transform.summarize(
|
|
106
|
+
joined_data,
|
|
107
|
+
group_by=["CustomerID", "CustomerName", "Region"],
|
|
108
|
+
aggregations={
|
|
109
|
+
"OrderAmount": ["sum", "mean"],
|
|
110
|
+
"OrderID": "count distinct"
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Apply business logic using Formula
|
|
115
|
+
final_data = Preparation.formula(
|
|
116
|
+
summary_data,
|
|
117
|
+
column="CustomerTier",
|
|
118
|
+
expression="IF Sum_OrderAmount > 10000 THEN 'Platinum' ELSE 'Standard' ENDIF"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ==========================================
|
|
122
|
+
# 5. Data Quality Testing
|
|
123
|
+
# ==========================================
|
|
124
|
+
# Developer.test evaluates natively on the Spark DataFrame
|
|
125
|
+
Developer.test(
|
|
126
|
+
final_data,
|
|
127
|
+
condition_func=lambda df: df["Sum_OrderAmount"].min() >= 0,
|
|
128
|
+
error_msg="Data Quality Failure: Negative LTV detected."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ==========================================
|
|
132
|
+
# 6. Load Output to Delta Lake
|
|
133
|
+
# ==========================================
|
|
134
|
+
# Write the output back to the Databricks Lakehouse as a Delta table
|
|
135
|
+
InOut.output_data(
|
|
136
|
+
final_data,
|
|
137
|
+
"dbfs:/mnt/lakehouse/curated/customer_360",
|
|
138
|
+
format="delta",
|
|
139
|
+
mode="overwrite"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
print("Pipeline executed successfully. Output written to Curated layer.")
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
run_customer_360_pipeline()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Step 4: The Databricks Entrypoint (`notebooks/01_run_pipeline.py`)
|
|
151
|
+
|
|
152
|
+
In Databricks, you typically create a notebook to serve as the entry point for your Workflows/Jobs. Because we defined our code in a `src` module, the notebook is extremely clean:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# COMMAND ----------
|
|
156
|
+
# MAGIC %pip install -r ../requirements.txt
|
|
157
|
+
# COMMAND ----------
|
|
158
|
+
|
|
159
|
+
import sys
|
|
160
|
+
import os
|
|
161
|
+
|
|
162
|
+
# Ensure the src directory is in the Python path
|
|
163
|
+
sys.path.append(os.path.abspath("../src"))
|
|
164
|
+
|
|
165
|
+
from pipeline import run_customer_360_pipeline
|
|
166
|
+
|
|
167
|
+
# COMMAND ----------
|
|
168
|
+
# Run the pipeline
|
|
169
|
+
run_customer_360_pipeline()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Step 5: Scheduling via Databricks Workflows
|
|
175
|
+
|
|
176
|
+
To run this pipeline automatically:
|
|
177
|
+
|
|
178
|
+
1. Navigate to **Workflows** in the Databricks sidebar and click **Create Job**.
|
|
179
|
+
2. Name the Job: `Etlpipe_Customer360_ETL`.
|
|
180
|
+
3. In the task configuration:
|
|
181
|
+
- **Type**: Notebook
|
|
182
|
+
- **Source**: Workspace (or Git if using Repos).
|
|
183
|
+
- **Path**: Select `notebooks/01_run_pipeline.py`.
|
|
184
|
+
- **Compute**: Select the cluster you configured in Step 1 (or define a Job Cluster for cheaper, ephemeral execution).
|
|
185
|
+
4. **Schedule**: Set a trigger (e.g., Daily at 2:00 AM) and configure Failure Alerts to notify your team via Email/Slack.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Databricks Specific Best Practices
|
|
190
|
+
|
|
191
|
+
- **Leverage Delta Lake**: When using `InOut.input_data` and `InOut.output_data`, specify paths starting with `dbfs:/` and utilize the `format="delta"` argument to leverage Databricks' optimized storage layer.
|
|
192
|
+
- **Job Clusters vs. All-Purpose Clusters**: For scheduled Etlpipe pipelines, use **Job Clusters**. They are significantly cheaper and automatically terminate when the Etlpipe workflow completes.
|
|
193
|
+
- **Avoid `.browse()` in Production**: While `InOut.browse()` is fantastic for debugging interactively in a Notebook, remove it from production scripts, as it forces Spark to collect data to the driver node, which can cause Out-Of-Memory (OOM) errors on massive datasets.
|
etlpipe-2.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 etlpipe Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|