Week 9 of 16

Build: The Prompt Class

Replace raw dictionaries with a proper Prompt object that validates, formats, and serializes itself.

Day 43 75 minutes Build

Day 43 of 80

What You're Building Today

You're creating models.py — a new file in your project that contains the Prompt class.

This class replaces the raw dictionaries your app currently uses. It handles:

The app's behavior won't change for users. But the code becomes significantly easier to reason about.

Step 1: Create models.py

In your project root (same directory as vault.py), create a new file called models.py. Build the class up piece by piece.

models.py — complete Prompt class Python
from datetime import datetime


class Prompt:
    """A single AI video prompt with platform, shot description, and prompt text."""

    VALID_PLATFORMS = ["Kling", "Runway", "Veo"]  # class variable — shared by all instances

    def __init__(self, platform, shot, prompt_text, created_at=None):
        self.platform = self._validate_platform(platform)
        self.shot = shot.strip()
        self.prompt_text = prompt_text.strip()
        self.created_at = created_at or datetime.now().isoformat()

    def _validate_platform(self, platform):
        """Validate and normalize platform name. Raises ValueError if invalid."""
        cleaned = platform.strip().title()
        if cleaned not in self.VALID_PLATFORMS:
            raise ValueError(
                f"Unknown platform '{platform}'. Valid: {', '.join(self.VALID_PLATFORMS)}"
            )
        return cleaned

    def __str__(self):
        """Human-readable representation — for print() and f-strings."""
        return f"[{self.platform}] {self.shot}"

    def __repr__(self):
        """Developer representation — shows how to recreate the object."""
        return f"Prompt(platform='{self.platform}', shot='{self.shot}')"

    def __len__(self):
        """Returns the character count of the prompt text."""
        return len(self.prompt_text)

    def preview(self, max_length=80):
        """Returns a truncated version of prompt_text for display."""
        if len(self.prompt_text) <= max_length:
            return self.prompt_text
        return self.prompt_text[:max_length] + "..."

    def to_dict(self):
        """Serialize to a dictionary for JSON storage."""
        return {
            "platform":   self.platform,
            "shot":       self.shot,
            "prompt":     self.prompt_text,
            "created_at": self.created_at
        }

    @classmethod
    def from_dict(cls, data):
        """Create a Prompt instance from a dictionary (e.g., loaded from JSON)."""
        return cls(
            platform=data["platform"],
            shot=data["shot"],
            prompt_text=data["prompt"],
            created_at=data.get("created_at")
        )

_validate_platform starts with an underscore — this is Python's convention for "private" methods. It works like any method, but the underscore signals to other developers "this is an internal detail, don't call it directly."

.strip().title() normalizes input: removes whitespace and capitalizes correctly. " kling " becomes "Kling". Users won't get a validation error from a typo like "kling" or " Kling ".

created_at=None with or datetime.now() is a Python idiom for optional parameters that need a default generated at call time. Never use a mutable default like created_at=datetime.now() — that evaluates once when the class is defined, not on each call.

@classmethod and cls: A classmethod receives the class itself as the first argument (called cls by convention, just like instance methods use self). from_dict is an "alternative constructor" — a second way to create a Prompt, in addition to calling Prompt(...) directly. This pattern is common in professional Python code.

Step 2: Test It in a Scratch Script

Create a temporary file test_models.py and run through the API manually. Delete it when you're satisfied.

test_models.py — manual API test Python
from models import Prompt

# --- Basic creation ---
p = Prompt(
    platform="kling",                      # lowercase — should be normalized
    shot="wide aerial over foggy mountains",
    prompt_text="A sweeping drone shot ascending slowly over a dense mountain range, "
               "thick fog rolling through the valleys, golden hour light, cinematic."
)

# --- str and repr ---
print(p)           # [Kling] wide aerial over foggy mountains
print(repr(p))     # Prompt(platform='Kling', shot='wide aerial over foggy mountains')

# --- len ---
print(len(p))      # character count of prompt_text

# --- preview ---
print(p.preview())        # truncated at 80 chars
print(p.preview(20))      # truncated at 20 chars

# --- to_dict ---
d = p.to_dict()
print(d)  # {'platform': 'Kling', 'shot': '...', 'prompt': '...', 'created_at': '...'}

# --- from_dict (round-trip) ---
p2 = Prompt.from_dict(d)
print(p2)          # Same output as print(p)
print(p2.created_at)  # Same timestamp — preserved through round-trip

# --- Validation error ---
try:
    bad = Prompt("Midjourney", "portrait", "A person...")
except ValueError as e:
    print(f"Caught expected error: {e}")
    # Output: Caught expected error: Unknown platform 'Midjourney'. Valid: Kling, Runway, Veo

Run this and read the output carefully. If anything is wrong — wrong format, wrong truncation, validation not firing — fix it in models.py before moving on.

The round-trip test (to_dict then from_dict) is critical. This is exactly what happens when your app saves and loads a prompt. If p2.platform == p.platform and p2.created_at == p.created_at, the round-trip is correct.

Step 3: Update helpers.py to Use the Class

Change load_prompts() to return Prompt objects instead of raw dicts. This is a one-line change in the list comprehension.

helpers.py — updated load_prompts Python
from models import Prompt
import json

PROMPTS_FILE = "prompts.json"

def load_prompts():
    try:
        with open(PROMPTS_FILE, "r") as f:
            raw_data = json.load(f)
        # Convert each dict to a Prompt object
        return [Prompt.from_dict(item) for item in raw_data]
    except FileNotFoundError:
        return []
    except json.JSONDecodeError:
        print("WARNING: prompts.json is corrupted — starting fresh")
        return []


def save_prompts(prompts):
    # Convert Prompt objects back to dicts before saving
    data = [p.to_dict() for p in prompts]
    with open(PROMPTS_FILE, "w") as f:
        json.dump(data, f, indent=2)

The JSON file doesn't change at all. The data on disk is still plain JSON dictionaries. What changes is that in memory, you work with Prompt objects that have helpful methods.

In your templates, you'll need to update references from p['platform'] to p.platform (dot notation instead of bracket notation). Jinja2 supports both, but dot notation is cleaner and matches how you'd use objects in Python.

End of Day Checklist

Tomorrow

Day 44 introduces the PromptLibrary class — a class that manages the entire collection of prompts. This replaces the loose load_prompts() / save_prompts() functions with a single object that owns both the data and the operations on it.