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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install deps
run: |
python -m pip install --upgrade pip
pip install -r app/requirements.txt
- name: Lint
run: |
black --check app tests
isort --check-only app tests
flake8 app tests
- name: Tests
run: pytest -q
51 changes: 51 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
.eggs/
.build/
dist/
build/

# Environments
.venv/
venv/
.env
.env.*
.python-version

# Testing
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.mypy_cache/
.ruff_cache/

# IDEs and editors
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Node
node_modules/

# Docker
*.log
cache/
.docker/

# Alembic
app/src/alembic/versions/__pycache__/

# SQLite
*.sqlite3

# Misc
*.bak
20 changes: 20 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
repos:
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
language_version: python3
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-added-large-files
38 changes: 38 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
PYTHON := python3
PIP := pip3
APP_DIR := app

.PHONY: setup run lint format test docker-build docker-up docker-down migrate revision

setup:
$(PIP) install -r $(APP_DIR)/requirements.txt

run:
uvicorn src.main:create_app --factory --host 0.0.0.0 --port 8000 --app-dir $(APP_DIR) --reload

lint:
black --check $(APP_DIR) tests
isort --check-only $(APP_DIR) tests
flake8 $(APP_DIR) tests

format:
black $(APP_DIR) tests
isort $(APP_DIR) tests

test:
pytest -q

revision:
alembic -c $(APP_DIR)/alembic.ini revision -m "manual"

migrate:
alembic -c $(APP_DIR)/alembic.ini upgrade head

docker-build:
docker compose -f infra/docker-compose.yml build

docker-up:
docker compose -f infra/docker-compose.yml up

docker-down:
docker compose -f infra/docker-compose.yml down -v
73 changes: 61 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,61 @@
- 👋 Hi, I’m @Qomserver
- 👀 I’m interested in ...
- 🌱 I’m currently learning ...
- 💞️ I’m looking to collaborate on ...
- 📫 How to reach me ...
- 😄 Pronouns: ...
- ⚡ Fun fact: ...

<!---
Qomserver/Qomserver is a ✨ special ✨ repository because its `README.md` (this file) appears on your GitHub profile.
You can click the Preview link to take a look at your changes.
--->
## ProdReadyApp (FastAPI + Postgres)

A production-ready web application with authentication and task management built with FastAPI, SQLAlchemy, Alembic, and PostgreSQL. Includes Docker, CI, tests, and secure defaults.

### Features
- JWT auth (access + refresh), password hashing
- Users + Tasks CRUD with ownership and admin controls
- SQLAlchemy 2.0 + Alembic migrations
- Health checks and Prometheus metrics
- Rate limiting, CORS, secure headers
- Gunicorn + Uvicorn workers
- Docker Compose for local/dev/prod
- GitHub Actions CI (lint + tests)

### Quickstart (Docker)
1. Copy env file:
```bash
cp .env.example .env
```
2. Start services:
```bash
docker compose -f infra/docker-compose.yml up --build
```
3. App available at `http://localhost:8000` (API docs at `/docs`).

### Local (no Docker)
```bash
python -m venv .venv && source .venv/bin/activate
pip install -r app/requirements.txt
export $(grep -v '^#' .env.example | xargs) # or create .env
alembic -c app/alembic.ini upgrade head
uvicorn src.main:create_app --factory --host 0.0.0.0 --port 8000 --app-dir app
```

### Useful Make targets
```bash
make setup # install deps
make run # run dev server
make test # run tests
make lint # run linters
make format # format code
make docker-up
```

### Migrations
```bash
alembic -c app/alembic.ini revision -m "message"
alembic -c app/alembic.ini upgrade head
```

### Configuration
Set environment variables (see `.env.example`). Defaults are safe for local dev. For production, always set `JWT_SECRET`, use strong DB credentials, and terminate TLS at a reverse proxy or load balancer.

### Security Notes
- Tokens are short-lived, refresh tokens persisted and revocable
- Rate limits applied to auth routes
- CORS is restricted via env
- Non-root container user

### License
MIT
41 changes: 41 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# syntax=docker/dockerfile:1.7-labs

FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app

# System deps
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*

# Install deps
COPY app/requirements.txt /app/requirements.txt
RUN pip install -r /app/requirements.txt

# Runtime image
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app

# Create non-root user
RUN useradd -m -u 10001 appuser

# Copy installed packages and source
COPY --from=base /usr/local /usr/local
COPY app /app

# Ensure runtime dirs
RUN mkdir -p /app/src/alembic/versions && chown -R appuser:appuser /app

USER appuser

EXPOSE 8000

ENTRYPOINT ["/bin/bash", "-lc"]
CMD ["bash", "docker/entrypoint.sh"]
37 changes: 37 additions & 0 deletions app/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[alembic]
script_location = app/src/alembic
prepend_sys_path = .

sqlalchemy.url = %(DATABASE_URL)s

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers = console
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
25 changes: 25 additions & 0 deletions app/docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail

# Wait for DB if DATABASE_URL points to a service
python - <<'PY'
import os, time
from urllib.parse import urlparse
import socket

url = os.environ.get('DATABASE_URL', '')
if url:
parsed = urlparse(url)
host = parsed.hostname
port = parsed.port or 5432
if host not in (None, 'localhost', '127.0.0.1'):
for _ in range(60):
try:
with socket.create_connection((host, port), timeout=1):
break
except OSError:
time.sleep(1)
PY

alembic -c app/alembic.ini upgrade head
exec gunicorn 'src.main:create_app()' -k uvicorn.workers.UvicornWorker -c app/gunicorn_conf.py
12 changes: 12 additions & 0 deletions app/gunicorn_conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
threads = 2
keepalive = 30
timeout = 60
accesslog = "-"
errorlog = "-"
loglevel = "info"
forwarded_allow_ips = "*"
22 changes: 22 additions & 0 deletions app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
fastapi==0.110.2
uvicorn[standard]==0.30.0
gunicorn==21.2.0
SQLAlchemy==2.0.36
psycopg[binary]==3.2.9
alembic==1.13.1
pydantic[email]==2.9.2
pydantic-settings==2.2.1
passlib[bcrypt]==1.7.4
python-jose[cryptography]==3.3.0
slowapi==0.1.9
prometheus-client==0.19.0
Jinja2==3.1.4
httpx==0.27.0

# Dev
pytest==8.2.0
pytest-asyncio==0.23.6
black==24.4.2
isort==5.13.2
flake8==7.0.0
mypy==1.10.0
Empty file added app/src/__init__.py
Empty file.
Loading
Loading