rework server startup and cli
Some checks failed
Some checks failed
This changes the dockerfile as well.
This commit is contained in:
@@ -66,8 +66,8 @@ jobs:
|
||||
echo "## Installation" >> release-notes.md
|
||||
echo "" >> release-notes.md
|
||||
echo '```bash' >> release-notes.md
|
||||
echo 'uv sync' >> release-notes.md
|
||||
echo 'uv run python main.py' >> release-notes.md
|
||||
echo 'pip install embeddingbuddy' >> release-notes.md
|
||||
echo 'embeddingbuddy serve' >> release-notes.md
|
||||
echo '```' >> release-notes.md
|
||||
|
||||
- name: Create Release
|
||||
|
32
CLAUDE.md
32
CLAUDE.md
@@ -21,29 +21,23 @@ uv sync
|
||||
|
||||
**Run the application:**
|
||||
|
||||
Development mode (with auto-reload):
|
||||
Using the CLI (recommended):
|
||||
|
||||
```bash
|
||||
uv run run_dev.py
|
||||
# Production mode (no debug, no auto-reload)
|
||||
embeddingbuddy serve
|
||||
|
||||
# Development mode (debug + auto-reload on code changes)
|
||||
embeddingbuddy serve --dev
|
||||
|
||||
# Debug logging only (no auto-reload)
|
||||
embeddingbuddy serve --debug
|
||||
|
||||
# With custom host/port
|
||||
embeddingbuddy serve --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
Production mode (with Gunicorn WSGI server):
|
||||
|
||||
```bash
|
||||
# First install production dependencies
|
||||
uv sync --extra prod
|
||||
|
||||
# Then run in production mode
|
||||
uv run run_prod.py
|
||||
```
|
||||
|
||||
Legacy mode (basic Dash server):
|
||||
|
||||
```bash
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
The app will be available at <http://127.0.0.1:8050>
|
||||
The app will be available at <http://127.0.0.1:8050> by default
|
||||
|
||||
**Run tests:**
|
||||
|
||||
|
10
Dockerfile
10
Dockerfile
@@ -23,9 +23,6 @@ COPY pyproject.toml uv.lock ./
|
||||
|
||||
# Copy source code (needed for editable install)
|
||||
COPY src/ src/
|
||||
COPY main.py .
|
||||
COPY wsgi.py .
|
||||
COPY run_prod.py .
|
||||
COPY assets/ assets/
|
||||
|
||||
# Change ownership of source files before building (lighter I/O)
|
||||
@@ -59,10 +56,7 @@ RUN chown appuser:appuser /app
|
||||
# Copy files from builder with correct ownership
|
||||
COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
|
||||
COPY --from=builder --chown=appuser:appuser /app/src /app/src
|
||||
COPY --from=builder --chown=appuser:appuser /app/main.py /app/main.py
|
||||
COPY --from=builder --chown=appuser:appuser /app/assets /app/assets
|
||||
COPY --from=builder --chown=appuser:appuser /app/wsgi.py /app/wsgi.py
|
||||
COPY --from=builder --chown=appuser:appuser /app/run_prod.py /app/run_prod.py
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
@@ -86,5 +80,5 @@ EXPOSE 8050
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD python -c "import requests; requests.get('http://localhost:8050/', timeout=5)" || exit 1
|
||||
|
||||
# Run application with Gunicorn in production
|
||||
CMD ["python", "run_prod.py"]
|
||||
# Run application in production mode (no debug, no auto-reload)
|
||||
CMD ["embeddingbuddy", "serve"]
|
32
README.md
32
README.md
@@ -39,7 +39,7 @@ documents and prompts to understand how queries relate to your content.
|
||||
uv tool install embeddingbuddy
|
||||
|
||||
# Run the application
|
||||
embeddingbuddy
|
||||
embeddingbuddy serve
|
||||
```
|
||||
|
||||
**Option 2: Install with pip/pipx**
|
||||
@@ -124,26 +124,18 @@ uv sync
|
||||
|
||||
2. **Run the application:**
|
||||
|
||||
**Development mode** (with auto-reload):
|
||||
|
||||
```bash
|
||||
uv run run_dev.py
|
||||
```
|
||||
# Production mode (no debug, no auto-reload)
|
||||
embeddingbuddy serve
|
||||
|
||||
**Production mode** (with Gunicorn WSGI server):
|
||||
# Development mode (debug + auto-reload on code changes)
|
||||
embeddingbuddy serve --dev
|
||||
|
||||
```bash
|
||||
# Install production dependencies
|
||||
uv sync --extra prod
|
||||
# Debug logging only (no auto-reload)
|
||||
embeddingbuddy serve --debug
|
||||
|
||||
# Run in production mode
|
||||
uv run run_prod.py
|
||||
```
|
||||
|
||||
**Legacy mode** (basic Dash server):
|
||||
|
||||
```bash
|
||||
uv run main.py
|
||||
# Custom host/port
|
||||
embeddingbuddy serve --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
3. **Open your browser** to <http://127.0.0.1:8050>
|
||||
@@ -231,10 +223,8 @@ src/embeddingbuddy/
|
||||
│ └── interactions.py # User interaction callbacks
|
||||
└── utils/ # Utility functions
|
||||
|
||||
main.py # Application runner (at project root)
|
||||
main.py # Application runner (at project root)
|
||||
run_dev.py # Development server runner
|
||||
run_prod.py # Production server runner
|
||||
# CLI entry point
|
||||
embeddingbuddy serve # Main CLI command to start the server
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
10
main.py
10
main.py
@@ -1,10 +0,0 @@
|
||||
from src.embeddingbuddy.app import create_app, run_app
|
||||
|
||||
|
||||
def main():
|
||||
app = create_app()
|
||||
run_app(app)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -18,7 +18,8 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
embeddingbuddy = "embeddingbuddy.app:main"
|
||||
embeddingbuddy = "embeddingbuddy.cli:main"
|
||||
embeddingbuddy-serve = "embeddingbuddy.app:serve"
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
|
32
run_dev.py
32
run_dev.py
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Development runner with auto-reload enabled.
|
||||
This runs the Dash development server with hot reloading.
|
||||
"""
|
||||
import os
|
||||
from src.embeddingbuddy.app import create_app, run_app
|
||||
|
||||
def main():
|
||||
"""Run the application in development mode with auto-reload."""
|
||||
# Force development settings
|
||||
os.environ["EMBEDDINGBUDDY_ENV"] = "development"
|
||||
os.environ["EMBEDDINGBUDDY_DEBUG"] = "true"
|
||||
|
||||
# Check for OpenSearch disable flag (optional for testing)
|
||||
# Set EMBEDDINGBUDDY_OPENSEARCH_ENABLED=false to test without OpenSearch
|
||||
opensearch_status = os.getenv("EMBEDDINGBUDDY_OPENSEARCH_ENABLED", "true")
|
||||
opensearch_enabled = opensearch_status.lower() == "true"
|
||||
|
||||
print("🚀 Starting EmbeddingBuddy in development mode...")
|
||||
print("📁 Auto-reload enabled - changes will trigger restart")
|
||||
print("🌐 Server will be available at http://127.0.0.1:8050")
|
||||
print(f"🔍 OpenSearch: {'Enabled' if opensearch_enabled else 'Disabled'}")
|
||||
print("⏹️ Press Ctrl+C to stop")
|
||||
|
||||
app = create_app()
|
||||
|
||||
# Run with development server (includes auto-reload when debug=True)
|
||||
run_app(app, debug=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
52
run_prod.py
52
run_prod.py
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Production runner using Gunicorn WSGI server.
|
||||
This provides better performance and stability for production deployments.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from src.embeddingbuddy.config.settings import AppSettings
|
||||
|
||||
def main():
|
||||
"""Run the application in production mode with Gunicorn."""
|
||||
# Force production settings
|
||||
os.environ["EMBEDDINGBUDDY_ENV"] = "production"
|
||||
os.environ["EMBEDDINGBUDDY_DEBUG"] = "false"
|
||||
# Disable OpenSearch by default in production (can be overridden by setting env var)
|
||||
if "EMBEDDINGBUDDY_OPENSEARCH_ENABLED" not in os.environ:
|
||||
os.environ["EMBEDDINGBUDDY_OPENSEARCH_ENABLED"] = "false"
|
||||
|
||||
print("🚀 Starting EmbeddingBuddy in production mode...")
|
||||
print(f"⚙️ Workers: {AppSettings.GUNICORN_WORKERS}")
|
||||
print(f"🌐 Server will be available at http://{AppSettings.GUNICORN_BIND}")
|
||||
print("⏹️ Press Ctrl+C to stop")
|
||||
|
||||
# Gunicorn command
|
||||
cmd = [
|
||||
"gunicorn",
|
||||
"--workers", str(AppSettings.GUNICORN_WORKERS),
|
||||
"--bind", AppSettings.GUNICORN_BIND,
|
||||
"--timeout", str(AppSettings.GUNICORN_TIMEOUT),
|
||||
"--keep-alive", str(AppSettings.GUNICORN_KEEPALIVE),
|
||||
"--access-logfile", "-",
|
||||
"--error-logfile", "-",
|
||||
"--log-level", "info",
|
||||
"wsgi:application"
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Shutting down...")
|
||||
sys.exit(0)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Error running Gunicorn: {e}")
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
print("❌ Gunicorn not found. Install it with: uv add gunicorn")
|
||||
print("💡 Or run in development mode with: python run_dev.py")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -1,15 +1,21 @@
|
||||
"""
|
||||
EmbeddingBuddy application factory and server functions.
|
||||
|
||||
This module contains the main application creation logic with imports
|
||||
moved inside functions to avoid loading heavy dependencies at module level.
|
||||
"""
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create and configure the Dash application instance."""
|
||||
import os
|
||||
import dash
|
||||
import dash_bootstrap_components as dbc
|
||||
from .config.settings import AppSettings
|
||||
from .ui.layout import AppLayout
|
||||
from .ui.callbacks.data_processing import DataProcessingCallbacks
|
||||
from .ui.callbacks.visualization import VisualizationCallbacks
|
||||
from .ui.callbacks.interactions import InteractionCallbacks
|
||||
|
||||
|
||||
def create_app():
|
||||
import os
|
||||
|
||||
# Get the project root directory (two levels up from this file)
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
assets_path = os.path.join(project_root, "assets")
|
||||
@@ -124,6 +130,9 @@ def _register_client_side_callbacks(app):
|
||||
|
||||
|
||||
def run_app(app=None, debug=None, host=None, port=None):
|
||||
"""Run the Dash application with specified settings."""
|
||||
from .config.settings import AppSettings
|
||||
|
||||
if app is None:
|
||||
app = create_app()
|
||||
|
||||
@@ -134,10 +143,69 @@ def run_app(app=None, debug=None, host=None, port=None):
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the embeddingbuddy CLI command."""
|
||||
def serve(host=None, port=None, dev=False, debug=False):
|
||||
"""Start the EmbeddingBuddy web server.
|
||||
|
||||
Args:
|
||||
host: Host to bind to (default: 127.0.0.1)
|
||||
port: Port to bind to (default: 8050)
|
||||
dev: Development mode - enable debug logging and auto-reload (default: False)
|
||||
debug: Enable debug logging only, no auto-reload (default: False)
|
||||
"""
|
||||
import os
|
||||
from .config.settings import AppSettings
|
||||
|
||||
# Determine actual values to use
|
||||
actual_host = host if host is not None else AppSettings.HOST
|
||||
actual_port = port if port is not None else AppSettings.PORT
|
||||
|
||||
# Determine mode
|
||||
# --dev takes precedence and enables both debug and auto-reload
|
||||
# --debug enables only debug logging
|
||||
# No flags = production mode (no debug, no auto-reload)
|
||||
use_reloader = dev
|
||||
use_debug = dev or debug
|
||||
|
||||
# Only print startup messages in main process (not in Flask reloader)
|
||||
if not os.environ.get('WERKZEUG_RUN_MAIN'):
|
||||
mode = "development" if dev else ("debug" if debug else "production")
|
||||
print(f"Starting EmbeddingBuddy in {mode} mode...")
|
||||
print("Loading dependencies (this may take a few seconds)...")
|
||||
print(f"Server will start at http://{actual_host}:{actual_port}")
|
||||
if use_reloader:
|
||||
print("Auto-reload enabled - server will restart on code changes")
|
||||
|
||||
app = create_app()
|
||||
run_app(app)
|
||||
|
||||
# Suppress Flask development server warning in production mode
|
||||
if not use_debug and not use_reloader:
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
# Suppress the werkzeug warning
|
||||
warnings.filterwarnings('ignore', message='.*development server.*')
|
||||
|
||||
# Set werkzeug logger to ERROR level to suppress the warning
|
||||
werkzeug_logger = logging.getLogger('werkzeug')
|
||||
werkzeug_logger.setLevel(logging.ERROR)
|
||||
|
||||
# Use Flask's built-in server with appropriate settings
|
||||
app.run(
|
||||
debug=use_debug,
|
||||
host=actual_host,
|
||||
port=actual_port,
|
||||
use_reloader=use_reloader
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Legacy entry point - redirects to cli module.
|
||||
|
||||
This is kept for backward compatibility but the main CLI
|
||||
is now in embeddingbuddy.cli for faster startup.
|
||||
"""
|
||||
from .cli import main as cli_main
|
||||
cli_main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
74
src/embeddingbuddy/cli.py
Normal file
74
src/embeddingbuddy/cli.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Lightweight CLI entry point for EmbeddingBuddy.
|
||||
|
||||
This module provides a fast command-line interface that only imports
|
||||
heavy dependencies when actually needed by subcommands.
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point with minimal imports for fast help text."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="embeddingbuddy",
|
||||
description="EmbeddingBuddy - Interactive embedding visualization tool",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
embeddingbuddy serve # Production mode (no debug, no auto-reload)
|
||||
embeddingbuddy serve --dev # Development mode (debug + auto-reload)
|
||||
embeddingbuddy serve --debug # Debug logging only (no auto-reload)
|
||||
embeddingbuddy serve --port 8080 # Custom port
|
||||
embeddingbuddy serve --host 0.0.0.0 # Bind to all interfaces
|
||||
"""
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="command",
|
||||
help="Available commands",
|
||||
metavar="<command>"
|
||||
)
|
||||
|
||||
# Serve subcommand
|
||||
serve_parser = subparsers.add_parser(
|
||||
"serve",
|
||||
help="Start the web server",
|
||||
description="Start the EmbeddingBuddy web server for interactive visualization"
|
||||
)
|
||||
serve_parser.add_argument(
|
||||
"--host",
|
||||
default=None,
|
||||
help="Host to bind to (default: 127.0.0.1)"
|
||||
)
|
||||
serve_parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Port to bind to (default: 8050)"
|
||||
)
|
||||
serve_parser.add_argument(
|
||||
"--dev",
|
||||
action="store_true",
|
||||
help="Development mode: enable debug logging and auto-reload"
|
||||
)
|
||||
serve_parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
help="Enable debug logging (no auto-reload)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "serve":
|
||||
# Only import heavy dependencies when actually running serve
|
||||
from embeddingbuddy.app import serve
|
||||
serve(host=args.host, port=args.port, dev=args.dev, debug=args.debug)
|
||||
else:
|
||||
# No command specified, show help
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -69,7 +69,7 @@ class AppSettings:
|
||||
TEXT_PREVIEW_LENGTH = 100
|
||||
|
||||
# App Configuration
|
||||
DEBUG = os.getenv("EMBEDDINGBUDDY_DEBUG", "True").lower() == "true"
|
||||
DEBUG = os.getenv("EMBEDDINGBUDDY_DEBUG", "False").lower() == "true"
|
||||
HOST = os.getenv("EMBEDDINGBUDDY_HOST", "127.0.0.1")
|
||||
PORT = int(os.getenv("EMBEDDINGBUDDY_PORT", "8050"))
|
||||
|
||||
|
11
src/embeddingbuddy/wsgi.py
Normal file
11
src/embeddingbuddy/wsgi.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
WSGI entry point for production deployment.
|
||||
Use this with a production WSGI server like Gunicorn.
|
||||
"""
|
||||
from embeddingbuddy.app import create_app
|
||||
|
||||
# Create the application instance
|
||||
application = create_app()
|
||||
|
||||
# For compatibility with different WSGI servers
|
||||
app = application
|
2
uv.lock
generated
2
uv.lock
generated
@@ -412,7 +412,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "embeddingbuddy"
|
||||
version = "0.5.1"
|
||||
version = "0.6.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "dash" },
|
||||
|
20
wsgi.py
20
wsgi.py
@@ -1,20 +0,0 @@
|
||||
"""
|
||||
WSGI entry point for production deployment.
|
||||
Use this with a production WSGI server like Gunicorn.
|
||||
"""
|
||||
from src.embeddingbuddy.app import create_app
|
||||
|
||||
# Create the application instance
|
||||
application = create_app()
|
||||
|
||||
# For compatibility with different WSGI servers
|
||||
app = application
|
||||
|
||||
if __name__ == "__main__":
|
||||
# This won't be used in production, but useful for testing
|
||||
from src.embeddingbuddy.config.settings import AppSettings
|
||||
application.run(
|
||||
host=AppSettings.HOST,
|
||||
port=AppSettings.PORT,
|
||||
debug=False
|
||||
)
|
Reference in New Issue
Block a user