PyReverseETL 3.1.7__tar.gz → 3.3.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 (95) hide show
  1. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/Cargo.lock +2 -2
  2. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/Cargo.toml +1 -1
  3. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/PKG-INFO +16 -1
  4. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/README.md +15 -0
  5. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/pyproject.toml +1 -1
  6. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/python/pyreverseetl/cli.py +141 -0
  7. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/LICENSE +0 -0
  8. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/Cargo.toml +0 -0
  9. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/benches/load_test.rs +0 -0
  10. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/bin/pyreverseetl_dashboard.rs +0 -0
  11. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/activation.rs +0 -0
  12. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/alert_compat.rs +0 -0
  13. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/error.rs +0 -0
  14. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/http_client.rs +0 -0
  15. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/hubspot.rs +0 -0
  16. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/mapping.rs +0 -0
  17. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/marketo.rs +0 -0
  18. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/mod.rs +0 -0
  19. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/oauth_manager.rs +0 -0
  20. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/retry_policy.rs +0 -0
  21. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/salesforce.rs +0 -0
  22. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/schema_detect.rs +0 -0
  23. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/adapters/webhook.rs +0 -0
  24. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/cdc/change_detector.rs +0 -0
  25. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/cdc/changelog.rs +0 -0
  26. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/cdc/checkpoint.rs +0 -0
  27. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/cdc/mod.rs +0 -0
  28. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/config.rs +0 -0
  29. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/connectors_db.rs +0 -0
  30. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/database_advanced.rs +0 -0
  31. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/destination.rs +0 -0
  32. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/error.rs +0 -0
  33. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/hdfs.rs +0 -0
  34. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/mod.rs +0 -0
  35. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/mysql.rs +0 -0
  36. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/object_storage.rs +0 -0
  37. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/postgres.rs +0 -0
  38. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/rate_limiting.rs +0 -0
  39. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/registry.rs +0 -0
  40. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/saas.rs +0 -0
  41. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/source.rs +0 -0
  42. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/connectors/web.rs +0 -0
  43. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/destination.rs +0 -0
  44. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/dlq.rs +0 -0
  45. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/entity.rs +0 -0
  46. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/error.rs +0 -0
  47. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/executor.rs +0 -0
  48. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/governance/cached_gate.rs +0 -0
  49. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/governance/compliance_rules.rs +0 -0
  50. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/governance/credentials.rs +0 -0
  51. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/governance/mod.rs +0 -0
  52. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/governance/quality_gate.rs +0 -0
  53. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/governance/retry_policy.rs +0 -0
  54. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/governance/schema_evolution.rs +0 -0
  55. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/governance/statguardian_client.rs +0 -0
  56. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/idempotency.rs +0 -0
  57. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/lib.rs +0 -0
  58. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/lineage.rs +0 -0
  59. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/observability/dashboard_launcher.rs +0 -0
  60. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/observability/logs.rs +0 -0
  61. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/observability/metrics.rs +0 -0
  62. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/observability/metrics_server.rs +0 -0
  63. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/observability/mod.rs +0 -0
  64. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/observability/traces.rs +0 -0
  65. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/pipeline/activation_pipeline.rs +0 -0
  66. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/pipeline/backpressure.rs +0 -0
  67. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/pipeline/latency_tracker.rs +0 -0
  68. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/pipeline/mod.rs +0 -0
  69. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/sources/kafka.rs +0 -0
  70. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/sources/mod.rs +0 -0
  71. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/sources/polling.rs +0 -0
  72. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/sources/sync_config.rs +0 -0
  73. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/statguardian.rs +0 -0
  74. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/storage/mod.rs +0 -0
  75. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/storage/repository.rs +0 -0
  76. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/storage/schema.rs +0 -0
  77. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/streaming/event_processor.rs +0 -0
  78. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/streaming/event_schema.rs +0 -0
  79. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/streaming/mod.rs +0 -0
  80. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/streampdf.rs +0 -0
  81. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/streamxl.rs +0 -0
  82. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/sync.rs +0 -0
  83. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/testing/connector_test.rs +0 -0
  84. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/testing/harness.rs +0 -0
  85. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/testing/metrics.rs +0 -0
  86. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/testing/mock_http.rs +0 -0
  87. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/testing/mod.rs +0 -0
  88. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/testing/test_data.rs +0 -0
  89. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/transformers/mod.rs +0 -0
  90. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/transformers/spark.rs +0 -0
  91. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/core/src/workflow.rs +0 -0
  92. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/python/Cargo.toml +0 -0
  93. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/python/pyreverseetl/__init__.py +0 -0
  94. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/python/pyreverseetl/server.py +0 -0
  95. {pyreverseetl-3.1.7 → pyreverseetl-3.3.0}/python/src/lib.rs +0 -0
@@ -3199,7 +3199,7 @@ dependencies = [
3199
3199
 
3200
3200
  [[package]]
3201
3201
  name = "pyreverseetl"
3202
- version = "3.1.7"
3202
+ version = "3.3.0"
3203
3203
  dependencies = [
3204
3204
  "chrono",
3205
3205
  "pyo3",
@@ -3210,7 +3210,7 @@ dependencies = [
3210
3210
 
3211
3211
  [[package]]
3212
3212
  name = "pyreverseetl-core"
3213
- version = "3.1.7"
3213
+ version = "3.3.0"
3214
3214
  dependencies = [
3215
3215
  "async-trait",
3216
3216
  "aws-config",
@@ -3,7 +3,7 @@ members = ["core", "python"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "3.1.7"
6
+ version = "3.3.0"
7
7
  authors = ["Georgi Mammen Mullassery <mullassery@gmail.com>"]
8
8
  license = "Apache-2.0"
9
9
  repository = "https://github.com/Mullassery/PyReverseETL"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyReverseETL
3
- Version: 3.1.7
3
+ Version: 3.3.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -249,6 +249,21 @@ Python CLI / API → pyreverseetl._core (PyO3 bindings) → pyreverseetl_cor
249
249
  - **`python/pyreverseetl/`** — the installed Python package: `cli.py` (the `pyreverseetl`
250
250
  command) and `server.py` (an optional Flask REST wrapper around the same engine).
251
251
 
252
+ ## Validation gate
253
+
254
+ ```bash
255
+ # File-based report
256
+ pyreverseetl execute-activation my-activation --validation-report statguardian-report.json
257
+
258
+ # Live StatGuardian service: validate a sample before the real sync
259
+ pyreverseetl execute-activation my-activation --statguardian-url http://127.0.0.1:8000
260
+ ```
261
+
262
+ A file report must have `"passed": true`; anything else blocks the sync. The
263
+ live `--statguardian-url` gate dry-runs a sample and calls the service's
264
+ `/validate`; a failure or network error blocks the real sync. It is a sample
265
+ gate, not a full-table scan.
266
+
252
267
  ## Documentation
253
268
 
254
269
  - [ROADMAP_HONEST.md](ROADMAP_HONEST.md) — what's tested vs. untested vs. broken vs. not
@@ -226,6 +226,21 @@ Python CLI / API → pyreverseetl._core (PyO3 bindings) → pyreverseetl_cor
226
226
  - **`python/pyreverseetl/`** — the installed Python package: `cli.py` (the `pyreverseetl`
227
227
  command) and `server.py` (an optional Flask REST wrapper around the same engine).
228
228
 
229
+ ## Validation gate
230
+
231
+ ```bash
232
+ # File-based report
233
+ pyreverseetl execute-activation my-activation --validation-report statguardian-report.json
234
+
235
+ # Live StatGuardian service: validate a sample before the real sync
236
+ pyreverseetl execute-activation my-activation --statguardian-url http://127.0.0.1:8000
237
+ ```
238
+
239
+ A file report must have `"passed": true`; anything else blocks the sync. The
240
+ live `--statguardian-url` gate dry-runs a sample and calls the service's
241
+ `/validate`; a failure or network error blocks the real sync. It is a sample
242
+ gate, not a full-table scan.
243
+
229
244
  ## Documentation
230
245
 
231
246
  - [ROADMAP_HONEST.md](ROADMAP_HONEST.md) — what's tested vs. untested vs. broken vs. not
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "PyReverseETL"
7
- version = "3.1.7"
7
+ version = "3.3.0"
8
8
  description = "The Open Operational Data Activation Runtime"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -27,10 +27,71 @@ from __future__ import annotations
27
27
  import json
28
28
  import os
29
29
  import sys
30
+ import urllib.error
31
+ import urllib.request
30
32
 
31
33
  DEFAULT_STATE_PATH = os.path.join(os.path.expanduser("~"), ".pyreverseetl", "state.json")
32
34
 
33
35
 
36
+ def validate_activation_rows(
37
+ rows: list[dict],
38
+ service_url: str,
39
+ *,
40
+ limit: int = 100,
41
+ timeout_seconds: float = 10.0,
42
+ ) -> tuple[bool, dict]:
43
+ """POST sample rows to StatGuardian's ``/validate`` endpoint.
44
+
45
+ Fails closed on network/HTTP/parse errors. Only the first ``limit`` rows
46
+ are checked; this is a pre-activation sample gate, not a full-table scan.
47
+ """
48
+ url = service_url.rstrip("/") + "/validate"
49
+ checked = 0
50
+ for row in rows[: max(1, limit)]:
51
+ checked += 1
52
+ body = json.dumps({"entity": {"attributes": row}}).encode("utf-8")
53
+ request = urllib.request.Request(
54
+ url,
55
+ data=body,
56
+ headers={"Content-Type": "application/json"},
57
+ method="POST",
58
+ )
59
+ try:
60
+ with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
61
+ payload = json.loads(response.read().decode("utf-8"))
62
+ except (urllib.error.URLError, OSError, ValueError) as exc:
63
+ return False, {"reason": f"StatGuardian request failed: {exc}", "checked": checked}
64
+ if payload.get("passed") is not True:
65
+ return False, {
66
+ "reason": "StatGuardian rejected a sample row",
67
+ "checked": checked,
68
+ "issues": payload.get("issues", []),
69
+ }
70
+ return True, {"checked": checked}
71
+
72
+
73
+ def validation_report_allows(path: str) -> tuple[bool, dict]:
74
+ """Read a StatGuardian-style JSON report and decide whether sync may run.
75
+
76
+ Expected shape: ``{"passed": bool, "violations": [...], ...}``. A missing
77
+ or malformed file fails closed. This is a file-based gate until
78
+ StatGuardian exposes a service endpoint; it does not call StatGuardian.
79
+ """
80
+ try:
81
+ with open(path, encoding="utf-8") as handle:
82
+ report = json.load(handle)
83
+ except (OSError, ValueError) as exc:
84
+ return False, {"reason": f"cannot read validation report: {exc}"}
85
+ if not isinstance(report, dict) or "passed" not in report:
86
+ return False, {"reason": "validation report must be a JSON object with a 'passed' field"}
87
+ if report.get("passed") is not True:
88
+ return False, {
89
+ "reason": "validation report did not pass",
90
+ "violations": report.get("violations", []),
91
+ }
92
+ return True, {}
93
+
94
+
34
95
  class CLIInterface:
35
96
  """Command-line interface for PyReverseETL workflow integration."""
36
97
 
@@ -162,6 +223,9 @@ class CLIInterface:
162
223
  schema_store_path: str | None = None,
163
224
  idempotency_store_path: str | None = None,
164
225
  dlq_store_path: str | None = None,
226
+ validation_report_path: str | None = None,
227
+ statguardian_url: str | None = None,
228
+ statguardian_limit: int = 100,
165
229
  ) -> dict:
166
230
  """Execute an activation: run a real sync through the Rust engine.
167
231
 
@@ -200,6 +264,16 @@ class CLIInterface:
200
264
  changed is still sent. Only applies to the adapter-based
201
265
  destinations (webhook/salesforce/hubspot/marketo). Omit to
202
266
  disable idempotency checking entirely (default).
267
+ validation_report_path: Optional path to a JSON validation report
268
+ (``{"passed": bool, ...}``). If supplied and not passing, the
269
+ sync is blocked before any source read or destination write.
270
+ This is a file-based StatGuardian gate.
271
+ statguardian_url: Optional base URL of a running StatGuardian
272
+ service. A dry-run reads a sample of source rows and POSTs them
273
+ to ``/validate``; any failure (or network error) blocks the real
274
+ sync. This doubles the source read for the sample pass.
275
+ statguardian_limit: Maximum sample rows sent to StatGuardian
276
+ (default 100).
203
277
  dlq_store_path: Optional path to a SQLite file used as a
204
278
  dead-letter queue (quarantine) for records the destination
205
279
  rejects. Each failed record is stored with its run id, the
@@ -220,6 +294,15 @@ class CLIInterface:
220
294
  }
221
295
 
222
296
  activation = self.activations[activation_id]
297
+ if validation_report_path:
298
+ allowed, detail = validation_report_allows(validation_report_path)
299
+ if not allowed:
300
+ return {
301
+ "status": "error",
302
+ "activation_id": activation_id,
303
+ "message": "Blocked by validation gate",
304
+ "validation": detail,
305
+ }
223
306
  workflow = self.workflows.get(activation["workflow_id"])
224
307
  if workflow is None:
225
308
  return {
@@ -239,6 +322,38 @@ class CLIInterface:
239
322
  source_config.setdefault("table", workflow.get("table"))
240
323
  destination_config = activation.get("destination_config") or {}
241
324
 
325
+ if statguardian_url:
326
+ try:
327
+ preview = _core.run_sync(
328
+ source_type=workflow["source"],
329
+ source_config=json.dumps(source_config),
330
+ destination_type=activation["destination"],
331
+ destination_config=json.dumps(destination_config),
332
+ limit=limit,
333
+ compliance_rules=json.dumps(compliance_rules) if compliance_rules else None,
334
+ dry_run=True,
335
+ schema_store_path=None,
336
+ idempotency_store_path=None,
337
+ dlq_store_path=None,
338
+ )
339
+ except Exception as exc: # noqa: BLE001
340
+ return {
341
+ "status": "error",
342
+ "activation_id": activation_id,
343
+ "message": f"StatGuardian sample read failed: {exc}",
344
+ }
345
+ sample_rows = [json.loads(row) for row in preview.dry_run_preview]
346
+ allowed, detail = validate_activation_rows(
347
+ sample_rows, statguardian_url, limit=statguardian_limit
348
+ )
349
+ if not allowed:
350
+ return {
351
+ "status": "error",
352
+ "activation_id": activation_id,
353
+ "message": "Blocked by StatGuardian sample validation",
354
+ "validation": detail,
355
+ }
356
+
242
357
  try:
243
358
  result = _core.run_sync(
244
359
  source_type=workflow["source"],
@@ -501,6 +616,29 @@ def main():
501
616
  sys.exit(1)
502
617
  dlq_store_path = sys.argv[idx + 1]
503
618
 
619
+ validation_report_path = None
620
+ if "--validation-report" in sys.argv:
621
+ idx = sys.argv.index("--validation-report")
622
+ if idx + 1 >= len(sys.argv):
623
+ print(json.dumps({"error": "--validation-report requires a file path"}))
624
+ sys.exit(1)
625
+ validation_report_path = sys.argv[idx + 1]
626
+
627
+ statguardian_url = None
628
+ if "--statguardian-url" in sys.argv:
629
+ idx = sys.argv.index("--statguardian-url")
630
+ if idx + 1 >= len(sys.argv):
631
+ print(json.dumps({"error": "--statguardian-url requires a URL"}))
632
+ sys.exit(1)
633
+ statguardian_url = sys.argv[idx + 1]
634
+ statguardian_limit = 100
635
+ if "--statguardian-limit" in sys.argv:
636
+ idx = sys.argv.index("--statguardian-limit")
637
+ if idx + 1 >= len(sys.argv):
638
+ print(json.dumps({"error": "--statguardian-limit requires a number"}))
639
+ sys.exit(1)
640
+ statguardian_limit = int(sys.argv[idx + 1])
641
+
504
642
  result = cli.execute_activation(
505
643
  activation_id,
506
644
  limit,
@@ -509,6 +647,9 @@ def main():
509
647
  schema_store_path,
510
648
  idempotency_store_path,
511
649
  dlq_store_path,
650
+ validation_report_path,
651
+ statguardian_url,
652
+ statguardian_limit,
512
653
  )
513
654
  print(json.dumps(result))
514
655
 
File without changes