Skip to content

Commit

Permalink
Add FastAPI task template
Browse files Browse the repository at this point in the history
  • Loading branch information
frimro committed Jan 18, 2023
1 parent 9e53deb commit 7ec6d04
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 85 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
name: 'Lint Python code'

name: "Lint Python code"
on: [push, pull_request]

jobs:
lint-python:
runs-on: ubuntu-latest
strategy:
matrix:
python_version: ['3.6', '3.8', '3.10']
python_version: ["3.10"]
steps:
- name: Setup repository
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Set up python ${{ matrix.python_version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python_version }}

Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Template API tests
on: [push, pull_request]

jobs:
testing:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11"]
steps:
- name: Checkout repository with submodules
uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: pip3 install -r requirements.txt

- name: Test code
run: pytest -vv tests
12 changes: 6 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM ghcr.io/seravo/flask:latest

FROM ghcr.io/seravo/fastapi:latest
ARG APT_PROXY
USER user

COPY requirements.txt /app/requirements.txt
COPY --chown=user:user requirements.txt .
RUN pip3 install -r requirements.txt

RUN pip install -r /app/requirements.txt
COPY seravo $APPDIR/seravo/

ENV FLASK_APP hello:app
COPY /app/ /app/
CMD ["seravo.main:app"]
29 changes: 18 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
APT_PROXY ?=
DOCKER ?= docker
IMAGE ?= ypcs/FIXME:latest
CONTAINER ?= FIXME
SUDO ?=

all:

clean:
$(DOCKER) rm -f "$(CONTAINER)" || :
purge:
$(SUDO) docker-compose down -v --remove-orphans

stop:
$(SUDO) docker-compose down

build:
$(DOCKER) build --build-arg APT_PROXY="$(APT_PROXY)" -t $(IMAGE) .
$(SUDO) docker-compose build

run: build
$(SUDO) docker-compose up -d

develop: run
$(SUDO) docker-compose logs --follow

run: clean
$(DOCKER) run --name "$(CONTAINER)" --rm --volume "$(shell pwd)/app/:/app/:ro" -p 127.0.42.1:8080:8080 -d $(IMAGE)
test:
@$(SUDO) docker-compose exec api pytest tests -vv

reload:
$(DOCKER) exec "$(CONTAINER)" touch /tmp/reload-app
@$(SUDO) docker-compose exec api reload-gunicorn

logs:
$(DOCKER) logs "$(CONTAINER)"
exec:
@$(SUDO) docker-compose exec api bash
114 changes: 84 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,107 @@
# Flask template
# Seravo development team recruit template

Flask project template
_We are currently not hiring, but you can still clone the repository, give the tasks a spin, and send us an open application at [email protected]._

## Usage
## Using the supplied Docker environment with Makefile

To run hello world app, :8080 provides HTTP endpoint (to uWSGI), and :9000 provides uWSGI endpoint. So, let's try accessing via HTTP:
If you have Docker installed, and are familiar with the `make` command, you can use the following commands to start the development environment.

docker run --rm -it -v $(pwd)/app:/app -e FLASK_APP=hello:app -p 127.0.0.1:8080:8080 ghcr.io/seravo/flask:latest
### Usage

now try to open http://127.0.0.1:8080/ , you should see familiar greeting.
Run `make develop` to start the container. You can access the API at `localhost:8080`.

To publish your own app, mount your app to `/app`, and provide FLASK_APP environment variable with correct value.
### Reloading changes

Eg.
Run `make reload` to reload the gunicorn workers. If you think that your changes are not reflected in the application after the reload, you can run `make develop` again.

docker run --rm -it -v $(pwd)/app:/app -e FLASK_APP=app:app -p 127.0.0.1:8080:8080 ghcr.io/seravo/flask:latest
### Cleanin up

if you had app code like
Run `make purge` to remove the container.

app/app.py:
### Tests

from flask import Flask
app = Flask(__name__)
You can use `make test` to execute tests inside the container.

@app.route("/")
def some_view():
return "Hello myapp!"
## Running FastAPI from the terminal

Usually you shouldn't expose this HTTP endpoint directly to internet, but use eg. `ypcs/nginx:latest` as a reverse proxy.
Alternatively, you can also run the application from your terminal with `uvicorn`. See [FastAPI docs](https://fastapi.tiangolo.com/#installation) on how to install the required packages.

## Live reload
Remember to also install the dependencies defined in `requirements.txt`!

By default, you can manually reload the application by touching `/tmp/reload-app`. If you want your application to reload automatically after every save, set `FLASK_RELOAD` as `true`.
```console
pip install -r requirements.txt
```

docker run --rm -it -v $(pwd)/app:/app -e FLASK_APP=app:app -e FLASK_RELOAD=true -p 127.0.0.1:8080:8080 ghcr.io/seravo/flask:latest
Finally, run `uvicorn seravo.main:app --reload` to start our base application.

Now, if you modify the code in `app.py` and save, the app will reload and your changes will be in effect.
You can run the tests with `pytest -vv tests`, granted you have `pytest` installed.

## Tests
## The tasks

Tests can be ran by executing `pytest-3` inside the container. For example, to run the tests for `hello.py` you would do it like this:
The tasks should be possible to implement with the given packages in `requirements.txt`. If you want to use a new package (e.g. `requests` instead of `httpx`), you can just add it to the requirements file and use it in your code.

docker exec -t {CONTAINER_NAME} pytest-3 hello.py -vv
1. Create a new endpoint `/pokemon/{name}`, which can be accessed with a GET request.

Test output should look something like this:
- When triggered, the application queries `https://pokeapi.co/api/v2/pokemon/{name}/` for a JSON blob about the chosen pokemon
- Application should handle possible errors
- Data should be modified to match the following structure:

platform linux -- Python 3.9.2, pytest-6.0.2, py-1.10.0, pluggy-0.13.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /app
collected 1 item
```python
name: str
height: int
weight: int
types: List[str]
```

hello.py::test_hello PASSED
- Application returns the modifed JSON blob to the user
- So, for example, `/pokemon/pikachu` would return this:

```json
{
"name": "pikachu",
"height": 4,
"weight": 60,
"types": ["electric"]
}
```

2. Create a new endpoint `/caps/`, which can be accessed with a POST request.

- Endpoint can be queried with the following body:

```json
{
"message": "string"
}
```

- The API will return the content of the `message` field in all CAPS.
- The API should reject the request if the body does not match the one above
- The API should reject the request if there already is a capitalized letter in the data

### Grading

We will give points based on the following criteria:

1. Code readability, code documentation, version control
2. Code structure: is everything bundled in a single file or distributed
3. Error handling: HTTP status codes
4. Test coverage: Were the tests extended to test the new features.

### Helpful links

[Path parameters](https://fastapi.tiangolo.com/tutorial/path-params/)

[Request body](https://fastapi.tiangolo.com/tutorial/body/)

[HTTPX QuickStart](https://www.python-httpx.org/quickstart/)

[HTTPX Async](https://www.python-httpx.org/async/)

[Pydantic validators](https://docs.pydantic.dev/usage/validators/)

[Test mocking](https://docs.pytest.org/en/6.2.x/monkeypatch.html#monkeypatching-returned-objects-building-mock-classes)

## Submitting your work

Set up a private repository with your modifications, share it to @seravo-hrm and notify us at [email protected] with your application and we will get back to you as soon as possible!
32 changes: 0 additions & 32 deletions app/hello.py

This file was deleted.

14 changes: 14 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: "3.7"

services:
api:
image: seravo-api
build: .
environment:
- PYTHONPATH=/app
healthcheck:
disable: true
ports: ["8080:8000"]
volumes:
- "./seravo/:/app/seravo:rw"
- "./tests/:/app/tests:rw"
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# your packages here
fastapi==0.88
pytest==7.1
httpx==0.23
13 changes: 13 additions & 0 deletions seravo/common/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from fastapi import FastAPI


def create_app() -> FastAPI:
"""Create a FastAPI application"""
app = FastAPI()

@app.get("/")
async def root():
"""Return API information"""
return {'detail': "Seravo API"}

return app
3 changes: 3 additions & 0 deletions seravo/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from seravo.common.app import create_app

app = create_app()
Empty file added tests/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions tests/test_00_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from collections.abc import Generator

from fastapi.testclient import TestClient
from pytest import fixture

from seravo.common.app import create_app


class BaseTestSuite:
"""Common operations to be used in testing"""

@fixture
def client(self) -> Generator[TestClient, None, None]:
"""Return an API client for testing purposes"""
app = create_app()
yield TestClient(app)
11 changes: 11 additions & 0 deletions tests/test_01_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .test_00_base import BaseTestSuite


class TestCommon(BaseTestSuite):
"""Test the common operations of the API"""

def test_api_root(self, client) -> None:
"""Test that the API root returns content"""
res = client.get("/")
assert res.status_code == 200
assert res.json().get('detail') == "Seravo API"

0 comments on commit 7ec6d04

Please sign in to comment.