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
19 changes: 19 additions & 0 deletions .github/actions/setup-python-core/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Setup Python Environment (Core)
description: Sets up Python with uv and installs core dependencies

runs:
using: composite
steps:
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install dependencies
run: |
uv sync
shell: bash
19 changes: 19 additions & 0 deletions .github/actions/setup-python-dev/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Setup Python Environment (Dev)
description: Sets up Python with uv and installs dev dependencies

runs:
using: composite
steps:
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install dependencies
run: |
uv sync --extra dev
shell: bash
61 changes: 10 additions & 51 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,61 +13,31 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install dependencies
run: |
uv sync --extra dev
- uses: ./.github/actions/setup-python-dev
- name: Validate pyproject.toml
run: |
uv run validate-pyproject pyproject.toml
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
- uses: ./.github/actions/setup-python-dev
- name: Check with ruff
run: |
uv run -m ruff check
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install dependencies
run: |
uv sync --extra dev
- uses: ./.github/actions/setup-python-dev
- name: Check with mypy
run: |
uv run -m mypy .
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install dependencies
run: |
uv sync --extra dev
- uses: ./.github/actions/setup-python-dev
- name: Test with pytest
run: |
uv run -m pytest --cov-report html
Expand All @@ -80,25 +50,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install dependencies
run: |
uv sync --extra dev
- uses: ./.github/actions/setup-python-dev
- name: Check version consistency
run: |
echo "Checking version consistency across all package files..."

PACKAGE_NAME="example"
PACKAGE_NAME="python-template-server"
TOML_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
LOCK_VERSION=$(uv run python -c "import tomllib; lock_data = tomllib.load(open('uv.lock', 'rb')); pkg = next((p for p in lock_data['package'] if p['name'] == '${PACKAGE_NAME/_/-}'), None); print(pkg['version'] if pkg else 'not found')")
LOCK_VERSION=$(uv run python -c "import tomllib; lock_data = tomllib.load(open('uv.lock', 'rb')); pkg = next((p for p in lock_data['package'] if p['name'] == '${PACKAGE_NAME}'), None); print(pkg['version'] if pkg else 'not found')")

echo "Version in pyproject.toml: $TOML_VERSION"
echo "Version in uv.lock: $LOCK_VERSION"
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ ENV/
env.bak/
venv.bak/

# SSL/TLS Certificates
*.pem
*.key
*.crt
*.csr

# Spyder project settings
.spyderproject
.spyproject
Expand Down
File renamed without changes.
21 changes: 21 additions & 0 deletions configuration/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"server": {
"host": "0.0.0.0",
"port": 443
},
"security": {
"hsts_max_age": 31536000,
"content_security_policy": "default-src 'self'"
},
"rate_limit": {
"enabled": true,
"rate_limit": "100/minute",
"storage_uri": ""
},
"certificate": {
"directory": "certs",
"ssl_keyfile": "key.pem",
"ssl_certfile": "cert.pem",
"days_valid": 365
}
}
6 changes: 0 additions & 6 deletions example/main.py

This file was deleted.

33 changes: 28 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "example"
name = "python-template-server"
version = "0.1.0"
description = "An example pyproject.toml."
description = "A template FastAPI server with authentication, rate limiting and Prometheus metrics."
readme = "README.md"
requires-python = ">=3.12"
license = { file = "LICENSE" }
Expand All @@ -18,20 +18,43 @@ classifiers = [
"License :: OSI Approved :: MIT License",
]
dependencies = [
"numpy",
"cryptography>=46.0.3",
"fastapi>=0.121.3",
"httpx>=0.28.1",
"prometheus-fastapi-instrumentator>=7.1.0",
"pydantic>=2.12.4",
"pyhere>=1.0.3",
"python-dotenv>=1.2.1",
"slowapi>=0.1.9",
"uvicorn[standard]>=0.38.0",
]

[project.optional-dependencies]
dev = [
"ruff",
"mypy",
"pytest",
"pytest-asyncio",
"pytest-cov",
"validate-pyproject",
]

[project.urls]
repository = "https://github.com/javidahmed64592/template-python"
repository = "https://github.com/javidahmed64592/python-template-server"

[project.scripts]
python-template-server = "python_template_server.main:run"
generate-certificate = "python_template_server.certificate_handler:generate_self_signed_certificate"
generate-new-token = "python_template_server.authentication_handler:generate_new_token"

[tool.hatch.build]
include = [
"configuration/**",
"python_template_server/**",
".here",
"LICENSE",
"README.md",
]

[tool.pytest.ini_options]
addopts = [
Expand Down Expand Up @@ -140,6 +163,6 @@ pretty = true

[[tool.mypy.overrides]]
module = [
"numpy.*",
"pyhere.*"
]
ignore_missing_imports = true
Empty file.
80 changes: 80 additions & 0 deletions python_template_server/authentication_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Authentication handler for the server."""

import hashlib
import logging
import os
import secrets

import dotenv

from python_template_server.config import ROOT_DIR
from python_template_server.constants import ENV_FILE_NAME, ENV_VAR_NAME, TOKEN_LENGTH

logger = logging.getLogger(__name__)

ENV_FILE = ROOT_DIR / ENV_FILE_NAME


def generate_token() -> str:
"""Generate a secure random token.

:return str: A URL-safe token string
"""
return secrets.token_urlsafe(TOKEN_LENGTH)


def hash_token(token: str) -> str:
"""Hash a token string using SHA-256.

:param str token: The plain text token to hash
:return str: The hexadecimal representation of the hashed token
"""
return hashlib.sha256(token.encode()).hexdigest()


def save_hashed_token(token: str) -> None:
"""Hash a token and save it to the .env file.

:param str token: The plain text token to hash and save
"""
hashed = hash_token(token)

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

dotenv.set_key(ENV_FILE, 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)
return os.getenv(ENV_VAR_NAME, "")


def verify_token(token: str, hashed_token: str) -> bool:
"""Verify a token against the stored hash.

:param str token: The plain text token to verify
:param str hashed_token: The stored hashed token for comparison
:return bool: True if the token matches the stored hash, False otherwise
"""
if not hashed_token:
msg = "No stored token hash found for verification."
raise ValueError(msg)

return hash_token(token) == hashed_token


def generate_new_token() -> None:
"""Generate a new token, hash it, and save the hash to the .env file.

This function generates a new secure random token, hashes it using SHA-256,
and saves the hashed token to the .env file for future verification.
"""
new_token = generate_token()
save_hashed_token(new_token)
logger.info("New API token generated and saved.")
print(f"Token: {new_token}") # Prevent logging token to log file
Loading