restructure imports to work better with docker

This commit is contained in:
Micha R. Albert 2025-07-17 14:49:38 -04:00
parent 4a4d5fe4dd
commit 844e015dfa
Signed by: mra
SSH key fingerprint: SHA256:vjiZInsq3FRnDJk1YYWFhC/N62SAmVmY5H5wvViHhdg
14 changed files with 145 additions and 36 deletions

50
Dockerfile.dev Normal file
View file

@ -0,0 +1,50 @@
# Development Docker image with hot reload
FROM python:3.13-slim
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libc6-dev \
zlib1g \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user for development
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN mkdir -p /home/appuser
RUN chown -R appuser:appuser /home/appuser
# Set working directory
WORKDIR /app
COPY LICENSE README.md ./
# Install hatch for dependency management
RUN pip install --no-cache-dir hatch
# Copy pyproject.toml first for dependency caching
COPY pyproject.toml ./
# Generate and install dependencies
RUN hatch dep show requirements > requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy templates directory (needed at runtime)
COPY templates/ ./templates/
# Change ownership of the app directory to appuser
RUN chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Expose port
EXPOSE 80
# The source code will be mounted as a volume
# Install in editable mode and run with auto-reload
CMD ["sh", "-c", "pip install -e . && uvicorn random_access.main:app --host 0.0.0.0 --port 80 --reload --reload-dir /app/src"]

20
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,20 @@
# Development overrides for docker-compose
# Use with: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
services:
# Override the api service to use the development configuration
api:
build:
context: .
dockerfile: Dockerfile.dev
env_file: .env
ports:
- "8000:80"
volumes:
# Mount source code for hot reload
- ./src:/app/src:ro
- ./pyproject.toml:/app/pyproject.toml:ro
# Mount templates if they change during development
- ./templates:/app/templates:ro
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/')"]

36
run.sh Executable file
View file

@ -0,0 +1,36 @@
#!/bin/bash
# Script to aid with running Docker containers for development and production environments
case "$1" in
"dev")
echo "🚀 Starting development environment with hot reload..."
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build
;;
"prod")
echo "🚀 Starting production environment..."
docker-compose up --build
;;
"stop")
echo "🛑 Stopping all services..."
docker-compose down
;;
"logs")
echo "📝 Showing logs..."
docker-compose logs -f
;;
"clean")
echo "🧹 Cleaning up containers and images..."
docker-compose down --rmi all --volumes
;;
*)
echo "Usage: $0 {dev|prod|stop|logs|clean}"
echo ""
echo "Commands:"
echo " dev - Start development environment with hot reload"
echo " prod - Start production environment"
echo " stop - Stop all services"
echo " logs - Show logs"
echo " clean - Clean up everything"
exit 1
;;
esac

View file

@ -4,4 +4,10 @@ __version__ = "0.0.1"
__author__ = "Micha R. Albert" __author__ = "Micha R. Albert"
__email__ = "info@micha.zone" __email__ = "info@micha.zone"
__all__ = ["__version__", "__author__", "__email__"] # Import the main app for easier access
try:
from .main import app
__all__ = ["__version__", "__author__", "__email__", "app"]
except ImportError:
# If imports fail (e.g., missing env vars), just export metadata
__all__ = ["__version__", "__author__", "__email__"]

View file

@ -8,8 +8,8 @@ from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pyairtable.formulas import match from pyairtable.formulas import match
from database import get_session_by_token_cached from .database import get_session_by_token_cached
from settings import settings from .settings import settings
# Create HTTPBearer security scheme # Create HTTPBearer security scheme
security = HTTPBearer( security = HTTPBearer(

View file

@ -12,7 +12,7 @@ from aiocache.serializers import PickleSerializer
from pyairtable import Api as AirtableApi from pyairtable import Api as AirtableApi
from pyairtable.formulas import match from pyairtable.formulas import match
from settings import settings from .settings import settings
logger = logging.getLogger("uvicorn.error") logger = logging.getLogger("uvicorn.error")
@ -548,7 +548,7 @@ async def check_display_name_exists(display_name: str, users_table) -> bool:
async def cleanup_expired_sessions_worker(sessions_table): async def cleanup_expired_sessions_worker(sessions_table):
"""Background worker to clean up expired sessions.""" """Background worker to clean up expired sessions."""
# Import here to avoid circular imports (auth imports database) # Import here to avoid circular imports (auth imports database)
from auth_utils import is_session_expired from .auth_utils import is_session_expired
# Run cleanup immediately at startup # Run cleanup immediately at startup
logger.info("Starting initial expired session cleanup at startup") logger.info("Starting initial expired session cleanup at startup")

View file

@ -19,7 +19,7 @@ import logging.handlers
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from settings import settings from .settings import settings
class JSONFormatter(logging.Formatter): class JSONFormatter(logging.Formatter):

View file

@ -16,21 +16,21 @@ from slowapi import Limiter
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware from slowapi.middleware import SlowAPIMiddleware
from database import ( from .database import (
airtable_write_worker, airtable_write_worker,
cleanup_expired_sessions_worker, cleanup_expired_sessions_worker,
get_airtable_base, get_airtable_base,
get_table, get_table,
validate_all_schemas, validate_all_schemas,
) )
from logging_config import setup_logging, get_logger, log_request_context, log_performance_context from .logging_config import setup_logging, get_logger, log_request_context, log_performance_context
from routes.auth import create_auth_router from .routes.auth import create_auth_router
from routes.items import create_items_router, create_user_items_router from .routes.items import create_items_router, create_user_items_router
from routes.system import create_system_router from .routes.system import create_system_router
from routes.users import create_users_router from .routes.users import create_users_router
from security import SecurityHeaders, get_client_ip from .security import SecurityHeaders, get_client_ip
from settings import settings from .settings import settings
from slack_integration import create_slack_app, setup_slack_handlers from .slack_integration import create_slack_app, setup_slack_handlers
# Setup enhanced logging that integrates with uvicorn # Setup enhanced logging that integrates with uvicorn
setup_logging() setup_logging()

View file

@ -14,14 +14,13 @@ from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from slowapi import Limiter from slowapi import Limiter
from auth_utils import ( from ..auth_utils import (
decode_oidc_state, decode_oidc_state,
get_session_by_token, get_session_by_token,
hash_token, hash_token,
is_session_expired, is_session_expired,
) )
from ..database import (
from database import (
check_display_name_exists, check_display_name_exists,
create_session, create_session,
create_user, create_user,
@ -29,14 +28,14 @@ from database import (
get_user_record, get_user_record,
update_user_and_session, update_user_and_session,
) )
from security import ( from ..security import (
create_safe_error_response, create_safe_error_response,
generate_secure_token, generate_secure_token,
get_client_ip, get_client_ip,
validate_airtable_id, validate_airtable_id,
) )
from settings import settings from ..settings import settings
from slack_integration import get_slack_user_id from ..slack_integration import get_slack_user_id
# Rate limiter for auth endpoints # Rate limiter for auth endpoints
limiter = Limiter(key_func=get_client_ip) limiter = Limiter(key_func=get_client_ip)

View file

@ -8,22 +8,20 @@ from fastapi.security import HTTPAuthorizationCredentials
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from slowapi import Limiter from slowapi import Limiter
from auth_utils import extract_and_validate_auth, get_auth_credentials from ..auth_utils import extract_and_validate_auth, get_auth_credentials
from database import ( from ..database import (
add_item_to_user, add_item_to_user,
get_all_items, get_all_items,
get_item_by_id, get_item_by_id,
)
from database import get_user_items as get_user_items_cached
from database import (
invalidate_user_items_cache, invalidate_user_items_cache,
get_user_items as get_user_items_cached,
) )
from security import ( from ..security import (
create_safe_error_response, create_safe_error_response,
get_client_ip, get_client_ip,
validate_airtable_id, validate_airtable_id,
) )
from settings import settings from ..settings import settings
# Rate limiter for item endpoints # Rate limiter for item endpoints
limiter = Limiter(key_func=get_client_ip) limiter = Limiter(key_func=get_client_ip)

View file

@ -5,8 +5,8 @@ from pydantic import BaseModel, Field
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
from slowapi import Limiter from slowapi import Limiter
from security import get_client_ip from ..security import get_client_ip
from settings import settings from ..settings import settings
# Rate limiter for system endpoints # Rate limiter for system endpoints
limiter = Limiter(key_func=get_client_ip) limiter = Limiter(key_func=get_client_ip)

View file

@ -5,9 +5,9 @@ from fastapi.security import HTTPAuthorizationCredentials
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from slowapi import Limiter from slowapi import Limiter
from auth_utils import extract_and_validate_auth, get_auth_credentials from ..auth_utils import extract_and_validate_auth, get_auth_credentials
from security import get_client_ip from ..security import get_client_ip
from settings import settings from ..settings import settings
# Rate limiter for user endpoints # Rate limiter for user endpoints
limiter = Limiter(key_func=get_client_ip) limiter = Limiter(key_func=get_client_ip)

View file

@ -6,7 +6,7 @@ from typing import Any
from fastapi import HTTPException, Request, status from fastapi import HTTPException, Request, status
from settings import settings from .settings import settings
# Validation patterns # Validation patterns
AIRTABLE_ID_PATTERN = re.compile(r"^rec[A-Za-z0-9]{14}$") AIRTABLE_ID_PATTERN = re.compile(r"^rec[A-Za-z0-9]{14}$")

View file

@ -9,7 +9,7 @@ from zoneinfo import ZoneInfo
from slack_bolt.async_app import AsyncAck, AsyncApp, AsyncRespond, AsyncSay from slack_bolt.async_app import AsyncAck, AsyncApp, AsyncRespond, AsyncSay
from slack_bolt.response import BoltResponse from slack_bolt.response import BoltResponse
from database import ( from .database import (
get_all_games, get_all_games,
get_detailed_user_items_for_slack, get_detailed_user_items_for_slack,
get_game_record, get_game_record,
@ -17,7 +17,7 @@ from database import (
get_user_by_slack_id, get_user_by_slack_id,
get_user_sessions, get_user_sessions,
) )
from settings import settings from .settings import settings
def get_ordinal_suffix(day: int) -> str: def get_ordinal_suffix(day: int) -> str: