Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,16 @@ RUN SITE_PACKAGES_DIR=$(find /usr/local/lib -name "site-packages" -type d | head

# Create startup script
RUN echo '#!/bin/sh\n\
CONFIG_FILE="config.json"\n\
if [ ! -f .env ]; then\n\
echo "Generating new token..."\n\
generate-new-token\n\
export $(grep -v "^#" .env | xargs)\n\
fi\n\
if [ ! -f certs/cert.pem ] || [ ! -f certs/key.pem ]; then\n\
echo "Generating self-signed certificates..."\n\
generate-certificate --config-file="$CONFIG_FILE"\n\
generate-certificate\n\
fi\n\
exec python-template-server --config-file="$CONFIG_FILE"' > /app/start.sh && \
exec python-template-server' > /app/start.sh && \
chmod +x /app/start.sh && \
chown template_server_user:template_server_user /app/start.sh

Expand Down
15 changes: 7 additions & 8 deletions python_template_server/authentication_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@

import dotenv

from python_template_server.config import ROOT_DIR
from python_template_server.constants import ENV_FILE_NAME, ENV_VAR_NAME, TOKEN_LENGTH
from python_template_server.constants import ENV_FILE_PATH, ENV_VAR_NAME, TOKEN_LENGTH
from python_template_server.logging_setup import setup_logging

setup_logging()
logger = logging.getLogger(__name__)

ENV_FILE = ROOT_DIR / ENV_FILE_NAME


def generate_token() -> str:
"""Generate a secure random token.
Expand All @@ -39,18 +38,18 @@ def save_hashed_token(token: str) -> None:
"""
hashed = hash_token(token)

if not ENV_FILE.exists():
ENV_FILE.touch()
if not ENV_FILE_PATH.exists():
ENV_FILE_PATH.touch()

dotenv.set_key(ENV_FILE, ENV_VAR_NAME, hashed)
dotenv.set_key(ENV_FILE_PATH, ENV_VAR_NAME, hashed)


def load_hashed_token() -> str:
"""Load the hashed token from environment variable.

:return str: The hashed token string, or an empty string if not found
"""
dotenv.load_dotenv(ENV_FILE)
dotenv.load_dotenv(ENV_FILE_PATH)
return os.getenv(ENV_VAR_NAME, "")


Expand Down
7 changes: 4 additions & 3 deletions python_template_server/certificate_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID

from python_template_server.config import load_config, parse_args
from python_template_server.logging_setup import setup_logging
from python_template_server.main import ExampleServer
from python_template_server.models import CertificateConfigModel

setup_logging()
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -136,8 +138,7 @@ def generate_self_signed_certificate() -> None:
:raise SystemExit: If certificate generation fails
"""
try:
args = parse_args()
config = load_config(args.config_file)
config = ExampleServer().config
handler = CertificateHandler(config.certificate)
handler.generate_self_signed_cert()
except (OSError, PermissionError):
Expand Down
112 changes: 0 additions & 112 deletions python_template_server/config.py

This file was deleted.

20 changes: 16 additions & 4 deletions python_template_server/constants.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
"""Constants used across the server."""

from pyhere import here

# General constants
ROOT_DIR = here()
CONFIG_DIR_NAME = "configuration"
LOG_DIR_NAME = "logs"
CONFIG_DIR = ROOT_DIR / CONFIG_DIR_NAME
LOG_DIR = ROOT_DIR / LOG_DIR_NAME

CONFIG_FILE_NAME = "config.json"
LOG_FILE_NAME = "server.log"
ENV_FILE_NAME = ".env"

CONFIG_FILE_PATH = CONFIG_DIR / CONFIG_FILE_NAME
LOG_FILE_PATH = LOG_DIR / LOG_FILE_NAME
ENV_FILE_PATH = ROOT_DIR / ENV_FILE_NAME

BYTES_TO_MB = 1024 * 1024

# Main constants
PACKAGE_NAME = "python-template-server"
API_PREFIX = "/api"
API_KEY_HEADER_NAME = "X-API-Key"
CONFIG_FILE_NAME = "config.json"

# Authentication constants
ENV_FILE_NAME = ".env"
ENV_VAR_NAME = "API_TOKEN_HASH"
TOKEN_LENGTH = 32

# Logging constants
LOG_DIR_NAME = "logs"
LOG_FILE_NAME = "server.log"
LOG_MAX_BYTES = 10 * BYTES_TO_MB # 10 MB
LOG_BACKUP_COUNT = 5
LOG_FORMAT = "[%(asctime)s] (%(levelname)s) %(module)s: %(message)s"
Expand Down
50 changes: 50 additions & 0 deletions python_template_server/logging_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Logging setup for the server."""

import logging
import sys
from logging.handlers import RotatingFileHandler

from python_template_server.constants import (
LOG_BACKUP_COUNT,
LOG_DATE_FORMAT,
LOG_DIR,
LOG_FILE_PATH,
LOG_FORMAT,
LOG_LEVEL,
LOG_MAX_BYTES,
)


def setup_logging() -> None:
"""Configure logging with both console and rotating file handlers.

Creates a logs directory if it doesn't exist and sets up:
- Console handler for stdout
- Rotating file handler with size-based rotation
"""
# Create logs directory if it doesn't exist
LOG_DIR.mkdir(exist_ok=True)

# Get the root logger
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, LOG_LEVEL))

# Remove any existing handlers
root_logger.handlers.clear()

# Create formatter
formatter = logging.Formatter(LOG_FORMAT, datefmt=LOG_DATE_FORMAT)

# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(getattr(logging, LOG_LEVEL))
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)

# Rotating file handler
file_handler = RotatingFileHandler(
LOG_FILE_PATH, maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUP_COUNT, encoding="utf-8"
)
file_handler.setLevel(getattr(logging, LOG_LEVEL))
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
18 changes: 12 additions & 6 deletions python_template_server/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
"""FastAPI template server using uvicorn."""

from python_template_server.config import load_config, parse_args
from typing import Any

from python_template_server.models import TemplateServerConfig
from python_template_server.template_server import TemplateServer


class ExampleServer(TemplateServer):
"""Example server inheriting from TemplateServer."""

def __init__(self, config: TemplateServerConfig) -> None:
def __init__(self) -> None:
"""Initialize the ExampleServer by delegating to the template server.

:param TemplateServerConfig config: Example server configuration
"""
super().__init__(config)
super().__init__()

def validate_config(self, config_data: dict[str, Any]) -> TemplateServerConfig:
"""Validate configuration from the config.json file.

:return TemplateServerConfig: Loaded configuration
"""
return super().validate_config(config_data)

def setup_routes(self) -> None:
"""Set up API routes."""
Expand All @@ -25,7 +33,5 @@ def run() -> None:

:raise SystemExit: If configuration fails to load or SSL certificate files are missing
"""
args = parse_args()
config = load_config(args.config_file)
server = ExampleServer(config)
server = ExampleServer()
server.run()
7 changes: 0 additions & 7 deletions python_template_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

from pydantic import BaseModel, Field

from python_template_server.constants import API_PREFIX


# Template Server Configuration Models
class ServerConfigModel(BaseModel):
Expand All @@ -26,11 +24,6 @@ def url(self) -> str:
"""Get the server URL."""
return f"https://{self.address}"

@property
def full_url(self) -> str:
"""Get the full server URL including API prefix."""
return f"{self.url}{API_PREFIX}"


class SecurityConfigModel(BaseModel):
"""Security headers configuration model."""
Expand Down
Loading