feat: add pre-commit hooks for local CI checks

- Black (auto-fix formatting on commit)
- Flake8 (block on syntax errors and undefined names)
- Bandit (security scan, non-blocking to match CI)
- Tests (run on pre-push only)
- Apply Black formatting fixes caught by the new hooks
This commit is contained in:
Vito Sansevero
2026-02-08 05:27:03 -08:00
parent ed40be066c
commit be205f3dcc
6 changed files with 110 additions and 39 deletions
+28
View File
@@ -0,0 +1,28 @@
repos:
- repo: https://github.com/psf/black
rev: 25.1.0
hooks:
- id: black
args: [--line-length=88, --target-version=py310]
- repo: local
hooks:
- id: flake8
name: flake8
entry: flake8 --select=E9,F63,F7,F82 --show-source --exclude=reference/,web/
language: system
types: [python]
- id: bandit
name: bandit
entry: bash -c 'bandit "$@" -ll -q 2>&1; true'
language: system
types: [python]
exclude: ^(tests/|reference/)
- id: tests
name: tests
entry: python -m unittest discover tests/ -v
language: system
pass_filenames: false
stages: [pre-push]
+44 -22
View File
@@ -68,7 +68,8 @@ class PromptModel:
- prompts table: Stores prompt text and metadata
- generated_images table: Links generated images to their source prompts
"""
conn.execute("""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
@@ -80,10 +81,12 @@ class PromptModel:
notes TEXT,
hash TEXT UNIQUE
)
""")
"""
)
# Create images table for gallery functionality
conn.execute("""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS generated_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prompt_id INTEGER NOT NULL,
@@ -100,16 +103,20 @@ class PromptModel:
FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE,
UNIQUE(prompt_id, filename)
)
""")
"""
)
# Create normalized tag tables (junction table pattern)
conn.execute("""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)
""")
conn.execute("""
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS prompt_tags (
prompt_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
@@ -117,7 +124,8 @@ class PromptModel:
FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
)
""")
"""
)
# Add unique constraint to existing databases (migration)
self._migrate_add_unique_constraint(conn)
@@ -200,7 +208,8 @@ class PromptModel:
self.logger.info("Migrating database: removing workflow_name column")
# Create new table without workflow_name
conn.execute("""
conn.execute(
"""
CREATE TABLE prompts_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
@@ -212,14 +221,17 @@ class PromptModel:
notes TEXT,
hash TEXT UNIQUE
)
""")
"""
)
# Copy data from old table to new table
conn.execute("""
conn.execute(
"""
INSERT INTO prompts_new (id, text, created_at, updated_at, category, tags, rating, notes, hash)
SELECT id, text, created_at, updated_at, category, tags, rating, notes, hash
FROM prompts
""")
"""
)
# Drop old table and rename new one
conn.execute("DROP TABLE prompts")
@@ -252,7 +264,8 @@ class PromptModel:
)
# Create new table with correct types
conn.execute("""
conn.execute(
"""
CREATE TABLE generated_images_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prompt_id INTEGER NOT NULL,
@@ -268,10 +281,12 @@ class PromptModel:
parameters TEXT,
FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE
)
""")
"""
)
# Copy data, converting prompt_id from TEXT to INTEGER
conn.execute("""
conn.execute(
"""
INSERT INTO generated_images_new
(id, prompt_id, image_path, filename, generation_time, file_size,
width, height, format, workflow_data, prompt_metadata, parameters)
@@ -280,7 +295,8 @@ class PromptModel:
FROM generated_images
WHERE prompt_id != '' AND prompt_id IS NOT NULL
AND CAST(prompt_id AS INTEGER) IN (SELECT id FROM prompts)
""")
"""
)
# Drop old table and rename new one
conn.execute("DROP TABLE generated_images")
@@ -329,13 +345,15 @@ class PromptModel:
)
# First, remove duplicates keeping only the most recent (highest id)
conn.execute("""
conn.execute(
"""
DELETE FROM generated_images
WHERE id NOT IN (
SELECT MAX(id) FROM generated_images
GROUP BY prompt_id, filename
)
""")
"""
)
duplicates_removed = conn.total_changes
if duplicates_removed > 0:
@@ -344,7 +362,8 @@ class PromptModel:
)
# Create new table with UNIQUE constraint
conn.execute("""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS generated_images_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prompt_id INTEGER NOT NULL,
@@ -361,17 +380,20 @@ class PromptModel:
FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE,
UNIQUE(prompt_id, filename)
)
""")
"""
)
# Copy data
conn.execute("""
conn.execute(
"""
INSERT INTO generated_images_new
(id, prompt_id, image_path, filename, generation_time, file_size,
width, height, format, workflow_data, prompt_metadata, parameters)
SELECT id, prompt_id, image_path, filename, generation_time, file_size,
width, height, format, workflow_data, prompt_metadata, parameters
FROM generated_images
""")
"""
)
# Drop old table and rename new one
conn.execute("DROP TABLE generated_images")
+20 -10
View File
@@ -946,14 +946,16 @@ class PromptDatabase:
# Find duplicates by text content (case-insensitive)
# Note: Removed ORDER BY from GROUP_CONCAT for SQLite compatibility
# We'll sort the IDs manually after fetching
cursor = conn.execute("""
cursor = conn.execute(
"""
SELECT LOWER(TRIM(text)) as normalized_text, COUNT(*) as count,
GROUP_CONCAT(id) as ids,
GROUP_CONCAT(created_at) as created_dates
FROM prompts
GROUP BY LOWER(TRIM(text))
HAVING COUNT(*) > 1
""")
"""
)
duplicate_groups = cursor.fetchall()
self.logger.debug(
@@ -1018,14 +1020,16 @@ class PromptDatabase:
# Find duplicates by text content (case-insensitive)
# Note: Removed ORDER BY from GROUP_CONCAT for SQLite compatibility
# We'll sort the IDs manually after fetching
cursor = conn.execute("""
cursor = conn.execute(
"""
SELECT LOWER(TRIM(text)) as normalized_text, COUNT(*) as count,
GROUP_CONCAT(id) as ids,
GROUP_CONCAT(created_at) as created_dates
FROM prompts
GROUP BY LOWER(TRIM(text))
HAVING COUNT(*) > 1
""")
"""
)
duplicates = cursor.fetchall()
self.logger.debug(
@@ -1745,7 +1749,8 @@ class PromptDatabase:
Returns number of prompts removed.
"""
with self.model.get_connection() as conn:
cursor = conn.execute("""
cursor = conn.execute(
"""
SELECT p.id FROM prompts p
LEFT JOIN generated_images gi ON p.id = gi.prompt_id
WHERE gi.prompt_id IS NULL
@@ -1754,7 +1759,8 @@ class PromptDatabase:
JOIN tags t ON pt.tag_id = t.id
WHERE pt.prompt_id = p.id AND t.name = '__protected__'
)
""")
"""
)
orphaned = [row["id"] for row in cursor.fetchall()]
if not orphaned:
return 0
@@ -1770,22 +1776,26 @@ class PromptDatabase:
issues: List[str] = []
with self.model.get_connection() as conn:
# Check for orphaned prompt_tags entries
cursor = conn.execute("""
cursor = conn.execute(
"""
SELECT pt.prompt_id, pt.tag_id FROM prompt_tags pt
LEFT JOIN prompts p ON pt.prompt_id = p.id
WHERE p.id IS NULL
""")
"""
)
for ref in cursor.fetchall():
issues.append(
f"prompt_tags entry references non-existent prompt {ref['prompt_id']}"
)
# Check for orphaned image entries
cursor = conn.execute("""
cursor = conn.execute(
"""
SELECT gi.id, gi.prompt_id FROM generated_images gi
LEFT JOIN prompts p ON gi.prompt_id = p.id
WHERE p.id IS NULL
""")
"""
)
for ref in cursor.fetchall():
issues.append(
f"Image {ref['id']} references non-existent prompt {ref['prompt_id']}"
+3 -1
View File
@@ -780,7 +780,9 @@ class ImageRoutesMixin:
self.logger.warning(
f"Failed to generate thumbnail for {media_file.name}: {e}"
)
error_msg = f"Failed to generate thumbnail for {media_file.name}"
error_msg = (
f"Failed to generate thumbnail for {media_file.name}"
)
errors.append(error_msg)
if len(errors) <= 5:
+3
View File
@@ -9,3 +9,6 @@ flake8>=7.0.0
# Security scanning
bandit[toml]>=1.7.0
# Pre-commit hooks
pre-commit>=4.0.0
+12 -6
View File
@@ -137,10 +137,12 @@ class GalleryDiagnostics:
self.logger.info(f" [NOTE] Prompts in database: {prompt_count}")
# Check if generated_images table exists
cursor = conn.execute("""
cursor = conn.execute(
"""
SELECT name FROM sqlite_master
WHERE type='table' AND name='generated_images'
""")
"""
)
has_images_table = cursor.fetchone() is not None
self.logger.info(f" [IMG] Images table exists: {has_images_table}")
@@ -174,10 +176,12 @@ class GalleryDiagnostics:
conn.row_factory = sqlite3.Row
# Check if table exists
cursor = conn.execute("""
cursor = conn.execute(
"""
SELECT name FROM sqlite_master
WHERE type='table' AND name='generated_images'
""")
"""
)
if not cursor.fetchone():
return {
@@ -191,13 +195,15 @@ class GalleryDiagnostics:
self.logger.info(f" [STATS] Images in database: {image_count}")
# Get recent images
cursor = conn.execute("""
cursor = conn.execute(
"""
SELECT gi.*, p.text
FROM generated_images gi
LEFT JOIN prompts p ON gi.prompt_id = p.id
ORDER BY gi.generation_time DESC
LIMIT 5
""")
"""
)
recent_images = [dict(row) for row in cursor.fetchall()]
self.logger.info(f" [TIME] Recent images: {len(recent_images)}")