initial: novel-app snapshot
This commit is contained in:
5
repositories/__init__.py
Normal file
5
repositories/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Data-access layer for the novel app.
|
||||
|
||||
Each module exposes plain functions taking an explicit sqlite3 connection
|
||||
(row_factory=sqlite3.Row) and returning plain dicts. No Flask imports.
|
||||
"""
|
||||
109
repositories/chapter_repo.py
Normal file
109
repositories/chapter_repo.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Chapter-related queries.
|
||||
|
||||
Functions take an explicit sqlite3 connection with row_factory=sqlite3.Row
|
||||
and return plain dicts. No Flask imports.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
CHAPTER_COLUMNS = (
|
||||
"id, novel_id, volume, chapter_number, title, outline, content, "
|
||||
"word_count, status, created_at, updated_at"
|
||||
)
|
||||
|
||||
ALLOWED_UPDATE_FIELDS = {
|
||||
"title",
|
||||
"outline",
|
||||
"content",
|
||||
"word_count",
|
||||
"status",
|
||||
"volume",
|
||||
"chapter_number",
|
||||
}
|
||||
|
||||
|
||||
def _rows(cur):
|
||||
return [dict(row) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def list_chapters(db, novel_id):
|
||||
"""All chapters of a novel ordered by volume, chapter_number."""
|
||||
return _rows(
|
||||
db.execute(
|
||||
f"SELECT {CHAPTER_COLUMNS} FROM chapters WHERE novel_id = ? "
|
||||
"ORDER BY volume, chapter_number",
|
||||
(novel_id,),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_chapter(db, chapter_id):
|
||||
"""Single chapter row as dict, or None."""
|
||||
row = db.execute(
|
||||
f"SELECT {CHAPTER_COLUMNS} FROM chapters WHERE id = ?", (chapter_id,)
|
||||
).fetchone()
|
||||
return dict(row) if row is not None else None
|
||||
|
||||
|
||||
def get_chapter_by_number(db, novel_id, volume, chapter_number):
|
||||
"""Chapter identified by novel + volume + chapter_number, or None."""
|
||||
row = db.execute(
|
||||
f"SELECT {CHAPTER_COLUMNS} FROM chapters "
|
||||
"WHERE novel_id = ? AND volume = ? AND chapter_number = ?",
|
||||
(novel_id, volume, chapter_number),
|
||||
).fetchone()
|
||||
return dict(row) if row is not None else None
|
||||
|
||||
|
||||
def create_chapter(db, novel_id, volume, chapter_number, title=None, outline=None, status="pending"):
|
||||
"""Insert a new chapter and return its id."""
|
||||
now = _now()
|
||||
cur = db.execute(
|
||||
"INSERT INTO chapters (novel_id, volume, chapter_number, title, outline, status, created_at, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(novel_id, volume, chapter_number, title, outline, status, now, now),
|
||||
)
|
||||
db.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def update_chapter(db, chapter_id, **fields):
|
||||
"""Update allowed chapter fields."""
|
||||
updates = {k: v for k, v in fields.items() if k in ALLOWED_UPDATE_FIELDS}
|
||||
if not updates:
|
||||
return
|
||||
updates["updated_at"] = _now()
|
||||
assignments = ", ".join(f"{key} = ?" for key in updates)
|
||||
db.execute(
|
||||
f"UPDATE chapters SET {assignments} WHERE id = ?",
|
||||
(*updates.values(), chapter_id),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def get_previous_chapters(db, novel_id, volume, chapter_number, limit=2):
|
||||
"""Chapters strictly before (volume, chapter_number), newest first, with content."""
|
||||
return _rows(
|
||||
db.execute(
|
||||
f"SELECT {CHAPTER_COLUMNS} FROM chapters "
|
||||
"WHERE novel_id = ? AND (volume < ? OR (volume = ? AND chapter_number < ?)) "
|
||||
"ORDER BY volume DESC, chapter_number DESC LIMIT ?",
|
||||
(novel_id, volume, volume, chapter_number, limit),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def list_done_chapters(db, novel_id, limit=20):
|
||||
"""Done chapters, newest first."""
|
||||
return _rows(
|
||||
db.execute(
|
||||
f"SELECT {CHAPTER_COLUMNS} FROM chapters "
|
||||
"WHERE novel_id = ? AND status = 'done' "
|
||||
"ORDER BY volume DESC, chapter_number DESC LIMIT ?",
|
||||
(novel_id, limit),
|
||||
)
|
||||
)
|
||||
83
repositories/character_repo.py
Normal file
83
repositories/character_repo.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Character-related queries.
|
||||
|
||||
Functions take an explicit sqlite3 connection with row_factory=sqlite3.Row
|
||||
and return plain dicts. No Flask imports.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
CHARACTER_COLUMNS = (
|
||||
"id, novel_id, name, role, description, first_appearance_chapter, "
|
||||
"status, created_at, updated_at"
|
||||
)
|
||||
|
||||
ALLOWED_UPDATE_FIELDS = {
|
||||
"name",
|
||||
"role",
|
||||
"description",
|
||||
"first_appearance_chapter",
|
||||
"status",
|
||||
}
|
||||
|
||||
|
||||
def _rows(cur):
|
||||
return [dict(row) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def list_characters(db, novel_id):
|
||||
"""All characters of a novel ordered by id."""
|
||||
return _rows(
|
||||
db.execute(
|
||||
f"SELECT {CHARACTER_COLUMNS} FROM characters WHERE novel_id = ? ORDER BY id",
|
||||
(novel_id,),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_character(db, character_id):
|
||||
"""Single character row as dict, or None."""
|
||||
row = db.execute(
|
||||
f"SELECT {CHARACTER_COLUMNS} FROM characters WHERE id = ?", (character_id,)
|
||||
).fetchone()
|
||||
return dict(row) if row is not None else None
|
||||
|
||||
|
||||
def create_character(db, novel_id, name, role=None, description=None, first_appearance_chapter=None):
|
||||
"""Insert a new character and return its id."""
|
||||
now = _now()
|
||||
cur = db.execute(
|
||||
"INSERT INTO characters (novel_id, name, role, description, first_appearance_chapter, status, created_at, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, 'alive', ?, ?)",
|
||||
(novel_id, name, role, description, first_appearance_chapter, now, now),
|
||||
)
|
||||
db.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def update_character(db, character_id, **fields):
|
||||
"""Update allowed character fields."""
|
||||
updates = {k: v for k, v in fields.items() if k in ALLOWED_UPDATE_FIELDS}
|
||||
if not updates:
|
||||
return
|
||||
updates["updated_at"] = _now()
|
||||
assignments = ", ".join(f"{key} = ?" for key in updates)
|
||||
db.execute(
|
||||
f"UPDATE characters SET {assignments} WHERE id = ?",
|
||||
(*updates.values(), character_id),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def find_characters_in_text(db, novel_id, text):
|
||||
"""Characters whose name appears as a substring of text."""
|
||||
characters = _rows(
|
||||
db.execute(
|
||||
f"SELECT {CHARACTER_COLUMNS} FROM characters WHERE novel_id = ?",
|
||||
(novel_id,),
|
||||
)
|
||||
)
|
||||
return [c for c in characters if c["name"] and c["name"] in text]
|
||||
74
repositories/foreshadowing_repo.py
Normal file
74
repositories/foreshadowing_repo.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Foreshadowing-related queries.
|
||||
|
||||
Functions take an explicit sqlite3 connection with row_factory=sqlite3.Row
|
||||
and return plain dicts. No Flask imports.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
FORESHADOWING_COLUMNS = (
|
||||
"id, novel_id, description, planted_chapter, resolved_chapter, "
|
||||
"status, created_at, updated_at"
|
||||
)
|
||||
|
||||
ALLOWED_UPDATE_FIELDS = {
|
||||
"description",
|
||||
"planted_chapter",
|
||||
"resolved_chapter",
|
||||
"status",
|
||||
}
|
||||
|
||||
|
||||
def _rows(cur):
|
||||
return [dict(row) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def list_foreshadowing(db, novel_id):
|
||||
"""All foreshadowing entries of a novel ordered by id."""
|
||||
return _rows(
|
||||
db.execute(
|
||||
f"SELECT {FORESHADOWING_COLUMNS} FROM foreshadowing WHERE novel_id = ? ORDER BY id",
|
||||
(novel_id,),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_foreshadowing(db, novel_id, description, planted_chapter=None, resolved_chapter=None, status="pending"):
|
||||
"""Insert a new foreshadowing entry and return its id."""
|
||||
now = _now()
|
||||
cur = db.execute(
|
||||
"INSERT INTO foreshadowing (novel_id, description, planted_chapter, resolved_chapter, status, created_at, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(novel_id, description, planted_chapter, resolved_chapter, status, now, now),
|
||||
)
|
||||
db.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def update_foreshadowing(db, fs_id, **fields):
|
||||
"""Update allowed foreshadowing fields."""
|
||||
updates = {k: v for k, v in fields.items() if k in ALLOWED_UPDATE_FIELDS}
|
||||
if not updates:
|
||||
return
|
||||
updates["updated_at"] = _now()
|
||||
assignments = ", ".join(f"{key} = ?" for key in updates)
|
||||
db.execute(
|
||||
f"UPDATE foreshadowing SET {assignments} WHERE id = ?",
|
||||
(*updates.values(), fs_id),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def list_open_foreshadowing(db, novel_id):
|
||||
"""Open (pending/planted) foreshadowing entries ordered by id."""
|
||||
return _rows(
|
||||
db.execute(
|
||||
f"SELECT {FORESHADOWING_COLUMNS} FROM foreshadowing "
|
||||
"WHERE novel_id = ? AND status IN ('pending', 'planted') ORDER BY id",
|
||||
(novel_id,),
|
||||
)
|
||||
)
|
||||
69
repositories/job_repo.py
Normal file
69
repositories/job_repo.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Background-job-related queries.
|
||||
|
||||
Functions take an explicit sqlite3 connection with row_factory=sqlite3.Row
|
||||
and return plain dicts. No Flask imports.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
JOB_COLUMNS = (
|
||||
"id, novel_id, job_type, status, input_json, output_json, error, "
|
||||
"created_at, completed_at"
|
||||
)
|
||||
|
||||
ALLOWED_UPDATE_FIELDS = {
|
||||
"status",
|
||||
"output_json",
|
||||
"error",
|
||||
"completed_at",
|
||||
}
|
||||
|
||||
|
||||
def _rows(cur):
|
||||
return [dict(row) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def create_job(db, novel_id, job_type, input_json=None):
|
||||
"""Insert a new job with status 'pending' and return its id."""
|
||||
cur = db.execute(
|
||||
"INSERT INTO jobs (novel_id, job_type, status, input_json, created_at) "
|
||||
"VALUES (?, ?, 'pending', ?, ?)",
|
||||
(novel_id, job_type, input_json, _now()),
|
||||
)
|
||||
db.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def update_job(db, job_id, **fields):
|
||||
"""Update allowed job fields."""
|
||||
updates = {k: v for k, v in fields.items() if k in ALLOWED_UPDATE_FIELDS}
|
||||
if not updates:
|
||||
return
|
||||
assignments = ", ".join(f"{key} = ?" for key in updates)
|
||||
db.execute(
|
||||
f"UPDATE jobs SET {assignments} WHERE id = ?",
|
||||
(*updates.values(), job_id),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def get_job(db, job_id):
|
||||
"""Single job row as dict, or None."""
|
||||
row = db.execute(
|
||||
f"SELECT {JOB_COLUMNS} FROM jobs WHERE id = ?", (job_id,)
|
||||
).fetchone()
|
||||
return dict(row) if row is not None else None
|
||||
|
||||
|
||||
def list_jobs(db, novel_id, limit=20):
|
||||
"""Most recent jobs for a novel, newest first."""
|
||||
return _rows(
|
||||
db.execute(
|
||||
f"SELECT {JOB_COLUMNS} FROM jobs WHERE novel_id = ? ORDER BY id DESC LIMIT ?",
|
||||
(novel_id, limit),
|
||||
)
|
||||
)
|
||||
99
repositories/novel_repo.py
Normal file
99
repositories/novel_repo.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Novel-related queries.
|
||||
|
||||
Functions take an explicit sqlite3 connection with row_factory=sqlite3.Row
|
||||
and return plain dicts. No Flask imports.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
NOVEL_COLUMNS = (
|
||||
"id, title, genre, style, target_words, current_words, status, "
|
||||
"created_at, updated_at"
|
||||
)
|
||||
|
||||
ALLOWED_UPDATE_FIELDS = {
|
||||
"title",
|
||||
"genre",
|
||||
"style",
|
||||
"target_words",
|
||||
"current_words",
|
||||
"status",
|
||||
}
|
||||
|
||||
|
||||
def _rows(cur):
|
||||
return [dict(row) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def list_novels(db):
|
||||
"""All novels ordered by updated_at desc."""
|
||||
return _rows(
|
||||
db.execute(f"SELECT {NOVEL_COLUMNS} FROM novels ORDER BY updated_at DESC")
|
||||
)
|
||||
|
||||
|
||||
def get_novel(db, novel_id):
|
||||
"""Single novel row as dict, or None."""
|
||||
row = db.execute(
|
||||
f"SELECT {NOVEL_COLUMNS} FROM novels WHERE id = ?", (novel_id,)
|
||||
).fetchone()
|
||||
return dict(row) if row is not None else None
|
||||
|
||||
|
||||
def create_novel(db, title, genre=None, style=None, target_words=None, status="planning"):
|
||||
"""Insert a new novel and return its id."""
|
||||
now = _now()
|
||||
cur = db.execute(
|
||||
"INSERT INTO novels (title, genre, style, target_words, status, created_at, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(title, genre, style, target_words, status, now, now),
|
||||
)
|
||||
db.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def update_novel(db, novel_id, **fields):
|
||||
"""Update allowed novel fields and refresh updated_at."""
|
||||
updates = {k: v for k, v in fields.items() if k in ALLOWED_UPDATE_FIELDS}
|
||||
if not updates:
|
||||
return
|
||||
updates["updated_at"] = _now()
|
||||
assignments = ", ".join(f"{key} = ?" for key in updates)
|
||||
db.execute(
|
||||
f"UPDATE novels SET {assignments} WHERE id = ?",
|
||||
(*updates.values(), novel_id),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def get_stats(db, novel_id):
|
||||
"""Aggregate stats for one novel."""
|
||||
chapter_count = db.execute(
|
||||
"SELECT COUNT(*) FROM chapters WHERE novel_id = ?", (novel_id,)
|
||||
).fetchone()[0]
|
||||
done_chapters = db.execute(
|
||||
"SELECT COUNT(*) FROM chapters WHERE novel_id = ? AND status = 'done'",
|
||||
(novel_id,),
|
||||
).fetchone()[0]
|
||||
total_words = db.execute(
|
||||
"SELECT COALESCE(SUM(word_count), 0) FROM chapters WHERE novel_id = ?",
|
||||
(novel_id,),
|
||||
).fetchone()[0]
|
||||
character_count = db.execute(
|
||||
"SELECT COUNT(*) FROM characters WHERE novel_id = ?", (novel_id,)
|
||||
).fetchone()[0]
|
||||
open_foreshadowing = db.execute(
|
||||
"SELECT COUNT(*) FROM foreshadowing WHERE novel_id = ? AND status IN ('pending', 'planted')",
|
||||
(novel_id,),
|
||||
).fetchone()[0]
|
||||
return {
|
||||
"chapter_count": chapter_count,
|
||||
"done_chapters": done_chapters,
|
||||
"total_words": total_words,
|
||||
"character_count": character_count,
|
||||
"open_foreshadowing": open_foreshadowing,
|
||||
}
|
||||
Reference in New Issue
Block a user