phase1: bootstrap containerized django stack and project scaffold
This commit is contained in:
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@ -0,0 +1,16 @@
|
||||
.git
|
||||
.gitignore
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.db
|
||||
.pytest_cache
|
||||
.mypy_cache
|
||||
.ruff_cache
|
||||
.env
|
||||
.env.*
|
||||
venv
|
||||
.venv
|
||||
media
|
||||
staticfiles
|
||||
29
.env.example
Normal file
29
.env.example
Normal file
@ -0,0 +1,29 @@
|
||||
# Django
|
||||
DJANGO_SETTINGS_MODULE=config.settings.development
|
||||
DJANGO_SECRET_KEY=change-me-in-production
|
||||
DJANGO_DEBUG=1
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
|
||||
DJANGO_TIME_ZONE=UTC
|
||||
DJANGO_SUPERUSER_USERNAME=admin
|
||||
DJANGO_SUPERUSER_EMAIL=admin@example.com
|
||||
DJANGO_SUPERUSER_PASSWORD=adminpass
|
||||
|
||||
# Database (PostgreSQL only)
|
||||
POSTGRES_DB=hoopscout
|
||||
POSTGRES_USER=hoopscout
|
||||
POSTGRES_PASSWORD=hoopscout
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# Redis / Celery
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
CELERY_BROKER_URL=redis://redis:6379/0
|
||||
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
|
||||
# Runtime behavior
|
||||
AUTO_APPLY_MIGRATIONS=1
|
||||
AUTO_COLLECTSTATIC=1
|
||||
GUNICORN_WORKERS=3
|
||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.so
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
media/
|
||||
staticfiles/
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
46
CONTRIBUTING.md
Normal file
46
CONTRIBUTING.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Contributing to HoopScout
|
||||
|
||||
## Branching Model (GitFlow Required)
|
||||
|
||||
- `main`: production-ready releases only
|
||||
- `develop`: integration branch for completed features
|
||||
- `feature/*`: feature development branches from `develop`
|
||||
- `release/*`: release hardening branches from `develop`
|
||||
- `hotfix/*`: urgent production fixes from `main`
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Create branch from the correct base:
|
||||
- `feature/*` from `develop`
|
||||
- `release/*` from `develop`
|
||||
- `hotfix/*` from `main`
|
||||
2. Keep branch scope small and focused.
|
||||
3. Open PR into the proper target branch.
|
||||
4. Require passing CI checks and at least one review.
|
||||
5. Squash or rebase merge according to repository policy.
|
||||
|
||||
## Local Development
|
||||
|
||||
1. `cp .env.example .env`
|
||||
2. `docker compose up --build`
|
||||
3. `docker compose exec web python manage.py migrate`
|
||||
4. `docker compose exec web python manage.py createsuperuser`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
docker compose exec web pytest
|
||||
```
|
||||
|
||||
## Commit Guidance
|
||||
|
||||
- Use clear commit messages with intent and scope.
|
||||
- Avoid mixing refactors with feature behavior changes.
|
||||
- Include migration files when model changes are introduced.
|
||||
|
||||
## Definition of Done (MVP)
|
||||
|
||||
- Feature works in Dockerized environment.
|
||||
- Tests added/updated for behavior change.
|
||||
- Documentation updated when commands, config, or workflows change.
|
||||
- No secrets committed.
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential libpq-dev postgresql-client curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements/base.txt /tmp/requirements/base.txt
|
||||
RUN pip install --upgrade pip && pip install -r /tmp/requirements/base.txt
|
||||
|
||||
COPY . /app
|
||||
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
RUN mkdir -p /app/staticfiles /app/media /app/runtime
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||
102
README.md
102
README.md
@ -1,2 +1,102 @@
|
||||
# hoopscout
|
||||
# HoopScout
|
||||
|
||||
Production-minded basketball scouting and player search platform built with Django, HTMX, PostgreSQL, Redis, Celery, and nginx.
|
||||
|
||||
## Stack
|
||||
|
||||
- Python 3.12+
|
||||
- Django + Django Templates + HTMX (no SPA)
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- Celery + Celery Beat
|
||||
- nginx
|
||||
- Docker / Docker Compose
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```text
|
||||
.
|
||||
├── apps/
|
||||
├── config/
|
||||
├── nginx/
|
||||
├── requirements/
|
||||
├── templates/
|
||||
├── tests/
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
└── entrypoint.sh
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
1. Copy environment file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Build and start services:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
3. Apply migrations (if not using auto migrations):
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py migrate
|
||||
```
|
||||
|
||||
4. Create superuser:
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
5. Access app:
|
||||
|
||||
- Application: http://localhost
|
||||
- Admin: http://localhost/admin/
|
||||
- Health endpoint: http://localhost/health/
|
||||
|
||||
## Development Commands
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
docker compose exec web pytest
|
||||
```
|
||||
|
||||
Run Django shell:
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py shell
|
||||
```
|
||||
|
||||
Collect static files:
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
Create migration:
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py makemigrations
|
||||
```
|
||||
|
||||
Apply migration:
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py migrate
|
||||
```
|
||||
|
||||
## Ingestion / Sync (Phase Placeholder)
|
||||
|
||||
Data provider ingestion flow will be added in a later phase with adapter-based provider isolation.
|
||||
|
||||
## GitFlow
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for branch model and PR workflow.
|
||||
|
||||
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
0
apps/core/__init__.py
Normal file
0
apps/core/__init__.py
Normal file
6
apps/core/apps.py
Normal file
6
apps/core/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.core"
|
||||
0
apps/core/migrations/__init__.py
Normal file
0
apps/core/migrations/__init__.py
Normal file
8
apps/core/urls.py
Normal file
8
apps/core/urls.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import health, home
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path("health/", health, name="health"),
|
||||
]
|
||||
10
apps/core/views.py
Normal file
10
apps/core/views.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def home(request):
|
||||
return render(request, "home.html")
|
||||
|
||||
|
||||
def health(request):
|
||||
return JsonResponse({"status": "ok"})
|
||||
3
config/__init__.py
Normal file
3
config/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ("celery_app",)
|
||||
6
config/asgi.py
Normal file
6
config/asgi.py
Normal file
@ -0,0 +1,6 @@
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development")
|
||||
|
||||
application = get_asgi_application()
|
||||
8
config/celery.py
Normal file
8
config/celery.py
Normal file
@ -0,0 +1,8 @@
|
||||
import os
|
||||
from celery import Celery
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development")
|
||||
|
||||
app = Celery("hoopscout")
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
app.autodiscover_tasks()
|
||||
0
config/settings/__init__.py
Normal file
0
config/settings/__init__.py
Normal file
99
config/settings/base.py
Normal file
99
config/settings/base.py
Normal file
@ -0,0 +1,99 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
def env_bool(key: str, default: bool = False) -> bool:
|
||||
value = os.getenv(key)
|
||||
if value is None:
|
||||
return default
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def env_list(key: str, default: str = "") -> list[str]:
|
||||
value = os.getenv(key, default)
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
|
||||
|
||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "insecure-development-secret")
|
||||
DEBUG = env_bool("DJANGO_DEBUG", False)
|
||||
ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1")
|
||||
CSRF_TRUSTED_ORIGINS = env_list("DJANGO_CSRF_TRUSTED_ORIGINS", "http://localhost,http://127.0.0.1")
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"apps.core",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
ASGI_APPLICATION = "config.asgi.application"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.getenv("POSTGRES_DB", "hoopscout"),
|
||||
"USER": os.getenv("POSTGRES_USER", "hoopscout"),
|
||||
"PASSWORD": os.getenv("POSTGRES_PASSWORD", "hoopscout"),
|
||||
"HOST": os.getenv("POSTGRES_HOST", "postgres"),
|
||||
"PORT": os.getenv("POSTGRES_PORT", "5432"),
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "UTC")
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0")
|
||||
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://redis:6379/0")
|
||||
CELERY_ACCEPT_CONTENT = ["json"]
|
||||
CELERY_TASK_SERIALIZER = "json"
|
||||
CELERY_RESULT_SERIALIZER = "json"
|
||||
CELERY_TIMEZONE = TIME_ZONE
|
||||
3
config/settings/development.py
Normal file
3
config/settings/development.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .base import * # noqa: F403,F401
|
||||
|
||||
DEBUG = True
|
||||
1
config/settings/local.py
Normal file
1
config/settings/local.py
Normal file
@ -0,0 +1 @@
|
||||
from .development import * # noqa: F403,F401
|
||||
13
config/settings/production.py
Normal file
13
config/settings/production.py
Normal file
@ -0,0 +1,13 @@
|
||||
from .base import * # noqa: F403,F401
|
||||
import os
|
||||
|
||||
DEBUG = False
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
SECURE_HSTS_SECONDS = int(os.getenv("DJANGO_SECURE_HSTS_SECONDS", "31536000"))
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
7
config/urls.py
Normal file
7
config/urls.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("apps.core.urls")),
|
||||
]
|
||||
6
config/wsgi.py
Normal file
6
config/wsgi.py
Normal file
@ -0,0 +1,6 @@
|
||||
import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development")
|
||||
|
||||
application = get_wsgi_application()
|
||||
119
docker-compose.yml
Normal file
119
docker-compose.yml
Normal file
@ -0,0 +1,119 @@
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
depends_on:
|
||||
web:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- static_data:/var/www/static:ro
|
||||
- media_data:/var/www/media:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/health/ || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers ${GUNICORN_WORKERS:-3}
|
||||
volumes:
|
||||
- .:/app
|
||||
- static_data:/app/staticfiles
|
||||
- media_data:/app/media
|
||||
- runtime_data:/app/runtime
|
||||
expose:
|
||||
- "8000"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://127.0.0.1:8000/health/ || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
restart: unless-stopped
|
||||
|
||||
celery_worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
command: celery -A config worker -l info
|
||||
volumes:
|
||||
- .:/app
|
||||
- runtime_data:/app/runtime
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "celery -A config inspect ping -d celery@$$HOSTNAME | grep -q pong || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
celery_beat:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
command: celery -A config beat -l info --schedule=/app/runtime/celerybeat-schedule
|
||||
volumes:
|
||||
- .:/app
|
||||
- runtime_data:/app/runtime
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "test -f /app/runtime/celerybeat-schedule || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --save 60 1 --loglevel warning
|
||||
volumes:
|
||||
- runtime_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
static_data:
|
||||
media_data:
|
||||
runtime_data:
|
||||
21
entrypoint.sh
Executable file
21
entrypoint.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "Waiting for PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}..."
|
||||
until pg_isready -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}"; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "PostgreSQL is available."
|
||||
|
||||
if [ "${AUTO_APPLY_MIGRATIONS:-0}" = "1" ] && [ "$1" = "gunicorn" ]; then
|
||||
echo "Applying database migrations..."
|
||||
python manage.py migrate --noinput
|
||||
fi
|
||||
|
||||
if [ "${AUTO_COLLECTSTATIC:-0}" = "1" ] && [ "$1" = "gunicorn" ]; then
|
||||
echo "Collecting static files..."
|
||||
python manage.py collectstatic --noinput
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
19
manage.py
Normal file
19
manage.py
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and available on your "
|
||||
"PYTHONPATH environment variable?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
51
nginx/nginx.conf
Normal file
51
nginx/nginx.conf
Normal file
@ -0,0 +1,51 @@
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 20m;
|
||||
|
||||
upstream django_upstream {
|
||||
server web:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
location /static/ {
|
||||
alias /var/www/static/;
|
||||
expires 30d;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location /media/ {
|
||||
alias /var/www/media/;
|
||||
expires 30d;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location /health/ {
|
||||
proxy_pass http://django_upstream/health/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://django_upstream;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
pytest.ini
Normal file
3
pytest.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE = config.settings.development
|
||||
python_files = tests.py test_*.py *_tests.py
|
||||
6
requirements/base.txt
Normal file
6
requirements/base.txt
Normal file
@ -0,0 +1,6 @@
|
||||
Django>=5.1,<6.0
|
||||
psycopg[binary]>=3.2,<4.0
|
||||
gunicorn>=22.0,<23.0
|
||||
celery[redis]>=5.4,<6.0
|
||||
redis>=5.2,<6.0
|
||||
python-dotenv>=1.0,<2.0
|
||||
3
requirements/dev.txt
Normal file
3
requirements/dev.txt
Normal file
@ -0,0 +1,3 @@
|
||||
-r base.txt
|
||||
pytest>=8.3,<9.0
|
||||
pytest-django>=4.9,<5.0
|
||||
12
templates/home.html
Normal file
12
templates/home.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HoopScout</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>HoopScout</h1>
|
||||
<p>Phase 1 bootstrap is running.</p>
|
||||
</body>
|
||||
</html>
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
9
tests/test_health.py
Normal file
9
tests/test_health.py
Normal file
@ -0,0 +1,9 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_health_endpoint(client):
|
||||
response = client.get(reverse("health"))
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "ok"
|
||||
Reference in New Issue
Block a user