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
1 change: 1 addition & 0 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
uses: docker/build-push-action@v2
with:
push: true
file: prod.Dockerfile
tags: ${{ secrets.DOCKER_REPO_API }}

- name: Redeploy on Server
Expand Down
17 changes: 12 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
FROM python:3.8-slim

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1

# Let service stop gracefully
STOPSIGNAL SIGQUIT

# Copy project files into working directory
WORKDIR /app
ADD . /app

# Install project dependencies
RUN pip install -U pipenv
RUN pipenv install --system --deploy
RUN apt-get update && apt-get install gcc -y

COPY Pipfile Pipfile.lock ./

RUN pip install pipenv
RUN pipenv install --deploy --system

ADD . /app

# Run the API.
CMD python launch.py runserver --host 0.0.0.0 --port 5000 --initdb --debug
CMD python launch.py runserver --initdb --verbose --debug
8 changes: 5 additions & 3 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ verify_ssl = true
[dev-packages]
black = "*"
flake8 = "*"
requests = "*"
pre-commit = "*"
pytest-asyncio = "*"

[packages]
quart = "*"
quart-cors = "*"
postdb = "*"
pyjwt = "*"
postdb = "*"
aiohttp = "*"
fastapi = "*"
uvicorn = {extras = ["standard"], version = "*"}
uvloop = {markers = "platform_system == 'linux'", version = "*"}

[requires]
Expand All @@ -24,4 +25,5 @@ python_version = "3.8"
allow_prereleases = true

[scripts]
test = "python -m pytest"
lint = "pre-commit run --all-files"
740 changes: 377 additions & 363 deletions Pipfile.lock

Large diffs are not rendered by default.

28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
[![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](/LICENCE)
[![Discord](https://discord.com/api/guilds/501090983539245061/widget.png?style=shield)](https://discord.gg/twt)
[![Test and deploy](https://github.com/Tech-With-Tim/API/workflows/Release%20-%20Test%2C%20Build%20%26%20Redeploy/badge.svg)](https://github.com/Tech-With-Tim/API/actions?query=workflow%3A%22Release+-+Test%2C+Build+%26+Redeploy%22)

<!-- TODO: Lint & Test status -->

</div>

API for the Tech With Tim website using [Quart](https://pgjones.gitlab.io/quart/).
API for the Tech With Tim website using [FastAPI](https://fastapi.tiangolo.com/).

## 📝 Table of Contents

<!-- - [🧐 About](#-about) -->

- [🏁 Getting Started](#-getting-started)
- [Discord application](#discord-application)
- [Prerequisites](#prerequisites)
Expand Down Expand Up @@ -81,10 +83,11 @@ DISCORD_CLIENT_SECRET=

And fill in the variables with the values below:

- ``SECRET_KEY`` is the key used for the JWT token encoding.
- ``DB_URI`` is the PostgreSQL database URI.
- ``DISCORD_CLIENT_ID`` is the Discord application ID. Copy it from your Discord application page (see below).
- ``DISCORD_CLIENT_SECRET`` is the Discord application secret. Copy it from your Discord application page (see below).
- `POSTGRES_URI` is the PostgreSQL database URI.
- `SECRET_KEY` is the key used for JWT token encoding.
- `TEST_POSTGRES_URI` is the PostgreSQL database URI for tests.
- `DISCORD_CLIENT_ID` is the Discord application ID. Copy it from your Discord application page (see below).
- `DISCORD_CLIENT_SECRET` is the Discord application secret. Copy it from your Discord application page (see below).

![Client ID and secret](https://cdn.discordapp.com/attachments/721750194797936823/794646777840140298/unknown.png)

Expand All @@ -110,9 +113,9 @@ Both the API and the [frontend](https://github.com/Tech-With-Tim/Frontend) can b

- Deploy the API:

```sh
docker-compose up
```
```sh
docker-compose up --build api
```

## ✅ Linting

Expand All @@ -135,17 +138,18 @@ To test the API, we use the [pytest](https://docs.pytest.org/en/stable/) framewo
Run the tests:

```sh
pipenv run pytest
pipenv run test
```

**When you contribute, you need to add tests on the features you add.** An example can be seen in [tests/test_index.py](/tests/test_index.py).
**When you contribute, you need to add tests on the features you add.**

## ⛏️ Built Using

- [Python](https://www.python.org/) - Language
- [Quart](https://pgjones.gitlab.io/quart/) - Backend module
- [FastAPI](https://fastapi.tiangolo.com/) - Backend framework
- [PostDB](https://github.com/SylteA/postDB) - Database module
- [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) - Test framework (asynchronous version of [pytest](https://docs.pytest.org/en/stable/))
- [pytest](https://docs.pytest.org/en/stable/) - Testing framework
- [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) - Testing plugin for [pytest](https://docs.pytest.org/en/stable/)

## ✍️ Authors

Expand Down
79 changes: 22 additions & 57 deletions api/app.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,19 @@
from quart import Quart, Response, exceptions, jsonify
from datetime import datetime, date
from aiohttp import ClientSession
from typing import Any, Optional
from quart_cors import cors
from fastapi import FastAPI, HTTPException
from utils.response import JSONResponse
from api import versions
import logging
import json

import utils

from api.blueprints import auth, guilds, users


log = logging.getLogger()


class JSONEncoder(json.JSONEncoder):
def default(self, o: Any) -> Any:
if isinstance(o, (datetime, date)):
o.replace(microsecond=0)
return o.isoformat()

return super().default(o)


class API(Quart):
"""Quart subclass to implement more API like handling."""

http_session: Optional[ClientSession] = None
request_class = utils.Request
json_encoder = JSONEncoder
class API(FastAPI):
"""FastAPI subclass to implement more API like handling."""

def __init__(self, *args, **kwargs):
kwargs.setdefault("static_folder", None)
super().__init__(*args, **kwargs)

async def handle_request(self, request: utils.Request) -> Response:
response = await super().handle_request(request)
log.info(f"{request.method} @ {request.base_url} -> {response.status_code}")
return response

async def handle_http_exception(self, error: exceptions.HTTPException):
async def handle_http_exception(self, error: HTTPException):
"""
Returns errors as JSON instead of default HTML
Uses custom error handler if one exists.
Expand All @@ -53,35 +27,23 @@ async def handle_http_exception(self, error: exceptions.HTTPException):
headers = error.get_headers()
headers["Content-Type"] = "application/json"

return (
jsonify(error=error.name, message=error.description),
error.status_code,
headers,
return JSONResponse(
headers=headers,
status_code=error.status_code,
content={"error": error.name, "message": error.description},
)

async def startup(self) -> None:
self.http_session = ClientSession()
return await super().startup()


# Set up app
app = API(__name__)
app.asgi_app = utils.TokenAuthMiddleware(app.asgi_app, app)
app = cors(app, allow_origin="*") # TODO: Restrict the origin(s) in production.
# Set up blueprints
auth.setup(app=app, url_prefix="/auth")
users.setup(app=app, url_prefix="/users")
guilds.setup(app=app, url_prefix="/guilds")
app = API()
app.router.default_response_class = JSONResponse

app.include_router(versions.v1.router)

@app.route("/")
async def index():
"""Index endpoint used for testing."""
return jsonify(status="OK")
app.add_exception_handler(HTTPException, app.handle_http_exception)


@app.errorhandler(500)
async def error_500(error: BaseException):
@app.exception_handler(500)
async def error_500(request, error: HTTPException):
"""
TODO: Handle the error with our own error handling system.
"""
Expand All @@ -90,7 +52,10 @@ async def error_500(error: BaseException):
exc_info=(type(error), error, error.__traceback__),
)

return (
jsonify(error="Internal Server Error", message="Server got itself in trouble"),
500,
return JSONResponse(
status_code=500,
content={
"error": "Internal Server Error",
"message": "Server got itself in trouble",
},
)
13 changes: 0 additions & 13 deletions api/blueprints/auth/__init__.py

This file was deleted.

1 change: 0 additions & 1 deletion api/blueprints/auth/views/__init__.py

This file was deleted.

Loading