Skip to content

Commit 57ff5da

Browse files
committed
modernize fastapi stack
1 parent 873bc55 commit 57ff5da

30 files changed

Lines changed: 1303 additions & 755 deletions

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ coverage.xml
2020
.github
2121
.hypothesis
2222
.venv
23+
.DS_Store
24+
build
25+
dist
26+
*.egg-info

.env

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# ENV
2-
PYTHON_VERSION="313" # which dockerfile to use. see in dockerfiles/python*/Dockerfile
2+
PYTHON_VERSION=314 # which dockerfile to use. see in dockerfiles/python*/Dockerfile
33

44
# App config.
55

6-
API_USERNAME="ubuntu"
7-
API_PASSWORD="debian"
6+
API_USERNAME=ubuntu
7+
API_PASSWORD=debian
88

99
# To get a string like this run:
1010
# openssl rand -hex 32
11-
API_SECRET_KEY="09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
12-
API_ALGORITHM="HS256"
13-
# infinity
14-
API_ACCESS_TOKEN_EXPIRE_MINUTES="5256000000"
11+
API_SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
12+
API_ALGORITHM=HS256
13+
API_ACCESS_TOKEN_EXPIRE_MINUTES=30
14+
CORS_ALLOW_ORIGINS=["http://localhost","http://localhost:5002","http://127.0.0.1","http://127.0.0.1:5002"]

.github/dependabot.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ updates:
33
- package-ecosystem: "pip" # See documentation for possible values.
44
directory: "/" # Location of package manifests.
55
schedule:
6-
interval: "monthly"
6+
interval: "weekly"
77

88
# Maintain dependencies for GitHub Actions.
99
- package-ecosystem: "github-actions"
1010
directory: "/"
1111
schedule:
12-
interval: "monthly"
12+
interval: "weekly"

.github/workflows/build.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,19 @@ jobs:
2020

2121
strategy:
2222
matrix:
23-
python-version: [311, 312, 313]
23+
python-version: [313, 314]
2424

2525
steps:
2626
- name: Checkout repository
27-
uses: actions/checkout@v4
27+
uses: actions/checkout@v6
2828

2929
- name: Set up Docker Buildx
30-
uses: docker/setup-buildx-action@v3
30+
uses: docker/setup-buildx-action@v4
3131

3232
- name: Spin up container
3333
run: |
3434
sed -i "s/PYTHON_VERSION=.*/PYTHON_VERSION=${{ matrix.python-version }}/" '.env'
3535
echo "Building container for Python ${{ matrix.python-version }}."
36-
cat
3736
docker compose up -d
3837
3938
- name: Wait and check the health of the container

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ jobs:
2525
# Use matrix strategy to run the tests on multiple Py versions on multiple OSs.
2626
matrix:
2727
os: [ubuntu-latest, macos-latest]
28-
python-version: ["3.11", "3.12", "3.13"]
28+
python-version: ["3.13", "3.14"]
2929

3030
steps:
31-
- uses: actions/checkout@v4
32-
- uses: actions/setup-python@v5
31+
- uses: actions/checkout@v6
32+
- uses: actions/setup-python@v6
3333
with:
3434
python-version: ${{ matrix.python-version }}
3535

3636
- name: Install uv
37-
uses: astral-sh/setup-uv@v3
37+
uses: astral-sh/setup-uv@v8.1.0
3838
with:
3939
enable-cache: true
4040
cache-dependency-glob: "uv.lock"

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.13
1+
3.14

README.md

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
![logo](https://user-images.githubusercontent.com/30027932/134270064-baecfbec-b3e7-4cb7-a07e-c11a58526260.png)
44

55
[![Mentioned in Awesome <INSERT LIST NAME>](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/mjhea0/awesome-fastapi#boilerplate)
6-
[![License](https://img.shields.io/cocoapods/l/AFNetworking?style=flat-square)](https://github.com/rednafi/think-asyncio/blob/master/LICENSE)
6+
[![License](https://img.shields.io/github/license/rednafi/fastapi-nano?style=flat-square)](https://github.com/rednafi/fastapi-nano/blob/master/LICENSE)
77

88
</div>
99

@@ -16,22 +16,29 @@ directory structure.
1616

1717
- Uses [FastAPI][fastapi] to build the HTTP API endpoints.
1818

19-
- Served via [Gunicorn](gunicorn) with multiple [Uvicorn][uvicorn] workers. Uvicorn is a
20-
lightning-fast "ASGI" server. It runs asynchronous Python web code in a single process.
19+
- Served with the [FastAPI CLI][fastapi_cli] using the `fastapi run` command, which
20+
runs [Uvicorn][uvicorn] under the hood.
2121

2222
- Simple reverse-proxying with [Caddy][caddy].
2323

24-
- OAuth2 (with hashed password and Bearer with JWT) based authentication.
24+
- OAuth2 authentication with [Argon2][argon2] password hashing via [pwdlib][pwdlib]
25+
and Bearer JWT tokens via [PyJWT][pyjwt].
2526

26-
- [CORS (Cross Origin Resource Sharing)][cors] enabled.
27+
- [CORS (Cross Origin Resource Sharing)][cors] enabled with explicit allowed origins
28+
so credentialed requests follow FastAPI's current CORS guidance.
2729

2830
- Flask inspired divisional directory structure, suitable for small to medium backend
2931
development.
3032

31-
- Uses [uv][uv] for dependency management, enabling shorter build time.
33+
- Uses [FastAPI standard dependencies][fastapi_standard] and [uv][uv] for dependency
34+
management.
3235

33-
- Dockerized using **python:3.13-slim** image and optimized for size. Dockerfile for
34-
Python 3.12 and 3.11 can also be found in the `dockerfiles` directory.
36+
- Uses [Pydantic Settings][pydantic_settings] for typed environment configuration.
37+
38+
- Supports Python 3.14 and 3.13.
39+
40+
- Dockerized with multi-stage Dockerfiles based on **python:3.14-slim** by default.
41+
Dockerfiles for Python 3.14 and 3.13 can be found in the `dockerfiles` directory.
3542

3643
## Quickstart
3744

@@ -61,7 +68,7 @@ If you want to run the app locally, without using Docker, then:
6168
```
6269

6370
This will set up a virtual environment `.venv` in the current directory with Python
64-
3.13, install dependencies, and start the Uvicorn server.
71+
3.14, install dependencies, and start the FastAPI development server.
6572

6673
### Explore the endpoints
6774

@@ -76,8 +83,8 @@ If you want to run the app locally, without using Docker, then:
7683
![Screenshot from 2020-06-21 22-15-18][screenshot_1]
7784

7885
- Press the `authorize` button on the right and add _username_ and _password_. The APIs
79-
use OAuth2 (with hashed password and Bearer with JWT) based authentication. In this
80-
case, the username and password is `ubuntu` and `debian` respectively.
86+
use OAuth2 with Argon2 password hashing and Bearer JWT authentication. In this case,
87+
the username and password are `ubuntu` and `debian` respectively.
8188

8289
![Screenshot from 2020-06-21 22-18-25][screenshot_2]
8390

@@ -106,7 +113,7 @@ If you want to run the app locally, without using Docker, then:
106113
-d "username=ubuntu&password=debian" | jq -r ".access_token")"
107114
```
108115
109-
This should show a response like this:
116+
This should show a response like this. The random values will vary.
110117
111118
```json
112119
{
@@ -118,10 +125,13 @@ If you want to run the app locally, without using Docker, then:
118125
119126
### Housekeeping
120127
121-
- Run tests with `make tests` (uses [pytest][pytest]).
128+
- Run tests with `make test` (uses [pytest][pytest]).
122129
- Lint with [ruff] and check types with [mypy] using `make lint`.
123130
- Update dependencies with `make dep-update`.
124131
- Stop containers with `make kill-container`.
132+
- Configure credentialed CORS origins with `CORS_ALLOW_ORIGINS` in `.env`. The value is
133+
parsed as a JSON array, for example
134+
`["http://localhost","http://localhost:5002"]`.
125135
126136
## Directory structure
127137
@@ -135,24 +145,26 @@ fastapi-nano
135145
│ │ │ ├── __init__.py # empty init file to make the api_a folder a package
136146
│ │ │ ├── mainmod.py # main module of api_a package
137147
│ │ │ └── submod.py # submodule of api_a package
138-
│ │ └── api_b # api_b package
139-
│ │ ├── __init__.py # empty init file to make the api_b folder a package
140-
│ │ ├── mainmod.py # main module of api_b package
141-
│ │ └── submod.py # submodule of api_b package
148+
│ │ ├── api_b # api_b package
149+
│ │ │ ├── __init__.py # empty init file to make the api_b folder a package
150+
│ │ │ ├── mainmod.py # main module of api_b package
151+
│ │ │ └── submod.py # submodule of api_b package
152+
│ │ └── schemas.py # shared Pydantic response models
142153
│ ├── core # this is where the configs live
143154
│ │ ├── auth.py # authentication with OAuth2
144-
│ │ ├── config.py # sample config file
155+
│ │ ├── config.py # typed environment settings
145156
│ │ └── __init__.py # empty init file to make the config folder a package
146-
│ ├── __init__.py # empty init file to make the app folder a package
147-
│ ├── main.py # main file where the fastAPI() class is called
157+
│ ├── __init__.py # empty init file to make the svc folder a package
158+
│ ├── main.py # main file where the FastAPI() class is called
148159
│ ├── routes # this is where all the routes live
149160
│ │ └── views.py # file containing the endpoints for api_a and api_b
150161
│ └── tests # test package
151162
│ ├── __init__.py # empty init file to make the tests folder a package
152-
│ ├── test_api.py # integration testing the API responses
163+
│ ├── test_apis.py # integration testing the API responses
164+
│ ├── test_logger.py # unit testing logger configuration
153165
│ └── test_functions.py # unit testing the underlying functions
154-
├── dockerfiles # directory containing all the dockerfiles
155-
├── .env # env file containing app variables
166+
├── dockerfiles # Dockerfiles for supported Python versions
167+
├── .env # env file containing app variables and Docker Python target
156168
├── Caddyfile # simple reverse-proxy with caddy
157169
├── docker-compose.yml # docker-compose file
158170
├── pyproject.toml # pep-518 compliant config file
@@ -167,57 +179,61 @@ APIs in the template is to demonstrate how you can decouple the logics of multip
167179
then assemble their endpoints in the routes directory. The following snippets show the logic
168180
behind the dummy APIs.
169181
170-
This is a dummy submodule that houses a function called `random_gen` which generates a
171-
dictionary of random integers.
182+
This is a dummy submodule that houses a function called `rand_gen` which generates a
183+
Pydantic response model with random integers.
172184
173185
```python
174-
# This a dummy module
175-
# This gets called in the module_main.py file
176-
from __future__ import annotations
177186
import random
178187
188+
from svc.apis.schemas import RandomNumbers
179189
180-
def rand_gen(num: int) -> dict[str, int]:
190+
191+
def rand_gen(num: int) -> RandomNumbers:
181192
num = int(num)
182-
d = {
183-
"seed": num,
184-
"random_first": random.randint(0, num),
185-
"random_second": random.randint(0, num),
186-
}
187-
return d
193+
return RandomNumbers(
194+
seed=num,
195+
random_first=random.randint(0, num),
196+
random_second=random.randint(0, num),
197+
)
188198
```
189199
190200
The `main_func` in the primary module calls the `rand_gen` function from the submodule.
191201
192202
```python
193-
from __future__ import annotations
194-
from svc.api_a.submod import rand_gen
203+
from svc.apis.schemas import RandomNumbers
204+
from svc.apis.api_a.submod import rand_gen
195205
196206
197-
def main_func(num: int) -> dict[str, int]:
198-
d = rand_gen(num)
199-
return d
207+
def main_func(num: int) -> RandomNumbers:
208+
return rand_gen(num)
200209
```
201210
202211
The endpoint is exposed like this:
203212
204213
```python
205214
# svc/routes/views.py
206-
from __future__ import annotations
207-
#... codes regarding authentication ...
215+
from typing import Annotated
216+
217+
from fastapi import Depends
218+
219+
from svc.apis.schemas import RandomNumbers
220+
from svc.core.auth import UserInDB, get_current_user
221+
222+
CurrentUser = Annotated[UserInDB, Depends(get_current_user)]
208223
209224
# endpoint for api_a (api_b looks identical)
210225
@router.get("/api_a/{num}", tags=["api_a"])
211-
async def view_a(num: int, auth: Depends =Depends(get_current_user)) -> dict[str, int]:
226+
async def view_a(num: int, _auth: CurrentUser) -> RandomNumbers:
212227
return main_func_a(num)
213228
```
214229
215-
So hitting the API with a random integer will give you a response like the following:
230+
So hitting the API with a random integer will give you a response like the following. The
231+
random values will vary.
216232
217233
```json
218234
{
219235
"seed": 22,
220-
"random_first": 27,
236+
"random_first": 5,
221237
"random_second": 20
222238
}
223239
```
@@ -226,7 +242,7 @@ So hitting the API with a random integer will give you a response like the follo
226242
227243
- You can put your own API logic following the shape of `api_a` and `api_b` packages.
228244
You'll have to add additional directories like `api_a` or `api_b` if you need to expose
229-
more endponts.
245+
more endpoints.
230246

231247
- Then expose the API URLs in the `routes/views.py` file. You may choose to create
232248
multiple `views` files to organize your endpoint URLs.
@@ -249,9 +265,13 @@ So hitting the API with a random integer will give you a response like the follo
249265
[cors]: https://fastapi.tiangolo.com/tutorial/cors/
250266
[docker]: https://www.docker.com/
251267
[fastapi]: https://fastapi.tiangolo.com/
252-
[fastapi_security]: https://fastapi.tiangolo.com/tutorial/security/
253-
[gunicorn]: https://gunicorn.org/
254-
[httpx]: https://www.python-httpx.org/
268+
[argon2]: https://argon2-cffi.readthedocs.io/
269+
[fastapi_cli]: https://fastapi.tiangolo.com/fastapi-cli/
270+
[fastapi_security]: https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/
271+
[fastapi_standard]: https://fastapi.tiangolo.com/#standard-dependencies
272+
[pydantic_settings]: https://fastapi.tiangolo.com/advanced/settings/
273+
[pwdlib]: https://github.com/frankie567/pwdlib
274+
[pyjwt]: https://pyjwt.readthedocs.io/
255275
[pytest]: https://docs.pytest.org/en/stable/
256276
[ruff]: https://astral.sh/ruff
257277
[uvicorn]: https://uvicorn.org/

bin/Dockerfile-template

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ SHELL ["sh", "-exc"]
1111
# Set working directory
1212
WORKDIR /app
1313

14+
ENV UV_LINK_MODE=copy
15+
1416
# Install dependencies
1517
RUN --mount=type=cache,target=/root/.cache/uv \
1618
--mount=type=bind,source=uv.lock,target=uv.lock \
@@ -40,5 +42,5 @@ ENV PATH="/app/.venv/bin:$PATH"
4042
EXPOSE 5001
4143

4244
# Entry point for running the application
43-
ENTRYPOINT ["gunicorn", "svc.main:app", "--workers", "2", "--worker-class", \
44-
"uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:5001" ]
45+
ENTRYPOINT ["fastapi", "run", "--workers", "2", "svc/main.py", \
46+
"--host", "0.0.0.0", "--port", "5001" ]

bin/generate_dockerfile.sh

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,19 @@ set -euo pipefail
55
# Template Dockerfile path
66
template_dockerfile="bin/Dockerfile-template"
77

8-
# Read the content of the template Dockerfile (python 3.13)
8+
# Read the content of the Dockerfile template.
99
if [[ ! -f "$template_dockerfile" ]]; then
1010
echo "Template Dockerfile does not exist: $template_dockerfile"
1111
exit 1
1212
fi
1313

14-
# Read the content of the Dockerfile template
1514
dockerfile_content=$(<"$template_dockerfile")
1615

1716
# Python versions to generate Dockerfiles for
18-
python_versions=("3.11" "3.12" "3.13")
17+
python_versions=("3.13" "3.14")
1918

2019
# Corresponding directories for each version
21-
directories=("dockerfiles/python311" "dockerfiles/python312" "dockerfiles/python313")
20+
directories=("dockerfiles/python313" "dockerfiles/python314")
2221

2322
# Loop over the Python versions and directories
2423
for i in "${!python_versions[@]}"; do
@@ -28,7 +27,6 @@ for i in "${!python_versions[@]}"; do
2827
# Create the directory if it doesn't exist
2928
mkdir -p "$dir"
3029

31-
# Replace only the ARG PYTHON_VERSION=3.13 line with the specific version
3230
updated_content="${dockerfile_content//ARG PYTHON_VERSION=bleh/ARG PYTHON_VERSION=$version}"
3331

3432
# Save the new Dockerfile

bin/update_deps.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ rm -f uv.lock
1010
rm -rf .venv || true
1111

1212
# Create a new virtual environment.
13-
uv venv -p 3.13
13+
uv venv -p 3.14
1414

1515
# Install the latest versions of the dependencies.
1616
uv lock && uv sync

0 commit comments

Comments
 (0)