Skip to content
Draft
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,22 @@ python manage.py runserver & celery -A config worker --beat --scheduler django -

Go to: http://localhost:8000

### OpenAPI schema generation

The API spec in [openapi.yaml](openapi.yaml) can be generated automatically from Django/DRF using [`drf-spectacular`](https://github.com/tfranzel/drf-spectacular).

From the project root, run:

```bash
cd src
python manage.py generate_openapi --validate
```

When schema serving is enabled (default: `DEBUG=True`, or by setting `SPECTACULAR_ENABLE_SERVE=True`), docs are available at:

- `http://localhost:8000/api/v1/schema/`
- `http://localhost:8000/api/v1/docs/`

## 💪 Support the Project

There are many ways you can support Yamtrack's development:
Expand Down
10 changes: 8 additions & 2 deletions src/api/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@

from users.models import User

AUTHORIZATION_HEADER = "Authorization"
API_KEY_HEADER = "X-API-Key"


class BearerAuthentication(BaseAuthentication):
"""Bearer Authentication."""

keyword = "Bearer"
header_name = AUTHORIZATION_HEADER

def authenticate(self, request):
"""Authenticate the user with Bearer token."""
auth = request.headers.get("Authorization")
auth = request.headers.get(self.header_name)
if not auth:
return None
parts = auth.split()
Expand All @@ -29,9 +33,11 @@ def authenticate(self, request):
class APIKeyAuthentication(BaseAuthentication):
"""API Key Authentication."""

header_name = API_KEY_HEADER

def authenticate(self, request):
"""Authenticate the user with API Key."""
auth = request.headers.get("X-API-Key")
auth = request.headers.get(self.header_name)
if not auth:
return None
try:
Expand Down
Empty file added src/api/management/__init__.py
Empty file.
Empty file.
43 changes: 43 additions & 0 deletions src/api/management/commands/generate_openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from pathlib import Path

from django.core.management import BaseCommand, call_command


class Command(BaseCommand):
"""Generate the OpenAPI schema file from DRF endpoints."""

help = "Generate openapi.yaml using drf-spectacular."

def add_arguments(self, parser):
"""Add optional CLI arguments."""
parser.add_argument(
"--file",
default=None,
help="Output path for the schema file. Defaults to repository openapi.yaml.", # noqa: E501
)
parser.add_argument(
"--validate",
action="store_true",
help="Validate generated schema during export.",
)

def handle(self, *args, **options): # noqa: ARG002
"""Generate OpenAPI file using spectacular command."""
if options["file"]:
output_path = Path(options["file"]).expanduser().resolve()
else:
repo_root = Path(__file__).resolve().parents[4]
output_path = repo_root / "openapi.yaml"
Comment thread
66Bunz marked this conversation as resolved.
Outdated

output_path.parent.mkdir(parents=True, exist_ok=True)

call_command(
"spectacular",
file=str(output_path),
validate=options["validate"],
color=True,
)

self.stdout.write(
self.style.SUCCESS(f"OpenAPI schema generated: {output_path}"),
)
15 changes: 15 additions & 0 deletions src/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from django.conf import settings
from django.urls import re_path
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
)

from . import views

Expand Down Expand Up @@ -165,3 +170,13 @@
),
re_path(r"^statistics/?$", views.StatisticsView.as_view(), name="api_statistics"),
]

if settings.SPECTACULAR_ENABLE_SERVE:
urlpatterns += [
re_path(r"^schema/?$", SpectacularAPIView.as_view(), name="api_schema"),
re_path(
r"^docs/?$",
SpectacularSwaggerView.as_view(url_name="api_schema"),
name="api_docs",
),
]
30 changes: 27 additions & 3 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def secret(key, default=undefined, **kwargs):
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"api",
"app",
"events",
"integrations",
Expand All @@ -131,7 +132,6 @@ def secret(key, default=undefined, **kwargs):
"allauth.socialaccount",
"django.contrib.humanize",
"rest_framework",
"api",
"drf_spectacular",
]

Expand All @@ -143,11 +143,10 @@ def secret(key, default=undefined, **kwargs):
"api.authentication.BearerAuthentication",
"api.authentication.APIKeyAuthentication",
],
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
}


APPEND_SLASH = True

MIDDLEWARE = [
Expand Down Expand Up @@ -352,6 +351,31 @@ def secret(key, default=undefined, **kwargs):

TRACK_TIME = config("TRACK_TIME", default=True, cast=bool)

SPECTACULAR_ENABLE_SERVE = config(
"SPECTACULAR_ENABLE_SERVE",
default=DEBUG,
cast=bool,
)

SPECTACULAR_SETTINGS = {
"TITLE": "Yamtrack API",
"DESCRIPTION": "OpenAPI schema for Yamtrack's API",
"VERSION": "0.0.25",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The version string is hardcoded here, but a VERSION constant is already defined on line 348 (retrieved from environment variables). Using the existing constant ensures that the OpenAPI documentation stays in sync with the application version.

Suggested change
"VERSION": "0.0.25",
"VERSION": VERSION,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe when the API will be stable, not now

"LICENSE": {
"name": "GNU AFFERO GENERAL PUBLIC LICENSE v3.0",
"url": "https://github.com/FuzzyGrim/Yamtrack/blob/dev/LICENSE",
},
"SCHEMA_PATH_PREFIX": "/api/v1",
"SORT_OPERATIONS": True,
"COMPONENT_SPLIT_REQUEST": True,
"SERVERS": [
{
"url": "http://localhost:8000/",
"description": "Local development server",
},
],
}

TZ = zoneinfo.ZoneInfo(TIME_ZONE)

IMG_NONE = "https://www.themoviedb.org/assets/2/v4/glyphicons/basic/glyphicons-basic-38-picture-grey-c2ebdbb057f2a7614185931650f8cee23fa137b93812ccb132b9df511df1cfac.svg"
Expand Down
7 changes: 0 additions & 7 deletions src/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from django.contrib import admin
from django.contrib.auth.decorators import login_not_required
from django.urls import include, path
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from health_check.views import HealthCheckView
from redis.asyncio import Redis as RedisClient

Expand Down Expand Up @@ -44,12 +43,6 @@
),
),
path("api/v1/", include("api.urls")),
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/docs/",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
]

# Build the accounts URLs
Expand Down
Loading