Artikel

5. Datenmodell

KILinuxMarketingPython

Das Datenmodell bildet die Arbeitsobjekte des Systems ab:

Bücher
Autoren
Beitragsentwürfe
freigegebene Beiträge
veröffentlichte Beiträge
Interaktionskandidaten
Interaktionsprotokolle

Für das MVP reichen JSON-Dateien. Sobald mehrere Plattformen, wiederkehrende Jobs, Reports und Dublettenprüfung dazukommen, ist SQLite sinnvoller.

5.1 Book

Book beschreibt ein Buch als Content-Quelle.

Minimalfelder:

id
title
author_id
language
category
description
shop_url
cover_image_path
tags
created_at
updated_at

Beispiel als JSON:

{
  "id": "book_001",
  "title": "Stolz und Vorurteil",
  "author_id": "author_001",
  "language": "de",
  "category": "klassiker",
  "description": "Ein Gesellschaftsroman über Herkunft, Heirat, Stolz und Selbsttäuschung.",
  "shop_url": "https://example.com/stolz-und-vorurteil",
  "cover_image_path": "storage/images/stolz-und-vorurteil.jpg",
  "tags": [
    "klassiker",
    "jane-austen",
    "gesellschaftsroman"
  ],
  "created_at": "2026-05-11T09:00:00+02:00",
  "updated_at": "2026-05-11T09:00:00+02:00"
}

Python-Modell:

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True)
class Book:
    id: str
    title: str
    author_id: str
    language: str
    category: str
    description: str
    shop_url: str
    cover_image_path: str
    tags: list[str]
    created_at: datetime
    updated_at: datetime

5.2 Author

Author beschreibt den Autor eines Buches.

Minimalfelder:

id
name
birth_year
death_year
country
description
tags
created_at
updated_at

Beispiel:

{
  "id": "author_001",
  "name": "Jane Austen",
  "birth_year": 1775,
  "death_year": 1817,
  "country": "Großbritannien",
  "description": "Englische Schriftstellerin, bekannt für Gesellschaftsromane mit präziser Beobachtung sozialer Verhältnisse.",
  "tags": [
    "klassiker",
    "englische-literatur",
    "gesellschaftsroman"
  ],
  "created_at": "2026-05-11T09:00:00+02:00",
  "updated_at": "2026-05-11T09:00:00+02:00"
}

Python-Modell:

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True)
class Author:
    id: str
    name: str
    birth_year: int | None
    death_year: int | None
    country: str
    description: str
    tags: list[str]
    created_at: datetime
    updated_at: datetime

5.3 PostDraft

PostDraft ist ein erzeugter, aber noch nicht freigegebener Beitrag.

Minimalfelder:

id
source_type
source_id
platform
post_type
status
text
image_path
target_url
prompt_name
prompt_version
model
quality_result
created_at
updated_at

Beispiel:

{
  "id": "draft_20260511_001",
  "source_type": "book",
  "source_id": "book_001",
  "platform": "linkedin",
  "post_type": "book_intro",
  "status": "draft",
  "text": "Jane Austens Stolz und Vorurteil ist weniger eine romantische Komödie als eine genaue Gesellschaftsbeobachtung.",
  "image_path": null,
  "target_url": "https://example.com/stolz-und-vorurteil",
  "prompt_name": "linkedin_book_intro",
  "prompt_version": "1.0.0",
  "model": "gpt-4.1-mini",
  "quality_result": null,
  "created_at": "2026-05-11T09:10:00+02:00",
  "updated_at": "2026-05-11T09:10:00+02:00"
}

Python-Modell:

from dataclasses import dataclass
from datetime import datetime
from typing import Any


@dataclass(frozen=True)
class PostDraft:
    id: str
    source_type: str
    source_id: str
    platform: str
    post_type: str
    status: str
    text: str
    image_path: str | None
    target_url: str | None
    prompt_name: str
    prompt_version: str
    model: str
    quality_result: dict[str, Any] | None
    created_at: datetime
    updated_at: datetime

5.4 ApprovedPost

ApprovedPost ist ein freigegebener Beitrag. Er darf veröffentlicht werden.

Minimalfelder:

id
draft_id
platform
post_type
text
image_path
target_url
approved_by
approval_mode
approved_at
scheduled_at
status

Beispiel:

{
  "id": "approved_20260511_001",
  "draft_id": "draft_20260511_001",
  "platform": "linkedin",
  "post_type": "book_intro",
  "text": "Jane Austens Stolz und Vorurteil ist weniger eine romantische Komödie als eine genaue Gesellschaftsbeobachtung.",
  "image_path": null,
  "target_url": "https://example.com/stolz-und-vorurteil",
  "approved_by": "system",
  "approval_mode": "manual",
  "approved_at": "2026-05-11T09:20:00+02:00",
  "scheduled_at": "2026-05-11T09:30:00+02:00",
  "status": "approved"
}

Python-Modell:

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True)
class ApprovedPost:
    id: str
    draft_id: str
    platform: str
    post_type: str
    text: str
    image_path: str | None
    target_url: str | None
    approved_by: str
    approval_mode: str
    approved_at: datetime
    scheduled_at: datetime | None
    status: str

5.5 PublishedPost

PublishedPost beschreibt einen tatsächlich veröffentlichten Beitrag.

Minimalfelder:

id
approved_post_id
platform
post_type
text
image_path
target_url
platform_post_url
screenshot_path
published_at
status
log_id

Beispiel:

{
  "id": "published_20260511_001",
  "approved_post_id": "approved_20260511_001",
  "platform": "linkedin",
  "post_type": "book_intro",
  "text": "Jane Austens Stolz und Vorurteil ist weniger eine romantische Komödie als eine genaue Gesellschaftsbeobachtung.",
  "image_path": null,
  "target_url": "https://example.com/stolz-und-vorurteil",
  "platform_post_url": null,
  "screenshot_path": "storage/screenshots/linkedin/post_20260511_001_success.png",
  "published_at": "2026-05-11T09:30:00+02:00",
  "status": "published",
  "log_id": "log_20260511_001"
}

Python-Modell:

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True)
class PublishedPost:
    id: str
    approved_post_id: str
    platform: str
    post_type: str
    text: str
    image_path: str | None
    target_url: str | None
    platform_post_url: str | None
    screenshot_path: str
    published_at: datetime
    status: str
    log_id: str

5.6 InteractionCandidate

InteractionCandidate beschreibt eine mögliche Interaktion, bevor sie ausgeführt wird.

Typische Fälle:

Antwort auf Kommentar unter eigenem Beitrag
Kommentar unter fremdem Beitrag
Like-Kandidat
Repost-Kandidat

Minimalfelder:

id
platform
interaction_type
source_url
source_author
source_text
relevance_score
risk_score
status
suggested_text
created_at
updated_at

Beispiel:

{
  "id": "interaction_20260511_001",
  "platform": "linkedin",
  "interaction_type": "comment_reply",
  "source_url": "https://www.linkedin.com/feed/update/example",
  "source_author": "Max Beispiel",
  "source_text": "Interessanter Punkt zur Sichtbarkeit kleiner Anbieter.",
  "relevance_score": 0.82,
  "risk_score": 0.12,
  "status": "suggested",
  "suggested_text": "Der Punkt ist im Buchmarkt ähnlich: Technisch kann ein kleines Angebot sehr gut sein, aber Auffindbarkeit entscheidet oft früher als Qualität.",
  "created_at": "2026-05-11T10:00:00+02:00",
  "updated_at": "2026-05-11T10:01:00+02:00"
}

Python-Modell:

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True)
class InteractionCandidate:
    id: str
    platform: str
    interaction_type: str
    source_url: str
    source_author: str
    source_text: str
    relevance_score: float
    risk_score: float
    status: str
    suggested_text: str | None
    created_at: datetime
    updated_at: datetime

5.7 InteractionLog

InteractionLog protokolliert eine ausgeführte oder fehlgeschlagene Interaktion.

Minimalfelder:

id
candidate_id
platform
interaction_type
executed_text
status
screenshot_path
executed_at
error_message

Beispiel:

{
  "id": "interaction_log_20260511_001",
  "candidate_id": "interaction_20260511_001",
  "platform": "linkedin",
  "interaction_type": "comment_reply",
  "executed_text": "Der Punkt ist im Buchmarkt ähnlich: Technisch kann ein kleines Angebot sehr gut sein, aber Auffindbarkeit entscheidet oft früher als Qualität.",
  "status": "executed",
  "screenshot_path": "storage/screenshots/linkedin/interaction_20260511_001_success.png",
  "executed_at": "2026-05-11T10:10:00+02:00",
  "error_message": null
}

Python-Modell:

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True)
class InteractionLog:
    id: str
    candidate_id: str
    platform: str
    interaction_type: str
    executed_text: str | None
    status: str
    screenshot_path: str | None
    executed_at: datetime
    error_message: str | None

5.8 JSON-Dateien vs. SQLite

Für den Start reicht JSON.

Vorteile:

leicht lesbar
leicht versionierbar
kein Datenbank-Setup
gut für MVP
direkt manuell korrigierbar

Nachteile:

keine sauberen Abfragen
schwierig bei parallelen Prozessen
keine Transaktionen
Dublettenprüfung umständlicher
Reporting schwächer

SQLite ist sinnvoll, sobald mehr als ein einfacher Ablauf existiert.

Vorteile:

relationale Struktur
Transaktionen
einfache Reports
bessere Statusabfragen
bessere Dublettenprüfung
kein Server nötig
stabiler für Scheduler

Empfohlene Entwicklung:

Phase 1: JSON-Dateien
Phase 2: SQLite
Phase 3: optional PostgreSQL oder MariaDB

Für dieses Projekt ist SQLite der sinnvolle Produktivstandard, sobald automatisches Posting aktiv ist.

JSON-Dateien für MVP

data/authors.json
data/books.json
data/post_drafts.json
data/approved_posts.json
data/published_posts.json
data/interaction_candidates.json
data/interaction_logs.json

Beispiel: data/post_drafts.json

[
  {
    "id": "draft_20260511_001",
    "source_type": "book",
    "source_id": "book_001",
    "platform": "linkedin",
    "post_type": "book_intro",
    "status": "draft",
    "text": "Jane Austens Stolz und Vorurteil ist weniger eine romantische Komödie als eine genaue Gesellschaftsbeobachtung.",
    "image_path": null,
    "target_url": "https://example.com/stolz-und-vorurteil",
    "prompt_name": "linkedin_book_intro",
    "prompt_version": "1.0.0",
    "model": "gpt-4.1-mini",
    "quality_result": null,
    "created_at": "2026-05-11T09:10:00+02:00",
    "updated_at": "2026-05-11T09:10:00+02:00"
  }
]

Ein einfacher JSON-Store:

import json
from pathlib import Path
from typing import Any


class JsonStore:
    def __init__(self, file_path: Path) -> None:
        self.file_path = file_path

    def load_list(self) -> list[dict[str, Any]]:
        if self.file_path.exists() is False:
            return []

        with self.file_path.open("r", encoding="utf-8") as file:
            data = json.load(file)

        if isinstance(data, list) is False:
            raise RuntimeError(f"JSON file must contain a list: {self.file_path}")

        result: list[dict[str, Any]] = []

        for item in data:
            if isinstance(item, dict) is False:
                raise RuntimeError(f"JSON list contains non-object item: {self.file_path}")

            result.append(item)

        return result

    def save_list(self, data: list[dict[str, Any]]) -> None:
        self.file_path.parent.mkdir(parents=True, exist_ok=True)

        with self.file_path.open("w", encoding="utf-8") as file:
            json.dump(data, file, ensure_ascii=False, indent=2)
            file.write("\n")

5.9 Beispiel-Schema SQLite

SQLite-Datei:

data/social_publisher.sqlite

Schema:

CREATE TABLE authors (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    birth_year INTEGER NULL,
    death_year INTEGER NULL,
    country TEXT NOT NULL,
    description TEXT NOT NULL,
    tags_json TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE books (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    author_id TEXT NOT NULL,
    language TEXT NOT NULL,
    category TEXT NOT NULL,
    description TEXT NOT NULL,
    shop_url TEXT NOT NULL,
    cover_image_path TEXT NOT NULL,
    tags_json TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    FOREIGN KEY (author_id) REFERENCES authors (id)
);

CREATE TABLE post_drafts (
    id TEXT PRIMARY KEY,
    source_type TEXT NOT NULL,
    source_id TEXT NOT NULL,
    platform TEXT NOT NULL,
    post_type TEXT NOT NULL,
    status TEXT NOT NULL,
    text TEXT NOT NULL,
    image_path TEXT NULL,
    target_url TEXT NULL,
    prompt_name TEXT NOT NULL,
    prompt_version TEXT NOT NULL,
    model TEXT NOT NULL,
    quality_result_json TEXT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE approved_posts (
    id TEXT PRIMARY KEY,
    draft_id TEXT NOT NULL,
    platform TEXT NOT NULL,
    post_type TEXT NOT NULL,
    text TEXT NOT NULL,
    image_path TEXT NULL,
    target_url TEXT NULL,
    approved_by TEXT NOT NULL,
    approval_mode TEXT NOT NULL,
    approved_at TEXT NOT NULL,
    scheduled_at TEXT NULL,
    status TEXT NOT NULL,
    FOREIGN KEY (draft_id) REFERENCES post_drafts (id)
);

CREATE TABLE published_posts (
    id TEXT PRIMARY KEY,
    approved_post_id TEXT NOT NULL,
    platform TEXT NOT NULL,
    post_type TEXT NOT NULL,
    text TEXT NOT NULL,
    image_path TEXT NULL,
    target_url TEXT NULL,
    platform_post_url TEXT NULL,
    screenshot_path TEXT NOT NULL,
    published_at TEXT NOT NULL,
    status TEXT NOT NULL,
    log_id TEXT NOT NULL,
    FOREIGN KEY (approved_post_id) REFERENCES approved_posts (id)
);

CREATE TABLE interaction_candidates (
    id TEXT PRIMARY KEY,
    platform TEXT NOT NULL,
    interaction_type TEXT NOT NULL,
    source_url TEXT NOT NULL,
    source_author TEXT NOT NULL,
    source_text TEXT NOT NULL,
    relevance_score REAL NOT NULL,
    risk_score REAL NOT NULL,
    status TEXT NOT NULL,
    suggested_text TEXT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE interaction_logs (
    id TEXT PRIMARY KEY,
    candidate_id TEXT NOT NULL,
    platform TEXT NOT NULL,
    interaction_type TEXT NOT NULL,
    executed_text TEXT NULL,
    status TEXT NOT NULL,
    screenshot_path TEXT NULL,
    executed_at TEXT NOT NULL,
    error_message TEXT NULL,
    FOREIGN KEY (candidate_id) REFERENCES interaction_candidates (id)
);

CREATE TABLE event_logs (
    id TEXT PRIMARY KEY,
    event TEXT NOT NULL,
    level TEXT NOT NULL,
    platform TEXT NULL,
    reference_id TEXT NULL,
    payload_json TEXT NOT NULL,
    created_at TEXT NOT NULL
);

Nützliche Indizes:

CREATE INDEX idx_books_author_id
ON books (author_id);

CREATE INDEX idx_post_drafts_status
ON post_drafts (status);

CREATE INDEX idx_post_drafts_platform_status
ON post_drafts (platform, status);

CREATE INDEX idx_approved_posts_status
ON approved_posts (status);

CREATE INDEX idx_approved_posts_scheduled_at
ON approved_posts (scheduled_at);

CREATE INDEX idx_published_posts_platform_published_at
ON published_posts (platform, published_at);

CREATE INDEX idx_interaction_candidates_status
ON interaction_candidates (status);

CREATE INDEX idx_interaction_candidates_platform_status
ON interaction_candidates (platform, status);

CREATE INDEX idx_event_logs_created_at
ON event_logs (created_at);

Initialisierung per Python:

import sqlite3
from pathlib import Path


class SqliteSchemaInstaller:
    def __init__(self, database_path: Path, schema_path: Path) -> None:
        self.database_path = database_path
        self.schema_path = schema_path

    def install(self) -> None:
        if self.schema_path.exists() is False:
            raise RuntimeError(f"Schema file does not exist: {self.schema_path}")

        self.database_path.parent.mkdir(parents=True, exist_ok=True)

        schema_sql = self.schema_path.read_text(encoding="utf-8")

        with sqlite3.connect(self.database_path) as connection:
            connection.executescript(schema_sql)
            connection.commit()

Beispielkommando:

mkdir -p database
nano database/schema.sql

Dann:

from pathlib import Path

from src.sqlite_schema_installer import SqliteSchemaInstaller


def main() -> None:
    root_dir = Path(__file__).resolve().parent.parent

    installer = SqliteSchemaInstaller(
        database_path=root_dir / "data" / "social_publisher.sqlite",
        schema_path=root_dir / "database" / "schema.sql",
    )

    installer.install()


if __name__ == "__main__":
    main()

Empfohlener MVP-Stand

Für den ersten lauffähigen Stand:

JSON verwenden
Dataclasses definieren
Status sauber führen
später SQLite ergänzen
IDs stabil erzeugen
Zeitpunkte immer als ISO-String speichern
Tags und Prüfberichte als JSON speichern

Für den ersten produktiven Automationsbetrieb:

SQLite verwenden
Transaktionen nutzen
Statuswechsel atomar speichern
Indizes für Statusabfragen anlegen
Event-Log zentral führen
Screenshots nur als Dateipfad speichern

0 Kommentare