migrate-prove 0.1.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.
Files changed (39) hide show
  1. migrate_prove-0.1.0/LICENSE +21 -0
  2. migrate_prove-0.1.0/PKG-INFO +239 -0
  3. migrate_prove-0.1.0/README.md +204 -0
  4. migrate_prove-0.1.0/pyproject.toml +52 -0
  5. migrate_prove-0.1.0/setup.cfg +4 -0
  6. migrate_prove-0.1.0/src/migrate_prove/__init__.py +5 -0
  7. migrate_prove-0.1.0/src/migrate_prove/__main__.py +4 -0
  8. migrate_prove-0.1.0/src/migrate_prove/checks.py +774 -0
  9. migrate_prove-0.1.0/src/migrate_prove/cli.py +110 -0
  10. migrate_prove-0.1.0/src/migrate_prove/connectors.py +218 -0
  11. migrate_prove-0.1.0/src/migrate_prove/contract.py +23 -0
  12. migrate_prove-0.1.0/src/migrate_prove/demo/__init__.py +5 -0
  13. migrate_prove-0.1.0/src/migrate_prove/demo/banking.py +158 -0
  14. migrate_prove-0.1.0/src/migrate_prove/demo/cte_api.py +185 -0
  15. migrate_prove-0.1.0/src/migrate_prove/demo/hops_vs_e2e.py +96 -0
  16. migrate_prove-0.1.0/src/migrate_prove/engine.py +142 -0
  17. migrate_prove-0.1.0/src/migrate_prove/eval.py +63 -0
  18. migrate_prove-0.1.0/src/migrate_prove/hashing.py +15 -0
  19. migrate_prove-0.1.0/src/migrate_prove/models.py +245 -0
  20. migrate_prove-0.1.0/src/migrate_prove/normalize.py +47 -0
  21. migrate_prove-0.1.0/src/migrate_prove/reporting.py +434 -0
  22. migrate_prove-0.1.0/src/migrate_prove/rules.py +55 -0
  23. migrate_prove-0.1.0/src/migrate_prove/salesforce.py +232 -0
  24. migrate_prove-0.1.0/src/migrate_prove/sampling.py +54 -0
  25. migrate_prove-0.1.0/src/migrate_prove/secrets.py +171 -0
  26. migrate_prove-0.1.0/src/migrate_prove/sql.py +58 -0
  27. migrate_prove-0.1.0/src/migrate_prove.egg-info/PKG-INFO +239 -0
  28. migrate_prove-0.1.0/src/migrate_prove.egg-info/SOURCES.txt +37 -0
  29. migrate_prove-0.1.0/src/migrate_prove.egg-info/dependency_links.txt +1 -0
  30. migrate_prove-0.1.0/src/migrate_prove.egg-info/entry_points.txt +2 -0
  31. migrate_prove-0.1.0/src/migrate_prove.egg-info/requires.txt +18 -0
  32. migrate_prove-0.1.0/src/migrate_prove.egg-info/top_level.txt +1 -0
  33. migrate_prove-0.1.0/tests/test_contract.py +19 -0
  34. migrate_prove-0.1.0/tests/test_cte_api.py +111 -0
  35. migrate_prove-0.1.0/tests/test_engine_demo.py +41 -0
  36. migrate_prove-0.1.0/tests/test_hashing.py +17 -0
  37. migrate_prove-0.1.0/tests/test_hops_vs_e2e.py +39 -0
  38. migrate_prove-0.1.0/tests/test_reporting.py +142 -0
  39. migrate_prove-0.1.0/tests/test_secrets.py +105 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dan Webb
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.
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: migrate-prove
3
+ Version: 0.1.0
4
+ Summary: Prove that migrated target data matches the Source-to-Target Mapping contract — not just that the ETL job exited 0.
5
+ Author: Dan Webb
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/4pconsulting/migrate-prove
8
+ Project-URL: Repository, https://github.com/4pconsulting/migrate-prove
9
+ Project-URL: Issues, https://github.com/4pconsulting/migrate-prove/issues
10
+ Keywords: data-migration,etl,reconciliation,qa,validation
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: pydantic>=2.7
21
+ Requires-Dist: pyyaml>=6.0
22
+ Requires-Dist: python-dotenv>=1.0
23
+ Requires-Dist: sqlalchemy>=2.0
24
+ Requires-Dist: typer>=0.12
25
+ Requires-Dist: rich>=13.7
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Provides-Extra: excel
29
+ Requires-Dist: openpyxl>=3.1; extra == "excel"
30
+ Provides-Extra: postgres
31
+ Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
32
+ Provides-Extra: athena
33
+ Requires-Dist: pyathena[sqlalchemy]>=3.0; extra == "athena"
34
+ Dynamic: license-file
35
+
36
+ # migrate-prove
37
+
38
+ A Python validation suite for data migrations. It does **not** treat a green ETL exit code as success. It proves that the **target dataset is the result of applying an agreed Source-to-Target Mapping (STM) to the source**, then that the target is still internally coherent.
39
+
40
+ This repository is the executable version of a five-layer migration test strategy: contract-as-oracle, risk-calibrated checks, row hashing, failure-mode probes, and an evidence scorecard.
41
+
42
+ ## Why this exists
43
+
44
+ A completed pipeline answers: *did the job finish?*
45
+
46
+ This suite answers: **can we systematically prove the target represents the source correctly, according to the contract and business invariants?**
47
+
48
+ `COUNT(*)` matching on both sides is not that proof. The banking demo plants a status misclassification that keeps totals identical and still fails sliced volume, hashing, and invariants.
49
+
50
+ ## Architecture
51
+
52
+ ```
53
+ STM YAML (oracle)
54
+ │
55
+ ▼
56
+ ┌─────────────────────────────────────┐
57
+ │ ValidationEngine │
58
+ │ 1 Schema 2 Volume 3 Metrics │
59
+ │ 4 Transforms HASH 5 Invariants │
60
+ │ + failure-mode probes │
61
+ └─────────────────────────────────────┘
62
+ │
63
+ ▼
64
+ JSON + HTML evidence scorecard
65
+ tagged: migration | transformation | reconciliation
66
+ ```
67
+
68
+ ### The contract is the test oracle
69
+
70
+ Target correctness is **not** `source == target`. Different can be right (status `A` → `Active`). The STM file is the shared, versioned specification:
71
+
72
+ | Dimension | How the suite tests it |
73
+ | --- | --- |
74
+ | Direct projections | Canonicalise, then compare / hash |
75
+ | Transformations | Lookup tables and `when`/`then` rules run against source, asserted on target |
76
+ | Type coercions | Decimal scale, string length, identifier quoting |
77
+ | Null / empty / `N/A` | Failure probe `null_empty_literal` |
78
+ | Dedup / filters | Optional `filter_source` / `filter_target` |
79
+ | Cardinality | Total counts **and** slices (`volume_slices`) |
80
+ | Surrogate keys | Join on mapped business keys, not physical PKs |
81
+
82
+ ### Five layers (+ hash + probes)
83
+
84
+ | Layer | Question | Typical concern tag |
85
+ | --- | --- | --- |
86
+ | L1 Schema | Do mapped objects/columns exist? | migration |
87
+ | Probes | Truncation, precision, encoding, leading zeros | transformation |
88
+ | L2 Volume | Totals and slices after applying STM lookups | migration / transformation |
89
+ | L3 Metrics | Sums and grouped financials within tolerance | reconciliation |
90
+ | L4 Transforms | Every STM rule vs actual target value | transformation |
91
+ | HASH | Missing/extra keys vs row SHA-256 | migration / transformation |
92
+ | L5 Invariants | Temporal order, orphans, state machines | reconciliation |
93
+
94
+ Separating those tags is how you debug cutover night: dropped rows vs bad lookup vs legacy junk copied faithfully.
95
+
96
+ ### Risk calibration
97
+
98
+ Each entity has `risk: critical | master | high_volume`. Critical failures are called out on the HTML scorecard. Sampling policy (`full` or `stratified` with `where` + `rate`) is on the entity so high-volume logs are not brute-forced during a four-hour window. v1 hashing still runs in-process (SQLite/demo scale); SQL-pushdown hashing is the next connector increment.
99
+
100
+ ## Install
101
+
102
+ ```powershell
103
+ pip install migrate-prove
104
+ pip install "migrate-prove[postgres]"
105
+ pip install "migrate-prove[athena]"
106
+ pip install "migrate-prove[excel]"
107
+ ```
108
+
109
+ `postgres` installs the psycopg driver, `athena` installs the PyAthena SQLAlchemy dialect, and `excel` installs openpyxl. The demos and their YAML live in this repository, so clone it and follow the quick start below to seed and run them.
110
+
111
+ ## Releasing
112
+
113
+ Bump `version` in `pyproject.toml` in the pull request. Merging that change to `main` publishes the new version to PyPI. A push that does not change the version does not upload again.
114
+
115
+ ## Quick start
116
+
117
+ Ensure you are in the root `migrate-prove` directory.
118
+
119
+ ```powershell
120
+ python -m venv .venv
121
+ .venv\Scripts\Activate.ps1
122
+ python -m pip install --upgrade pip
123
+ python -m pip install -e ".[dev]"
124
+ migrate-prove seed-demo examples/banking_demo
125
+ migrate-prove compile examples/banking_demo/stm.yaml
126
+ migrate-prove run examples/banking_demo/suite.yaml
127
+ ```
128
+
129
+ Open `examples/banking_demo/artifacts/report.html`. You should see a **passing total count** and failing sliced volume, transforms, hashes, probes, and invariants — that is the point.
130
+
131
+ ### Hops vs end-to-end
132
+
133
+ Layered pipelines need both thin hop gates and a finalised source → target STM. Runnable demo (hops pass; e2e fails on status misclassification):
134
+
135
+ ```powershell
136
+ migrate-prove seed-hops-demo examples/hops_vs_e2e
137
+ migrate-prove run examples/hops_vs_e2e/suites/hop_01.yaml
138
+ migrate-prove run examples/hops_vs_e2e/suites/hop_02.yaml
139
+ migrate-prove run examples/hops_vs_e2e/suites/e2e.yaml
140
+ ```
141
+
142
+ See [examples/hops_vs_e2e/README.md](examples/hops_vs_e2e/README.md) and [ASSURANCE.md](ASSURANCE.md).
143
+
144
+ ### CTE source + Salesforce (mock) target
145
+
146
+ Not every migration is table→table. A third demo joins two source tables via a **CTE** and proves against **paginated Salesforce JSON fixtures**:
147
+
148
+ ```powershell
149
+ migrate-prove seed-cte-api-demo examples/cte_api_demo
150
+ migrate-prove run examples/cte_api_demo/suite.yaml
151
+ ```
152
+
153
+ See [examples/cte_api_demo/README.md](examples/cte_api_demo/README.md) and [ADVANCED.md](ADVANCED.md).
154
+
155
+ ```powershell
156
+ pytest
157
+ ```
158
+
159
+ ## Connections and secrets
160
+
161
+ Suite YAML must **never** contain passwords. Reference environment variables with `${VAR}` or `${VAR:-default}`. Locally, copy [`.env.example`](.env.example) to `.env` (gitignored). In CI, set the same names from the pipeline secrets store — `python-dotenv` only fills gaps; real process env always wins.
162
+
163
+ **URL form** (any SQLAlchemy URL after expansion):
164
+
165
+ ```yaml
166
+ source:
167
+ url: "postgresql+psycopg://${SOURCE_USER}:${SOURCE_PASSWORD}@${SOURCE_HOST}:${SOURCE_PORT:-5432}/${SOURCE_DB}"
168
+ target:
169
+ url: "${TARGET_URL}"
170
+ ```
171
+
172
+ **Structured form** (builds the URL; same `${}` rules):
173
+
174
+ ```yaml
175
+ source:
176
+ dialect: postgresql+psycopg
177
+ host: ${SOURCE_HOST}
178
+ port: ${SOURCE_PORT:-5432}
179
+ database: ${SOURCE_DB}
180
+ username: ${SOURCE_USER}
181
+ password: ${SOURCE_PASSWORD}
182
+ options:
183
+ sslmode: require
184
+ ```
185
+
186
+ Install drivers as extras when needed:
187
+
188
+ ```powershell
189
+ pip install -e ".[postgres]" # psycopg
190
+ pip install -e ".[athena]" # PyAthena SQLAlchemy dialect
191
+ ```
192
+
193
+ Athena stays URL-shaped; AWS credentials come from the environment or the runner IAM role:
194
+
195
+ ```yaml
196
+ target:
197
+ url: "awsathena+rest://athena.${AWS_REGION}.amazonaws.com:443/${ATHENA_SCHEMA}?s3_staging_dir=${ATHENA_S3_STAGING}&work_group=${ATHENA_WORKGROUP}"
198
+ ```
199
+
200
+ Optional: `migrate-prove run suite.yaml --env-file path\to\.env`. The CLI prints **redacted** connection labels (SQLAlchemy URLs or `salesforce_mock://…` fixture paths).
201
+
202
+ **API mock target** (no network; see [ADVANCED.md](ADVANCED.md)):
203
+
204
+ ```yaml
205
+ target:
206
+ kind: salesforce_mock
207
+ fixtures: fixtures/salesforce
208
+ ```
209
+
210
+ ## Authoring a contract
211
+
212
+ See `examples/banking_demo/stm.yaml`. A suite file points at the contract and the two connections (SQLite needs no secrets):
213
+
214
+ ```yaml
215
+ name: my-cutover
216
+ contract: stm.yaml
217
+ source: { url: sqlite:///source.db }
218
+ target: { url: "${TARGET_URL}" }
219
+ output_dir: artifacts
220
+ ```
221
+
222
+ Transform rules are the oracle, not a second copy of ETL Python. Keep ETL and this file aligned; do not reimplement warehouse SQL ad hoc in tests.
223
+
224
+ ## Roadmap (v1 is the skeleton that already fails the right way)
225
+
226
+ 1. **Now** — STM YAML, SQLAlchemy + Salesforce-mock connectors, CTE `source_sql`, env-backed secrets, L1–L5, hashing, probes, CLI, HTML/JSON, demos.
227
+ 2. **Excel STM import** — optional extra `openpyxl` to ingest the spreadsheet everyone still has.
228
+ 3. **Dialect hash pushdown** — `SHA2` / `sha256` in Postgres, SQL Server, Snowflake so row signatures never leave the estate.
229
+ 4. **Stratified sampling at warehouse scale** — deterministic modulus on business keys, 100% for HNW / recent / edge strata.
230
+ 5. **JUnit XML + CI gate** — cutover sign-off as a pipeline artifact.
231
+ 6. **Rehearsal profiles** — synthetic → prod-replica → dress-rehearsal, with runtime budgets.
232
+
233
+ ## Design rules
234
+
235
+ - The STM is executable. If it cannot be checked, it is not specified.
236
+ - Prefer evidence over exit codes.
237
+ - Hunt known failure modes on purpose; do not only sample happy-path rows.
238
+ - Never claim success from a global row count.
239
+ - Never commit secrets; suite YAML references `${ENV}` only.
@@ -0,0 +1,204 @@
1
+ # migrate-prove
2
+
3
+ A Python validation suite for data migrations. It does **not** treat a green ETL exit code as success. It proves that the **target dataset is the result of applying an agreed Source-to-Target Mapping (STM) to the source**, then that the target is still internally coherent.
4
+
5
+ This repository is the executable version of a five-layer migration test strategy: contract-as-oracle, risk-calibrated checks, row hashing, failure-mode probes, and an evidence scorecard.
6
+
7
+ ## Why this exists
8
+
9
+ A completed pipeline answers: *did the job finish?*
10
+
11
+ This suite answers: **can we systematically prove the target represents the source correctly, according to the contract and business invariants?**
12
+
13
+ `COUNT(*)` matching on both sides is not that proof. The banking demo plants a status misclassification that keeps totals identical and still fails sliced volume, hashing, and invariants.
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ STM YAML (oracle)
19
+ │
20
+ ▼
21
+ ┌─────────────────────────────────────┐
22
+ │ ValidationEngine │
23
+ │ 1 Schema 2 Volume 3 Metrics │
24
+ │ 4 Transforms HASH 5 Invariants │
25
+ │ + failure-mode probes │
26
+ └─────────────────────────────────────┘
27
+ │
28
+ ▼
29
+ JSON + HTML evidence scorecard
30
+ tagged: migration | transformation | reconciliation
31
+ ```
32
+
33
+ ### The contract is the test oracle
34
+
35
+ Target correctness is **not** `source == target`. Different can be right (status `A` → `Active`). The STM file is the shared, versioned specification:
36
+
37
+ | Dimension | How the suite tests it |
38
+ | --- | --- |
39
+ | Direct projections | Canonicalise, then compare / hash |
40
+ | Transformations | Lookup tables and `when`/`then` rules run against source, asserted on target |
41
+ | Type coercions | Decimal scale, string length, identifier quoting |
42
+ | Null / empty / `N/A` | Failure probe `null_empty_literal` |
43
+ | Dedup / filters | Optional `filter_source` / `filter_target` |
44
+ | Cardinality | Total counts **and** slices (`volume_slices`) |
45
+ | Surrogate keys | Join on mapped business keys, not physical PKs |
46
+
47
+ ### Five layers (+ hash + probes)
48
+
49
+ | Layer | Question | Typical concern tag |
50
+ | --- | --- | --- |
51
+ | L1 Schema | Do mapped objects/columns exist? | migration |
52
+ | Probes | Truncation, precision, encoding, leading zeros | transformation |
53
+ | L2 Volume | Totals and slices after applying STM lookups | migration / transformation |
54
+ | L3 Metrics | Sums and grouped financials within tolerance | reconciliation |
55
+ | L4 Transforms | Every STM rule vs actual target value | transformation |
56
+ | HASH | Missing/extra keys vs row SHA-256 | migration / transformation |
57
+ | L5 Invariants | Temporal order, orphans, state machines | reconciliation |
58
+
59
+ Separating those tags is how you debug cutover night: dropped rows vs bad lookup vs legacy junk copied faithfully.
60
+
61
+ ### Risk calibration
62
+
63
+ Each entity has `risk: critical | master | high_volume`. Critical failures are called out on the HTML scorecard. Sampling policy (`full` or `stratified` with `where` + `rate`) is on the entity so high-volume logs are not brute-forced during a four-hour window. v1 hashing still runs in-process (SQLite/demo scale); SQL-pushdown hashing is the next connector increment.
64
+
65
+ ## Install
66
+
67
+ ```powershell
68
+ pip install migrate-prove
69
+ pip install "migrate-prove[postgres]"
70
+ pip install "migrate-prove[athena]"
71
+ pip install "migrate-prove[excel]"
72
+ ```
73
+
74
+ `postgres` installs the psycopg driver, `athena` installs the PyAthena SQLAlchemy dialect, and `excel` installs openpyxl. The demos and their YAML live in this repository, so clone it and follow the quick start below to seed and run them.
75
+
76
+ ## Releasing
77
+
78
+ Bump `version` in `pyproject.toml` in the pull request. Merging that change to `main` publishes the new version to PyPI. A push that does not change the version does not upload again.
79
+
80
+ ## Quick start
81
+
82
+ Ensure you are in the root `migrate-prove` directory.
83
+
84
+ ```powershell
85
+ python -m venv .venv
86
+ .venv\Scripts\Activate.ps1
87
+ python -m pip install --upgrade pip
88
+ python -m pip install -e ".[dev]"
89
+ migrate-prove seed-demo examples/banking_demo
90
+ migrate-prove compile examples/banking_demo/stm.yaml
91
+ migrate-prove run examples/banking_demo/suite.yaml
92
+ ```
93
+
94
+ Open `examples/banking_demo/artifacts/report.html`. You should see a **passing total count** and failing sliced volume, transforms, hashes, probes, and invariants — that is the point.
95
+
96
+ ### Hops vs end-to-end
97
+
98
+ Layered pipelines need both thin hop gates and a finalised source → target STM. Runnable demo (hops pass; e2e fails on status misclassification):
99
+
100
+ ```powershell
101
+ migrate-prove seed-hops-demo examples/hops_vs_e2e
102
+ migrate-prove run examples/hops_vs_e2e/suites/hop_01.yaml
103
+ migrate-prove run examples/hops_vs_e2e/suites/hop_02.yaml
104
+ migrate-prove run examples/hops_vs_e2e/suites/e2e.yaml
105
+ ```
106
+
107
+ See [examples/hops_vs_e2e/README.md](examples/hops_vs_e2e/README.md) and [ASSURANCE.md](ASSURANCE.md).
108
+
109
+ ### CTE source + Salesforce (mock) target
110
+
111
+ Not every migration is table→table. A third demo joins two source tables via a **CTE** and proves against **paginated Salesforce JSON fixtures**:
112
+
113
+ ```powershell
114
+ migrate-prove seed-cte-api-demo examples/cte_api_demo
115
+ migrate-prove run examples/cte_api_demo/suite.yaml
116
+ ```
117
+
118
+ See [examples/cte_api_demo/README.md](examples/cte_api_demo/README.md) and [ADVANCED.md](ADVANCED.md).
119
+
120
+ ```powershell
121
+ pytest
122
+ ```
123
+
124
+ ## Connections and secrets
125
+
126
+ Suite YAML must **never** contain passwords. Reference environment variables with `${VAR}` or `${VAR:-default}`. Locally, copy [`.env.example`](.env.example) to `.env` (gitignored). In CI, set the same names from the pipeline secrets store — `python-dotenv` only fills gaps; real process env always wins.
127
+
128
+ **URL form** (any SQLAlchemy URL after expansion):
129
+
130
+ ```yaml
131
+ source:
132
+ url: "postgresql+psycopg://${SOURCE_USER}:${SOURCE_PASSWORD}@${SOURCE_HOST}:${SOURCE_PORT:-5432}/${SOURCE_DB}"
133
+ target:
134
+ url: "${TARGET_URL}"
135
+ ```
136
+
137
+ **Structured form** (builds the URL; same `${}` rules):
138
+
139
+ ```yaml
140
+ source:
141
+ dialect: postgresql+psycopg
142
+ host: ${SOURCE_HOST}
143
+ port: ${SOURCE_PORT:-5432}
144
+ database: ${SOURCE_DB}
145
+ username: ${SOURCE_USER}
146
+ password: ${SOURCE_PASSWORD}
147
+ options:
148
+ sslmode: require
149
+ ```
150
+
151
+ Install drivers as extras when needed:
152
+
153
+ ```powershell
154
+ pip install -e ".[postgres]" # psycopg
155
+ pip install -e ".[athena]" # PyAthena SQLAlchemy dialect
156
+ ```
157
+
158
+ Athena stays URL-shaped; AWS credentials come from the environment or the runner IAM role:
159
+
160
+ ```yaml
161
+ target:
162
+ url: "awsathena+rest://athena.${AWS_REGION}.amazonaws.com:443/${ATHENA_SCHEMA}?s3_staging_dir=${ATHENA_S3_STAGING}&work_group=${ATHENA_WORKGROUP}"
163
+ ```
164
+
165
+ Optional: `migrate-prove run suite.yaml --env-file path\to\.env`. The CLI prints **redacted** connection labels (SQLAlchemy URLs or `salesforce_mock://…` fixture paths).
166
+
167
+ **API mock target** (no network; see [ADVANCED.md](ADVANCED.md)):
168
+
169
+ ```yaml
170
+ target:
171
+ kind: salesforce_mock
172
+ fixtures: fixtures/salesforce
173
+ ```
174
+
175
+ ## Authoring a contract
176
+
177
+ See `examples/banking_demo/stm.yaml`. A suite file points at the contract and the two connections (SQLite needs no secrets):
178
+
179
+ ```yaml
180
+ name: my-cutover
181
+ contract: stm.yaml
182
+ source: { url: sqlite:///source.db }
183
+ target: { url: "${TARGET_URL}" }
184
+ output_dir: artifacts
185
+ ```
186
+
187
+ Transform rules are the oracle, not a second copy of ETL Python. Keep ETL and this file aligned; do not reimplement warehouse SQL ad hoc in tests.
188
+
189
+ ## Roadmap (v1 is the skeleton that already fails the right way)
190
+
191
+ 1. **Now** — STM YAML, SQLAlchemy + Salesforce-mock connectors, CTE `source_sql`, env-backed secrets, L1–L5, hashing, probes, CLI, HTML/JSON, demos.
192
+ 2. **Excel STM import** — optional extra `openpyxl` to ingest the spreadsheet everyone still has.
193
+ 3. **Dialect hash pushdown** — `SHA2` / `sha256` in Postgres, SQL Server, Snowflake so row signatures never leave the estate.
194
+ 4. **Stratified sampling at warehouse scale** — deterministic modulus on business keys, 100% for HNW / recent / edge strata.
195
+ 5. **JUnit XML + CI gate** — cutover sign-off as a pipeline artifact.
196
+ 6. **Rehearsal profiles** — synthetic → prod-replica → dress-rehearsal, with runtime budgets.
197
+
198
+ ## Design rules
199
+
200
+ - The STM is executable. If it cannot be checked, it is not specified.
201
+ - Prefer evidence over exit codes.
202
+ - Hunt known failure modes on purpose; do not only sample happy-path rows.
203
+ - Never claim success from a global row count.
204
+ - Never commit secrets; suite YAML references `${ENV}` only.
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "migrate-prove"
7
+ version = "0.1.0"
8
+ description = "Prove that migrated target data matches the Source-to-Target Mapping contract — not just that the ETL job exited 0."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Dan Webb" }]
14
+ keywords = ["data-migration", "etl", "reconciliation", "qa", "validation"]
15
+ classifiers = [
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Software Development :: Testing",
22
+ ]
23
+ dependencies = [
24
+ "pydantic>=2.7",
25
+ "pyyaml>=6.0",
26
+ "python-dotenv>=1.0",
27
+ "sqlalchemy>=2.0",
28
+ "typer>=0.12",
29
+ "rich>=13.7",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=8.0"]
34
+ excel = ["openpyxl>=3.1"]
35
+ postgres = ["psycopg[binary]>=3.1"]
36
+ athena = ["pyathena[sqlalchemy]>=3.0"]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/4pconsulting/migrate-prove"
40
+ Repository = "https://github.com/4pconsulting/migrate-prove"
41
+ Issues = "https://github.com/4pconsulting/migrate-prove/issues"
42
+
43
+ [project.scripts]
44
+ migrate-prove = "migrate_prove.cli:app"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+ pythonpath = ["src"]
52
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """migrate-prove: contract-driven data migration validation."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("migrate-prove")
@@ -0,0 +1,4 @@
1
+ from migrate_prove.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()