style: apply Black formatter (line-length=88) to all Python files

Automated formatting pass across 30 files to establish consistent code
style enforced by CI. No logic changes.
This commit is contained in:
Vito Sansevero
2026-02-07 08:30:09 -08:00
parent 224fcc7803
commit 2737a4b1ef
30 changed files with 3278 additions and 2531 deletions
+122 -76
View File
@@ -40,20 +40,20 @@ except ImportError:
def _get_project_root():
"""Get the project root directory (3 levels up from py/api/__init__.py)."""
return os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# ── Gzip compression middleware ────────────────────────────────────────
_GZIP_MIN_SIZE = 1024 # Only compress bodies larger than 1 KB
_GZIP_TYPES = frozenset((
'application/json',
'text/html',
'text/css',
'application/javascript',
'text/plain',
))
_GZIP_TYPES = frozenset(
(
"application/json",
"text/html",
"text/css",
"application/javascript",
"text/plain",
)
)
_gzip_registered = False
@@ -62,24 +62,24 @@ async def _gzip_middleware(request, handler):
"""Compress PromptManager responses when the client accepts gzip."""
response = await handler(request)
if not request.path.startswith('/prompt_manager/'):
if not request.path.startswith("/prompt_manager/"):
return response
# Only handle regular Response objects (not StreamResponse/WebSocket)
if not isinstance(response, web.Response):
return response
if 'gzip' not in request.headers.get('Accept-Encoding', ''):
if "gzip" not in request.headers.get("Accept-Encoding", ""):
return response
if 'Content-Encoding' in response.headers:
if "Content-Encoding" in response.headers:
return response
body = response.body
if body is None or len(body) < _GZIP_MIN_SIZE:
return response
content_type = response.content_type or ''
content_type = response.content_type or ""
if not any(ct in content_type for ct in _GZIP_TYPES):
return response
@@ -88,8 +88,8 @@ async def _gzip_middleware(request, handler):
return response
response.body = compressed
response.headers['Content-Encoding'] = 'gzip'
response.headers['Vary'] = 'Accept-Encoding'
response.headers["Content-Encoding"] = "gzip"
response.headers["Vary"] = "Accept-Encoding"
return response
@@ -117,7 +117,7 @@ class PromptManagerAPI(
def __init__(self):
"""Initialize the PromptManager API with database connection and cleanup."""
self.logger = get_logger('prompt_manager.api')
self.logger = get_logger("prompt_manager.api")
self.logger.info("Initializing PromptManager API")
self.db = PromptDatabase()
@@ -131,7 +131,9 @@ class PromptManagerAPI(
try:
removed = self.db.cleanup_duplicates()
if removed > 0:
self.logger.info(f"Startup cleanup: removed {removed} duplicate prompts")
self.logger.info(
f"Startup cleanup: removed {removed} duplicate prompts"
)
except Exception as e:
self.logger.error(f"Startup cleanup failed: {e}")
@@ -207,7 +209,9 @@ class PromptManagerAPI(
async def serve_gallery_ui(request):
try:
html_path = os.path.join(
_get_project_root(), "web", "metadata.html",
_get_project_root(),
"web",
"metadata.html",
)
if html_path not in self._html_cache:
@@ -222,7 +226,9 @@ class PromptManagerAPI(
)
return web.Response(
text=self._html_cache[html_path], content_type="text/html", charset="utf-8"
text=self._html_cache[html_path],
content_type="text/html",
charset="utf-8",
)
except Exception as e:
@@ -236,7 +242,9 @@ class PromptManagerAPI(
async def serve_admin_ui(request):
try:
html_path = os.path.join(
_get_project_root(), "web", "admin.html",
_get_project_root(),
"web",
"admin.html",
)
if html_path not in self._html_cache:
@@ -251,7 +259,9 @@ class PromptManagerAPI(
)
return web.Response(
text=self._html_cache[html_path], content_type="text/html", charset="utf-8"
text=self._html_cache[html_path],
content_type="text/html",
charset="utf-8",
)
except Exception as e:
@@ -265,7 +275,9 @@ class PromptManagerAPI(
async def serve_gallery_admin_ui(request):
try:
html_path = os.path.join(
_get_project_root(), "web", "gallery.html",
_get_project_root(),
"web",
"gallery.html",
)
if html_path not in self._html_cache:
@@ -280,7 +292,9 @@ class PromptManagerAPI(
)
return web.Response(
text=self._html_cache[html_path], content_type="text/html", charset="utf-8"
text=self._html_cache[html_path],
content_type="text/html",
charset="utf-8",
)
except Exception as e:
@@ -362,6 +376,7 @@ class PromptManagerAPI(
if not _gzip_registered:
try:
from ..config import server_instance
server_instance.app.middlewares.append(_gzip_middleware)
_gzip_registered = True
self.logger.info("Gzip compression middleware registered")
@@ -380,30 +395,36 @@ class PromptManagerAPI(
output_path = Path(output_dir) if output_dir else None
for prompt in prompts:
for image in prompt.get('images', []):
image_path_str = image.get('image_path', '')
for image in prompt.get("images", []):
image_path_str = image.get("image_path", "")
if not image_path_str:
continue
img_path = Path(image_path_str)
# Set fallback url via image ID
if image.get('id'):
image['url'] = f"/prompt_manager/images/{image['id']}/file"
if image.get("id"):
image["url"] = f"/prompt_manager/images/{image['id']}/file"
# Try to compute relative path and thumbnail URL
if output_path:
try:
rel_path = img_path.resolve().relative_to(output_path.resolve())
image['relative_path'] = str(rel_path)
image['url'] = f"/prompt_manager/images/serve/{url_quote(rel_path.as_posix(), safe='/')}"
image["relative_path"] = str(rel_path)
image["url"] = (
f"/prompt_manager/images/serve/{url_quote(rel_path.as_posix(), safe='/')}"
)
# Check for thumbnail
rel_no_ext = rel_path.with_suffix('')
thumb_rel = f"thumbnails/{rel_no_ext.as_posix()}_thumb{rel_path.suffix}"
rel_no_ext = rel_path.with_suffix("")
thumb_rel = (
f"thumbnails/{rel_no_ext.as_posix()}_thumb{rel_path.suffix}"
)
thumb_abs = output_path / thumb_rel
if thumb_abs.exists():
image['thumbnail_url'] = f"/prompt_manager/images/serve/{url_quote(thumb_rel, safe='/')}"
image["thumbnail_url"] = (
f"/prompt_manager/images/serve/{url_quote(thumb_rel, safe='/')}"
)
except (ValueError, RuntimeError):
pass
@@ -415,7 +436,7 @@ class PromptManagerAPI(
return {key: self._clean_nan_recursive(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [self._clean_nan_recursive(item) for item in obj]
elif isinstance(obj, float) and str(obj) == 'nan':
elif isinstance(obj, float) and str(obj) == "nan":
return None
else:
return obj
@@ -442,11 +463,13 @@ class PromptManagerAPI(
for i in range(max_depth):
# Check if current directory contains ComfyUI markers
comfyui_markers = ['main.py', 'nodes.py', 'server.py']
comfyui_markers = ["main.py", "nodes.py", "server.py"]
if any((current_dir / marker).exists() for marker in comfyui_markers):
output_dir = current_dir / "output"
if output_dir.exists() and output_dir.is_dir():
self.logger.debug(f"Found ComfyUI output directory via upward search: {output_dir}")
self.logger.debug(
f"Found ComfyUI output directory via upward search: {output_dir}"
)
self._cached_output_dir = str(output_dir)
return self._cached_output_dir
@@ -500,7 +523,7 @@ class PromptManagerAPI(
try:
with Image.open(image_path) as img:
metadata = {}
if hasattr(img, 'text'):
if hasattr(img, "text"):
for key, value in img.text.items():
metadata[key] = value
return metadata
@@ -511,11 +534,11 @@ class PromptManagerAPI(
def _parse_comfyui_prompt(self, metadata):
"""Parse ComfyUI and A1111 prompt data from metadata."""
result = {
'prompt': None,
'workflow': None,
'parameters': {},
'positive_prompt': None,
'negative_prompt': None
"prompt": None,
"workflow": None,
"parameters": {},
"positive_prompt": None,
"negative_prompt": None,
}
# Check for A1111 style parameters first (like parse-metadata.py)
@@ -523,40 +546,48 @@ class PromptManagerAPI(
params = metadata["parameters"]
lines = params.splitlines()
if lines:
result['positive_prompt'] = lines[0].strip()
result["positive_prompt"] = lines[0].strip()
for line in lines:
if line.lower().startswith("negative prompt:"):
result['negative_prompt'] = line.split(":", 1)[1].strip()
result["negative_prompt"] = line.split(":", 1)[1].strip()
break
# Store raw parameters too
result['parameters']['parameters'] = params
result["parameters"]["parameters"] = params
# If no A1111 format found, proceed with ComfyUI parsing
if result['positive_prompt'] is None:
if result["positive_prompt"] is None:
# Check for direct prompt field
if 'prompt' in metadata:
if "prompt" in metadata:
try:
prompt_data = json.loads(metadata['prompt'])
result['prompt'] = prompt_data
prompt_data = json.loads(metadata["prompt"])
result["prompt"] = prompt_data
except json.JSONDecodeError:
result['prompt'] = metadata['prompt']
result["prompt"] = metadata["prompt"]
# Check for workflow
if 'workflow' in metadata:
if "workflow" in metadata:
try:
workflow_data = json.loads(metadata['workflow'])
result['workflow'] = workflow_data
workflow_data = json.loads(metadata["workflow"])
result["workflow"] = workflow_data
except json.JSONDecodeError:
result['workflow'] = metadata['workflow']
result["workflow"] = metadata["workflow"]
# Check for other common ComfyUI fields
common_fields = ['positive', 'negative', 'steps', 'cfg', 'sampler', 'scheduler', 'seed']
common_fields = [
"positive",
"negative",
"steps",
"cfg",
"sampler",
"scheduler",
"seed",
]
for field in common_fields:
if field in metadata:
try:
result['parameters'][field] = json.loads(metadata[field])
result["parameters"][field] = json.loads(metadata[field])
except json.JSONDecodeError:
result['parameters'][field] = metadata[field]
result["parameters"][field] = metadata[field]
return result
@@ -567,40 +598,46 @@ class PromptManagerAPI(
if isinstance(value, str):
return value
elif isinstance(value, list):
return ' '.join(str(item) for item in value if item)
return " ".join(str(item) for item in value if item)
elif value is not None:
return str(value)
return None
# First check if we already extracted a positive prompt (A1111 format)
if parsed_data.get('positive_prompt'):
return parsed_data['positive_prompt']
if parsed_data.get("positive_prompt"):
return parsed_data["positive_prompt"]
# Check if prompt is already a string
if isinstance(parsed_data.get('prompt'), str):
return parsed_data['prompt']
if isinstance(parsed_data.get("prompt"), str):
return parsed_data["prompt"]
# Check if prompt is a simple value that can be converted
if parsed_data.get('prompt') and not isinstance(parsed_data.get('prompt'), dict):
return safe_to_string(parsed_data['prompt'])
if parsed_data.get("prompt") and not isinstance(
parsed_data.get("prompt"), dict
):
return safe_to_string(parsed_data["prompt"])
prompt_data = parsed_data.get('prompt')
prompt_data = parsed_data.get("prompt")
if isinstance(prompt_data, dict):
# Use the enhanced logic from parse-metadata.py
positive_prompt = self._extract_positive_prompt_from_comfyui_data(prompt_data)
positive_prompt = self._extract_positive_prompt_from_comfyui_data(
prompt_data
)
if positive_prompt:
return positive_prompt
# Check workflow data if available
workflow_data = parsed_data.get('workflow')
workflow_data = parsed_data.get("workflow")
if isinstance(workflow_data, dict):
positive_prompt = self._extract_positive_prompt_from_comfyui_data(workflow_data)
positive_prompt = self._extract_positive_prompt_from_comfyui_data(
workflow_data
)
if positive_prompt:
return positive_prompt
# Check parameters for positive prompt
if parsed_data.get('parameters', {}).get('positive'):
return safe_to_string(parsed_data['parameters']['positive'])
if parsed_data.get("parameters", {}).get("positive"):
return safe_to_string(parsed_data["parameters"]["positive"])
return None
@@ -652,12 +689,21 @@ class PromptManagerAPI(
# Strategy 2: For text encoder nodes, check widgets_values
class_type = node.get("class_type", node.get("type", ""))
text_encoder_types = [
'CLIPTextEncode', 'CLIPTextEncodeSDXL', 'CLIPTextEncodeSDXLRefiner',
'CLIPTextEncodeFlux', 'PromptManager', 'PromptManagerText',
'BNK_CLIPTextEncoder', 'Text Encoder', 'CLIP Text Encode'
"CLIPTextEncode",
"CLIPTextEncodeSDXL",
"CLIPTextEncodeSDXLRefiner",
"CLIPTextEncodeFlux",
"PromptManager",
"PromptManagerText",
"BNK_CLIPTextEncoder",
"Text Encoder",
"CLIP Text Encode",
]
if any(encoder_type.lower() in class_type.lower() for encoder_type in text_encoder_types):
if any(
encoder_type.lower() in class_type.lower()
for encoder_type in text_encoder_types
):
widgets_values = node.get("widgets_values", [])
if widgets_values and len(widgets_values) > 0:
if isinstance(widgets_values[0], str) and widgets_values[0].strip():
@@ -721,8 +767,8 @@ class PromptManagerAPI(
text_val = self._find_text_in_node(node)
if text_val:
# Try to determine if this is positive or negative
node_title = node.get('title', '').lower()
if 'neg' not in node_title and 'negative' not in node_title:
node_title = node.get("title", "").lower()
if "neg" not in node_title and "negative" not in node_title:
# Prioritize non-negative prompts
text_nodes.insert(0, text_val)
else:
+454 -278
View File
File diff suppressed because it is too large Load Diff
+145 -138
View File
@@ -47,24 +47,23 @@ class AutotagRoutesMixin:
service = get_autotag_service()
models_status = service.get_models_status()
return web.json_response({
'success': True,
'models': models_status,
'default_prompt': service.default_prompt,
'model_loaded': service.is_model_loaded(),
'loaded_model_type': service.get_loaded_model_type()
})
return web.json_response(
{
"success": True,
"models": models_status,
"default_prompt": service.default_prompt,
"model_loaded": service.is_model_loaded(),
"loaded_model_type": service.get_loaded_model_type(),
}
)
except Exception as e:
self.logger.error(f"Get autotag models error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
async def download_autotag_model(self, request):
"""Download an AutoTag model with streaming progress."""
model_type = request.match_info.get('model_type')
model_type = request.match_info.get("model_type")
async def stream_response():
try:
@@ -78,15 +77,14 @@ class AutotagRoutesMixin:
yield f"data: {json.dumps({'type': 'progress', 'progress': 0, 'status': 'Starting download...'})}\n\n"
progress_data = {'last_progress': 0}
progress_data = {"last_progress": 0}
def progress_callback(status: str, progress: float):
progress_data['last_progress'] = progress
progress_data["last_progress"] = progress
loop = asyncio.get_event_loop()
success = await loop.run_in_executor(
None,
lambda: service.download_model(model_type, progress_callback)
None, lambda: service.download_model(model_type, progress_callback)
)
if success:
@@ -100,28 +98,28 @@ class AutotagRoutesMixin:
response = web.StreamResponse(
status=200,
reason='OK',
reason="OK",
headers={
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
)
await response.prepare(request)
async for chunk in stream_response():
await response.write(chunk.encode('utf-8'))
await response.write(chunk.encode("utf-8"))
await response.write_eof()
return response
async def start_autotag(self, request):
"""Start batch auto-tagging with streaming progress."""
model_type = request.query.get('model_type', 'gguf')
custom_prompt = request.query.get('prompt', '')
skip_tagged = request.query.get('skip_tagged', 'true').lower() == 'true'
keep_in_memory = request.query.get('keep_in_memory', 'true').lower() == 'true'
model_type = request.query.get("model_type", "gguf")
custom_prompt = request.query.get("prompt", "")
skip_tagged = request.query.get("skip_tagged", "true").lower() == "true"
keep_in_memory = request.query.get("keep_in_memory", "true").lower() == "true"
use_gpu = True
async def stream_response():
@@ -132,7 +130,7 @@ class AutotagRoutesMixin:
service = get_autotag_service()
status = service.get_models_status()
if not status.get(model_type, {}).get('downloaded'):
if not status.get(model_type, {}).get("downloaded"):
yield f"data: {json.dumps({'type': 'error', 'message': f'Model {model_type} not downloaded'})}\n\n"
return
@@ -141,8 +139,7 @@ class AutotagRoutesMixin:
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(
None,
lambda: service.load_model(model_type, use_gpu)
None, lambda: service.load_model(model_type, use_gpu)
)
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'message': f'Failed to load model: {str(e)}'})}\n\n"
@@ -172,8 +169,8 @@ class AutotagRoutesMixin:
last_update_time = _time.monotonic()
for i, image_data in enumerate(images):
image_path = image_data.get('image_path')
prompt_id = image_data.get('prompt_id')
image_path = image_data.get("image_path")
prompt_id = image_data.get("prompt_id")
if not image_path or not prompt_id:
skipped += 1
@@ -200,10 +197,12 @@ class AutotagRoutesMixin:
continue
if skip_tagged:
prompt_tags = image_data.get('prompt_tags', [])
prompt_tags = image_data.get("prompt_tags", [])
if isinstance(prompt_tags, str):
prompt_tags = [t.strip() for t in prompt_tags.split(',') if t.strip()]
real_tags = [t for t in prompt_tags if t != 'auto-scanned']
prompt_tags = [
t.strip() for t in prompt_tags.split(",") if t.strip()
]
real_tags = [t for t in prompt_tags if t != "auto-scanned"]
if real_tags:
tagged_prompt_ids.add(prompt_id)
skipped += 1
@@ -217,18 +216,23 @@ class AutotagRoutesMixin:
try:
tags = await loop.run_in_executor(
None,
lambda p=str(image_path): service.generate_tags(p)
None, lambda p=str(image_path): service.generate_tags(p)
)
processed += 1
if tags:
existing_prompt = await self._run_in_executor(self.db.get_prompt_by_id, prompt_id)
existing_prompt = await self._run_in_executor(
self.db.get_prompt_by_id, prompt_id
)
if existing_prompt:
existing_tags = existing_prompt.get('tags', [])
existing_tags = existing_prompt.get("tags", [])
if isinstance(existing_tags, str):
existing_tags = [t.strip() for t in existing_tags.split(',') if t.strip()]
existing_tags = [
t.strip()
for t in existing_tags.split(",")
if t.strip()
]
new_tags = [t for t in tags if t not in existing_tags]
if new_tags:
@@ -236,7 +240,7 @@ class AutotagRoutesMixin:
await self._run_in_executor(
self.db.update_prompt_metadata,
prompt_id,
tags=all_tags
tags=all_tags,
)
tagged += 1
else:
@@ -260,32 +264,33 @@ class AutotagRoutesMixin:
if not keep_in_memory:
service.unload_model()
model_status = 'Model unloaded'
model_status = "Model unloaded"
else:
model_status = 'Model kept in memory'
model_status = "Model kept in memory"
yield f"data: {json.dumps({'type': 'complete', 'progress': 100, 'processed': processed, 'tagged': tagged, 'skipped': skipped, 'errors': errors, 'status': 'Complete', 'model_status': model_status, 'model_loaded': keep_in_memory})}\n\n"
except Exception as e:
self.logger.error(f"AutoTag error: {e}")
import traceback
self.logger.error(f"AutoTag traceback: {traceback.format_exc()}")
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
response = web.StreamResponse(
status=200,
reason='OK',
reason="OK",
headers={
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
)
await response.prepare(request)
async for chunk in stream_response():
await response.write(chunk.encode('utf-8'))
await response.write(chunk.encode("utf-8"))
await response.write_eof()
return response
@@ -294,26 +299,27 @@ class AutotagRoutesMixin:
"""Generate tags for a single image."""
try:
data = await request.json()
image_path = data.get('image_path')
model_type = data.get('model_type', 'gguf')
custom_prompt = data.get('prompt')
use_gpu = data.get('use_gpu', True)
image_path = data.get("image_path")
model_type = data.get("model_type", "gguf")
custom_prompt = data.get("prompt")
use_gpu = data.get("use_gpu", True)
if not image_path:
return web.json_response({
'success': False,
'error': 'image_path is required'
}, status=400)
return web.json_response(
{"success": False, "error": "image_path is required"}, status=400
)
from ..autotag import get_autotag_service
service = get_autotag_service()
if not service.is_model_loaded() or service.get_loaded_model_type() != model_type:
if (
not service.is_model_loaded()
or service.get_loaded_model_type() != model_type
):
loop = asyncio.get_event_loop()
await loop.run_in_executor(
None,
lambda: service.load_model(model_type, use_gpu)
None, lambda: service.load_model(model_type, use_gpu)
)
if custom_prompt:
@@ -321,81 +327,74 @@ class AutotagRoutesMixin:
loop = asyncio.get_event_loop()
tags = await loop.run_in_executor(
None,
lambda: service.generate_tags(image_path)
None, lambda: service.generate_tags(image_path)
)
prompt_id = None
try:
prompt_id = await self._run_in_executor(self.db.get_prompt_id_for_image, image_path)
prompt_id = await self._run_in_executor(
self.db.get_prompt_id_for_image, image_path
)
except Exception as e:
self.logger.warning(f"Could not find linked prompt: {e}")
return web.json_response({
'success': True,
'tags': tags,
'prompt_id': prompt_id,
'image_path': image_path
})
return web.json_response(
{
"success": True,
"tags": tags,
"prompt_id": prompt_id,
"image_path": image_path,
}
)
except Exception as e:
self.logger.error(f"AutoTag single error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
async def apply_autotag(self, request):
"""Apply selected tags to a prompt."""
try:
data = await request.json()
prompt_id = data.get('prompt_id')
tags = data.get('tags', [])
prompt_id = data.get("prompt_id")
tags = data.get("tags", [])
if not prompt_id:
return web.json_response({
'success': False,
'error': 'prompt_id is required'
}, status=400)
return web.json_response(
{"success": False, "error": "prompt_id is required"}, status=400
)
if not tags:
return web.json_response({
'success': True,
'message': 'No tags to apply'
})
return web.json_response(
{"success": True, "message": "No tags to apply"}
)
prompt = await self._run_in_executor(self.db.get_prompt_by_id, prompt_id)
if not prompt:
return web.json_response({
'success': False,
'error': f'Prompt {prompt_id} not found'
}, status=404)
return web.json_response(
{"success": False, "error": f"Prompt {prompt_id} not found"},
status=404,
)
existing_tags = prompt.get('tags', [])
existing_tags = prompt.get("tags", [])
if isinstance(existing_tags, str):
existing_tags = [t.strip() for t in existing_tags.split(',') if t.strip()]
existing_tags = [
t.strip() for t in existing_tags.split(",") if t.strip()
]
new_tags = [t for t in tags if t not in existing_tags]
all_tags = existing_tags + new_tags
await self._run_in_executor(
self.db.update_prompt_metadata,
prompt_id,
tags=all_tags
self.db.update_prompt_metadata, prompt_id, tags=all_tags
)
return web.json_response({
'success': True,
'added_tags': new_tags,
'total_tags': len(all_tags)
})
return web.json_response(
{"success": True, "added_tags": new_tags, "total_tags": len(all_tags)}
)
except Exception as e:
self.logger.error(f"Apply autotag error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
async def unload_autotag_model(self, request):
"""Manually unload the AutoTag model from memory."""
@@ -405,39 +404,45 @@ class AutotagRoutesMixin:
service = get_autotag_service()
if not service.is_model_loaded():
return web.json_response({
'success': True,
'message': 'No model was loaded'
})
return web.json_response(
{"success": True, "message": "No model was loaded"}
)
model_type = service.get_loaded_model_type()
service.unload_model()
return web.json_response({
'success': True,
'message': f'{model_type.upper()} model unloaded successfully',
'model_loaded': False
})
return web.json_response(
{
"success": True,
"message": f"{model_type.upper()} model unloaded successfully",
"model_loaded": False,
}
)
except Exception as e:
self.logger.error(f"Unload autotag model error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
async def scan_output_dir(self, request):
"""Scan ComfyUI output directory for images."""
try:
output_dir = self._find_comfyui_output_dir()
if not output_dir:
return web.json_response({
'success': False,
'error': 'ComfyUI output directory not found'
}, status=404)
return web.json_response(
{"success": False, "error": "ComfyUI output directory not found"},
status=404,
)
output_path = Path(output_dir)
image_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff']
image_extensions = [
".png",
".jpg",
".jpeg",
".gif",
".webp",
".bmp",
".tiff",
]
images = []
seen_paths = set()
@@ -445,7 +450,7 @@ class AutotagRoutesMixin:
for ext in image_extensions:
for pattern in [f"*{ext.lower()}", f"*{ext.upper()}"]:
for image_path in output_path.rglob(pattern):
if 'thumbnails' not in image_path.parts:
if "thumbnails" not in image_path.parts:
normalized_path = str(image_path).lower()
if normalized_path not in seen_paths:
seen_paths.add(normalized_path)
@@ -455,35 +460,37 @@ class AutotagRoutesMixin:
thumbnail_url = None
thumbnails_dir = output_path / "thumbnails"
if thumbnails_dir.exists():
rel_path_no_ext = rel_path.with_suffix('')
rel_path_no_ext = rel_path.with_suffix("")
thumbnail_rel_path = f"thumbnails/{rel_path_no_ext.as_posix()}_thumb{image_path.suffix}"
thumbnail_abs_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb{image_path.suffix}"
thumbnail_abs_path = (
thumbnails_dir
/ f"{rel_path_no_ext.as_posix()}_thumb{image_path.suffix}"
)
if thumbnail_abs_path.exists():
from urllib.parse import quote
thumbnail_url = f'/prompt_manager/images/serve/{quote(thumbnail_rel_path, safe="/")}'
from urllib.parse import quote as url_quote
images.append({
'filename': image_path.name,
'path': str(image_path),
'relative_path': str(rel_path),
'url': f'/prompt_manager/images/serve/{url_quote(rel_path.as_posix(), safe="/")}',
'thumbnail_url': thumbnail_url
})
images.sort(key=lambda x: x['filename'])
images.append(
{
"filename": image_path.name,
"path": str(image_path),
"relative_path": str(rel_path),
"url": f'/prompt_manager/images/serve/{url_quote(rel_path.as_posix(), safe="/")}',
"thumbnail_url": thumbnail_url,
}
)
images.sort(key=lambda x: x["filename"])
self.logger.info(f"Found {len(images)} images in output directory")
return web.json_response({
'success': True,
'images': images,
'count': len(images)
})
return web.json_response(
{"success": True, "images": images, "count": len(images)}
)
except Exception as e:
self.logger.error(f"Scan output dir error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
+515 -383
View File
File diff suppressed because it is too large Load Diff
+69 -86
View File
@@ -43,7 +43,10 @@ class LoggingRoutesMixin:
from ...utils.logging_config import get_logger_manager
except ImportError:
import sys
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
sys.path.insert(0, current_dir)
from utils.logging_config import get_logger_manager
return get_logger_manager()
@@ -53,8 +56,8 @@ class LoggingRoutesMixin:
try:
logger_manager = self._get_logger_manager()
limit = int(request.query.get('limit', 100))
level = request.query.get('level', None)
limit = int(request.query.get("limit", 100))
level = request.query.get("level", None)
if limit > 1000:
limit = 1000
@@ -63,21 +66,21 @@ class LoggingRoutesMixin:
logs = logger_manager.get_recent_logs(limit=limit, level=level)
return web.json_response({
'success': True,
'logs': logs,
'count': len(logs),
'level_filter': level,
'limit': limit
})
return web.json_response(
{
"success": True,
"logs": logs,
"count": len(logs),
"level_filter": level,
"limit": limit,
}
)
except Exception as e:
self.logger.error(f"Get logs error: {e}")
return web.json_response({
'success': False,
'error': str(e),
'logs': []
}, status=500)
return web.json_response(
{"success": False, "error": str(e), "logs": []}, status=500
)
async def get_log_files(self, request):
"""Get information about available log files."""
@@ -85,58 +88,49 @@ class LoggingRoutesMixin:
logger_manager = self._get_logger_manager()
log_files = logger_manager.get_log_files()
return web.json_response({
'success': True,
'files': log_files,
'count': len(log_files)
})
return web.json_response(
{"success": True, "files": log_files, "count": len(log_files)}
)
except Exception as e:
self.logger.error(f"Get log files error: {e}")
return web.json_response({
'success': False,
'error': str(e),
'files': []
}, status=500)
return web.json_response(
{"success": False, "error": str(e), "files": []}, status=500
)
async def download_log_file(self, request):
"""Download a specific log file."""
try:
filename = request.match_info['filename']
filename = request.match_info["filename"]
logger_manager = self._get_logger_manager()
if not filename or '..' in filename or '/' in filename or '\\' in filename:
return web.json_response({
'success': False,
'error': 'Invalid filename'
}, status=400)
if not filename or ".." in filename or "/" in filename or "\\" in filename:
return web.json_response(
{"success": False, "error": "Invalid filename"}, status=400
)
log_file_path = logger_manager.log_dir / filename
if not log_file_path.exists():
return web.json_response({
'success': False,
'error': 'Log file not found'
}, status=404)
return web.json_response(
{"success": False, "error": "Log file not found"}, status=404
)
with open(log_file_path, 'rb') as f:
with open(log_file_path, "rb") as f:
file_content = f.read()
return web.Response(
body=file_content,
content_type='text/plain',
content_type="text/plain",
headers={
'Content-Disposition': f'attachment; filename="{filename}"',
'Content-Length': str(len(file_content))
}
"Content-Disposition": f'attachment; filename="{filename}"',
"Content-Length": str(len(file_content)),
},
)
except Exception as e:
self.logger.error(f"Download log file error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
async def truncate_logs(self, request):
"""Truncate all log files."""
@@ -144,18 +138,17 @@ class LoggingRoutesMixin:
logger_manager = self._get_logger_manager()
results = logger_manager.truncate_logs()
return web.json_response({
'success': True,
'message': f"Truncated {len(results['truncated'])} log files",
'results': results
})
return web.json_response(
{
"success": True,
"message": f"Truncated {len(results['truncated'])} log files",
"results": results,
}
)
except Exception as e:
self.logger.error(f"Truncate logs error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
async def get_log_config(self, request):
"""Get current logging configuration."""
@@ -163,17 +156,11 @@ class LoggingRoutesMixin:
logger_manager = self._get_logger_manager()
config = logger_manager.get_config()
return web.json_response({
'success': True,
'config': config
})
return web.json_response({"success": True, "config": config})
except Exception as e:
self.logger.error(f"Get log config error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
async def update_log_config(self, request):
"""Update logging configuration."""
@@ -181,29 +168,31 @@ class LoggingRoutesMixin:
data = await request.json()
logger_manager = self._get_logger_manager()
if 'level' in data:
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
if data['level'].upper() not in valid_levels:
return web.json_response({
'success': False,
'error': f'Invalid log level. Must be one of: {valid_levels}'
}, status=400)
data['level'] = data['level'].upper()
if "level" in data:
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
if data["level"].upper() not in valid_levels:
return web.json_response(
{
"success": False,
"error": f"Invalid log level. Must be one of: {valid_levels}",
},
status=400,
)
data["level"] = data["level"].upper()
logger_manager.update_config(data)
return web.json_response({
'success': True,
'message': 'Logging configuration updated',
'config': logger_manager.get_config()
})
return web.json_response(
{
"success": True,
"message": "Logging configuration updated",
"config": logger_manager.get_config(),
}
)
except Exception as e:
self.logger.error(f"Update log config error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
async def get_log_stats(self, request):
"""Get logging statistics."""
@@ -211,14 +200,8 @@ class LoggingRoutesMixin:
logger_manager = self._get_logger_manager()
stats = logger_manager.get_log_stats()
return web.json_response({
'success': True,
'stats': stats
})
return web.json_response({"success": True, "stats": stats})
except Exception as e:
self.logger.error(f"Get log stats error: {e}")
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
return web.json_response({"success": False, "error": str(e)}, status=500)
+217 -128
View File
@@ -153,22 +153,26 @@ class PromptRoutesMixin:
elif limit < 1:
limit = 1
results = await self._run_in_executor(self.db.get_recent_prompts, limit=limit, offset=offset)
self._enrich_prompt_images(results['prompts'])
results = await self._run_in_executor(
self.db.get_recent_prompts, limit=limit, offset=offset
)
self._enrich_prompt_images(results["prompts"])
return web.json_response({
"success": True,
"results": results['prompts'],
"pagination": {
"total": results['total'],
"limit": results['limit'],
"offset": results['offset'],
"page": results['page'],
"total_pages": results['total_pages'],
"has_more": results['has_more'],
"count": len(results['prompts'])
return web.json_response(
{
"success": True,
"results": results["prompts"],
"pagination": {
"total": results["total"],
"limit": results["limit"],
"offset": results["offset"],
"page": results["page"],
"total_pages": results["total_pages"],
"has_more": results["has_more"],
"count": len(results["prompts"]),
},
}
})
)
except Exception as e:
self.logger.error(f"Recent prompts error: {e}", exc_info=True)
@@ -177,7 +181,7 @@ class PromptRoutesMixin:
"success": False,
"error": f"Failed to get recent prompts: {str(e)}",
"results": [],
"pagination": {"total": 0, "page": 1, "total_pages": 0}
"pagination": {"total": 0, "page": 1, "total_pages": 0},
},
status=500,
)
@@ -190,7 +194,11 @@ class PromptRoutesMixin:
except Exception as e:
self.logger.error(f"Categories error: {e}")
return web.json_response(
{"success": False, "error": f"Failed to get categories: {str(e)}", "categories": []},
{
"success": False,
"error": f"Failed to get categories: {str(e)}",
"categories": [],
},
status=500,
)
@@ -202,7 +210,11 @@ class PromptRoutesMixin:
except Exception as e:
self.logger.error(f"Tags error: {e}")
return web.json_response(
{"success": False, "error": f"Failed to get tags: {str(e)}", "tags": []},
{
"success": False,
"error": f"Failed to get tags: {str(e)}",
"tags": [],
},
status=500,
)
@@ -214,25 +226,32 @@ class PromptRoutesMixin:
offset = int(request.query.get("offset", 0))
except (ValueError, TypeError):
return web.json_response(
{"success": False, "error": "Invalid limit or offset parameter"}, status=400
{"success": False, "error": "Invalid limit or offset parameter"},
status=400,
)
search = request.query.get("search", "").strip() or None
sort = request.query.get("sort", "alpha_asc")
result = await self._run_in_executor(self.db.get_tags_with_counts, limit, offset, search, sort)
untagged_count = await self._run_in_executor(self.db.get_untagged_prompts_count)
result = await self._run_in_executor(
self.db.get_tags_with_counts, limit, offset, search, sort
)
untagged_count = await self._run_in_executor(
self.db.get_untagged_prompts_count
)
return web.json_response({
"success": True,
"tags": result['tags'],
"untagged_count": untagged_count,
"pagination": {
"total": result['total'],
"limit": result['limit'],
"offset": result['offset'],
"has_more": result['has_more']
return web.json_response(
{
"success": True,
"tags": result["tags"],
"untagged_count": untagged_count,
"pagination": {
"total": result["total"],
"limit": result["limit"],
"offset": result["offset"],
"has_more": result["has_more"],
},
}
})
)
except Exception as e:
self.logger.error(f"Tags stats error: {e}", exc_info=True)
return web.json_response({"success": False, "error": str(e)}, status=500)
@@ -241,6 +260,7 @@ class PromptRoutesMixin:
"""Get prompts for a single tag."""
try:
from urllib.parse import unquote
tag_name = unquote(request.match_info.get("tag_name", ""))
if not tag_name:
return web.json_response(
@@ -252,23 +272,28 @@ class PromptRoutesMixin:
offset = int(request.query.get("offset", 0))
except (ValueError, TypeError):
return web.json_response(
{"success": False, "error": "Invalid limit or offset parameter"}, status=400
{"success": False, "error": "Invalid limit or offset parameter"},
status=400,
)
result = await self._run_in_executor(self.db.get_prompts_by_tags, [tag_name], 'and', limit, offset)
self._enrich_prompt_images(result['prompts'])
result = await self._run_in_executor(
self.db.get_prompts_by_tags, [tag_name], "and", limit, offset
)
self._enrich_prompt_images(result["prompts"])
return web.json_response({
"success": True,
"tag": tag_name,
"prompts": result['prompts'],
"pagination": {
"total": result['total'],
"limit": result['limit'],
"offset": result['offset'],
"has_more": result['has_more']
return web.json_response(
{
"success": True,
"tag": tag_name,
"prompts": result["prompts"],
"pagination": {
"total": result["total"],
"limit": result["limit"],
"offset": result["offset"],
"has_more": result["has_more"],
},
}
})
)
except Exception as e:
self.logger.error(f"Tag prompts error: {e}", exc_info=True)
return web.json_response({"success": False, "error": str(e)}, status=500)
@@ -284,22 +309,30 @@ class PromptRoutesMixin:
offset = int(request.query.get("offset", 0))
except (ValueError, TypeError):
return web.json_response(
{"success": False, "error": "Invalid limit or offset parameter"}, status=400
{
"success": False,
"error": "Invalid limit or offset parameter",
},
status=400,
)
result = await self._run_in_executor(self.db.get_untagged_prompts, limit, offset)
self._enrich_prompt_images(result['prompts'])
return web.json_response({
"success": True,
"tags": [],
"mode": "untagged",
"prompts": result['prompts'],
"pagination": {
"total": result['total'],
"limit": result['limit'],
"offset": result['offset'],
"has_more": result['has_more']
result = await self._run_in_executor(
self.db.get_untagged_prompts, limit, offset
)
self._enrich_prompt_images(result["prompts"])
return web.json_response(
{
"success": True,
"tags": [],
"mode": "untagged",
"prompts": result["prompts"],
"pagination": {
"total": result["total"],
"limit": result["limit"],
"offset": result["offset"],
"has_more": result["has_more"],
},
}
})
)
tags_str = request.query.get("tags", "").strip()
if not tags_str:
@@ -317,24 +350,29 @@ class PromptRoutesMixin:
offset = int(request.query.get("offset", 0))
except (ValueError, TypeError):
return web.json_response(
{"success": False, "error": "Invalid limit or offset parameter"}, status=400
{"success": False, "error": "Invalid limit or offset parameter"},
status=400,
)
result = await self._run_in_executor(self.db.get_prompts_by_tags, tags_list, mode, limit, offset)
self._enrich_prompt_images(result['prompts'])
result = await self._run_in_executor(
self.db.get_prompts_by_tags, tags_list, mode, limit, offset
)
self._enrich_prompt_images(result["prompts"])
return web.json_response({
"success": True,
"tags": tags_list,
"mode": mode,
"prompts": result['prompts'],
"pagination": {
"total": result['total'],
"limit": result['limit'],
"offset": result['offset'],
"has_more": result['has_more']
return web.json_response(
{
"success": True,
"tags": tags_list,
"mode": mode,
"prompts": result["prompts"],
"pagination": {
"total": result["total"],
"limit": result["limit"],
"offset": result["offset"],
"has_more": result["has_more"],
},
}
})
)
except Exception as e:
self.logger.error(f"Tags filter error: {e}", exc_info=True)
return web.json_response({"success": False, "error": str(e)}, status=500)
@@ -343,6 +381,7 @@ class PromptRoutesMixin:
"""Rename a tag across all prompts."""
try:
from urllib.parse import unquote
tag_name = unquote(request.match_info.get("tag_name", ""))
if not tag_name:
return web.json_response(
@@ -361,16 +400,20 @@ class PromptRoutesMixin:
{"success": False, "error": "New tag name required"}, status=400
)
result = await self._run_in_executor(self.db.rename_tag_all_prompts, tag_name, new_name)
result = await self._run_in_executor(
self.db.rename_tag_all_prompts, tag_name, new_name
)
resp = {
"success": True,
"old_name": tag_name,
"new_name": new_name,
"affected_count": result['affected_count']
"affected_count": result["affected_count"],
}
if result.get('skipped_count', 0) > 0:
resp['skipped_count'] = result['skipped_count']
resp['warning'] = f"{result['skipped_count']} prompt(s) had corrupted tag data and were skipped"
if result.get("skipped_count", 0) > 0:
resp["skipped_count"] = result["skipped_count"]
resp["warning"] = (
f"{result['skipped_count']} prompt(s) had corrupted tag data and were skipped"
)
return web.json_response(resp)
except Exception as e:
self.logger.error(f"Rename tag error: {e}", exc_info=True)
@@ -380,21 +423,26 @@ class PromptRoutesMixin:
"""Delete a tag from all prompts."""
try:
from urllib.parse import unquote
tag_name = unquote(request.match_info.get("tag_name", ""))
if not tag_name:
return web.json_response(
{"success": False, "error": "Tag name required"}, status=400
)
result = await self._run_in_executor(self.db.delete_tag_all_prompts, tag_name)
result = await self._run_in_executor(
self.db.delete_tag_all_prompts, tag_name
)
resp = {
"success": True,
"tag_name": tag_name,
"affected_count": result['affected_count']
"affected_count": result["affected_count"],
}
if result.get('skipped_count', 0) > 0:
resp['skipped_count'] = result['skipped_count']
resp['warning'] = f"{result['skipped_count']} prompt(s) had corrupted tag data and were skipped"
if result.get("skipped_count", 0) > 0:
resp["skipped_count"] = result["skipped_count"]
resp["warning"] = (
f"{result['skipped_count']} prompt(s) had corrupted tag data and were skipped"
)
return web.json_response(resp)
except Exception as e:
self.logger.error(f"Delete tag error: {e}", exc_info=True)
@@ -421,16 +469,20 @@ class PromptRoutesMixin:
{"success": False, "error": "Target tag required"}, status=400
)
result = await self._run_in_executor(self.db.merge_tags, source_tags, target_tag)
result = await self._run_in_executor(
self.db.merge_tags, source_tags, target_tag
)
resp = {
"success": True,
"target_tag": target_tag,
"affected_count": result['affected_count'],
"tags_merged": result['tags_merged']
"affected_count": result["affected_count"],
"tags_merged": result["tags_merged"],
}
if result.get('skipped_count', 0) > 0:
resp['skipped_count'] = result['skipped_count']
resp['warning'] = f"{result['skipped_count']} prompt(s) had corrupted tag data and were skipped"
if result.get("skipped_count", 0) > 0:
resp["skipped_count"] = result["skipped_count"]
resp["warning"] = (
f"{result['skipped_count']} prompt(s) had corrupted tag data and were skipped"
)
return web.json_response(resp)
except Exception as e:
self.logger.error(f"Merge tags error: {e}", exc_info=True)
@@ -440,9 +492,13 @@ class PromptRoutesMixin:
"""Save a new prompt with metadata and duplicate detection."""
try:
from utils.validators import (
validate_prompt_text, validate_rating, validate_tags,
validate_category, sanitize_input,
validate_prompt_text,
validate_rating,
validate_tags,
validate_category,
sanitize_input,
)
data = await request.json()
text = data.get("text", "").strip()
@@ -469,25 +525,30 @@ class PromptRoutesMixin:
text = sanitize_input(text)
from utils.hashing import generate_prompt_hash
prompt_hash = generate_prompt_hash(text)
existing = await self._run_in_executor(self.db.get_prompt_by_hash, prompt_hash)
existing = await self._run_in_executor(
self.db.get_prompt_by_hash, prompt_hash
)
if existing:
if any([category, tags, rating, notes]):
await self._run_in_executor(
self.db.update_prompt_metadata,
prompt_id=existing['id'],
prompt_id=existing["id"],
category=category,
tags=tags,
rating=rating,
notes=notes
notes=notes,
)
return web.json_response({
"success": True,
"prompt_id": existing['id'],
"message": "Prompt already exists, metadata updated",
"is_duplicate": True
})
return web.json_response(
{
"success": True,
"prompt_id": existing["id"],
"message": "Prompt already exists, metadata updated",
"is_duplicate": True,
}
)
prompt_id = await self._run_in_executor(
self.db.save_prompt,
@@ -499,11 +560,13 @@ class PromptRoutesMixin:
prompt_hash=prompt_hash,
)
return web.json_response({
"success": True,
"prompt_id": prompt_id,
"message": "Prompt saved successfully",
})
return web.json_response(
{
"success": True,
"prompt_id": prompt_id,
"message": "Prompt saved successfully",
}
)
except Exception as e:
self.logger.error(f"Save error: {e}", exc_info=True)
@@ -524,7 +587,10 @@ class PromptRoutesMixin:
)
else:
return web.json_response(
{"success": False, "error": "Prompt not found or could not be deleted"},
{
"success": False,
"error": "Prompt not found or could not be deleted",
},
status=404,
)
@@ -562,7 +628,9 @@ class PromptRoutesMixin:
new_text = sanitize_input(new_text)
updated = await self._run_in_executor(self.db.update_prompt_text, prompt_id, new_text)
updated = await self._run_in_executor(
self.db.update_prompt_text, prompt_id, new_text
)
if updated:
return web.json_response(
{"success": True, "message": "Prompt updated successfully"}
@@ -599,7 +667,9 @@ class PromptRoutesMixin:
{"success": False, "error": str(ve)}, status=400
)
updated = await self._run_in_executor(self.db.update_prompt_rating, prompt_id, rating)
updated = await self._run_in_executor(
self.db.update_prompt_rating, prompt_id, rating
)
if updated:
return web.json_response(
{"success": True, "message": "Rating updated successfully"}
@@ -644,7 +714,9 @@ class PromptRoutesMixin:
if new_tag not in current_tags:
current_tags.append(new_tag)
await self._run_in_executor(self.db.set_prompt_tags, prompt_id, current_tags)
await self._run_in_executor(
self.db.set_prompt_tags, prompt_id, current_tags
)
return web.json_response(
{"success": True, "message": "Tag added successfully"}
@@ -674,7 +746,8 @@ class PromptRoutesMixin:
if not new_tags or not isinstance(new_tags, list):
return web.json_response(
{"success": False, "error": "Tags must be a non-empty list"}, status=400
{"success": False, "error": "Tags must be a non-empty list"},
status=400,
)
prompt = await self._run_in_executor(self.db.get_prompt_by_id, prompt_id)
@@ -695,7 +768,9 @@ class PromptRoutesMixin:
tags_added += 1
if tags_added > 0:
await self._run_in_executor(self.db.set_prompt_tags, prompt_id, current_tags)
await self._run_in_executor(
self.db.set_prompt_tags, prompt_id, current_tags
)
message = f"{tags_added} tag(s) added successfully"
if tags_added == 0:
@@ -734,7 +809,9 @@ class PromptRoutesMixin:
if tag_to_remove in current_tags:
current_tags.remove(tag_to_remove)
await self._run_in_executor(self.db.set_prompt_tags, prompt_id, current_tags)
await self._run_in_executor(
self.db.set_prompt_tags, prompt_id, current_tags
)
return web.json_response(
{"success": True, "message": "Tag removed successfully"}
@@ -762,13 +839,17 @@ class PromptRoutesMixin:
{"success": False, "error": "No prompt IDs provided"}, status=400
)
deleted_count = await self._run_in_executor(self.db.bulk_delete_prompts, prompt_ids)
deleted_count = await self._run_in_executor(
self.db.bulk_delete_prompts, prompt_ids
)
return web.json_response({
"success": True,
"message": f"Deleted {deleted_count} prompts",
"deleted_count": deleted_count,
})
return web.json_response(
{
"success": True,
"message": f"Deleted {deleted_count} prompts",
"deleted_count": deleted_count,
}
)
except Exception as e:
self.logger.error(f"Bulk delete error: {e}")
@@ -790,13 +871,17 @@ class PromptRoutesMixin:
status=400,
)
updated_count = await self._run_in_executor(self.db.bulk_add_tags, prompt_ids, new_tags)
updated_count = await self._run_in_executor(
self.db.bulk_add_tags, prompt_ids, new_tags
)
return web.json_response({
"success": True,
"message": f"Added tags to {updated_count} prompts",
"updated_count": updated_count,
})
return web.json_response(
{
"success": True,
"message": f"Added tags to {updated_count} prompts",
"updated_count": updated_count,
}
)
except Exception as e:
self.logger.error(f"Bulk add tags error: {e}")
@@ -816,13 +901,17 @@ class PromptRoutesMixin:
{"success": False, "error": "No prompt IDs provided"}, status=400
)
updated_count = await self._run_in_executor(self.db.bulk_set_category, prompt_ids, category)
updated_count = await self._run_in_executor(
self.db.bulk_set_category, prompt_ids, category
)
return web.json_response({
"success": True,
"message": f"Set category for {updated_count} prompts",
"updated_count": updated_count,
})
return web.json_response(
{
"success": True,
"message": f"Set category for {updated_count} prompts",
"updated_count": updated_count,
}
)
except Exception as e:
self.logger.error(f"Bulk set category error: {e}")