Spaces:
Sleeping
Sleeping
#!/usr/bin/env python3 | |
""" | |
π N8N Workflow Documentation Space for Hugging Face | |
Optimized entry point for Hugging Face Spaces deployment. | |
""" | |
import os | |
import sys | |
import asyncio | |
import logging | |
from pathlib import Path | |
BASE_DIR = Path(__file__).resolve().parent | |
if str(BASE_DIR) not in sys.path: | |
sys.path.insert(0, str(BASE_DIR)) | |
# Load environment configuration for Hugging Face Spaces | |
def load_environment(): | |
"""Load environment variables from .env.hf if it exists.""" | |
env_file = Path(".env.hf") | |
if env_file.exists(): | |
with open(env_file, 'r') as f: | |
for line in f: | |
line = line.strip() | |
if line and not line.startswith('#') and '=' in line: | |
key, value = line.split('=', 1) | |
if key not in os.environ: | |
os.environ[key] = value | |
# Load environment first | |
load_environment() | |
# Configure logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# Set environment variables for HF Spaces | |
os.environ.setdefault("HOST", "0.0.0.0") | |
os.environ.setdefault("PORT", "7860") # HF Spaces standard port | |
# Database configuration - Auto-detect production environment and use PostgreSQL | |
# In Hugging Face Spaces, prioritize PostgreSQL if available | |
db_type = os.environ.get("DB_TYPE") | |
# Auto-detect if we're in production (HF Spaces) and should use PostgreSQL | |
if not db_type: | |
# Check for production environment indicators | |
is_production = ( | |
os.environ.get("SPACE_ID") or | |
os.environ.get("SPACES_BUILDKIT_VERSION") or | |
os.path.exists("/.dockerenv") or | |
os.environ.get("RAILWAY_PROJECT_ID") or | |
os.environ.get("RENDER_SERVICE_ID") | |
) | |
if is_production: | |
# Check if filesystem is writable | |
try: | |
test_path = Path("./test_write.tmp") | |
test_path.touch() | |
test_path.unlink() | |
filesystem_writable = True | |
except (PermissionError, OSError): | |
filesystem_writable = False | |
if not filesystem_writable: | |
logger.info("π Detected read-only filesystem - will use temporary storage") | |
# Prefer PostgreSQL in production if credentials might be available | |
db_type = "postgresdb" | |
logger.info("π Auto-detected production environment - attempting PostgreSQL") | |
else: | |
db_type = "sqlite" | |
logger.info("π Auto-detected local environment - using SQLite") | |
if db_type == "postgresdb": | |
# PostgreSQL configuration for Supabase | |
os.environ.setdefault("DB_POSTGRESDB_HOST", "aws-1-sa-east-1.pooler.supabase.com") | |
os.environ.setdefault("DB_POSTGRESDB_PORT", "6543") | |
os.environ.setdefault("DB_POSTGRESDB_DATABASE", "postgres") | |
os.environ.setdefault("DB_POSTGRESDB_SSL", "true") | |
logger.info("π Using PostgreSQL database configuration") | |
else: | |
# SQLite fallback | |
os.environ.setdefault("WORKFLOW_DB_PATH", "database/workflows.db") | |
logger.info("π Using SQLite database configuration") | |
logger.info("π Using SQLite database configuration") | |
def setup_huggingface_environment(): | |
"""Setup directories and environment for HF Spaces.""" | |
logger.info("π§ Setting up Hugging Face Spaces environment...") | |
# Create necessary directories with error handling | |
directory_paths = { | |
"database": Path("database"), | |
"static": Path("static"), | |
"workflows": Path("workflows") | |
} | |
for name, path in directory_paths.items(): | |
try: | |
path.mkdir(exist_ok=True, parents=True, mode=0o755) | |
logger.info(f"β Directory created/verified: {name} ({path.absolute()})") | |
except (PermissionError, OSError) as e: | |
logger.warning(f"β οΈ Could not create {name} directory ({e}) - using fallback") | |
if name == "database": | |
# Force temp database location | |
os.environ["WORKFLOW_DB_PATH"] = "/tmp/workflows.db" | |
logger.info(f"π Database will use temp location: /tmp/workflows.db") | |
elif name == "static": | |
# Static files will be served from memory | |
logger.info("π Static files will be served from memory") | |
elif name == "workflows": | |
# Workflows directory fallback | |
logger.info("π Workflows will be loaded from embedded sources if available") | |
os.environ.setdefault("WORKFLOW_SOURCE_DIR", str(directory_paths["workflows"])) | |
os.environ.setdefault("STATIC_DIR", str(directory_paths["static"])) | |
# Initialize database | |
try: | |
from workflow_db import WorkflowDatabase | |
# Determine database configuration | |
db_type = os.environ.get("DB_TYPE", "sqlite") | |
if db_type == "postgresdb": | |
# Note: Current WorkflowDatabase only supports SQLite | |
# PostgreSQL support would need to be implemented in WorkflowDatabase class | |
logger.warning("β οΈ PostgreSQL requested but WorkflowDatabase only supports SQLite") | |
logger.info("π Falling back to SQLite (PostgreSQL support not implemented)...") | |
db_type = "sqlite" | |
os.environ["DB_TYPE"] = "sqlite" | |
if db_type == "sqlite": | |
# Try local database first, fall back to temp if read-only filesystem | |
try: | |
db_path = BASE_DIR / "database" / "workflows.db" | |
logger.info(f"π Attempting to use SQLite database: {db_path}") | |
# Test if we can write to the database directory | |
test_file = db_path.parent / "test_write.tmp" | |
test_file.touch() | |
test_file.unlink() | |
# If we get here, we can write to the directory | |
if not Path(db_path).exists() or Path(db_path).stat().st_size == 0: | |
logger.info("π Initializing SQLite workflows database...") | |
db = WorkflowDatabase(str(db_path)) | |
else: | |
db = WorkflowDatabase(str(db_path)) | |
except (PermissionError, OSError) as e: | |
logger.warning(f"β οΈ Cannot write to local database directory: {e}") | |
logger.info("π Using temporary database location...") | |
temp_db_path = "/tmp/workflows.db" | |
os.environ["WORKFLOW_DB_PATH"] = temp_db_path | |
logger.info(f"π Using temporary SQLite database: {temp_db_path}") | |
db = WorkflowDatabase(temp_db_path) | |
# Database will be checked and indexed in the background by the API server | |
logger.info("β Database setup complete. Indexing will be handled by the API server.") | |
except Exception as e: | |
logger.error(f"β Database setup error: {e}") | |
# Final fallback - try temp database | |
try: | |
logger.info("π Attempting final fallback to temporary database...") | |
temp_db_path = "/tmp/fallback_workflows.db" | |
os.environ["DB_TYPE"] = "sqlite" | |
os.environ["WORKFLOW_DB_PATH"] = temp_db_path | |
from workflow_db import WorkflowDatabase | |
db = WorkflowDatabase(temp_db_path) | |
logger.info(f"β Emergency fallback database created: {temp_db_path}") | |
except Exception as fallback_error: | |
logger.warning(f"β οΈ All database options failed: {fallback_error}") | |
logger.info("π API will start without pre-initialized database") | |
logger.info("π Database initialization will be attempted on first API request") | |
# Create a minimal environment setup so the API can still start | |
os.environ["DB_TYPE"] = "sqlite" | |
os.environ["WORKFLOW_DB_PATH"] = "/tmp/delayed_init_workflows.db" | |
async def startup_tasks(): | |
"""Perform startup tasks asynchronously.""" | |
try: | |
setup_huggingface_environment() | |
logger.info("π Hugging Face Spaces setup completed successfully!") | |
except Exception as e: | |
logger.error(f"β Startup error: {e}") | |
# Don't fail completely, let the app start anyway | |
def main(): | |
"""Main entry point for Hugging Face Spaces.""" | |
logger.info("π Starting N8N Workflow Documentation API on Hugging Face Spaces...") | |
# Run startup tasks | |
asyncio.run(startup_tasks()) | |
# Import and start the FastAPI app | |
try: | |
from api_server import app | |
import uvicorn | |
# Get configuration from environment | |
host = os.getenv("HOST", "0.0.0.0") | |
port = int(os.getenv("PORT", "7860")) | |
logger.info(f"π Server starting on {host}:{port}") | |
logger.info("π API Documentation will be available at /docs") | |
# Start the server | |
uvicorn.run( | |
app, | |
host=host, | |
port=port, | |
log_level="info", | |
access_log=True | |
) | |
except ImportError as e: | |
logger.error(f"β Failed to import required modules: {e}") | |
sys.exit(1) | |
except Exception as e: | |
logger.error(f"β Failed to start server: {e}") | |
sys.exit(1) | |
# For Hugging Face Spaces compatibility | |
if __name__ == "__main__": | |
main() | |
# Also expose the app directly for gunicorn/uvicorn | |
try: | |
# Ensure environment is set up | |
asyncio.run(startup_tasks()) | |
from api_server import app | |
except Exception as e: | |
logger.warning(f"β οΈ Could not set up environment during import: {e}") | |
# Create a minimal fallback app | |
from fastapi import FastAPI | |
app = FastAPI(title="N8N Workflows API - Setup Required") | |
async def root(): | |
return {"message": "N8N Workflows API - Setup in progress", "status": "initializing"} | |
async def health(): | |
return {"status": "starting"} | |