datavault-cli 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: datavault-cli
3
+ Version: 1.0.0
4
+ Summary: Cryptographic provenance and version control for AI training datasets
5
+ Home-page: https://github.com/thiminhkhuedao/DataVault
6
+ Author: Ry Nguyen
7
+ Author-email: thiminhkhuedao@gmail.com
8
+ Keywords: data provenance versioning AI machine-learning CSV sha256
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Classifier: Topic :: Software Development :: Version Control
13
+ Requires-Python: >=3.6
14
+ Description-Content-Type: text/markdown
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: keywords
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+ # DataVault
26
+
27
+ **Cryptographic provenance and version control for AI training datasets.**
28
+
29
+ Git tracks code. DataVault tracks data — with SHA-256 tamper detection, row-level CSV diffing, cryptographic hash chains, and multi-user signing workflows.
30
+
31
+ Zero dependencies. Pure Python stdlib.
32
+
33
+ ---
34
+
35
+ ## Why not Git or DVC?
36
+
37
+ | | Git | DVC | DataVault |
38
+ |---|---|---|---|
39
+ | Tracks file versions | ✓ | ✓ | ✓ |
40
+ | Works on large files | ✗ | ✓ | ✓ |
41
+ | Row-level CSV diff | ✗ | ✗ | ✓ |
42
+ | Tamper detection (SHA-256) | partial | ✗ | ✓ |
43
+ | Cryptographic hash chain | ✓ | ✗ | ✓ |
44
+ | Multi-user signing & approval | ✗ | ✗ | ✓ |
45
+ | Zero dependencies | ✗ | ✗ | ✓ |
46
+
47
+ ---
48
+
49
+ ## Quickstart
50
+
51
+ ```bash
52
+ # 1. Clone the repo
53
+ git clone https://github.com/thiminhkhuedao/DataVault.git
54
+ cd DataVault
55
+
56
+ # 2. Run the full demo (no setup needed)
57
+ python test_datavault.py
58
+ ```
59
+
60
+ That's it. No pip install, no config, no database. Just Python 3.6+.
61
+
62
+ ---
63
+
64
+ ## Usage
65
+
66
+ ```bash
67
+ # Initialize a project in any folder
68
+ python datavault.py init my-project
69
+
70
+ # Track a dataset
71
+ python datavault.py add training_data.csv "raw data from source"
72
+
73
+ # After editing the file, commit the change
74
+ python datavault.py commit training_data.csv "removed 42 duplicate rows"
75
+
76
+ # View full history
77
+ python datavault.py log training_data.csv
78
+
79
+ # Prove the file hasn't been tampered with
80
+ python datavault.py verify training_data.csv
81
+
82
+ # See which rows changed between versions (CSV only)
83
+ python datavault.py rowdiff training_data.csv v1 v3
84
+
85
+ # Verify the full cryptographic chain
86
+ python datavault.py chain training_data.csv
87
+
88
+ # Sign a version
89
+ python datavault.py sign training_data.csv v3 alice reviewer
90
+ python datavault.py sign training_data.csv v3 bob approver
91
+ python datavault.py approve training_data.csv v3
92
+
93
+ # Open the web dashboard
94
+ python datavault.py dashboard
95
+
96
+ # Export a provenance report (HTML → print as PDF)
97
+ python datavault.py export
98
+ ```
99
+
100
+ ---
101
+
102
+ ## All commands
103
+
104
+ | Command | Description |
105
+ |---|---|
106
+ | `init <name>` | Start a new project |
107
+ | `add <file> "msg"` | Start tracking a file |
108
+ | `commit <file> "msg"` | Save a new version |
109
+ | `log <file>` | Show version history |
110
+ | `status` | Show all tracked files |
111
+ | `verify <file>` | Check for tampering |
112
+ | `diff <file> v1 v2` | Text diff between versions |
113
+ | `checkout <file> v1` | Restore a version |
114
+ | `tag <file> v2 "label"` | Label a version |
115
+ | `rowlog <file>` | Row change stats (CSV) |
116
+ | `rowdiff <file> v1 v2` | Which rows changed (CSV) |
117
+ | `rowhistory <file> col val` | History of one specific row |
118
+ | `chain <file>` | Verify cryptographic chain |
119
+ | `chainshow <file>` | Display chain visually |
120
+ | `sign <file> v <name> [role]` | Sign a version |
121
+ | `signatures <file> [v]` | List or verify signatures |
122
+ | `approve <file> v` | Check approval status |
123
+ | `export` | Export HTML/PDF report |
124
+ | `dashboard` | Open web dashboard |
125
+
126
+ ---
127
+
128
+ ## How it works
129
+
130
+ **SHA-256 hashing** — every version gets a 64-character fingerprint. Change one character anywhere in the file, the fingerprint changes completely. `verify` catches any modification made outside DataVault.
131
+
132
+ **Hash chain** — each version's hash includes the previous version's hash, like a blockchain. You cannot delete or reorder history without breaking the chain. `chain` verifies the full sequence back to genesis.
133
+
134
+ **Row-level tracking** — for CSV files, DataVault tracks individual rows, not just the whole file. `rowdiff v1 v3` shows exactly which rows were added, deleted, or moved between any two versions.
135
+
136
+ **Multi-user signing** — reviewers and approvers sign specific versions. Signatures are cryptographically tied to the file hash — if the file changes after signing, the signature is invalidated automatically.
137
+
138
+ **Pi Bridge** — optional module that connects DataVault to a Raspberry Pi cluster, automatically committing hourly power and temperature readings as tracked CSV files with anomaly detection and auto-tagging.
139
+
140
+ ---
141
+
142
+ ## Files
143
+
144
+ | File | Purpose |
145
+ |---|---|
146
+ | `datavault.py` | CLI entrypoint — the only file you run |
147
+ | `vault_core.py` | Core engine — hashing, versioning, storage |
148
+ | `row_tracker.py` | Row-level CSV diffing |
149
+ | `chain.py` | Cryptographic hash chain |
150
+ | `signing.py` | Multi-user signing and approval |
151
+ | `dashboard.py` | Local web dashboard (port 5000) |
152
+ | `pi_bridge.py` | Auto-commit sensor data from Raspberry Pi |
153
+ | `migrate.py` | Backfill snapshots for existing versions |
154
+ | `test_datavault.py` | Full demo — run this first |
155
+
156
+ ---
157
+
158
+ ## Requirements
159
+
160
+ - Python 3.6+
161
+ - Zero external dependencies — pure standard library
162
+
163
+ ---
164
+
165
+ ## Why this matters
166
+
167
+ When an AI system makes a wrong decision in a medical, legal, or financial context, regulators will ask: where did the training data come from? Who modified it? Which version was used? Today there is no standard tool that answers these questions reliably.
168
+
169
+ DataVault is a working prototype of that provenance layer.
170
+
171
+ ---
172
+
173
+ ## Copyright
174
+
175
+ Copyright © 2026 Ry Nguyen. All rights reserved.
176
+
177
+ This project and its source code are public for portfolio viewing purposes only.
178
+ No permission is granted to copy, distribute, modify, or use this code for any other purpose.
@@ -0,0 +1,13 @@
1
+ chain.py,sha256=bW3zTN5-ICcLqu-0e0518xQpqOzjYURnYFSYn8yuLb0,6735
2
+ dashboard.py,sha256=koVXnGwqZ7UwXSTf7uCY-UKgyWJQc5dFdSQdRQH8ef0,10136
3
+ datavault.py,sha256=Y-N9EwaokH26bmzNOQlZ1S2ijkxqQj3eG3owG1Dxh3Q,8760
4
+ migrate.py,sha256=8A0kMzqR2Rii0icPwVlz2TCG2qdnIHRVOOzEPT-Vyd4,2535
5
+ pi_bridge.py,sha256=d6aZZsFzNREYAcM_vnFUO1wFfBkxgNTmVH1oGm46YyE,8449
6
+ row_tracker.py,sha256=gHFYWzieFt_SGHb-VZF5osnkAH3a0HKVSX5nNas5G5A,7680
7
+ signing.py,sha256=hPAY2OjjfKg3Xgin1jJL9_DizOB24KPBI4oLuvIiuTc,7013
8
+ vault_core.py,sha256=F0dGThUZUPCBwVYNLD8zqIJSnVAFix86kTHSIyQ8AP0,16697
9
+ datavault_cli-1.0.0.dist-info/METADATA,sha256=V9j_J_RLbASyC1qfnYaEZix8q4NKbhz-VYpIXGM8B5s,6124
10
+ datavault_cli-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ datavault_cli-1.0.0.dist-info/entry_points.txt,sha256=1yk981ex3zxgdjSm4V8myMyEurtZ4toi-1Xx9SNw26E,45
12
+ datavault_cli-1.0.0.dist-info/top_level.txt,sha256=TU0TmxIQy0xiZE0KyOfET3S4vkUuoKSY3y1Um8SypsY,75
13
+ datavault_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ datavault = datavault:main
@@ -0,0 +1,8 @@
1
+ chain
2
+ dashboard
3
+ datavault
4
+ migrate
5
+ pi_bridge
6
+ row_tracker
7
+ signing
8
+ vault_core
migrate.py ADDED
@@ -0,0 +1,70 @@
1
+ # ============================================================
2
+ # migrate.py — backfill row snapshots + hash chain
3
+ #
4
+ # Run once: python migrate.py
5
+ # ============================================================
6
+
7
+ import json
8
+ import os
9
+ import shutil
10
+ import row_tracker
11
+ import chain as chain_module
12
+
13
+ VAULT_DIR = ".datavault"
14
+ HISTORY_FILE = ".datavault/history.json"
15
+ VERSIONS_DIR = ".datavault/versions"
16
+
17
+ def migrate():
18
+ if not os.path.exists(HISTORY_FILE):
19
+ print("No DataVault project found.")
20
+ return
21
+
22
+ with open(HISTORY_FILE, "r") as f:
23
+ history = json.load(f)
24
+
25
+ print("DataVault migration — backfilling row snapshots + hash chain\n")
26
+
27
+ for filename, data in history["files"].items():
28
+ versions = data["versions"]
29
+ print(f"File: {filename} ({len(versions)} version(s))")
30
+
31
+ for v in versions:
32
+ version_id = v["version_id"]
33
+
34
+ # Find the stored version file in .datavault/versions/
35
+ safe_name = filename.replace("/", "_").replace("\\", "_")
36
+ stored_path = os.path.join(VERSIONS_DIR, f"{safe_name}__{version_id}")
37
+
38
+ if not os.path.exists(stored_path):
39
+ print(f" {version_id}: stored file not found, skipping")
40
+ continue
41
+
42
+ # Generate row snapshot from stored file (not current file)
43
+ if filename.lower().endswith(".csv"):
44
+ snapshot, headers = row_tracker.build_row_snapshot(stored_path)
45
+ if snapshot:
46
+ row_tracker.save_row_snapshot(filename, version_id, snapshot, headers)
47
+ print(f" {version_id}: row snapshot created ({len(snapshot)} rows)")
48
+ else:
49
+ print(f" {version_id}: empty CSV, skipped")
50
+ else:
51
+ print(f" {version_id}: not a CSV, skipping row snapshot")
52
+
53
+ # Build hash chain for this file
54
+ success, result = chain_module.build_chain(filename)
55
+ if success:
56
+ print(f" Chain built ({len(result)} blocks)")
57
+ else:
58
+ print(f" Chain failed: {result}")
59
+
60
+ print()
61
+
62
+ print("Migration complete.")
63
+ print("\nNow try:")
64
+ print(" python datavault.py rowlog training_data.csv")
65
+ print(" python datavault.py rowdiff training_data.csv v1 v3")
66
+ print(" python datavault.py chain training_data.csv")
67
+ print(" python datavault.py chainshow training_data.csv")
68
+
69
+ if __name__ == "__main__":
70
+ migrate()
pi_bridge.py ADDED
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import sys
4
+ import csv
5
+ import json
6
+ import time
7
+ import argparse
8
+ import subprocess
9
+ from datetime import datetime, timedelta
10
+ from collections import deque
11
+
12
+ EXPORT_INTERVAL_SECONDS = 3600
13
+ EXPORT_DIR = "pi_exports"
14
+ DATAVAULT_PROJECT = "pi-datacenter"
15
+ POWER_SPIKE_THRESHOLD = 5.5
16
+ TEMP_DANGER_THRESHOLD = 75.0
17
+
18
+ readings_buffer = deque(maxlen=3600)
19
+
20
+
21
+ def simulated_reading():
22
+ import random, math
23
+ t = time.time()
24
+ base_power = 3.5 + math.sin(t / 300) * 0.8
25
+ spike = random.random() < 0.02
26
+ power = base_power + (2.0 if spike else 0) + random.gauss(0, 0.1)
27
+ cpu_temp = 45 + power * 4 + random.gauss(0, 1.5)
28
+ sink_temp = cpu_temp - 15 + random.gauss(0, 0.5)
29
+ teg_power = max(0, (cpu_temp - sink_temp) * 0.008)
30
+ return {
31
+ "timestamp": datetime.now().isoformat(),
32
+ "pi_power_W": round(power, 4),
33
+ "teg_power_W": round(teg_power, 4),
34
+ "cpu_temp_C": round(cpu_temp, 2),
35
+ "heatsink_temp_C": round(sink_temp, 2),
36
+ "thermal_delta_C": round(cpu_temp - sink_temp, 2),
37
+ "fan_speed_pct": min(100, max(20, int((cpu_temp - 35) * 3))),
38
+ "efficiency_pct": round((teg_power / power) * 100, 3) if power > 0 else 0,
39
+ "anomaly": spike
40
+ }
41
+
42
+
43
+ def real_reading():
44
+ state_file = "/tmp/datavault_pi_state.json"
45
+ if not os.path.exists(state_file):
46
+ return None
47
+ try:
48
+ with open(state_file, "r") as f:
49
+ state = json.load(f)
50
+ state["timestamp"] = datetime.now().isoformat()
51
+ state["anomaly"] = (
52
+ state.get("pi_power_W", 0) > POWER_SPIKE_THRESHOLD or
53
+ state.get("cpu_temp_C", 0) > TEMP_DANGER_THRESHOLD
54
+ )
55
+ return state
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def run_datavault(command_args):
61
+ result = subprocess.run(
62
+ [sys.executable, "datavault.py"] + command_args,
63
+ capture_output=True, text=True,
64
+ cwd=os.path.dirname(os.path.abspath(__file__))
65
+ )
66
+ return result.returncode == 0, result.stdout + result.stderr
67
+
68
+
69
+ def ensure_datavault_initialized():
70
+ if not os.path.exists(".datavault"):
71
+ success, output = run_datavault(["init", DATAVAULT_PROJECT])
72
+ if success:
73
+ print(f"[DataVault] Initialized project '{DATAVAULT_PROJECT}'")
74
+ else:
75
+ print(f"[DataVault] Init failed: {output}")
76
+ return False
77
+ return True
78
+
79
+
80
+ def export_hourly_csv(readings, export_time):
81
+ os.makedirs(EXPORT_DIR, exist_ok=True)
82
+ if not readings:
83
+ return None
84
+ filename = f"pi_power_{export_time.strftime('%Y-%m-%d_%H-%M')}.csv"
85
+ filepath = os.path.join(EXPORT_DIR, filename)
86
+ with open(filepath, "w", newline="", encoding="utf-8") as f:
87
+ writer = csv.DictWriter(f, fieldnames=list(readings[0].keys()))
88
+ writer.writeheader()
89
+ writer.writerows(readings)
90
+ return filepath
91
+
92
+
93
+ def compute_hourly_stats(readings):
94
+ if not readings:
95
+ return {}
96
+ powers = [r["pi_power_W"] for r in readings if r.get("pi_power_W")]
97
+ temps = [r["cpu_temp_C"] for r in readings if r.get("cpu_temp_C")]
98
+ teg = [r["teg_power_W"] for r in readings if r.get("teg_power_W")]
99
+ anomalies = sum(1 for r in readings if r.get("anomaly"))
100
+ def avg(lst): return round(sum(lst)/len(lst), 3) if lst else 0
101
+ total_Wh = avg(powers) * (len(readings) / 3600)
102
+ teg_Wh = avg(teg) * (len(readings) / 3600)
103
+ return {
104
+ "readings": len(readings),
105
+ "avg_power_W": avg(powers),
106
+ "max_power_W": round(max(powers), 3) if powers else 0,
107
+ "avg_cpu_temp_C": avg(temps),
108
+ "max_cpu_temp_C": round(max(temps), 3) if temps else 0,
109
+ "total_energy_Wh": round(total_Wh, 6),
110
+ "total_teg_Wh": round(teg_Wh, 6),
111
+ "heat_recovery_pct": round((teg_Wh/total_Wh)*100, 3) if total_Wh > 0 else 0,
112
+ "anomalies": anomalies
113
+ }
114
+
115
+
116
+ def commit_hourly_export(readings, export_time):
117
+ filepath = export_hourly_csv(list(readings), export_time)
118
+ if not filepath:
119
+ print("[Bridge] No readings to export")
120
+ return
121
+
122
+ stats = compute_hourly_stats(list(readings))
123
+ hour_str = export_time.strftime("%Y-%m-%d %H:00")
124
+ anomaly_note = f" ⚠ {stats['anomalies']} anomalies" if stats.get("anomalies") else ""
125
+ message = (
126
+ f"Pi power data {hour_str} | "
127
+ f"avg:{stats['avg_power_W']}W max:{stats['max_power_W']}W "
128
+ f"temp:{stats['avg_cpu_temp_C']}C "
129
+ f"TEG:{stats['heat_recovery_pct']}% recovered"
130
+ f"{anomaly_note}"
131
+ )
132
+
133
+ history_file = ".datavault/history.json"
134
+ csv_filename = os.path.basename(filepath)
135
+ already_tracked = False
136
+ if os.path.exists(history_file):
137
+ with open(history_file) as f:
138
+ already_tracked = csv_filename in json.load(f).get("files", {})
139
+
140
+ action = "commit" if already_tracked else "add"
141
+ success, output = run_datavault([action, filepath, message])
142
+
143
+ if success:
144
+ print(f"[DataVault] {'Committed' if already_tracked else 'Added'}: {csv_filename}")
145
+ print(f" {message}")
146
+ if stats.get("anomalies", 0) > 0:
147
+ with open(history_file) as f:
148
+ hist = json.load(f)
149
+ latest_v = hist["files"][csv_filename]["versions"][-1]["version_id"]
150
+ run_datavault(["tag", filepath, latest_v, f"ANOMALY: {stats['anomalies']} power/temp events detected"])
151
+ print(f"[DataVault] Tagged {latest_v} with anomaly warning")
152
+ _, output2 = run_datavault(["chain", csv_filename])
153
+ print(f"[DataVault] Chain: {'✓ intact' if 'CHAIN INTACT' in output2 else '✗ BROKEN'}")
154
+ else:
155
+ print(f"[DataVault] Failed: {output}")
156
+
157
+ return filepath, stats
158
+
159
+
160
+ def main(simulate=False):
161
+ print("\n" + "="*55)
162
+ print(" PI BRIDGE — DataVault ↔ Mini Data Center")
163
+ print("="*55)
164
+ print(f" Mode: {'SIMULATED' if simulate else 'REAL (Raspberry Pi)'}")
165
+ print(f" Export: every {EXPORT_INTERVAL_SECONDS//60} minutes")
166
+ print(f"\n Press Ctrl+C to stop\n")
167
+
168
+ if not ensure_datavault_initialized():
169
+ sys.exit(1)
170
+
171
+ next_export = datetime.now() + timedelta(seconds=EXPORT_INTERVAL_SECONDS)
172
+ reading_fn = simulated_reading if simulate else real_reading
173
+ reading_count = 0
174
+
175
+ try:
176
+ while True:
177
+ reading = reading_fn()
178
+ if reading:
179
+ readings_buffer.append(reading)
180
+ reading_count += 1
181
+ if reading_count % 10 == 0:
182
+ r = reading
183
+ flag = " ⚠" if r.get("anomaly") else ""
184
+ print(f" [{datetime.now().strftime('%H:%M:%S')}] power:{r['pi_power_W']:.2f}W temp:{r['cpu_temp_C']:.1f}C TEG:{r['teg_power_W']:.4f}W fan:{r['fan_speed_pct']}%{flag}")
185
+
186
+ now = datetime.now()
187
+ if now >= next_export:
188
+ print(f"\n[{now.strftime('%H:%M:%S')}] Hourly export triggered...")
189
+ commit_hourly_export(readings_buffer, now)
190
+ next_export = now + timedelta(seconds=EXPORT_INTERVAL_SECONDS)
191
+ print(f"[Bridge] Next export in {EXPORT_INTERVAL_SECONDS//60} minutes\n")
192
+
193
+ time.sleep(1)
194
+
195
+ except KeyboardInterrupt:
196
+ print(f"\n\nBridge stopped. Readings: {reading_count} | Buffer: {len(readings_buffer)}")
197
+ if len(readings_buffer) > 10:
198
+ print("Running final export...")
199
+ commit_hourly_export(readings_buffer, datetime.now())
200
+ print("\nDataVault status:")
201
+ subprocess.run([sys.executable, "datavault.py", "status"], cwd=os.path.dirname(os.path.abspath(__file__)))
202
+
203
+
204
+ if __name__ == "__main__":
205
+ parser = argparse.ArgumentParser(description="Pi Bridge — connects mini data center to DataVault")
206
+ parser.add_argument("--simulate", action="store_true", help="Simulation mode (no real Pi sensors)")
207
+ parser.add_argument("--fast", action="store_true", help="Export every 60s instead of 60min (testing)")
208
+ args = parser.parse_args()
209
+
210
+ if args.fast:
211
+ EXPORT_INTERVAL_SECONDS = 60
212
+ print("[Bridge] Fast mode: exporting every 60 seconds")
213
+
214
+ main(simulate=args.simulate)