Files

425 lines
36 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import os
import subprocess
import sys
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
REPO_PATH = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
GITEA_BASE = os.environ.get("GITEA_BASE", "https://gitea.schaefinik.xyz")
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "").strip()
FILES = {
"README.md": """# bitcoin-programm
Lokales Projekt zur strukturierten Erfassung und Verwaltung von:
- To-dos
- Kalendereinträgen
- Dokumentation
- später: Bitcoin-Transaktionen und Auswertungen
## Ziel
Das Projekt dient als zentrales Arbeitsverzeichnis für persönliche Verwaltung und spätere Automatisierung.
Geplant ist:
- eine einfache Eingabemaske für To-dos und Kalendertermine
- CSV als einfache Zwischen- und Exportform
- später ein SQLite-Backend für Auswertung und Dashboard
- optional Sync mit Google Calendar oder iCloud Kalender
## Aktueller Fokus
Der aktuelle Fokus liegt auf:
1. Dokumentation
2. To-do-Erfassung
3. Kalender-Erfassung
4. saubere Datenstruktur für späteren Import nach SQLite
Bitcoin-Tracking kann später ergänzt werden.
""",
"docs/00_overview.md": """# Überblick
Dieses Projekt ist ein lokales Arbeits- und Dokumentationsprojekt für persönliche Organisation.
## Zweck
Das Projekt soll folgende Bereiche abdecken:
- To-dos
- Kalendertermine
- Projektdokumentation
- später Bitcoin-Tracking
- später SQLite-basierte Auswertung
## Leitprinzipien
1. Daten sauber strukturieren
2. Eingaben einfach halten
3. CSV als Zwischenformat nutzbar machen
4. später SQL-Backend ergänzen
5. lokale Kontrolle behalten
## Arbeitsweise
### 1. Erfassung
Neue Einträge werden über eine Eingabemaske erfasst.
### 2. Speicherung
Die Daten werden in CSV abgelegt und später in SQLite überführt.
## Zielbild
Am Ende soll das Projekt enthalten:
- eine einfache Eingabemaske
- eine saubere Dokumentation
- exportierbare Daten
- ein lokales Dashboard
- Kalenderanbindung
- To-do-Verwaltung
- später Bitcoin-Verwaltung
## Status
Der Fokus liegt aktuell auf Dokumentation, Kalender und To-do. Bitcoin-Tracking wird später ergänzt.
""",
"docs/01_requirements.md": """# Anforderungen
## Hauptziel
Das Projekt soll eine einfache und verlässliche Verwaltung von To-dos und Kalenderterminen ermöglichen.
## Funktionale Anforderungen
### To-dos
- To-dos sollen über eine Eingabemaske erfasst werden
- Pflichtfelder müssen vorhanden sein
- feste Werte sollen per Dropdown gewählt werden
- die Daten sollen als CSV exportierbar sein
### Kalender
- Kalendertermine sollen über eine Eingabemaske erfasst werden
- Datum und Uhrzeit müssen wählbar sein
- Terminarten sollen per Dropdown auswählbar sein
- Wiederholungen sollen unterstützt werden
- Erinnerungen sollen vorbereitet sein
### Datenhaltung
- CSV als Zwischenspeicher
- später SQLite als Datenbasis
- klare Trennung von Rohdaten und verarbeiteten Daten
## Nicht-funktionale Anforderungen
- lokale Nutzung ohne Cloud-Zwang
- später synchronisierbar mit Google Calendar oder iCloud
- leicht verständlich und wartbar
- importfreundlich für SQLite
- dokumentierbar und versionierbar
## UI-Anforderungen
Die Eingabemaske soll enthalten:
- Pflichtfelder
- Dropdowns
- Datumsauswahl
- Uhrzeitauswahl
- Textfelder für Notizen
- klare Trennung zwischen To-do und Kalender
## Datenqualität
- einheitliche Schreibweisen
- feste Statuswerte
- saubere Datumsformate
- keine freien Sonderformate in Pflichtfeldern
- keine Mischfelder für unterschiedliche Datentypen
""",
"docs/02_todo_calendar.md": """# To-do- und Kalenderkonzept
## Ziel
Dieses Projekt soll eine einfache Möglichkeit bieten, Aufgaben und Termine zentral zu erfassen.
## To-do-Felder
- id
- titel
- beschreibung
- kategorie
- prioritaet
- status
## Kalender-Felder
• später synchronisierbar mit Google Calendar oder iCloud
• leicht verständlich und wartbar
• importfreundlich für SQLite
• dokumentierbar und versionierbar
UI-Anforderungen
Die Eingabemaske soll enthalten:
• Pflichtfelder
• Dropdowns
• Datumsauswahl
• Uhrzeitauswahl
• Textfelder für Notizen
• klare Trennung zwischen To-do und Kalender
Datenqualität
• einheitliche Schreibweisen
• feste Statuswerte
• saubere Datumsformate
• keine freien Sonderformate in Pflichtfeldern
• keine Mischfelder für unterschiedliche Datentypen
""",
"docs/02_todo_calendar.md": """# To-do- und Kalenderkonzept
Ziel
Dieses Projekt soll eine einfache Möglichkeit bieten, Aufgaben und Termine zentral zu erfassen.
To-do-Felder
• id
• titel
• beschreibung
• kategorie
• prioritaet
• status
• faellig_am
• wiederholen
• notiz
Kalender-Felder
• id
• titel
• datum
• uhrzeit
• dauer_min
• kalender
• kategorie
• erinnerung_min
• wiederholen
• notiz
Dropdown-Werte
Priorität
• Niedrig
• Mittel
• Hoch
Status
• Offen
• In Arbeit
Kategorie
• Pflichtfelder müssen ausgefüllt sein
2. Daten als CSV speichern
beschreibung TEXT,
faellig_am TEXT,
id INTEGER PRIMARY KEY AUTOINCREMENT, (
"requirements.txt": """streamlit
return 1
ig(page_title="Bitcoin-Programm", layout="wide")
st.set_page_config(page_title="Bitcoin-Programm", layout="wide") nt(kalender);
faellig_am = st.date_input("Fällig am", value=date.today())
"id": new_todo_id,
"faellig_am": faellig_am.isoformat(),
st.markdown("### To-dos")
st.download_button("Kalender CSV herunterladen", data=cal_df.to_csv(index=False).encode("utf-8"),
tail = tail.split("/", 1)[1]
tail = remote_url.split(":", 1)[1]
prioritaetPTEXTeNOTtNULL DEFAULT 'Mittel', ("utf-8"),
status TEXT NOT NULL DEFAULTt'Offen',und: {REPO_PATH}")
faellig_am TEXT,
wiederholen TEXT NOT NULL DEFAULT 'Nein', {REPO_PATH}")
notiz TEXT,
prcreated_atpTEXT"NOTENULLADEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULTs(datetime('now'))out.strip()) de("utf-8"),
); "(clean)"))
CREATE TABLE IF NOT EXISTS calendar_event (
id INTEGER PRIMARY KEY AUTOINCREMENT,
retitelnTEXT NOToNULL,l = get_fetch_remote()
datum TEXT NOT NULL, te_url}") utput=capture)
uhrzeit TEXT NOT NULL,
dauer_min INTEGER NOT NULL DEFAULT}60,
kalender TEXT NOT NULL DEFAULT 'Privat',
fokategorieoTEXTtNOT NULLSDEFAULT)'Sonstiges',
erinnerung_min INTEGER NOT NULL DEFAULT 30,
wiederholen TEXT NOT NULL DEFAULT 'Nein',
notiz TEXT,
pycreated_ateTEXTuNOTsNULLxDEFAULTe(datetime('now')),e", "app.py"], cwd=REPO_PATH, text=True,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
); != 0:
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL"app.py syntax check failed")
);
CREATE"INDEX IFENOTEEXISTS"idx_todo_faellig_ameONitodo(faellig_am);p.py")
CREATE INDEX IF NOT EXISTS idx_todo_status ON todo(status);
CREATE INDEX IF NOT EXISTS idx_calendar_event_datum ON calendar_event(datum);
CREATE INDEX IF NOT EXISTS idx_calendar_event_kalender ON calendar_event(kalender);
""", it("commit", "-m", "docs: add todo-calendar workflow and streamlit form")
"requirements.txt": """streamlit
pandas
"""ranch = git("branch", "--show-current").stdout.strip()
}
APP_PY = """import streamlit as st, remote_name)
import pandas as pd
iffromapathlib importnPath
from datetime import date, time
─ ◉ DATA_DIR = Path("data") e
TODO_FILE = DATA_DIR / "todo.csv"
CALENDAR_FILEz=zDATA_DIRe/a"calendar.csv"
DATA_DIR.modir(parents=True, exist_ok=True)rn dein benannter Remote `bitcoin-programm`
TODO_COLUMNS = [ t a d t e
─ ( "id","titel","beschreibung","kategorie","prioritaet","status", e
─ "faellig_am","wiederholen","notiz" e
]-
CALENDAR_COLUMNSe=u[g / Kontext-Zusammenfassung
P "id","titel","datum","uhrzeit","dauer_min","kalender","kategorie",
─ ٩ "erinnerung_min","wiederholen","notiz"o-dos, Kalender und später Bitcoin-Tracking {"title
─ ( ] e : T o e b s L ext=True, {"title
bit def load_csv(pathe Path, columns: list[str]) -> pd.DataFrame: ` chaefinik.xyz/admin/bitcoin`
─ ( if path.exists(): e
**Sicherdfi=spd.read_csv(path)cht im Chat, nur via `GITEA_TOKEN` in der Host-Umgebung
─ ( for col in columns: e r u s i r d r pt statt direkter e
if col not in df.columns:
ridf[col]c=i"" lokal ausführen, dann `git status`/Commit/Push prüfen
return df[columns] n o n k r Z nfassung für
─ ( • return pd.DatmFrame(columns=columns) e E e {"title
def save_csv(df: p .DataFram , path: Path) -> None:
df.to_csv(path, index=False)
─ (´・def)next_id(df: pd.DataFrame)p->5int:ini none │ 94k/272k │ [████░░░░░░] 35% │ 2h 20m ─ {"title
if df.empty or "id" not in df.columns:
return 1
try:
return int(df["id"].max()) + 1
except Exception:
return 1
st.set_page_config(page_title="Bitcoin-Programm", layout="wide")
st.title("Bitcoin-Programm – To-do & Kalender")
tab1, tab2, tab3 = st.tabs(["To-do anlegen", "Kalendertermin anlegen", "Daten ansehen"])
with tab1:
st.subheader("Neues To-do")
todo_df = load_csv(TODO_FILE, TODO_COLUMNS)
new_todo_id = next_id(todo_df)
with st.form("todo_form", clear_on_submit=True):
titel = st.text_input("Titel")
beschreibung = st.text_area("Beschreibung")
kategorie = st.selectbox("Kategorie", ["Allgemein", "Bitcoin", "Dokumentation", "Kalender", "Admin",
"Sonstiges"])
prioritaet = st.selectbox("Priorität", ["Niedrig", "Mittel", "Hoch"])
status = st.selectbox("Status", ["Offen", "In Arbeit", "Wartet", "Erledigt"])
faellig_am = st.date_input("Fällig am", value=date.today())
wiederholen = st.selectbox("Wiederholen", ["Nein", "Täglich", "Wöchentlich", "Monatlich"])
notiz = st.text_area("Notiz")
submitted = st.form_submit_button("To-do speichern")
if submitted:
if not titel.strip():
st.error("Bitte mindestens einen Titel eingeben.")
else:
new_row = pd.DataFrame([{
"id": new_todo_id,
"titel": titel,
"beschreibung": beschreibung,
"kategorie": kategorie,
"prioritaet": prioritaet,
"status": status,
"faellig_am": faellig_am.isoformat(),
"wiederholen": wiederholen,
"notiz": notiz,
}])
todo_df = pd.concat([todo_df, new_row], ignore_index=True)
save_csv(todo_df, TODO_FILE)
st.success("To-do gespeichert.")
with tab2:
st.subheader("Neuen Kalendereintrag")
cal_df = load_csv(CALENDAR_FILE, CALENDAR_COLUMNS)
new_cal_id = next_id(cal_df)
with st.form("calendar_form", clear_on_submit=True):
titel = st.text_input("Titel")
datum = st.date_input("Datum", value=date.today())
uhrzeit = st.time_input("Uhrzeit", value=time(9, 0))
dauer_min = st.number_input("Dauer in Minuten", min_value=5, max_value=1440, value=60, step=5)
kalender = st.selectbox("Kalender", ["Arbeit", "Privat", "Bitcoin", "Todo"])
kategorie = st.selectbox("Kategorie", ["Meeting", "Aufgabe", "Erinnerung", "Termin", "Sonstiges"])
erinnerung_min = st.number_input("Erinnerung vorab in Minuten", min_value=0, max_value=10080,
value=30, step=5)
wiederholen = st.selectbox("Wiederholen", ["Nein", "Täglich", "Wöchentlich", "Monatlich"])
notiz = st.text_area("Notiz")
submitted = st.form_submit_button("Kalendertermin speichern")
if submitted:
if not titel.strip():
st.error("Bitte mindestens einen Titel eingeben.")
else:
new_row = pd.DataFrame([{
"id": new_cal_id,
"titel": titel,
"datum": datum.isoformat(),
"uhrzeit": uhrzeit.strftime("%H:%M"),
"dauer_min": int(dauer_min),
"kalender": kalender,
"kategorie": kategorie,
"erinnerung_min": int(erinnerung_min),
"wiederholen": wiederholen,
"notiz": notiz,
}])
cal_df = pd.concat([cal_df, new_row], ignore_index=True)
save_csv(cal_df, CALENDAR_FILE)
st.success("Kalendertermin gespeichert.")
with tab3:
st.subheader("Gespeicherte Daten")
col1, col2 = st.columns(2)
with col1:
st.markdown("### To-dos")
todo_df = load_csv(TODO_FILE, TODO_COLUMNS)
st.dataframe(todo_df, use_container_width=True)
st.download_button("To-do CSV herunterladen", data=todo_df.to_csv(index=False).encode("utf-8"),
file_name="todo.csv", mime="text/csv")
with col2:
st.markdown("### Kalender")
cal_df = load_csv(CALENDAR_FILE, CALENDAR_COLUMNS)
st.dataframe(cal_df, use_container_width=True)
st.download_button("Kalender CSV herunterladen", data=cal_df.to_csv(index=False).encode("utf-8"),
file_name="calendar.csv", mime="text/csv")
"""
def run(cmd, check=True, capture=True):
return subprocess.run(cmd, cwd=REPO_PATH, check=check, text=True, capture_output=capture)
def git(*args, check=True):
return run(["git", *args], check=check)
def write_file(path, content):
full = REPO_PATH / path
full.parent.mkdir(parents=True, exist_ok=True)
full.write_text(content, encoding="utf-8")
print(f"OK wrote {path}")
def get_fetch_remote():
remotes = git("remote", "-v").stdout.strip().splitlines()
if not remotes:
raise SystemExit("No git remotes configured")
for line in remotes:
parts = line.split()
if len(parts) >= 3 and parts[2] == "(fetch)":
return parts[0], parts[1]
parts = remotes[0].split()
return parts[0], parts[1]
def get_repo_owner_name():
_, remote_url = get_fetch_remote()
if remote_url.endswith(".git"):
remote_url = remote_url[:-4]
if "://" in remote_url:
tail = remote_url.split("://", 1)[1]
tail = tail.split("/", 1)[1]
else:
tail = remote_url.split(":", 1)[1]
owner, repo = tail.split("/", 1)
return owner, repo
def main():
if not REPO_PATH.exists():
raise SystemExit(f"Repo path not found: {REPO_PATH}")
if not (REPO_PATH / ".git").exists():
raise SystemExit(f"No .git directory in {REPO_PATH}")
print("Repo path:", REPO_PATH)
print("Branch:", git("branch", "--show-current").stdout.strip())
print("Status:\n" + (git("status", "--short").stdout.strip() or "(clean)"))
print("Remotes:\n" + git("remote", "-v").stdout.strip())
remote_name, remote_url = get_fetch_remote()
print(f"Using remote: {remote_name} -> {remote_url}")
owner, repo = get_repo_owner_name()
print(f"Detected repo: {owner}/{repo}")
for path, content in FILES.items():
write_file(path, content)
write_file("app.py", APP_PY)
py = subprocess.run([sys.executable, "-m", "py_compile", "app.py"], cwd=REPO_PATH, text=True,
capture_output=True)
if py.returncode != 0:
print(py.stdout)
print(py.stderr)
raise SystemExit("app.py syntax check failed")
git("add", "README.md", "docs", "sqlite", "requirements.txt", "app.py")
if git("diff", "--cached", "--quiet", check=False).returncode == 0:
print("No changes to commit.")
else:
git("commit", "-m", "docs: add todo-calendar workflow and streamlit form")
print("Committed.")
branch = git("branch", "--show-current").stdout.strip()
git("push", remote_name, branch)
print("Pushed to origin remote:", remote_name)
if __name__ == "__main__":
main()