Python-Kurs / Einen SQLite-Ausgabenmanager mit CLI und Integrationstest bauen
Das Abschlussprojekt erfasst Datum, Kategorie und Betrag einer Ausgabe, speichert sie in SQLite und zeigt Datensätze oder Kategoriesummen an. Das vollständige Kommandozeilenprogramm verbindet Eingabeprüfung, Initialisierung, Transaktionen und Abfragen mit einem eigenen Integrationstest.
Umfang und Datenregeln festlegen
Die Beträge sind nichtnegative ganze Won-Beträge in KRW und reichen von null bis 1.000.000.000.000 einschließlich. Das Datum muss einen tatsächlich vorhandenen Kalendertag in der Form YYYY-MM-DD angeben. Eine Kategorie darf nach Entfernen des äußeren Leerraums nicht leer sein. Zahlungsausführung, Bankanbindung und Anmeldung mehrerer Benutzer sind keine Bestandteile dieser Übung.
Wichtig ist die Bedeutung einer Wiederholung: Jeder erfolgreiche add-Aufruf fügt genau eine neue Ausgabe ein. Ein identischer erneuter Aufruf erzeugt eine weitere Zeile. Im Gegensatz zum vorherigen API-Upsert gibt es hier keine externe Kennung, anhand derer ein wiederholter Auftrag als derselbe Vorgang erkannt würde.
Verwenden Sie Python 3.12 oder neuer, die Standardbibliothek und einen eigenen neuen Übungsordner. ledger.db liegt standardmäßig im aktuellen Arbeitsordner. Mit --db können Sie einen anderen bewusst gewählten Übungspfad angeben; richten Sie das Programm nicht auf fremde oder geschäftliche Datenbanken. Elternordner eines neuen Datenbankpfads müssen bereits existieren.
Die Grundlagen bieten der Kurs zu SQLite und Parameterabfragen sowie die Python-Referenz zu sqlite3.
Das vollständige Programm speichern
Speichern Sie die Datei ledger.py als UTF-8:
import argparse
import sqlite3
from datetime import date
from pathlib import Path
MAX_AMOUNT = 1_000_000_000_000
def amount_value(text):
try:
value = int(text)
except ValueError as error:
raise argparse.ArgumentTypeError("expected an integer") from error
if not 0 <= value <= MAX_AMOUNT:
raise argparse.ArgumentTypeError("amount must be between 0 and 1000000000000")
return value
def date_value(text):
try:
parsed = date.fromisoformat(text)
except ValueError as error:
raise argparse.ArgumentTypeError("invalid date") from error
if parsed.isoformat() != text:
raise argparse.ArgumentTypeError("use YYYY-MM-DD")
return text
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--db", type=Path, default=Path("ledger.db"))
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("init")
add = commands.add_parser("add")
add.add_argument("day", type=date_value)
add.add_argument("category")
add.add_argument("amount", type=amount_value)
commands.add_parser("list")
commands.add_parser("summary")
args = parser.parse_args()
if args.command != "init" and not args.db.is_file():
parser.error("database not initialized")
if args.command == "add" and not args.category.strip():
parser.error("category is required")
con = sqlite3.connect(args.db, autocommit=False)
try:
if args.command == "init":
with con:
con.execute("CREATE TABLE IF NOT EXISTS expenses (id INTEGER PRIMARY KEY, day TEXT NOT NULL, category TEXT NOT NULL CHECK(length(trim(category))>0), amount INTEGER NOT NULL CHECK(amount BETWEEN 0 AND 1000000000000))")
print("initialized")
elif args.command == "add":
with con:
con.execute("INSERT INTO expenses(day,category,amount) VALUES(?,?,?)", (args.day, args.category.strip(), args.amount))
print("added")
elif args.command == "list":
for row in con.execute("SELECT id,day,category,amount FROM expenses ORDER BY day,id"):
print(*row)
else:
totals = {}
for category, amount in con.execute("SELECT category,amount FROM expenses"):
totals[category] = totals.get(category, 0) + amount
for category, total in sorted(totals.items()):
print(category, total)
finally:
con.close()
if __name__ == "__main__":
main()
amount_value wandelt den Argumenttext in eine Ganzzahl um und prüft den Betrag. date_value liest das Datum und vergleicht anschließend die kanonische ISO-Ausgabe mit der Eingabe. So werden auch andere von fromisoformat möglicherweise akzeptierte Schreibweisen nicht unbemerkt zu erlaubten Eingabeformaten dieser Anwendung. Die Datumsreferenz erläutert den Parser.
main definiert die vier Unterbefehle und steuert die Verbindungsdauer. Außer init verlangt jeder Befehl eine bereits vorhandene Datei. Dieser Dateitest bestätigt allerdings noch kein passendes Datenbankschema: Eine beliebige vorhandene Datei ist dadurch nicht automatisch eine initialisierte Ausgabendatenbank. Nutzen Sie den vorgesehenen neuen Testbestand.
Initialisieren, Ausgaben hinzufügen und Summen abfragen
Führen Sie die folgenden Befehle nacheinander im neuen Übungsordner aus:
python ledger.py init
python ledger.py add 2026-10-17 food 3500
python ledger.py add 2026-10-17 food 8000
python ledger.py add 2026-10-17 transport 2500
python ledger.py list
python ledger.py summary
In einer gewählten Projektumgebung verwenden Sie python; außerhalb einer virtuellen Umgebung kann der Befehl unter macOS oder Ubuntu python3 heißen. init meldet initialized, jeder erfolgreiche Eingabebefehl meldet added. list zeigt in einer frischen Datenbank die IDs 1 bis 3 zusammen mit Datum, Kategorie und Betrag. Die Sortierung erfolgt nach Datum und danach ID.
Die letzte Summenabfrage muss Folgendes ausgeben:
food 11500
transport 2500
Die Kategorien werden für diese Ausgabe sortiert. Das Programm addiert gespeicherte Ganzzahlen in Python, statt ein SQLite-SUM mit begrenztem Ganzzahlbereich zu verwenden. Die Datensätze werden nacheinander gelesen; das Dictionary für die Summen wächst mit der Anzahl verschiedener Kategorien.
Für einen getrennten Bestand schreiben Sie beispielsweise python ledger.py --db other-ledger.db init. Die globale Option --db muss vor dem Unterbefehl stehen. Verwenden Sie denselben Pfad anschließend auch bei add, list und summary. Pfade oder Kategorien mit Leerzeichen müssen in der Shell als ein Argument in Anführungszeichen stehen. Die Argparse-Referenz zu Unterbefehlen beschreibt die Struktur.
Ein erneutes init löscht keine Daten und ändert keine bestehende Tabellendefinition. Wenn Sie wieder bei null beginnen möchten, wählen Sie einen neuen Pfad oder Ordner. Ein Wiederherstellen der ursprünglichen Python-Datei setzt bereits gespeicherte Ausgaben ebenfalls nicht zurück.
Fehler und dauerhafte Speicherung prüfen
- Geben Sie versuchsweise
2026-02-30ein. Die Datumsprüfung muss den Befehl vor einer Speicherung ablehnen. - Versuchen Sie einen negativen Betrag, einen Betrag oberhalb der Grenze und eine nur aus Leerzeichen bestehende Kategorie. Vergleichen Sie anschließend die Liste; es dürfen keine entsprechenden Zeilen hinzugekommen sein.
- Starten Sie das Programm erneut und verwenden Sie
list. Die drei bereits gespeicherten Ausgaben müssen erhalten sein. - Wiederholen Sie den ursprünglichen Aufbau mit einer neuen Datenbank. So lässt sich die Summe reproduzieren, ohne die bisherigen Testdaten zu löschen.
Unzulässige Argumente führen hier typischerweise zu Gebrauchshinweisen und Status zwei. Ein SQL- oder Dateifehler kann dagegen als Ausnahme aus dem Programm herauslaufen. Prüfen Sie Status und Fehlermeldung, statt nach jedem Aufruf nur nach einem Bericht zu suchen. Der Integrationstest im nächsten Abschnitt kontrolliert mehrere dieser Grenzen automatisch.
SQL-Wertparameter trennen die Eingaben von der Abfragestruktur. Datenbankregeln verstärken den Betragsbereich und verbieten leere Kategorien im dort definierten Sinn. Sie ersetzen jedoch nicht alle Python-Regeln: Die Tabelle ist nicht STRICT, der Datumstext wird nicht als vollständiger Kalenderwert in SQL geprüft, und SQLite-trim hat nicht genau dieselbe Bedeutung wie Python-strip. Andere Schreibpfade benötigen ebenfalls eine bewusst definierte Validierung.
Ein Fehler, der den with con-Block verlässt, rollt dessen Transaktion zurück. Das Schließen der Verbindung ist separat im finally abgesichert. Migrationen, Sicherung und Wiederherstellung sowie gleichzeitige Benutzer benötigen zusätzliche Planung. Verwenden Sie keine Löschanweisung nur, um unbekannte Bestände an das Beispiel anzupassen.
Einen unabhängigen Integrationstest ausführen
Speichern Sie die folgende vollständige Datei check_ledger.py neben ledger.py und führen Sie python check_ledger.py aus:
import subprocess
import sys
import tempfile
from pathlib import Path
program = Path("ledger.py").resolve()
with tempfile.TemporaryDirectory() as folder:
db = Path(folder) / "test.db"
def run(*args):
return subprocess.run([sys.executable, str(program), "--db", str(db), *args], capture_output=True, text=True)
assert run("init").returncode == 0
assert run("add", "2026-10-17", "food", "8000").returncode == 0
assert run("add", "2026-02-30", "food", "8000").returncode != 0
assert run("add", "2026-10-17", "food", "-1").returncode != 0
assert run("add", "2026-10-17", "food", "1000000000001").returncode != 0
result = run("summary")
assert result.returncode == 0 and result.stdout.strip() == "food 8000"
assert len(run("list").stdout.splitlines()) == 1
print("integration checks passed")
Erwartete Ausgabe:
integration checks passed
Der Test öffnet eine eigene temporäre Datenbank, initialisiert sie und fügt eine gültige Ausgabe hinzu. Er erwartet die Ablehnung des ungültigen Datums, des negativen Betrags und der Überschreitung des Höchstbetrags. Danach müssen genau eine Zeile und die Summe food 8000 vorhanden sein. Der Test verwendet denselben Interpreter wie sein aufgerufener Unterprozess und bereinigt anschließend seinen eigenen temporären Ordner.
Starten Sie diesen kleinen Prüfhelfer ohne -O und ohne PYTHONOPTIMIZE, da normale assert-Anweisungen sonst entfallen können. Ein größeres Projekt kann diese Fälle in unittest.TestCase mit Vergleichsmethoden überführen; die Test-Übung zeigt diese Struktur. Dieser Test belegt die aufgeführten Fälle, keine ungeprüfte Mehrbenutzerfähigkeit oder Wiederherstellung.
Wenn die Anwendung wächst, trennen Sie Argumentauswertung, Validierung, Datenbankzugriff und Aggregation in passende Module. Behalten Sie den main-Einstiegspunkt, damit ein Import allein keine Datenbank anlegt. Für tatsächliche persönliche Ausgaben legen Sie außerdem Zugriffsrechte, Sicherungen und Freigabeorte bewusst fest. Prüfen Sie neben richtigen Summen immer die Ablehnung ungeeigneter Eingaben und die Bedeutung wiederholter Schreibbefehle.




