Prepare Windows portable packaging
This commit is contained in:
@@ -7,6 +7,7 @@ __pycache__/
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
# Sensitive vault exports, logs, and local runtime state
|
# Sensitive vault exports, logs, and local runtime state
|
||||||
*.json
|
*.json
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# PyInstaller one-file Windows build configuration.
|
||||||
|
# Build from the repository root with: pyinstaller --clean --noconfirm DataLoom.spec
|
||||||
|
|
||||||
|
from PyInstaller.utils.hooks import collect_submodules
|
||||||
|
|
||||||
|
hiddenimports = collect_submodules("PySide6")
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
["src/dataloom/app.py"],
|
||||||
|
pathex=["src"],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name="DataLoom",
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=False,
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
Write-Host "Creating or reusing the DataLoom Windows build environment..."
|
||||||
|
if (-not (Test-Path ".venv\\Scripts\\python.exe")) {
|
||||||
|
py -3 -m venv .venv
|
||||||
|
}
|
||||||
|
|
||||||
|
& .venv\\Scripts\\python.exe -m pip install --upgrade pip
|
||||||
|
& .venv\\Scripts\\python.exe -m pip install -e ".[build,test]"
|
||||||
|
& .venv\\Scripts\\python.exe -m pytest -q
|
||||||
|
& .venv\\Scripts\\python.exe -m PyInstaller --clean --noconfirm DataLoom.spec
|
||||||
|
|
||||||
|
Write-Host "Portable executable created at dist\\DataLoom.exe"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Windows 11 portable build
|
||||||
|
|
||||||
|
The final portable executable must be built on Windows 11 because PyInstaller produces a platform-specific executable.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Windows 11 64-bit
|
||||||
|
- Python 3.11 or newer from python.org, with the Python launcher (`py`) available
|
||||||
|
- Git, or a ZIP copy of this repository
|
||||||
|
- Internet access during the one-time dependency installation only
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Open PowerShell in the repository root and run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -Scope Process Bypass
|
||||||
|
.\\build-windows.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The script creates a project-local virtual environment, installs the application and build/test dependencies, runs the synthetic tests, and creates:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist\\DataLoom.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
The executable is intended to be portable and does not require Python on the destination Windows machine.
|
||||||
|
|
||||||
|
## Validation checklist
|
||||||
|
|
||||||
|
- Launch `dist\\DataLoom.exe` on Windows 11.
|
||||||
|
- Import `tests\\fixtures\\sample-export-a.json` and `sample-export-b.json`.
|
||||||
|
- Confirm 8 loaded, 7 included, 1 exact duplicate removed, and 2 review groups.
|
||||||
|
- Confirm secret logging is unchecked by default.
|
||||||
|
- Confirm the duplicate detail view shows both records and the newer revision preference.
|
||||||
|
- Export a merged JSON file and verify the displayed output and audit-log paths.
|
||||||
|
- Test on a second Windows 11 machine or clean user profile if practical.
|
||||||
|
|
||||||
|
## Sensitive data
|
||||||
|
|
||||||
|
Do not copy real Bitwarden exports, merged vaults, audit logs, `.venv`, `build`, or `dist` contents into the repository. Use copies of vault exports only for local testing.
|
||||||
@@ -12,6 +12,7 @@ dependencies = ["PySide6>=6.6"]
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = ["pytest>=8.0"]
|
test = ["pytest>=8.0"]
|
||||||
|
build = ["pyinstaller>=6.0"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
dataloom = "dataloom.app:main"
|
dataloom = "dataloom.app:main"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView, QApplication, QFileDialog, QLabel, QListWidget,
|
QAbstractItemView, QApplication, QFileDialog, QLabel, QListWidget,
|
||||||
QMainWindow, QMessageBox, QPushButton, QTabWidget, QTextEdit,
|
QMainWindow, QMessageBox, QPushButton, QTabWidget, QTextEdit,
|
||||||
QVBoxLayout, QWidget,
|
QCheckBox, QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dataloom.core.audit import AuditLog
|
from dataloom.core.audit import AuditLog
|
||||||
@@ -23,20 +23,26 @@ class MainWindow(QMainWindow):
|
|||||||
self.audit = None
|
self.audit = None
|
||||||
self.summary = QTextEdit(); self.summary.setReadOnly(True)
|
self.summary = QTextEdit(); self.summary.setReadOnly(True)
|
||||||
self.duplicates = QListWidget(); self.duplicates.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
self.duplicates = QListWidget(); self.duplicates.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
self.duplicate_detail = QTextEdit(); self.duplicate_detail.setReadOnly(True)
|
||||||
|
self.duplicates.currentRowChanged.connect(self.show_duplicate_detail)
|
||||||
self.log_view = QTextEdit(); self.log_view.setReadOnly(True)
|
self.log_view = QTextEdit(); self.log_view.setReadOnly(True)
|
||||||
self.entries_view = QListWidget()
|
self.entries_view = QListWidget()
|
||||||
tabs = QTabWidget()
|
tabs = QTabWidget()
|
||||||
tabs.addTab(self._page("Import Summary", self.summary), "Import Summary")
|
tabs.addTab(self._page("Import Summary", self.summary), "Import Summary")
|
||||||
tabs.addTab(self._page("Merged Entries", self.entries_view), "Merged Entries")
|
tabs.addTab(self._page("Merged Entries", self.entries_view), "Merged Entries")
|
||||||
tabs.addTab(self._page("Duplicate Review", self.duplicates), "Duplicate Review")
|
duplicate_page = QWidget(); duplicate_layout = QVBoxLayout(duplicate_page)
|
||||||
|
duplicate_layout.addWidget(QLabel("<h2>Duplicate Review</h2>")); duplicate_layout.addWidget(self.duplicates); duplicate_layout.addWidget(self.duplicate_detail)
|
||||||
|
tabs.addTab(duplicate_page, "Duplicate Review")
|
||||||
tabs.addTab(self._page("Audit Log", self.log_view), "Audit Log")
|
tabs.addTab(self._page("Audit Log", self.log_view), "Audit Log")
|
||||||
import_button = QPushButton("Import Bitwarden exports…")
|
import_button = QPushButton("Import Bitwarden exports…")
|
||||||
import_button.clicked.connect(self.import_files)
|
import_button.clicked.connect(self.import_files)
|
||||||
|
self.secret_logging = QCheckBox("Enable full secret logging (passwords, TOTP, notes, and vault contents)")
|
||||||
|
self.secret_logging.setChecked(False)
|
||||||
export_button = QPushButton("Export merged vault…")
|
export_button = QPushButton("Export merged vault…")
|
||||||
export_button.setEnabled(False)
|
export_button.setEnabled(False)
|
||||||
export_button.clicked.connect(self.export_merged)
|
export_button.clicked.connect(self.export_merged)
|
||||||
self.export_button = export_button
|
self.export_button = export_button
|
||||||
layout = QVBoxLayout(); layout.addWidget(QLabel("DataLoom processes exports locally. Do not import encrypted files.")); layout.addWidget(import_button); layout.addWidget(export_button); layout.addWidget(tabs)
|
layout = QVBoxLayout(); layout.addWidget(QLabel("DataLoom processes exports locally. Do not import encrypted files.")); layout.addWidget(self.secret_logging); layout.addWidget(import_button); layout.addWidget(export_button); layout.addWidget(tabs)
|
||||||
root = QWidget(); root.setLayout(layout); self.setCentralWidget(root)
|
root = QWidget(); root.setLayout(layout); self.setCentralWidget(root)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -55,7 +61,7 @@ class MainWindow(QMainWindow):
|
|||||||
if len(set(formats)) != 1: raise ImportError("All selected files must use the same format")
|
if len(set(formats)) != 1: raise ImportError("All selected files must use the same format")
|
||||||
self.records = records
|
self.records = records
|
||||||
self.result = merge_records(records)
|
self.result = merge_records(records)
|
||||||
self.audit = AuditLog(Path(paths[0]).parent / "dataloom-audit.log", include_secrets=True)
|
self.audit = AuditLog(Path(paths[0]).parent / "dataloom-audit.log", include_secrets=self.secret_logging.isChecked())
|
||||||
self.audit.record("IMPORT", f"Loaded {len(paths)} {formats[0]} file(s) and {len(records)} record(s)")
|
self.audit.record("IMPORT", f"Loaded {len(paths)} {formats[0]} file(s) and {len(records)} record(s)")
|
||||||
for record in records:
|
for record in records:
|
||||||
self.audit.record("VIEW", f"{record.source_file} record {record.source_index} ({record.item_type}) {record.name}", repr(record.data))
|
self.audit.record("VIEW", f"{record.source_file} record {record.source_index} ({record.item_type}) {record.name}", repr(record.data))
|
||||||
@@ -95,6 +101,17 @@ class MainWindow(QMainWindow):
|
|||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
QMessageBox.critical(self, "Export failed", str(exc))
|
QMessageBox.critical(self, "Export failed", str(exc))
|
||||||
|
|
||||||
|
def show_duplicate_detail(self, row: int) -> None:
|
||||||
|
if not self.result or row < 0 or row >= len(self.result.review_groups):
|
||||||
|
self.duplicate_detail.clear(); return
|
||||||
|
group = self.result.review_groups[row]
|
||||||
|
lines = [f"Classification: {group.classification}", f"Reason: {group.reason}"]
|
||||||
|
if group.selected:
|
||||||
|
lines.append(f"Automatic preference: {group.selected.source_file}:{group.selected.source_index} ({group.selected.name})")
|
||||||
|
for index, record in enumerate(group.records, 1):
|
||||||
|
lines.extend(["", f"Record {index}: {record.source_file}:{record.source_index} — {record.name}", repr(record.data)])
|
||||||
|
self.duplicate_detail.setPlainText("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
def run() -> int:
|
def run() -> int:
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
|
|||||||
Reference in New Issue
Block a user