Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ahosgood committed Aug 6, 2024
0 parents commit 3038fda
Show file tree
Hide file tree
Showing 24 changed files with 966 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.dockerignore
.git
docker-compose.yml
Dockerfile
README.md
.gitignore
20 changes: 20 additions & 0 deletions .github/actions/tests/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Run Python tests

runs:
using: "composite"
steps:
- uses: actions/setup-python@v5
with:
python-version: 3.12
- uses: snok/install-poetry@v1
with:
version: 1.8.1
virtualenvs-create: true
virtualenvs-in-project: true
virtualenvs-path: .venv
- name: Install Poetry dependencies
run: poetry install --no-interaction --no-root --with dev
shell: bash
- name: Run Python tests
run: poetry run python -m pytest
shell: bash
24 changes: 24 additions & 0 deletions .github/workflows/branch-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Clean up feature branch

on:
delete:

jobs:
delete:
if: github.event.ref_type == 'branch' && startsWith(github.event.ref, 'feature/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get tag from deleted branch
id: version-tag
run: echo "VERSION=$(echo "${{ github.event.ref }}" | sed -e 's,/,-,g')" >> "$GITHUB_OUTPUT"
- name: Debug
run: echo "Clean up Docker image example-fastapi-application:${{ steps.version-tag.outputs.VERSION }}"
- name: Delete image
if: ${{ steps.version-tag.outputs.VERSION }}
uses: bots-house/[email protected]
with:
owner: ${{ github.repository_owner }}
name: example-fastapi-application
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ steps.version-tag.outputs.VERSION }}
44 changes: 44 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Build and deploy

on:
workflow_dispatch:
push:

concurrency:
group: cd-${{ github.ref }}

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
uses: ./.github/actions/tests

version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get tag
id: version-tag
uses: nationalarchives/ds-docker-actions/.github/actions/get-version-tag@main
outputs:
version: ${{ steps.version-tag.outputs.version-tag }}

build:
runs-on: ubuntu-latest
needs:
- test
- version
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Build Docker image
uses: nationalarchives/ds-docker-actions/.github/actions/docker-build@main
with:
version: ${{ needs.version.outputs.version }}
latest: ${{ github.ref == 'refs/heads/main' }}
github-token: ${{ secrets.GITHUB_TOKEN }}
docker-image-name: example-fastapi-application
15 changes: 15 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Pull request

on:
pull_request:
types:
- opened
- synchronize

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
uses: ./.github/actions/tests
16 changes: 16 additions & 0 deletions .github/workflows/remove-untagged.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Remove untagged container images

on:
workflow_dispatch:
schedule:
- cron: "0 3 * * 1"

jobs:
remove-untagged:
runs-on: ubuntu-latest
steps:
- name: Remove untagged Docker images
uses: nationalarchives/ds-docker-actions/.github/actions/remove-untagged@main
with:
docker-image-name: example-fastapi-application
github-token: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
__pycache__
.pytest_cache
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
ARG IMAGE=ghcr.io/nationalarchives/tna-python
ARG IMAGE_TAG=latest

FROM "$IMAGE":"$IMAGE_TAG"

ARG BUILD_VERSION
ENV BUILD_VERSION="$BUILD_VERSION"

# Copy in the dependencies config
COPY --chown=app pyproject.toml poetry.lock ./

# Install the dependencies
RUN tna-build

# Copy in the application code
COPY --chown=app . .

# Run the application
CMD ["tna-run", "-a", "fastapi_app:app"]
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 The National Archives, UK

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# TNA Python FastAPI Application

## Quickstart

```sh
# Build and start the container
docker compose up -d
```

### Docs

http://localhost:83/docs

### Run tests

```sh
docker compose exec dev poetry run python -m pytest
```

### Format and lint code

```sh
docker compose exec dev format
```

## Environment variables

In addition to the [base Docker image variables](https://github.com/nationalarchives/docker/blob/main/docker/tna-python/README.md#environment-variables), this application has support for:

| Variable | Purpose | Default |
| ------------- | --------------------------------------------- | ------------------- |
| `CONFIG` | The configuration to use | `config.Production` |
| `BASE_URI` | The base URI for the API | `/api/v1` |
| `FORCE_HTTPS` | Redirect requests to HTTPS as part of the CSP | _none_ |
26 changes: 26 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from app.lib.get_config import get_config
from fastapi import FastAPI
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware


def create_app(config_class):
config = get_config(config_class)

app = FastAPI(
title="TNA FastAPI Application", log_level=config.get("LOG_LEVEL")
)
app.state.config = {"BASE_URI": config.get("BASE_URI")}
if config.get("FORCE_HTTPS"):
app.add_middleware(HTTPSRedirectMiddleware)

from .greetings import routes as greetings_routes
from .healthcheck import routes as healthcheck_routes

app.include_router(healthcheck_routes.router, prefix="/healthcheck")
app.include_router(
greetings_routes.router,
prefix=f"{config.get('BASE_URI')}/greetings",
tags=["Examples"],
)

return app
5 changes: 5 additions & 0 deletions app/greetings/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from fastapi import APIRouter

router = APIRouter()

from app.greetings import routes # noqa: E402,F401
26 changes: 26 additions & 0 deletions app/greetings/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from app.greetings import router
from pydantic import BaseModel


class GreetingResponse(BaseModel):
message: str

def __init__(self, greeting: str, name: str):
super().__init__(message=f"{greeting}, {name}")


@router.get("/hello/")
async def hello(
name: str,
) -> GreetingResponse:
response = GreetingResponse(greeting="Hello", name=name)
return response


@router.get("/{greeting}/")
async def greeting(
greeting: str,
name: str,
) -> GreetingResponse:
response = GreetingResponse(greeting=greeting, name=name)
return response
5 changes: 5 additions & 0 deletions app/healthcheck/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from fastapi import APIRouter

router = APIRouter()

from app.healthcheck import routes # noqa: E402,F401
6 changes: 6 additions & 0 deletions app/healthcheck/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from app.healthcheck import router


@router.get("/live/", include_in_schema=False)
async def healthcheck() -> str:
return "ok"
11 changes: 11 additions & 0 deletions app/lib/get_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
def get_config(config_class):
components = config_class.split(".")
mod = __import__(components[0])
for comp in components[1:]:
mod = getattr(mod, comp)
config_mod = mod()
config = {}
for key in dir(config_mod):
if key.isupper():
config[key] = getattr(config_mod, key)
return config
13 changes: 13 additions & 0 deletions app/lib/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def strtobool(val):
"""Convert a string representation of truth to true (1) or false (0).
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
'val' is anything else.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return 1
elif val in ("n", "no", "f", "false", "off", "0"):
return 0
else:
raise ValueError("invalid truth value %r" % (val,))
38 changes: 38 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import json
import os

from app.lib.util import strtobool


class Features(object):
pass


class Base(object):
ENVIRONMENT: str = os.environ.get("ENVIRONMENT", "production")

BUILD_VERSION: str = os.environ.get("BUILD_VERSION", "")

SECRET_KEY: str = os.environ.get("SECRET_KEY", "")

BASE_URI: str = os.environ.get("BASE_URI", "/api/v1")

FORCE_HTTPS: bool = strtobool(os.getenv("FORCE_HTTPS", "True"))


class Production(Base, Features):
pass


class Staging(Base, Features):
pass


class Develop(Base, Features):
FORCE_HTTPS = strtobool(os.getenv("FORCE_HTTPS", "False"))


class Test(Base, Features):
ENVIRONMENT = "test"

FORCE_HTTPS = False
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
app:
build:
context: .
args:
IMAGE: ghcr.io/nationalarchives/tna-python-root
IMAGE_TAG: preview
environment:
- ENVIRONMENT=develop
- CONFIG=config.Develop
- SECRET_KEY=abc123
ports:
- 83:8080
volumes:
- ./:/app
dev:
image: ghcr.io/nationalarchives/tna-python-dev:preview
volumes:
- ./:/app
7 changes: 7 additions & 0 deletions fastapi_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import os

from app import create_app

app = create_app(
os.getenv("CONFIG", "config.Production"),
)
Loading

0 comments on commit 3038fda

Please sign in to comment.