add build pipline
This commit is contained in:
1 parent
94e245a3a0
commit
c9dac7b9b8
5 files changed
+679
No files matched your search
@@ -8,6 +8,13 @@ from pathlib import Path
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
# Projekt-Root zum Python-Path hinzufügen
|
# Projekt-Root zum Python-Path hinzufügen
|
||||||
|
# Im gebundelten Modus (PyInstaller) zeigt APP_BASE_DIR auf das Verzeichnis der .exe,
|
||||||
|
# wo config/, input/, template/, output/ liegen. Im Entwicklungsmodus bleibt es None
|
||||||
|
# und PROJECT_ROOT wird aus __file__ abgeleitet.
|
||||||
|
_BASE_DIR = os.environ.get("APP_BASE_DIR")
|
||||||
|
if _BASE_DIR:
|
||||||
|
PROJECT_ROOT = Path(_BASE_DIR).resolve()
|
||||||
|
else:
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
cd /d "%~dp0"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo Tätigkeitsnachweis-Generator – Build-Pipeline
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM ── Virtuelle Umgebung aktivieren ────────────────────────────
|
||||||
|
if exist "0_application\.venv\Scripts\python.exe" (
|
||||||
|
set PYTHON=0_application\.venv\Scripts\python.exe
|
||||||
|
echo [OK] Virtuelle Umgebung gefunden
|
||||||
|
) else if exist ".venv\Scripts\python.exe" (
|
||||||
|
set PYTHON=.venv\Scripts\python.exe
|
||||||
|
echo [OK] Virtuelle Umgebung gefunden
|
||||||
|
) else (
|
||||||
|
echo [WARN] Keine virtuelle Umgebung gefunden, nutze System-Python
|
||||||
|
set PYTHON=python
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM ── Parameter durchreichen ──────────────────────────────────
|
||||||
|
set ARGS=%*
|
||||||
|
if "%ARGS%"=="" (
|
||||||
|
echo Starte interaktiven Build …
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
REM ── Build ausführen ─────────────────────────────────────────
|
||||||
|
%PYTHON% "1_build-pipeline\build.py" %ARGS%
|
||||||
|
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo [FEHLER] Build fehlgeschlagen!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Build abgeschlossen.
|
||||||
|
pause
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
"""Build-Orchestrierung mit Semantic Versioning.
|
||||||
|
|
||||||
|
Liest die aktuelle Version aus CHANGELOG.md, bump't sie gemäß
|
||||||
|
Benutzereingabe (major/minor/patch), aktualisiert das Changelog
|
||||||
|
und baut via PyInstaller eine standalone .exe nach 2_build/.
|
||||||
|
|
||||||
|
Verwendung:
|
||||||
|
python build.py # interaktiver Modus
|
||||||
|
python build.py --bump minor # nicht-interaktiv
|
||||||
|
python build.py --bump patch --message "Hotfix: PDF-Schriftgröße korrigiert"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# ── Pfade ───────────────────────────────────────────────────────
|
||||||
|
BUILD_PIPELINE_DIR = Path(__file__).resolve().parent
|
||||||
|
PROJECT_ROOT = BUILD_PIPELINE_DIR.parent
|
||||||
|
APP_DIR = PROJECT_ROOT / "0_application"
|
||||||
|
BUILD_OUTPUT_DIR = PROJECT_ROOT / "2_build"
|
||||||
|
CHANGELOG_PATH = APP_DIR / "CHANGELOG.md"
|
||||||
|
SPEC_PATH = BUILD_PIPELINE_DIR / "build.spec"
|
||||||
|
VERSION_FILE = BUILD_PIPELINE_DIR / ".last_version"
|
||||||
|
|
||||||
|
# ── Versions-Helfer ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _read_changelog() -> str:
|
||||||
|
"""Liest das gesamte CHANGELOG.md."""
|
||||||
|
if not CHANGELOG_PATH.exists():
|
||||||
|
return ""
|
||||||
|
return CHANGELOG_PATH.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _write_changelog(content: str) -> None:
|
||||||
|
"""Schreibt CHANGELOG.md."""
|
||||||
|
CHANGELOG_PATH.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_version() -> Optional[str]:
|
||||||
|
"""Ermittelt die aktuellste Version aus CHANGELOG.md.
|
||||||
|
|
||||||
|
Sucht nach dem ersten Vorkommen von '## [X.Y.Z]' oder '## [X.Y]'.
|
||||||
|
"""
|
||||||
|
content = _read_changelog()
|
||||||
|
if not content:
|
||||||
|
return None
|
||||||
|
match = re.search(r"##\s*\[(\d+\.\d+(?:\.\d+)?)\]", content)
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def bump_version(current: str, bump_type: str) -> str:
|
||||||
|
"""Erhöht eine Semantic-Version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current: Aktuelle Version (z.B. '1.1' oder '1.1.0')
|
||||||
|
bump_type: 'major', 'minor' oder 'patch'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Neue Version als String.
|
||||||
|
"""
|
||||||
|
parts = current.split(".")
|
||||||
|
# Normalisiere auf 3 Teile (MAJOR.MINOR.PATCH)
|
||||||
|
while len(parts) < 3:
|
||||||
|
parts.append("0")
|
||||||
|
major, minor, patch = map(int, parts[:3])
|
||||||
|
|
||||||
|
if bump_type == "major":
|
||||||
|
major += 1
|
||||||
|
minor = 0
|
||||||
|
patch = 0
|
||||||
|
elif bump_type == "minor":
|
||||||
|
minor += 1
|
||||||
|
patch = 0
|
||||||
|
elif bump_type == "patch":
|
||||||
|
patch += 1
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unbekannter Bump-Typ: {bump_type}")
|
||||||
|
|
||||||
|
# Gib kurzes Format zurück wenn Patch=0
|
||||||
|
if patch == 0:
|
||||||
|
return f"{major}.{minor}"
|
||||||
|
return f"{major}.{minor}.{patch}"
|
||||||
|
|
||||||
|
|
||||||
|
def update_changelog(
|
||||||
|
new_version: str,
|
||||||
|
bump_type: str,
|
||||||
|
message: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Fügt einen neuen Versionseintrag am Anfang des Changelogs ein.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_version: Die neue Version (z.B. '1.2')
|
||||||
|
bump_type: Art des Bumps (für die Sektions-Vorlage)
|
||||||
|
message: Optionale Beschreibung der Änderungen.
|
||||||
|
"""
|
||||||
|
today = date.today().isoformat()
|
||||||
|
old_content = _read_changelog()
|
||||||
|
|
||||||
|
# Sektionen-Vorlage je nach Bump-Typ
|
||||||
|
sections = _build_sections(bump_type, message)
|
||||||
|
|
||||||
|
header = (
|
||||||
|
f"## [{new_version}] – {today}\n\n"
|
||||||
|
f"{sections}\n"
|
||||||
|
f"---\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Nach dem ersten '---' einfügen (nach dem Dateikopf von Keep a Changelog)
|
||||||
|
# oder einfach am Anfang nach der ersten Zeile
|
||||||
|
lines = old_content.splitlines(keepends=True)
|
||||||
|
|
||||||
|
# Finde die erste Version-Header-Zeile (## [X.Y...)
|
||||||
|
insert_idx = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if re.match(r"##\s*\[\d+\.\d+", line):
|
||||||
|
insert_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if insert_idx is not None:
|
||||||
|
new_content = "".join(lines[:insert_idx]) + header + "".join(lines[insert_idx:])
|
||||||
|
else:
|
||||||
|
# Fallback: nach dem Dateikopf (nach dem ersten '---')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip() == "---":
|
||||||
|
insert_idx = i + 1
|
||||||
|
break
|
||||||
|
if insert_idx is not None:
|
||||||
|
new_content = "".join(lines[:insert_idx]) + "\n" + header + "".join(lines[insert_idx:])
|
||||||
|
else:
|
||||||
|
new_content = old_content + "\n" + header
|
||||||
|
|
||||||
|
_write_changelog(new_content)
|
||||||
|
print(f" ✓ CHANGELOG.md aktualisiert → [{new_version}]")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_sections(bump_type: str, message: Optional[str]) -> str:
|
||||||
|
"""Baut die Sektionen für den Changelog-Eintrag."""
|
||||||
|
if message:
|
||||||
|
if bump_type == "major":
|
||||||
|
return f"### Hinzugefügt\n\n- {message}\n"
|
||||||
|
elif bump_type == "minor":
|
||||||
|
return f"### Hinzugefügt\n\n- {message}\n"
|
||||||
|
else:
|
||||||
|
return f"### Behoben\n\n- {message}\n"
|
||||||
|
|
||||||
|
# Standard-Vorlagen
|
||||||
|
if bump_type == "major":
|
||||||
|
return (
|
||||||
|
"### Hinzugefügt\n\n"
|
||||||
|
"- \n\n"
|
||||||
|
"### Geändert\n\n"
|
||||||
|
"- \n\n"
|
||||||
|
"### Entfernt\n\n"
|
||||||
|
"- \n"
|
||||||
|
)
|
||||||
|
elif bump_type == "minor":
|
||||||
|
return (
|
||||||
|
"### Hinzugefügt\n\n"
|
||||||
|
"- \n\n"
|
||||||
|
"### Geändert\n\n"
|
||||||
|
"- \n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
"### Behoben\n\n"
|
||||||
|
"- \n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Build ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _ensure_pyinstaller() -> None:
|
||||||
|
"""Stellt sicher, dass PyInstaller installiert ist."""
|
||||||
|
try:
|
||||||
|
import PyInstaller # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
print("⚙ PyInstaller wird installiert …")
|
||||||
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "pyinstaller"])
|
||||||
|
print(" ✓ PyInstaller installiert")
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_build_dir() -> None:
|
||||||
|
"""Bereitet das 2_build/-Verzeichnis vor."""
|
||||||
|
BUILD_OUTPUT_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Erstelle/aktualisiere Unterordner
|
||||||
|
for folder in ["input", "output", "template", "config"]:
|
||||||
|
target = BUILD_OUTPUT_DIR / folder
|
||||||
|
target.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Kopiere config-Dateien, falls noch nicht vorhanden
|
||||||
|
config_src = APP_DIR / "config"
|
||||||
|
config_dst = BUILD_OUTPUT_DIR / "config"
|
||||||
|
if config_src.exists():
|
||||||
|
for f in config_src.iterdir():
|
||||||
|
if f.is_file() and not (config_dst / f.name).exists():
|
||||||
|
shutil.copy2(f, config_dst / f.name)
|
||||||
|
|
||||||
|
# README für input-Ordner
|
||||||
|
input_readme = BUILD_OUTPUT_DIR / "input" / "_DATEIEN_HIER_ABLEGEN_.txt"
|
||||||
|
if not input_readme.exists():
|
||||||
|
input_readme.write_text(
|
||||||
|
"Excel-Dateien (.xlsx) in diesen Ordner legen.\n"
|
||||||
|
"Die App erkennt sie automatisch.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
# README für template-Ordner
|
||||||
|
tpl_readme = BUILD_OUTPUT_DIR / "template" / "_DATEIEN_HIER_ABLEGEN_.txt"
|
||||||
|
if not tpl_readme.exists():
|
||||||
|
tpl_readme.write_text(
|
||||||
|
"PDF-Templates (.pdf) mit _Field-Platzhaltern in diesen Ordner legen.\n"
|
||||||
|
"Die App erkennt sie automatisch.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" ✓ 2_build/ vorbereitet")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_pyinstaller() -> Path:
|
||||||
|
"""Führt PyInstaller mit der Spec-Datei aus.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pfad zur erstellten .exe.
|
||||||
|
"""
|
||||||
|
print("\n🔨 PyInstaller baut .exe …")
|
||||||
|
print(" (Dies kann einige Minuten dauern)\n")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable, "-m", "PyInstaller",
|
||||||
|
str(SPEC_PATH),
|
||||||
|
"--distpath", str(BUILD_PIPELINE_DIR / "dist"),
|
||||||
|
"--workpath", str(BUILD_PIPELINE_DIR / "build_temp"),
|
||||||
|
"--noconfirm",
|
||||||
|
"--clean",
|
||||||
|
],
|
||||||
|
cwd=str(PROJECT_ROOT),
|
||||||
|
capture_output=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("\n❌ PyInstaller-Build fehlgeschlagen!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
exe_path = BUILD_PIPELINE_DIR / "dist" / "Tätigkeitsnachweis-Generator.exe"
|
||||||
|
if not exe_path.exists():
|
||||||
|
print(f"\n❌ .exe nicht gefunden: {exe_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f" ✓ .exe erstellt: {exe_path.name}")
|
||||||
|
return exe_path
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_to_build(exe_path: Path) -> Path:
|
||||||
|
"""Kopiert die .exe nach 2_build/."""
|
||||||
|
dest = BUILD_OUTPUT_DIR / exe_path.name
|
||||||
|
shutil.copy2(exe_path, dest)
|
||||||
|
print(f" ✓ .exe nach 2_build/ kopiert")
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup() -> None:
|
||||||
|
"""Räumt temporäre Build-Artefakte auf."""
|
||||||
|
for d in ["dist", "build_temp"]:
|
||||||
|
path = BUILD_PIPELINE_DIR / d
|
||||||
|
if path.exists():
|
||||||
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
|
print(" ✓ Temporäre Dateien entfernt")
|
||||||
|
|
||||||
|
|
||||||
|
def _save_version(version: str) -> None:
|
||||||
|
"""Speichert die Version für spätere Referenz."""
|
||||||
|
VERSION_FILE.write_text(version)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_summary(version: str, exe_path: Path) -> None:
|
||||||
|
"""Gibt eine Build-Zusammenfassung aus."""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" ✅ Build {version} erfolgreich!")
|
||||||
|
print(f" 📂 {exe_path}")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
print("Auslieferungs-Struktur (2_build/):")
|
||||||
|
print(" ├── Tätigkeitsnachweis-Generator.exe")
|
||||||
|
print(" ├── config/ ← Konfiguration (editierbar)")
|
||||||
|
print(" ├── input/ ← Excel-Dateien hier ablegen")
|
||||||
|
print(" ├── template/ ← PDF-Templates hier ablegen")
|
||||||
|
print(" └── output/ ← Generierte PDFs erscheinen hier")
|
||||||
|
|
||||||
|
|
||||||
|
# ── CLI ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Build mit Semantic Versioning",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--bump",
|
||||||
|
choices=["major", "minor", "patch"],
|
||||||
|
help="Version bump-Typ (ohne Angabe: interaktiv)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--message", "-m",
|
||||||
|
help="Changelog-Beschreibung für diese Version",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Nur anzeigen, was passieren würde (kein Build)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# ── Version ermitteln ────────────────────────────────────
|
||||||
|
current = get_current_version()
|
||||||
|
if not current:
|
||||||
|
print("❌ Keine Version in CHANGELOG.md gefunden. Beginne mit 0.1.")
|
||||||
|
current = "0.1"
|
||||||
|
|
||||||
|
print(f"\n📋 Aktuelle Version: {current}")
|
||||||
|
|
||||||
|
# Bump-Typ bestimmen
|
||||||
|
bump_type = args.bump
|
||||||
|
if not bump_type:
|
||||||
|
print("\nWelche Art von Änderung?")
|
||||||
|
print(" [1] major – inkompatible API-Änderungen")
|
||||||
|
print(" [2] minor – neue Funktionalität (abwärtskompatibel)")
|
||||||
|
print(" [3] patch – Bugfixes")
|
||||||
|
try:
|
||||||
|
choice = input(" → ").strip()
|
||||||
|
bump_type = {"1": "major", "2": "minor", "3": "patch"}.get(choice, "minor")
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nAbgebrochen.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
new_version = bump_version(current, bump_type)
|
||||||
|
print(f"🚀 Neue Version: {new_version}")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("\n[Trockenlauf – kein Build]")
|
||||||
|
print(f" Würde CHANGELOG.md aktualisieren: [{new_version}]")
|
||||||
|
print(f" Würde PyInstaller-Build starten")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── CHANGELOG aktualisieren ──────────────────────────────
|
||||||
|
print("\n📝 Changelog …")
|
||||||
|
update_changelog(new_version, bump_type, args.message)
|
||||||
|
|
||||||
|
# ── Build ────────────────────────────────────────────────
|
||||||
|
_ensure_pyinstaller()
|
||||||
|
_prepare_build_dir()
|
||||||
|
|
||||||
|
try:
|
||||||
|
exe_path = _run_pyinstaller()
|
||||||
|
final_exe = _copy_to_build(exe_path)
|
||||||
|
finally:
|
||||||
|
_cleanup()
|
||||||
|
|
||||||
|
_save_version(new_version)
|
||||||
|
_print_summary(new_version, final_exe)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
"""PyInstaller Spec-Datei für den Tätigkeitsnachweis-Generator.
|
||||||
|
|
||||||
|
Baut eine standalone .exe aus run.py, die Streamlit, openpyxl und
|
||||||
|
PyMuPDF (fitz) enthält.
|
||||||
|
|
||||||
|
Verwendung:
|
||||||
|
pyinstaller build.spec
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Pfade relativ zum Speicherort dieser Spec-Datei
|
||||||
|
# PyInstaller stellt SPECPATH bereit (__file__ existiert in exec()-Kontext nicht)
|
||||||
|
_SPEC_DIR = Path(SPECPATH).resolve()
|
||||||
|
_PROJECT_ROOT = _SPEC_DIR.parent
|
||||||
|
_APP_DIR = _PROJECT_ROOT / "0_application"
|
||||||
|
|
||||||
|
# ── Streamlit & PyMuPDF Hidden Imports ──────────────────────────
|
||||||
|
# Streamlit hat viele dynamische Imports; collect_all holt alles.
|
||||||
|
# PyMuPDF (fitz) braucht die native DLL.
|
||||||
|
|
||||||
|
hiddenimports = [
|
||||||
|
# Streamlit-Komponenten
|
||||||
|
"streamlit",
|
||||||
|
"streamlit.web.bootstrap",
|
||||||
|
"streamlit.runtime",
|
||||||
|
"streamlit.runtime.scriptrunner",
|
||||||
|
"streamlit.runtime.state",
|
||||||
|
"streamlit.runtime.caching",
|
||||||
|
"streamlit.watcher",
|
||||||
|
"streamlit.watcher.local_sources_watcher",
|
||||||
|
"streamlit.watcher.poll_path_watcher",
|
||||||
|
"streamlit.watcher.event_based_path_watcher",
|
||||||
|
"streamlit.elements",
|
||||||
|
"streamlit.elements.arrow",
|
||||||
|
"streamlit.elements.pyplot",
|
||||||
|
"streamlit.elements.altair",
|
||||||
|
"streamlit.elements.plotly",
|
||||||
|
"streamlit.proto",
|
||||||
|
# openpyxl
|
||||||
|
"openpyxl",
|
||||||
|
"openpyxl.cell",
|
||||||
|
"openpyxl.styles",
|
||||||
|
# PyMuPDF
|
||||||
|
"fitz",
|
||||||
|
# Standard-Libs die manchmal übersehen werden
|
||||||
|
"tornado",
|
||||||
|
"watchdog",
|
||||||
|
"pandas",
|
||||||
|
"numpy",
|
||||||
|
"PIL",
|
||||||
|
"altair",
|
||||||
|
"pyarrow",
|
||||||
|
"git",
|
||||||
|
"blinker",
|
||||||
|
"rich",
|
||||||
|
"pydeck",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── Binaries (PyMuPDF DLL) ──────────────────────────────────────
|
||||||
|
binaries = []
|
||||||
|
|
||||||
|
# Finde die PyMuPDF-DLL
|
||||||
|
try:
|
||||||
|
import fitz
|
||||||
|
fitz_dir = Path(fitz.__file__).parent
|
||||||
|
for f in fitz_dir.iterdir():
|
||||||
|
if f.suffix.lower() in (".dll", ".pyd", ".so", ".dylib"):
|
||||||
|
binaries.append((str(f), "fitz"))
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── Data Files ──────────────────────────────────────────────────
|
||||||
|
datas = [
|
||||||
|
(str(_APP_DIR / "app.py"), "."),
|
||||||
|
(str(_APP_DIR / "src"), "src"),
|
||||||
|
(str(_APP_DIR / "config"), "config"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── Alle Streamlit-Abhängigkeiten & Metadaten VOR Analysis() sammeln ─
|
||||||
|
from PyInstaller.utils.hooks import collect_all, copy_metadata
|
||||||
|
|
||||||
|
streamlit_datas, streamlit_binaries, streamlit_hidden = collect_all("streamlit")
|
||||||
|
datas.extend(streamlit_datas)
|
||||||
|
binaries.extend(streamlit_binaries)
|
||||||
|
hiddenimports.extend(streamlit_hidden)
|
||||||
|
|
||||||
|
# Package-Metadaten: streamlit & Co. nutzen importlib.metadata.version(),
|
||||||
|
# das die METADATA-Dateien aus dem dist-info-Ordner benötigt.
|
||||||
|
for pkg in ["streamlit", "pyarrow", "numpy", "pandas", "altair", "openpyxl"]:
|
||||||
|
try:
|
||||||
|
datas += copy_metadata(pkg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Deduplizieren (Datas und Hiddenimports)
|
||||||
|
hiddenimports = list(set(hiddenimports))
|
||||||
|
seen = set()
|
||||||
|
_unique_datas = []
|
||||||
|
for src, dst in datas:
|
||||||
|
key = (src, dst)
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
_unique_datas.append((src, dst))
|
||||||
|
datas = _unique_datas
|
||||||
|
|
||||||
|
# ── Analyse ─────────────────────────────────────────────────────
|
||||||
|
a = Analysis(
|
||||||
|
[str(_SPEC_DIR / "run.py")],
|
||||||
|
pathex=[str(_APP_DIR)],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[
|
||||||
|
"tkinter",
|
||||||
|
"matplotlib",
|
||||||
|
"scipy",
|
||||||
|
"IPython",
|
||||||
|
"jupyter",
|
||||||
|
"notebook",
|
||||||
|
"sqlalchemy",
|
||||||
|
],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=None,
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── PyZ ─────────────────────────────────────────────────────────
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
|
||||||
|
|
||||||
|
# ── EXE ─────────────────────────────────────────────────────────
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name="Tätigkeitsnachweis-Generator",
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True, # True = Konsolenfenster sichtbar (zeigt Streamlit-Logs)
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""Einstiegspunkt für die gebundelte Tätigkeitsnachweis-Generator .exe.
|
||||||
|
|
||||||
|
Wird von PyInstaller als Entry-Point verwendet. Im gebundelten Modus
|
||||||
|
(sys.frozen = True) liegen die Python-Dateien (app.py, src/) in sys._MEIPASS,
|
||||||
|
während die Daten-Ordner (config/, input/, template/, output/) neben der .exe
|
||||||
|
erwartet werden.
|
||||||
|
|
||||||
|
Dieses Skript:
|
||||||
|
1. Setzt APP_BASE_DIR auf das Verzeichnis der .exe
|
||||||
|
2. Fügt sys._MEIPASS zum Python-Pfad hinzu (für src/-Imports)
|
||||||
|
3. Startet den Streamlit-Server und öffnet den Browser
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import webbrowser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _get_base_dir() -> Path:
|
||||||
|
"""Verzeichnis der .exe (bzw. des Skripts im Entwicklungsmodus)."""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
return Path(sys.executable).resolve().parent
|
||||||
|
else:
|
||||||
|
# Entwicklungsmodus: run.py liegt in 1_build-pipeline/,
|
||||||
|
# die App in 0_application/
|
||||||
|
return Path(__file__).resolve().parent.parent / "0_application"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_app_path() -> str:
|
||||||
|
"""Pfad zur app.py (gebundelt oder im Entwicklungsmodus)."""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
return os.path.join(sys._MEIPASS, "app.py")
|
||||||
|
else:
|
||||||
|
return str(Path(__file__).resolve().parent.parent / "0_application" / "app.py")
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_dirs(base_dir: Path) -> None:
|
||||||
|
"""Stellt sicher, dass input/, output/, template/, config/ existieren."""
|
||||||
|
for folder in ["input", "output", "template", "config"]:
|
||||||
|
(base_dir / folder).mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
base_dir = _get_base_dir()
|
||||||
|
|
||||||
|
# Im gebundelten Modus: sys._MEIPASS enthält src/ und app.py
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
if sys._MEIPASS not in sys.path:
|
||||||
|
sys.path.insert(0, sys._MEIPASS)
|
||||||
|
|
||||||
|
# APP_BASE_DIR teilt app.py mit, wo die Daten-Ordner liegen
|
||||||
|
os.environ["APP_BASE_DIR"] = str(base_dir)
|
||||||
|
|
||||||
|
_ensure_dirs(base_dir)
|
||||||
|
|
||||||
|
# ── Streamlit-Konfiguration ──────────────────────────
|
||||||
|
os.environ["STREAMLIT_SERVER_ADDRESS"] = "localhost"
|
||||||
|
os.environ["STREAMLIT_SERVER_PORT"] = "8501"
|
||||||
|
os.environ["STREAMLIT_SERVER_HEADLESS"] = "true"
|
||||||
|
os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
|
||||||
|
os.environ["STREAMLIT_SERVER_ENABLE_CORS"] = "false"
|
||||||
|
os.environ["STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION"] = "false"
|
||||||
|
|
||||||
|
# Browser manuell öffnen
|
||||||
|
def _open_browser() -> None:
|
||||||
|
time.sleep(2)
|
||||||
|
webbrowser.open("http://localhost:8501")
|
||||||
|
|
||||||
|
threading.Thread(target=_open_browser, daemon=True).start()
|
||||||
|
|
||||||
|
# ── Streamlit via CLI-Codepfad starten ───────────────
|
||||||
|
# Dieser Weg ist identisch zu 'streamlit run app.py' und
|
||||||
|
# funktioniert im PyInstaller-Bundle zuverlässiger als
|
||||||
|
# bootstrap.run() mit Streamlit >= 1.58.
|
||||||
|
sys.argv = [
|
||||||
|
"streamlit", "run", _get_app_path(),
|
||||||
|
"--server.port=8501",
|
||||||
|
"--server.address=localhost",
|
||||||
|
"--server.headless=true",
|
||||||
|
"--browser.gatherUsageStats=false",
|
||||||
|
"--server.enableCORS=false",
|
||||||
|
"--server.enableXsrfProtection=false",
|
||||||
|
"--global.developmentMode=false",
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
from streamlit.cli import main as cli_main
|
||||||
|
except ImportError:
|
||||||
|
from streamlit.web.cli import main as cli_main
|
||||||
|
cli_main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user