diff --git a/README.md b/README.md index 23bfd21..4cc0b3e 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,30 @@ uvicorn main:app --host 0.0.0.0 --port 8000 --reload The API will be available at [http://localhost:8000](http://localhost:8000) +### Running with Docker + +You can also run the application using Docker: + +```bash +# Build the Docker image +docker build -t hrplatform-backend . + +# Run the container +docker run -p 8000:8000 -e DATABASE_PATH=/app/storage/db/db.sqlite hrplatform-backend +``` + +For production deployments, make sure to set the necessary environment variables: + +```bash +docker run -p 8000:8000 \ + -e SECRET_KEY="your-secure-key" \ + -e FIRST_SUPERUSER="admin@yourdomain.com" \ + -e FIRST_SUPERUSER_PASSWORD="secure-password" \ + -e DATABASE_PATH="/app/storage/db/db.sqlite" \ + -v /host/path/to/data:/app/storage \ + hrplatform-backend +``` + ## API Documentation Once the server is running, you can access the interactive API documentation: @@ -168,7 +192,15 @@ Once the server is running, you can access the interactive API documentation: | ACCESS_TOKEN_EXPIRE_MINUTES| JWT token expiration time in minutes | 11520 (8 days) | | FIRST_SUPERUSER | Email for the initial admin user | admin@example.com | | FIRST_SUPERUSER_PASSWORD | Password for the initial admin user | admin123 | -| SQLALCHEMY_DATABASE_URI | Database connection URI | sqlite:////app/storage/db/db.sqlite | +| DATABASE_PATH | Path to the SQLite database file | Auto-detected with fallbacks | + +The application will automatically try the following paths for the database (in order): +1. Path specified in the DATABASE_PATH environment variable (if set) +2. /app/storage/db/db.sqlite (standard Docker container path) +3. /tmp/hrplatform/db/db.sqlite (fallback for limited permissions) +4. ./db/db.sqlite (relative to the current working directory) + +This makes the application work reliably in different environments, including Docker containers, without requiring manual configuration. ## License diff --git a/alembic.ini b/alembic.ini index 634a012..b18518f 100644 --- a/alembic.ini +++ b/alembic.ini @@ -35,8 +35,8 @@ script_location = migrations # are written from script.py.mako # output_encoding = utf-8 -# SQLite URL example -sqlalchemy.url = sqlite:////app/storage/db/db.sqlite +# SQLite URL example - this will be overridden in env.py +sqlalchemy.url = sqlite:///db/db.sqlite [post_write_hooks] # post_write_hooks defines scripts or Python functions that are run diff --git a/app/core/config.py b/app/core/config.py index f2e417e..ce10d51 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,7 +1,8 @@ from typing import List, Optional from pydantic_settings import BaseSettings -from pydantic import validator, EmailStr +from pydantic import EmailStr import secrets +import os class Settings(BaseSettings): @@ -9,29 +10,23 @@ class Settings(BaseSettings): API_V1_STR: str = "/api/v1" # SECURITY - SECRET_KEY: str = secrets.token_urlsafe(32) + SECRET_KEY: str = os.environ.get("SECRET_KEY", secrets.token_urlsafe(32)) # 60 minutes * 24 hours * 8 days = 8 days - ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 + ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", 60 * 24 * 8)) # CORS BACKEND_CORS_ORIGINS: List[str] = ["*"] # First superuser - FIRST_SUPERUSER: EmailStr = "admin@example.com" - FIRST_SUPERUSER_PASSWORD: str = "admin123" + FIRST_SUPERUSER: EmailStr = os.environ.get("FIRST_SUPERUSER", "admin@example.com") + FIRST_SUPERUSER_PASSWORD: str = os.environ.get("FIRST_SUPERUSER_PASSWORD", "admin123") - # SQLITE DB + # Database path - this is just a placeholder, the actual path is determined in db/session.py + DATABASE_PATH: Optional[str] = os.environ.get("DATABASE_PATH") + + # SQLITE DB URI - will be set by db/session.py SQLALCHEMY_DATABASE_URI: Optional[str] = None - @validator("SQLALCHEMY_DATABASE_URI", pre=True) - def assemble_db_connection(cls, v: Optional[str], values: dict) -> str: - if v: - return v - from pathlib import Path - DB_DIR = Path("/app") / "storage" / "db" - DB_DIR.mkdir(parents=True, exist_ok=True) - return f"sqlite:///{DB_DIR}/db.sqlite" - class Config: case_sensitive = True env_file = ".env" diff --git a/app/db/session.py b/app/db/session.py index 55d8c97..3f6f021 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,13 +1,44 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from pathlib import Path +import os +# Try to get database path from environment variable +DB_PATH = os.environ.get("DATABASE_PATH") -DB_DIR = Path("/app") / "storage" / "db" -DB_DIR.mkdir(parents=True, exist_ok=True) - -SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite" +if DB_PATH: + # Use the provided path + SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}" +else: + # Default paths with fallbacks for different environments + possible_paths = [ + Path("/app/storage/db/db.sqlite"), # Docker container standard path + Path("/tmp/hrplatform/db/db.sqlite"), # Fallback to tmp directory + Path.cwd() / "db" / "db.sqlite" # Local development in current directory + ] + + # Find the first parent directory that is writable + for path in possible_paths: + try: + # Ensure directory exists + path.parent.mkdir(parents=True, exist_ok=True) + # Test if we can write to this directory + test_file = path.parent / ".write_test" + test_file.touch() + test_file.unlink() + SQLALCHEMY_DATABASE_URL = f"sqlite:///{path}" + print(f"Using database at: {path}") + break + except (PermissionError, OSError): + continue + else: + # If we get here, none of the paths worked + raise RuntimeError( + "Could not find a writable location for the database. " + "Please set the DATABASE_PATH environment variable." + ) +# Create the engine with the configured URL engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} diff --git a/migrations/env.py b/migrations/env.py index 596a66c..368040f 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -16,6 +16,12 @@ sys.path.append(str(BASE_DIR)) # access to the values within the .ini file in use. config = context.config +# Import database configuration to get the right database URL +from app.db.session import SQLALCHEMY_DATABASE_URL # noqa: E402 + +# Override the SQLAlchemy URL with our dynamically determined one +config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL) + # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: @@ -25,7 +31,7 @@ if config.config_file_name is not None: # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -from app.db.base import Base # noqa +from app.db.base import Base # noqa: E402 target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, diff --git a/run_migrations.py b/run_migrations.py index af307f4..f757117 100755 --- a/run_migrations.py +++ b/run_migrations.py @@ -9,6 +9,7 @@ Usage: python run_migrations.py --help """ +import os import sys from pathlib import Path @@ -17,9 +18,40 @@ BASE_DIR = Path(__file__).resolve().parent sys.path.append(str(BASE_DIR)) print(f"Added {BASE_DIR} to Python path") -if __name__ == "__main__": - # Import alembic's main function - from alembic.config import main +# Set environment variables for containerized environments if not already set +if "DATABASE_PATH" not in os.environ: + # Try multiple possible paths for the database + possible_paths = [ + "/app/storage/db/db.sqlite", + "/tmp/hrplatform/db/db.sqlite", + str(BASE_DIR / "db" / "db.sqlite") + ] + + for path in possible_paths: + try: + # Ensure directory exists + db_dir = Path(path).parent + db_dir.mkdir(parents=True, exist_ok=True) + # Test if we can write to this directory + test_file = db_dir / ".write_test" + test_file.touch() + test_file.unlink() + os.environ["DATABASE_PATH"] = path + print(f"Using database at: {path}") + break + except (PermissionError, OSError): + continue + else: + print("WARNING: Could not find a writable location for the database.") + print("Please set the DATABASE_PATH environment variable.") - # Execute alembic command with sys.argv (e.g., 'upgrade', 'head') - main(argv=sys.argv[1:]) \ No newline at end of file +if __name__ == "__main__": + try: + # Import alembic's main function + from alembic.config import main + + # Execute alembic command with sys.argv (e.g., 'upgrade', 'head') + main(argv=sys.argv[1:]) + except Exception as e: + print(f"Error running migrations: {e}") + sys.exit(1) \ No newline at end of file