├── .circleci └── config.yml ├── .gitignore ├── README.md ├── assignments └── add-put-and-delete-endpoints │ ├── README.md │ ├── app │ ├── __init__.py │ ├── main.py │ ├── recipe_data.py │ └── schemas.py │ ├── poetry.lock │ ├── pyproject.toml │ └── run.sh ├── part-01-hello-world ├── README.md ├── app │ ├── __init__.py │ └── main.py ├── poetry.lock ├── pyproject.toml └── run.sh ├── part-02-path-parameters ├── README.md ├── app │ ├── __init__.py │ └── main.py ├── poetry.lock ├── pyproject.toml └── run.sh ├── part-03-query-parameters ├── README.md ├── app │ ├── __init__.py │ └── main.py ├── poetry.lock ├── pyproject.toml └── run.sh ├── part-04-pydantic-schemas ├── README.md ├── app │ ├── __init__.py │ ├── main.py │ ├── recipe_data.py │ └── schemas.py ├── poetry.lock ├── pyproject.toml └── run.sh ├── part-05-basic-error-handling ├── README.md ├── app │ ├── __init__.py │ ├── main.py │ ├── recipe_data.py │ └── schemas.py ├── poetry.lock ├── pyproject.toml └── run.sh ├── part-06-jinja-templates ├── README.md ├── app │ ├── __init__.py │ ├── main.py │ ├── recipe_data.py │ ├── schemas.py │ └── templates │ │ └── index.html ├── poetry.lock ├── pyproject.toml └── run.sh ├── part-06b-basic-deploy-linode ├── Makefile ├── README.md ├── app │ ├── __init__.py │ ├── main.py │ ├── recipe_data.py │ ├── schemas.py │ └── templates │ │ └── index.html ├── nginx │ └── default.conf ├── poetry.lock ├── pyproject.toml └── run.sh ├── part-07-database ├── .flake8 ├── README.md ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── da9301b43279_add_recipe_and_user_tables.py ├── app │ ├── __init__.py │ ├── backend_pre_start.py │ ├── crud │ │ ├── __init__.py │ │ ├── base.py │ │ ├── crud_recipe.py │ │ └── crud_user.py │ ├── db │ │ ├── __init__.py │ │ ├── base.py │ │ ├── base_class.py │ │ ├── init_db.py │ │ └── session.py │ ├── deps.py │ ├── initial_data.py │ ├── main.py │ ├── models │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ ├── recipe_data.py │ ├── schemas │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ └── templates │ │ └── index.html ├── poetry.lock ├── prestart.py ├── prestart.sh ├── pyproject.toml └── run.sh ├── part-08-structure-and-versioning ├── .flake8 ├── README.md ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── da9301b43279_add_recipe_and_user_tables.py ├── app │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── api_v1 │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ └── endpoints │ │ │ │ ├── __init__.py │ │ │ │ └── recipe.py │ │ └── deps.py │ ├── backend_pre_start.py │ ├── core │ │ ├── __init__.py │ │ └── config.py │ ├── crud │ │ ├── __init__.py │ │ ├── base.py │ │ ├── crud_recipe.py │ │ └── crud_user.py │ ├── db │ │ ├── __init__.py │ │ ├── base.py │ │ ├── base_class.py │ │ ├── init_db.py │ │ └── session.py │ ├── initial_data.py │ ├── main.py │ ├── models │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ ├── schemas │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ └── templates │ │ └── index.html ├── poetry.lock ├── prestart.py ├── prestart.sh ├── pyproject.toml └── run.sh ├── part-09-async ├── .flake8 ├── README.md ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── da9301b43279_add_recipe_and_user_tables.py ├── app │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── api_v1 │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ └── endpoints │ │ │ │ ├── __init__.py │ │ │ │ └── recipe.py │ │ └── deps.py │ ├── backend_pre_start.py │ ├── core │ │ ├── __init__.py │ │ └── config.py │ ├── crud │ │ ├── __init__.py │ │ ├── base.py │ │ ├── crud_recipe.py │ │ └── crud_user.py │ ├── db │ │ ├── __init__.py │ │ ├── base.py │ │ ├── base_class.py │ │ ├── init_db.py │ │ └── session.py │ ├── initial_data.py │ ├── main.py │ ├── models │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ ├── schemas │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ └── templates │ │ └── index.html ├── poetry.lock ├── prestart.py ├── prestart.sh ├── pyproject.toml └── run.sh ├── part-10-jwt-auth ├── .flake8 ├── README.md ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── ec46ad7fc181_added_all_tables.py ├── app │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── api_v1 │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ └── endpoints │ │ │ │ ├── __init__.py │ │ │ │ ├── auth.py │ │ │ │ └── recipe.py │ │ └── deps.py │ ├── backend_pre_start.py │ ├── core │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── config.py │ │ └── security.py │ ├── crud │ │ ├── __init__.py │ │ ├── base.py │ │ ├── crud_recipe.py │ │ └── crud_user.py │ ├── db │ │ ├── __init__.py │ │ ├── base.py │ │ ├── base_class.py │ │ ├── init_db.py │ │ └── session.py │ ├── initial_data.py │ ├── main.py │ ├── models │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ ├── schemas │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ └── templates │ │ └── index.html ├── poetry.lock ├── prestart.py ├── prestart.sh ├── pyproject.toml └── run.sh ├── part-11-dependency-injection ├── .flake8 ├── README.md ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── ec46ad7fc181_added_all_tables.py ├── app │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── api_v1 │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ └── endpoints │ │ │ │ ├── __init__.py │ │ │ │ ├── auth.py │ │ │ │ └── recipe.py │ │ └── deps.py │ ├── backend_pre_start.py │ ├── clients │ │ ├── __init__.py │ │ └── reddit.py │ ├── core │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── config.py │ │ └── security.py │ ├── crud │ │ ├── __init__.py │ │ ├── base.py │ │ ├── crud_recipe.py │ │ └── crud_user.py │ ├── db │ │ ├── __init__.py │ │ ├── base.py │ │ ├── base_class.py │ │ ├── init_db.py │ │ └── session.py │ ├── initial_data.py │ ├── main.py │ ├── models │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ ├── schemas │ │ ├── __init__.py │ │ ├── recipe.py │ │ └── user.py │ ├── templates │ │ └── index.html │ └── tests │ │ ├── api │ │ ├── __init__.py │ │ └── test_recipe.py │ │ └── conftest.py ├── di_demo │ ├── main.py │ ├── main_with_di.py │ ├── patterns │ │ └── three_types.py │ ├── reddit.py │ ├── test_main.py │ └── test_main_with_di.py ├── poetry.lock ├── prestart.py ├── prestart.sh ├── pyproject.toml └── run.sh ├── part-12-react-frontend ├── README.md ├── backend │ ├── .flake8 │ ├── alembic.ini │ ├── alembic │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions │ │ │ └── ec46ad7fc181_added_all_tables.py │ ├── app │ │ ├── __init__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── api_v1 │ │ │ │ ├── __init__.py │ │ │ │ ├── api.py │ │ │ │ └── endpoints │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── auth.py │ │ │ │ │ └── recipe.py │ │ │ └── deps.py │ │ ├── backend_pre_start.py │ │ ├── clients │ │ │ ├── __init__.py │ │ │ └── reddit.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── config.py │ │ │ └── security.py │ │ ├── crud │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── crud_recipe.py │ │ │ └── crud_user.py │ │ ├── db │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── base_class.py │ │ │ ├── init_db.py │ │ │ └── session.py │ │ ├── initial_data.py │ │ ├── main.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── recipe.py │ │ │ └── user.py │ │ ├── schemas │ │ │ ├── __init__.py │ │ │ ├── recipe.py │ │ │ └── user.py │ │ ├── templates │ │ │ └── index.html │ │ └── tests │ │ │ ├── api │ │ │ ├── __init__.py │ │ │ └── test_recipe.py │ │ │ └── conftest.py │ ├── poetry.lock │ ├── prestart.py │ ├── prestart.sh │ ├── pyproject.toml │ └── run.sh └── frontend │ ├── .env │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.js │ ├── client.js │ ├── components │ │ ├── Button │ │ │ └── Button.jsx │ │ ├── DashboardHeader │ │ │ └── index.jsx │ │ ├── Footer │ │ │ └── index.jsx │ │ ├── FormInput │ │ │ └── FormInput.jsx │ │ ├── Idea │ │ │ └── index.jsx │ │ ├── IdeaTable │ │ │ └── index.jsx │ │ ├── Loader.jsx │ │ ├── Modal │ │ │ └── PopupModal.jsx │ │ ├── Recipe │ │ │ └── index.jsx │ │ └── RecipeTable │ │ │ └── index.jsx │ ├── config.js │ ├── index.css │ ├── index.js │ └── pages │ │ ├── error-page │ │ └── index.jsx │ │ ├── home │ │ └── index.jsx │ │ ├── login │ │ └── index.jsx │ │ ├── my-recipes │ │ ├── NotLoggedIn.jsx │ │ └── index.jsx │ │ └── sign-up │ │ └── index.jsx │ └── tailwind.config.js ├── part-13-docker-deployment ├── .dockerignore ├── Makefile ├── README.md ├── backend │ ├── Dockerfile │ └── app │ │ ├── .flake8 │ │ ├── alembic.ini │ │ ├── alembic │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions │ │ │ └── ec46ad7fc181_added_all_tables.py │ │ ├── app │ │ ├── __init__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── api_v1 │ │ │ │ ├── __init__.py │ │ │ │ ├── api.py │ │ │ │ └── endpoints │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── auth.py │ │ │ │ │ └── recipe.py │ │ │ └── deps.py │ │ ├── backend_pre_start.py │ │ ├── clients │ │ │ ├── __init__.py │ │ │ └── reddit.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── config.py │ │ │ └── security.py │ │ ├── crud │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── crud_recipe.py │ │ │ └── crud_user.py │ │ ├── db │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── base_class.py │ │ │ ├── init_db.py │ │ │ └── session.py │ │ ├── initial_data.py │ │ ├── main.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── recipe.py │ │ │ └── user.py │ │ ├── schemas │ │ │ ├── __init__.py │ │ │ ├── recipe.py │ │ │ └── user.py │ │ ├── templates │ │ │ └── index.html │ │ └── tests │ │ │ ├── api │ │ │ ├── __init__.py │ │ │ └── test_recipe.py │ │ │ └── conftest.py │ │ ├── poetry.lock │ │ ├── prestart.py │ │ ├── prestart.sh │ │ ├── pyproject.toml │ │ └── run.sh ├── docker-compose.local.yml └── frontend │ ├── .env │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.js │ ├── client.js │ ├── components │ │ ├── Button │ │ │ └── Button.jsx │ │ ├── DashboardHeader │ │ │ └── index.jsx │ │ ├── Footer │ │ │ └── index.jsx │ │ ├── FormInput │ │ │ └── FormInput.jsx │ │ ├── Idea │ │ │ └── index.jsx │ │ ├── IdeaTable │ │ │ └── index.jsx │ │ ├── Loader.jsx │ │ ├── Modal │ │ │ └── PopupModal.jsx │ │ ├── Recipe │ │ │ └── index.jsx │ │ └── RecipeTable │ │ │ └── index.jsx │ ├── config.js │ ├── index.css │ ├── index.js │ └── pages │ │ ├── error-page │ │ └── index.jsx │ │ ├── home │ │ └── index.jsx │ │ ├── login │ │ └── index.jsx │ │ ├── my-recipes │ │ ├── NotLoggedIn.jsx │ │ └── index.jsx │ │ └── sign-up │ │ └── index.jsx │ └── tailwind.config.js ├── part-14-send-email-in-background ├── .dockerignore ├── Makefile ├── README.md ├── backend │ ├── Dockerfile │ └── app │ │ ├── .flake8 │ │ ├── alembic.ini │ │ ├── alembic │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions │ │ │ └── ec46ad7fc181_added_all_tables.py │ │ ├── app │ │ ├── __init__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── api_v1 │ │ │ │ ├── __init__.py │ │ │ │ ├── api.py │ │ │ │ └── endpoints │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── auth.py │ │ │ │ │ └── recipe.py │ │ │ └── deps.py │ │ ├── backend_pre_start.py │ │ ├── clients │ │ │ ├── __init__.py │ │ │ ├── email.py │ │ │ └── reddit.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── config.py │ │ │ ├── email.py │ │ │ ├── logging.py │ │ │ └── security.py │ │ ├── crud │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── crud_recipe.py │ │ │ └── crud_user.py │ │ ├── db │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── base_class.py │ │ │ ├── init_db.py │ │ │ └── session.py │ │ ├── initial_data.py │ │ ├── main.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── recipe.py │ │ │ └── user.py │ │ ├── schemas │ │ │ ├── __init__.py │ │ │ ├── recipe.py │ │ │ └── user.py │ │ ├── templates │ │ │ └── index.html │ │ └── tests │ │ │ ├── api │ │ │ ├── __init__.py │ │ │ └── test_recipe.py │ │ │ └── conftest.py │ │ ├── poetry.lock │ │ ├── prestart.py │ │ ├── prestart.sh │ │ ├── pyproject.toml │ │ └── run.sh ├── docker-compose.local.yml └── frontend │ ├── .env │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.js │ ├── client.js │ ├── components │ │ ├── Button │ │ │ └── Button.jsx │ │ ├── DashboardHeader │ │ │ └── index.jsx │ │ ├── Footer │ │ │ └── index.jsx │ │ ├── FormInput │ │ │ └── FormInput.jsx │ │ ├── Idea │ │ │ └── index.jsx │ │ ├── IdeaTable │ │ │ └── index.jsx │ │ ├── Loader.jsx │ │ ├── Modal │ │ │ └── PopupModal.jsx │ │ ├── Recipe │ │ │ └── index.jsx │ │ └── RecipeTable │ │ │ └── index.jsx │ ├── config.js │ ├── index.css │ ├── index.js │ └── pages │ │ ├── error-page │ │ └── index.jsx │ │ ├── home │ │ └── index.jsx │ │ ├── login │ │ └── index.jsx │ │ ├── my-recipes │ │ ├── NotLoggedIn.jsx │ │ └── index.jsx │ │ └── sign-up │ │ └── index.jsx │ └── tailwind.config.js └── troubleshooting └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # ultimate-fastapi-tutorial 2 | The Ultimate FastAPI Tutorial 3 | 4 | For detailed explanations and to follow along: 5 | 6 | - Read the [blog post series](https://christophergs.com/tutorials/ultimate-fastapi-tutorial-pt-1-hello-world/) 7 | - [Pre-order the course](https://academy.christophergs.com/courses/fastapi-for-busy-engineers/) -------------------------------------------------------------------------------- /assignments/add-put-and-delete-endpoints/README.md: -------------------------------------------------------------------------------- 1 | ## Part 4 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 6 | 4. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 7 | 5. Open http://localhost:8001/ 8 | 9 | To stop the server, press CTRL+C 10 | 11 | If you get stuck, checkout the [troubleshooting readme](../troubleshooting/README.md) 12 | -------------------------------------------------------------------------------- /assignments/add-put-and-delete-endpoints/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/assignments/add-put-and-delete-endpoints/app/__init__.py -------------------------------------------------------------------------------- /assignments/add-put-and-delete-endpoints/app/recipe_data.py: -------------------------------------------------------------------------------- 1 | RECIPES = [ 2 | { 3 | "id": 1, 4 | "label": "Chicken Vesuvio", 5 | "source": "Serious Eats", 6 | "url": "http://www.seriouseats.com/recipes/2011/12/chicken-vesuvio-recipe.html", 7 | }, 8 | { 9 | "id": 2, 10 | "label": "Chicken Paprikash", 11 | "source": "No Recipes", 12 | "url": "http://norecipes.com/recipe/chicken-paprikash/", 13 | }, 14 | { 15 | "id": 3, 16 | "label": "Cauliflower and Tofu Curry Recipe", 17 | "source": "Serious Eats", 18 | "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /assignments/add-put-and-delete-endpoints/app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class Recipe(BaseModel): 7 | id: int 8 | label: str 9 | source: str 10 | url: HttpUrl 11 | 12 | 13 | class RecipeSearchResults(BaseModel): 14 | results: Sequence[Recipe] 15 | 16 | 17 | class RecipeCreate(BaseModel): 18 | label: str 19 | source: str 20 | url: HttpUrl 21 | submitter_id: int 22 | 23 | 24 | class RecipeUpdateRestricted(BaseModel): 25 | id: int 26 | 27 | # We decide to only allow label updates. 28 | label: str 29 | -------------------------------------------------------------------------------- /assignments/add-put-and-delete-endpoints/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial Part 4" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | uvicorn = "~0.11.3" 10 | fastapi = "~0.68.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = "~1.8.1"} 13 | -------------------------------------------------------------------------------- /assignments/add-put-and-delete-endpoints/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-01-hello-world/README.md: -------------------------------------------------------------------------------- 1 | ## Part 1 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 6 | 4. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 7 | 5. Open http://localhost:8001/ 8 | 9 | To stop the server, press CTRL+C 10 | 11 | If you get stuck, checkout the [troubleshooting readme](../troubleshooting/README.md) -------------------------------------------------------------------------------- /part-01-hello-world/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-01-hello-world/app/__init__.py -------------------------------------------------------------------------------- /part-01-hello-world/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, APIRouter 2 | 3 | 4 | app = FastAPI(title="Recipe API", openapi_url="/openapi.json") 5 | 6 | api_router = APIRouter() 7 | 8 | 9 | @api_router.get("/", status_code=200) 10 | def root() -> dict: 11 | """ 12 | Root GET 13 | """ 14 | return {"msg": "Hello, World!"} 15 | 16 | 17 | app.include_router(api_router) 18 | 19 | 20 | if __name__ == "__main__": 21 | # Use this for debugging purposes only 22 | import uvicorn 23 | 24 | uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug") 25 | -------------------------------------------------------------------------------- /part-01-hello-world/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial Part 1" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.8,<3.12" 9 | uvicorn = {extras=["standard"], version="~0.23.0"} 10 | fastapi = "^0.104.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = ">=2.0"} 13 | -------------------------------------------------------------------------------- /part-01-hello-world/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-02-path-parameters/README.md: -------------------------------------------------------------------------------- 1 | ## Part 2 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 6 | 4. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 7 | 5. Open http://localhost:8001/ 8 | 9 | To stop the server, press CTRL+C 10 | 11 | If you get stuck, checkout the [troubleshooting readme](../troubleshooting/README.md) 12 | -------------------------------------------------------------------------------- /part-02-path-parameters/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-02-path-parameters/app/__init__.py -------------------------------------------------------------------------------- /part-02-path-parameters/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial Part 2" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.8,<3.12" 9 | uvicorn = {extras=["standard"], version="~0.23.0"} 10 | fastapi = "^0.104.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = ">=2.0"} 13 | -------------------------------------------------------------------------------- /part-02-path-parameters/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-03-query-parameters/README.md: -------------------------------------------------------------------------------- 1 | ## Part 3 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 6 | 4. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 7 | 5. Open http://localhost:8001/ 8 | 9 | To stop the server, press CTRL+C 10 | 11 | If you get stuck, checkout the [troubleshooting readme](../troubleshooting/README.md) 12 | -------------------------------------------------------------------------------- /part-03-query-parameters/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-03-query-parameters/app/__init__.py -------------------------------------------------------------------------------- /part-03-query-parameters/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial Part 3" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.8,<3.12" 9 | uvicorn = {extras=["standard"], version="~0.23.0"} 10 | fastapi = "^0.104.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = ">=2.0"} 13 | 14 | -------------------------------------------------------------------------------- /part-03-query-parameters/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-04-pydantic-schemas/README.md: -------------------------------------------------------------------------------- 1 | ## Part 4 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 6 | 4. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 7 | 5. Open http://localhost:8001/ 8 | 9 | To stop the server, press CTRL+C 10 | 11 | If you get stuck, checkout the [troubleshooting readme](../troubleshooting/README.md) 12 | -------------------------------------------------------------------------------- /part-04-pydantic-schemas/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-04-pydantic-schemas/app/__init__.py -------------------------------------------------------------------------------- /part-04-pydantic-schemas/app/recipe_data.py: -------------------------------------------------------------------------------- 1 | RECIPES = [ 2 | { 3 | "id": 1, 4 | "label": "Chicken Vesuvio", 5 | "source": "Serious Eats", 6 | "url": "http://www.seriouseats.com/recipes/2011/12/chicken-vesuvio-recipe.html", 7 | }, 8 | { 9 | "id": 2, 10 | "label": "Chicken Paprikash", 11 | "source": "No Recipes", 12 | "url": "http://norecipes.com/recipe/chicken-paprikash/", 13 | }, 14 | { 15 | "id": 3, 16 | "label": "Cauliflower and Tofu Curry Recipe", 17 | "source": "Serious Eats", 18 | "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /part-04-pydantic-schemas/app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class Recipe(BaseModel): 7 | id: int 8 | label: str 9 | source: str 10 | url: HttpUrl 11 | 12 | 13 | class RecipeSearchResults(BaseModel): 14 | results: Sequence[Recipe] 15 | 16 | 17 | class RecipeCreate(BaseModel): 18 | label: str 19 | source: str 20 | url: HttpUrl 21 | submitter_id: int 22 | -------------------------------------------------------------------------------- /part-04-pydantic-schemas/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial Part 4" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.8,<3.12" 9 | uvicorn = {extras=["standard"], version="~0.23.0"} 10 | fastapi = "^0.104.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = ">=2.0"} 13 | -------------------------------------------------------------------------------- /part-04-pydantic-schemas/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-05-basic-error-handling/README.md: -------------------------------------------------------------------------------- 1 | ## Part 5 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 6 | 4. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 7 | 5. Open http://localhost:8001/ 8 | 9 | If you get stuck, checkout the [troubleshooting readme](../troubleshooting/README.md) 10 | -------------------------------------------------------------------------------- /part-05-basic-error-handling/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-05-basic-error-handling/app/__init__.py -------------------------------------------------------------------------------- /part-05-basic-error-handling/app/recipe_data.py: -------------------------------------------------------------------------------- 1 | RECIPES = [ 2 | { 3 | "id": 1, 4 | "label": "Chicken Vesuvio", 5 | "source": "Serious Eats", 6 | "url": "http://www.seriouseats.com/recipes/2011/12/chicken-vesuvio-recipe.html", 7 | }, 8 | { 9 | "id": 2, 10 | "label": "Chicken Paprikash", 11 | "source": "No Recipes", 12 | "url": "http://norecipes.com/recipe/chicken-paprikash/", 13 | }, 14 | { 15 | "id": 3, 16 | "label": "Cauliflower and Tofu Curry Recipe", 17 | "source": "Serious Eats", 18 | "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /part-05-basic-error-handling/app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class Recipe(BaseModel): 7 | id: int 8 | label: str 9 | source: str 10 | url: HttpUrl 11 | 12 | 13 | class RecipeSearchResults(BaseModel): 14 | results: Sequence[Recipe] 15 | 16 | 17 | class RecipeCreate(BaseModel): 18 | label: str 19 | source: str 20 | url: HttpUrl 21 | submitter_id: int 22 | -------------------------------------------------------------------------------- /part-05-basic-error-handling/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial Part 5" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.8,<3.12" 9 | uvicorn = {extras=["standard"], version="~0.23.0"} 10 | fastapi = "^0.104.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = ">=2.0"} 13 | -------------------------------------------------------------------------------- /part-05-basic-error-handling/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-06-jinja-templates/README.md: -------------------------------------------------------------------------------- 1 | ## Part 6 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 6 | 4. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 7 | 5. Open http://localhost:8001/ 8 | 9 | To stop the server, press CTRL+C 10 | 11 | If you get stuck, checkout the [troubleshooting readme](../troubleshooting/README.md) 12 | -------------------------------------------------------------------------------- /part-06-jinja-templates/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-06-jinja-templates/app/__init__.py -------------------------------------------------------------------------------- /part-06-jinja-templates/app/recipe_data.py: -------------------------------------------------------------------------------- 1 | RECIPES = [ 2 | { 3 | "id": 1, 4 | "label": "Chicken Vesuvio", 5 | "source": "Serious Eats", 6 | "url": "http://www.seriouseats.com/recipes/2011/12/chicken-vesuvio-recipe.html", 7 | }, 8 | { 9 | "id": 2, 10 | "label": "Chicken Paprikash", 11 | "source": "No Recipes", 12 | "url": "http://norecipes.com/recipe/chicken-paprikash/", 13 | }, 14 | { 15 | "id": 3, 16 | "label": "Cauliflower and Tofu Curry Recipe", 17 | "source": "Serious Eats", 18 | "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /part-06-jinja-templates/app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class Recipe(BaseModel): 7 | id: int 8 | label: str 9 | source: str 10 | url: HttpUrl 11 | 12 | 13 | class RecipeSearchResults(BaseModel): 14 | results: Sequence[Recipe] 15 | 16 | 17 | class RecipeCreate(BaseModel): 18 | label: str 19 | source: str 20 | url: HttpUrl 21 | submitter_id: int 22 | -------------------------------------------------------------------------------- /part-06-jinja-templates/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.8,<3.12" 9 | uvicorn = {extras=["standard"], version="~0.23.0"} 10 | fastapi = "^0.104.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = ">=2.0"} 13 | Jinja2 = "~3.0.3" 14 | -------------------------------------------------------------------------------- /part-06-jinja-templates/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-06b-basic-deploy-linode/Makefile: -------------------------------------------------------------------------------- 1 | linode_install: 2 | # sudo apt update 3 | # sudo apt install make 4 | sudo apt -y upgrade 5 | sudo apt -y install python3-pip 6 | pip install poetry 7 | poetry install 8 | sudo apt -y install nginx 9 | sudo cp nginx/default.conf /etc/nginx/sites-available/fastapi_app 10 | # Disable the NGINX’s default configuration file by removing its symlink 11 | sudo unlink /etc/nginx/sites-enabled/default 12 | sudo ln -s /etc/nginx/sites-available/fastapi_app /etc/nginx/sites-enabled/ 13 | 14 | linode_run: 15 | # Reload the NGINX configuration file: 16 | sudo nginx -s reload 17 | # restart the Nginx service 18 | sudo systemctl restart nginx.service 19 | # The recommended configuration for proxying from Nginx is to use a UNIX domain 20 | # socket between Nginx and whatever the process manager that is being used to run 21 | # Uvicorn. Note that when doing this you will need run Uvicorn with --forwarded-allow-ips='*' 22 | # to ensure that the domain socket is trusted as a source from which to proxy headers. 23 | poetry run gunicorn --bind=unix:///tmp/uvicorn.sock -w 2 --forwarded-allow-ips='*' -k uvicorn.workers.UvicornWorker app.main:app 24 | -------------------------------------------------------------------------------- /part-06b-basic-deploy-linode/README.md: -------------------------------------------------------------------------------- 1 | ## Part 6 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. Run the FastAPI server via poetry `poetry run ./run.sh` 6 | 4. Open http://localhost:8001/ 7 | -------------------------------------------------------------------------------- /part-06b-basic-deploy-linode/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-06b-basic-deploy-linode/app/__init__.py -------------------------------------------------------------------------------- /part-06b-basic-deploy-linode/app/recipe_data.py: -------------------------------------------------------------------------------- 1 | RECIPES = [ 2 | { 3 | "id": 1, 4 | "label": "Chicken Vesuvio", 5 | "source": "Serious Eats", 6 | "url": "http://www.seriouseats.com/recipes/2011/12/chicken-vesuvio-recipe.html", 7 | }, 8 | { 9 | "id": 2, 10 | "label": "Chicken Paprikash", 11 | "source": "No Recipes", 12 | "url": "http://norecipes.com/recipe/chicken-paprikash/", 13 | }, 14 | { 15 | "id": 3, 16 | "label": "Cauliflower and Tofu Curry Recipe", 17 | "source": "Serious Eats", 18 | "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /part-06b-basic-deploy-linode/app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class Recipe(BaseModel): 7 | id: int 8 | label: str 9 | source: str 10 | url: HttpUrl 11 | 12 | 13 | class RecipeSearchResults(BaseModel): 14 | results: Sequence[Recipe] 15 | 16 | 17 | class RecipeCreate(BaseModel): 18 | label: str 19 | source: str 20 | url: HttpUrl 21 | submitter_id: int 22 | -------------------------------------------------------------------------------- /part-06b-basic-deploy-linode/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | client_max_body_size 4G; 4 | 5 | server_name example.com; 6 | 7 | location / { 8 | proxy_set_header Host $http_host; 9 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 10 | proxy_set_header X-Forwarded-Proto $scheme; 11 | proxy_set_header Upgrade $http_upgrade; 12 | proxy_set_header Connection $connection_upgrade; 13 | proxy_redirect off; 14 | proxy_buffering off; 15 | proxy_pass http://uvicorn; 16 | } 17 | 18 | location /static { 19 | # path for static files 20 | root /path/to/app/static; 21 | } 22 | } 23 | 24 | map $http_upgrade $connection_upgrade { 25 | default upgrade; 26 | '' close; 27 | } 28 | 29 | upstream uvicorn { 30 | server unix:/tmp/uvicorn.sock; 31 | } -------------------------------------------------------------------------------- /part-06b-basic-deploy-linode/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.8,<3.12" 9 | uvicorn = {extras=["standard"], version="~0.23.0"} 10 | fastapi = "^0.104.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = ">=2.0"} 13 | Jinja2 = "^3.0.1" 14 | gunicorn = "^20.1.0" 15 | -------------------------------------------------------------------------------- /part-06b-basic-deploy-linode/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-07-database/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache -------------------------------------------------------------------------------- /part-07-database/README.md: -------------------------------------------------------------------------------- 1 | ## Part 7 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. Run the DB migrations via poetry `poetry run python app/prestart.py` (only required once) (Unix users can use 6 | the bash script if preferred) 7 | 4. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 8 | 5. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 9 | 6. Open http://localhost:8001/ 10 | -------------------------------------------------------------------------------- /part-07-database/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /part-07-database/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /part-07-database/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-07-database/app/__init__.py -------------------------------------------------------------------------------- /part-07-database/app/backend_pre_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 4 | 5 | from app.db.session import SessionLocal 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | max_tries = 60 * 5 # 5 minutes 11 | wait_seconds = 1 12 | 13 | 14 | @retry( 15 | stop=stop_after_attempt(max_tries), 16 | wait=wait_fixed(wait_seconds), 17 | before=before_log(logger, logging.INFO), 18 | after=after_log(logger, logging.WARN), 19 | ) 20 | def init() -> None: 21 | try: 22 | db = SessionLocal() 23 | # Try to create session to check if DB is awake 24 | db.execute("SELECT 1") 25 | except Exception as e: 26 | logger.error(e) 27 | raise e 28 | 29 | 30 | def main() -> None: 31 | logger.info("Initializing service") 32 | init() 33 | logger.info("Service finished initializing") 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /part-07-database/app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_recipe import recipe 2 | from .crud_user import user 3 | -------------------------------------------------------------------------------- /part-07-database/app/crud/crud_recipe.py: -------------------------------------------------------------------------------- 1 | from app.crud.base import CRUDBase 2 | from app.models.recipe import Recipe 3 | from app.schemas.recipe import RecipeCreate, RecipeUpdate 4 | 5 | 6 | class CRUDRecipe(CRUDBase[Recipe, RecipeCreate, RecipeUpdate]): 7 | ... 8 | 9 | 10 | recipe = CRUDRecipe(Recipe) 11 | -------------------------------------------------------------------------------- /part-07-database/app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.user import User 7 | from app.schemas.user import UserCreate, UserUpdate 8 | 9 | 10 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 11 | def get_by_email(self, db: Session, *, email: str) -> Optional[User]: 12 | return db.query(User).filter(User.email == email).first() 13 | 14 | def update( 15 | self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] 16 | ) -> User: 17 | if isinstance(obj_in, dict): 18 | update_data = obj_in 19 | else: 20 | update_data = obj_in.dict(exclude_unset=True) 21 | 22 | return super().update(db, db_obj=db_obj, obj_in=update_data) 23 | 24 | def is_superuser(self, user: User) -> bool: 25 | return user.is_superuser 26 | 27 | 28 | user = CRUDUser(User) 29 | -------------------------------------------------------------------------------- /part-07-database/app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-07-database/app/db/__init__.py -------------------------------------------------------------------------------- /part-07-database/app/db/base.py: -------------------------------------------------------------------------------- 1 | # Import all the models, so that Base has them before being 2 | # imported by Alembic 3 | from app.db.base_class import Base # noqa 4 | from app.models.user import User # noqa 5 | from app.models.recipe import Recipe # noqa 6 | -------------------------------------------------------------------------------- /part-07-database/app/db/base_class.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 4 | 5 | 6 | class_registry: t.Dict = {} 7 | 8 | 9 | @as_declarative(class_registry=class_registry) 10 | class Base: 11 | id: t.Any 12 | __name__: str 13 | 14 | # Generate __tablename__ automatically 15 | @declared_attr 16 | def __tablename__(cls) -> str: 17 | return cls.__name__.lower() 18 | -------------------------------------------------------------------------------- /part-07-database/app/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | SQLALCHEMY_DATABASE_URI = "sqlite:///example.db" 5 | 6 | 7 | engine = create_engine( 8 | SQLALCHEMY_DATABASE_URI, 9 | # required for sqlite 10 | connect_args={"check_same_thread": False}, 11 | ) 12 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 13 | -------------------------------------------------------------------------------- /part-07-database/app/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from app.db.session import SessionLocal 4 | 5 | 6 | def get_db() -> Generator: 7 | db = SessionLocal() 8 | db.current_user_id = None 9 | try: 10 | yield db 11 | finally: 12 | db.close() 13 | -------------------------------------------------------------------------------- /part-07-database/app/initial_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app.db.base import Base 4 | from app.db.init_db import init_db 5 | from app.db.session import SessionLocal 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def init() -> None: 12 | db = SessionLocal() 13 | init_db(db) 14 | 15 | 16 | def main() -> None: 17 | logger.info("Creating initial data") 18 | init() 19 | logger.info("Initial data created") 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /part-07-database/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-07-database/app/models/__init__.py -------------------------------------------------------------------------------- /part-07-database/app/models/recipe.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class Recipe(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | label = Column(String(256), nullable=False) 10 | url = Column(String(256), index=True, nullable=True) 11 | source = Column(String(256), nullable=True) 12 | submitter_id = Column(Integer, ForeignKey("user.id"), nullable=True) 13 | submitter = relationship("User", back_populates="recipes") 14 | -------------------------------------------------------------------------------- /part-07-database/app/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String, Column, Boolean 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class User(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | first_name = Column(String(256), nullable=True) 10 | surname = Column(String(256), nullable=True) 11 | email = Column(String, index=True, nullable=False) 12 | is_superuser = Column(Boolean, default=False) 13 | recipes = relationship( 14 | "Recipe", 15 | cascade="all,delete-orphan", 16 | back_populates="submitter", 17 | uselist=True, 18 | ) 19 | -------------------------------------------------------------------------------- /part-07-database/app/recipe_data.py: -------------------------------------------------------------------------------- 1 | RECIPES = [ 2 | { 3 | "id": 1, 4 | "label": "Chicken Vesuvio", 5 | "source": "Serious Eats", 6 | "url": "http://www.seriouseats.com/recipes/2011/12/chicken-vesuvio-recipe.html", 7 | }, 8 | { 9 | "id": 2, 10 | "label": "Chicken Paprikash", 11 | "source": "No Recipes", 12 | "url": "http://norecipes.com/recipe/chicken-paprikash/", 13 | }, 14 | { 15 | "id": 3, 16 | "label": "Cauliflower and Tofu Curry Recipe", 17 | "source": "Serious Eats", 18 | "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", # noqa 19 | }, 20 | ] 21 | -------------------------------------------------------------------------------- /part-07-database/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .recipe import Recipe, RecipeCreate 2 | from .user import User, UserCreate 3 | -------------------------------------------------------------------------------- /part-07-database/app/schemas/recipe.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class RecipeBase(BaseModel): 7 | label: str 8 | source: str 9 | url: HttpUrl 10 | 11 | 12 | class RecipeCreate(RecipeBase): 13 | label: str 14 | source: str 15 | url: HttpUrl 16 | submitter_id: int 17 | 18 | 19 | class RecipeUpdate(RecipeBase): 20 | label: str 21 | 22 | 23 | # Properties shared by models stored in DB 24 | class RecipeInDBBase(RecipeBase): 25 | id: int 26 | submitter_id: int 27 | 28 | class Config: 29 | orm_mode = True 30 | 31 | 32 | # Properties to return to client 33 | class Recipe(RecipeInDBBase): 34 | pass 35 | 36 | 37 | # Properties properties stored in DB 38 | class RecipeInDB(RecipeInDBBase): 39 | pass 40 | 41 | 42 | class RecipeSearchResults(BaseModel): 43 | results: Sequence[Recipe] 44 | -------------------------------------------------------------------------------- /part-07-database/app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class UserBase(BaseModel): 7 | first_name: Optional[str] 8 | surname: Optional[str] 9 | email: Optional[EmailStr] = None 10 | is_superuser: bool = False 11 | 12 | 13 | # Properties to receive via API on creation 14 | class UserCreate(UserBase): 15 | email: EmailStr 16 | 17 | 18 | # Properties to receive via API on update 19 | class UserUpdate(UserBase): 20 | ... 21 | 22 | 23 | class UserInDBBase(UserBase): 24 | id: Optional[int] = None 25 | 26 | class Config: 27 | orm_mode = True 28 | 29 | 30 | # Additional properties to return via API 31 | class User(UserInDBBase): 32 | pass 33 | -------------------------------------------------------------------------------- /part-07-database/prestart.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | from alembic.config import Config 5 | from alembic import command 6 | 7 | from app.main import ROOT 8 | 9 | 10 | alembic_cfg = Config(ROOT / "alembic.ini") 11 | 12 | subprocess.run([sys.executable, "./app/backend_pre_start.py"]) 13 | command.upgrade(alembic_cfg, "head") 14 | subprocess.run([sys.executable, "./app/initial_data.py"]) 15 | -------------------------------------------------------------------------------- /part-07-database/prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Let the DB start 4 | python ./app/backend_pre_start.py 5 | 6 | # Run migrations 7 | alembic upgrade head 8 | 9 | # Create initial data in DB 10 | python ./app/initial_data.py -------------------------------------------------------------------------------- /part-07-database/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | uvicorn = "~0.11.3" 10 | fastapi = "~0.68.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = "~1.8.1"} 13 | Jinja2 = "^3.0.1" 14 | SQLAlchemy = "^1.4.22" 15 | alembic = "^1.6.5" 16 | tenacity = "^8.0.1" 17 | greenlet = "^1.1.2" 18 | -------------------------------------------------------------------------------- /part-07-database/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-08-structure-and-versioning/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache -------------------------------------------------------------------------------- /part-08-structure-and-versioning/README.md: -------------------------------------------------------------------------------- 1 | ## Part 8 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. Run the DB migrations via poetry `poetry run python app/prestart.py` (only required once) (Unix users can use 6 | the bash script if preferred) 7 | 4. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 8 | 5. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 9 | 6. Open http://localhost:8001/ -------------------------------------------------------------------------------- /part-08-structure-and-versioning/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /part-08-structure-and-versioning/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-08-structure-and-versioning/app/__init__.py -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-08-structure-and-versioning/app/api/__init__.py -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/api/api_v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-08-structure-and-versioning/app/api/api_v1/__init__.py -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/api/api_v1/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.api.api_v1.endpoints import recipe 4 | 5 | 6 | api_router = APIRouter() 7 | api_router.include_router(recipe.router, prefix="/recipes", tags=["recipes"]) 8 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/api/api_v1/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-08-structure-and-versioning/app/api/api_v1/endpoints/__init__.py -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/api/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from app.db.session import SessionLocal 4 | 5 | 6 | def get_db() -> Generator: 7 | db = SessionLocal() 8 | db.current_user_id = None 9 | try: 10 | yield db 11 | finally: 12 | db.close() 13 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/backend_pre_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 4 | 5 | from app.db.session import SessionLocal 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | max_tries = 60 * 5 # 5 minutes 11 | wait_seconds = 1 12 | 13 | 14 | @retry( 15 | stop=stop_after_attempt(max_tries), 16 | wait=wait_fixed(wait_seconds), 17 | before=before_log(logger, logging.INFO), 18 | after=after_log(logger, logging.WARN), 19 | ) 20 | def init() -> None: 21 | try: 22 | db = SessionLocal() 23 | # Try to create session to check if DB is awake 24 | db.execute("SELECT 1") 25 | except Exception as e: 26 | logger.error(e) 27 | raise e 28 | 29 | 30 | def main() -> None: 31 | logger.info("Initializing service") 32 | init() 33 | logger.info("Service finished initializing") 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-08-structure-and-versioning/app/core/__init__.py -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/core/config.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from pydantic import AnyHttpUrl, BaseSettings, EmailStr, validator 4 | from typing import List, Optional, Union 5 | 6 | 7 | # Project Directories 8 | ROOT = pathlib.Path(__file__).resolve().parent.parent 9 | 10 | 11 | 12 | class Settings(BaseSettings): 13 | API_V1_STR: str = "/api/v1" 14 | # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins 15 | # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \ 16 | # "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]' 17 | BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] 18 | 19 | @validator("BACKEND_CORS_ORIGINS", pre=True) 20 | def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: 21 | if isinstance(v, str) and not v.startswith("["): 22 | return [i.strip() for i in v.split(",")] 23 | elif isinstance(v, (list, str)): 24 | return v 25 | raise ValueError(v) 26 | 27 | SQLALCHEMY_DATABASE_URI: Optional[str] = "sqlite:///example.db" 28 | FIRST_SUPERUSER: EmailStr = "admin@recipeapi.com" 29 | 30 | class Config: 31 | case_sensitive = True 32 | 33 | 34 | settings = Settings() 35 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_recipe import recipe 2 | from .crud_user import user 3 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/crud/crud_recipe.py: -------------------------------------------------------------------------------- 1 | from app.crud.base import CRUDBase 2 | from app.models.recipe import Recipe 3 | from app.schemas.recipe import RecipeCreate, RecipeUpdate 4 | 5 | 6 | class CRUDRecipe(CRUDBase[Recipe, RecipeCreate, RecipeUpdate]): 7 | ... 8 | 9 | 10 | recipe = CRUDRecipe(Recipe) 11 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.user import User 7 | from app.schemas.user import UserCreate, UserUpdate 8 | 9 | 10 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 11 | def get_by_email(self, db: Session, *, email: str) -> Optional[User]: 12 | return db.query(User).filter(User.email == email).first() 13 | 14 | def update( 15 | self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] 16 | ) -> User: 17 | if isinstance(obj_in, dict): 18 | update_data = obj_in 19 | else: 20 | update_data = obj_in.dict(exclude_unset=True) 21 | 22 | return super().update(db, db_obj=db_obj, obj_in=update_data) 23 | 24 | def is_superuser(self, user: User) -> bool: 25 | return user.is_superuser 26 | 27 | 28 | user = CRUDUser(User) 29 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-08-structure-and-versioning/app/db/__init__.py -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/db/base.py: -------------------------------------------------------------------------------- 1 | # Import all the models, so that Base has them before being 2 | # imported by Alembic 3 | from app.db.base_class import Base # noqa 4 | from app.models.user import User # noqa 5 | from app.models.recipe import Recipe # noqa 6 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/db/base_class.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 4 | 5 | 6 | class_registry: t.Dict = {} 7 | 8 | 9 | @as_declarative(class_registry=class_registry) 10 | class Base: 11 | id: t.Any 12 | __name__: str 13 | 14 | # Generate __tablename__ automatically 15 | @declared_attr 16 | def __tablename__(cls) -> str: 17 | return cls.__name__.lower() 18 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from app.core.config import settings 5 | 6 | 7 | engine = create_engine( 8 | settings.SQLALCHEMY_DATABASE_URI, 9 | # required for sqlite 10 | connect_args={"check_same_thread": False}, 11 | ) 12 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 13 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/initial_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app.db.init_db import init_db 4 | from app.db.session import SessionLocal 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def init() -> None: 11 | db = SessionLocal() 12 | init_db(db) 13 | 14 | 15 | def main() -> None: 16 | logger.info("Creating initial data") 17 | init() 18 | logger.info("Initial data created") 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/main.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from fastapi import FastAPI, APIRouter, Request, Depends 4 | from fastapi.templating import Jinja2Templates 5 | from sqlalchemy.orm import Session 6 | 7 | from app import crud 8 | from app.api import deps 9 | from app.api.api_v1.api import api_router 10 | from app.core.config import settings 11 | 12 | BASE_PATH = Path(__file__).resolve().parent 13 | TEMPLATES = Jinja2Templates(directory=str(BASE_PATH / "templates")) 14 | 15 | root_router = APIRouter() 16 | app = FastAPI(title="Recipe API") 17 | 18 | 19 | @root_router.get("/", status_code=200) 20 | def root( 21 | request: Request, 22 | db: Session = Depends(deps.get_db), 23 | ) -> dict: 24 | """ 25 | Root GET 26 | """ 27 | recipes = crud.recipe.get_multi(db=db, limit=10) 28 | return TEMPLATES.TemplateResponse( 29 | "index.html", 30 | {"request": request, "recipes": recipes}, 31 | ) 32 | 33 | 34 | app.include_router(api_router, prefix=settings.API_V1_STR) 35 | app.include_router(root_router) 36 | 37 | 38 | if __name__ == "__main__": 39 | # Use this for debugging purposes only 40 | import uvicorn 41 | 42 | uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug") 43 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-08-structure-and-versioning/app/models/__init__.py -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/models/recipe.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class Recipe(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | label = Column(String(256), nullable=False) 10 | url = Column(String(256), index=True, nullable=True) 11 | source = Column(String(256), nullable=True) 12 | submitter_id = Column(Integer, ForeignKey("user.id"), nullable=True) 13 | submitter = relationship("User", back_populates="recipes") 14 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String, Column, Boolean 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class User(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | first_name = Column(String(256), nullable=True) 10 | surname = Column(String(256), nullable=True) 11 | email = Column(String, index=True, nullable=False) 12 | is_superuser = Column(Boolean, default=False) 13 | recipes = relationship( 14 | "Recipe", 15 | cascade="all,delete-orphan", 16 | back_populates="submitter", 17 | uselist=True, 18 | ) 19 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .recipe import Recipe, RecipeCreate 2 | from .user import User, UserCreate 3 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/schemas/recipe.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class RecipeBase(BaseModel): 7 | label: str 8 | source: str 9 | url: HttpUrl 10 | 11 | 12 | class RecipeCreate(RecipeBase): 13 | label: str 14 | source: str 15 | url: HttpUrl 16 | submitter_id: int 17 | 18 | 19 | class RecipeUpdate(RecipeBase): 20 | label: str 21 | 22 | 23 | # Properties shared by models stored in DB 24 | class RecipeInDBBase(RecipeBase): 25 | id: int 26 | submitter_id: int 27 | 28 | class Config: 29 | orm_mode = True 30 | 31 | 32 | # Properties to return to client 33 | class Recipe(RecipeInDBBase): 34 | pass 35 | 36 | 37 | # Properties properties stored in DB 38 | class RecipeInDB(RecipeInDBBase): 39 | pass 40 | 41 | 42 | class RecipeSearchResults(BaseModel): 43 | results: Sequence[Recipe] 44 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class UserBase(BaseModel): 7 | first_name: Optional[str] 8 | surname: Optional[str] 9 | email: Optional[EmailStr] = None 10 | is_superuser: bool = False 11 | 12 | 13 | # Properties to receive via API on creation 14 | class UserCreate(UserBase): 15 | email: EmailStr 16 | 17 | 18 | # Properties to receive via API on update 19 | class UserUpdate(UserBase): 20 | ... 21 | 22 | 23 | class UserInDBBase(UserBase): 24 | id: Optional[int] = None 25 | 26 | class Config: 27 | orm_mode = True 28 | 29 | 30 | # Additional properties to return via API 31 | class User(UserInDBBase): 32 | pass 33 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/prestart.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | from alembic.config import Config 5 | from alembic import command 6 | 7 | from app.core.config import ROOT 8 | 9 | 10 | alembic_cfg = Config(ROOT.parent / "alembic.ini") 11 | 12 | subprocess.run([sys.executable, "./app/backend_pre_start.py"]) 13 | command.upgrade(alembic_cfg, "head") 14 | subprocess.run([sys.executable, "./app/initial_data.py"]) -------------------------------------------------------------------------------- /part-08-structure-and-versioning/prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Let the DB start 4 | python ./app/backend_pre_start.py 5 | 6 | # Run migrations 7 | alembic upgrade head 8 | 9 | # Create initial data in DB 10 | python ./app/initial_data.py -------------------------------------------------------------------------------- /part-08-structure-and-versioning/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | uvicorn = "~0.11.3" 10 | fastapi = "~0.68.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = "~1.8.1"} 13 | Jinja2 = "^3.0.1" 14 | SQLAlchemy = "^1.4.22" 15 | alembic = "^1.6.5" 16 | tenacity = "^8.0.1" 17 | greenlet = "^1.1.2" 18 | -------------------------------------------------------------------------------- /part-08-structure-and-versioning/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-09-async/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache -------------------------------------------------------------------------------- /part-09-async/README.md: -------------------------------------------------------------------------------- 1 | ## Part 9 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. Run the DB migrations via poetry `poetry run python app/prestart.py` (only required once) (Unix users can use 6 | the bash script if preferred) 7 | 4. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 8 | 5. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 9 | 6. Open http://localhost:8001/ 10 | -------------------------------------------------------------------------------- /part-09-async/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /part-09-async/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /part-09-async/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-09-async/app/__init__.py -------------------------------------------------------------------------------- /part-09-async/app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-09-async/app/api/__init__.py -------------------------------------------------------------------------------- /part-09-async/app/api/api_v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-09-async/app/api/api_v1/__init__.py -------------------------------------------------------------------------------- /part-09-async/app/api/api_v1/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.api.api_v1.endpoints import recipe 4 | 5 | 6 | api_router = APIRouter() 7 | api_router.include_router(recipe.router, prefix="/recipes", tags=["recipes"]) 8 | -------------------------------------------------------------------------------- /part-09-async/app/api/api_v1/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-09-async/app/api/api_v1/endpoints/__init__.py -------------------------------------------------------------------------------- /part-09-async/app/api/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from app.db.session import SessionLocal 4 | 5 | 6 | def get_db() -> Generator: 7 | db = SessionLocal() 8 | db.current_user_id = None 9 | try: 10 | yield db 11 | finally: 12 | db.close() 13 | -------------------------------------------------------------------------------- /part-09-async/app/backend_pre_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 4 | 5 | from app.db.session import SessionLocal 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | max_tries = 60 * 5 # 5 minutes 11 | wait_seconds = 1 12 | 13 | 14 | @retry( 15 | stop=stop_after_attempt(max_tries), 16 | wait=wait_fixed(wait_seconds), 17 | before=before_log(logger, logging.INFO), 18 | after=after_log(logger, logging.WARN), 19 | ) 20 | def init() -> None: 21 | try: 22 | db = SessionLocal() 23 | # Try to create session to check if DB is awake 24 | db.execute("SELECT 1") 25 | except Exception as e: 26 | logger.error(e) 27 | raise e 28 | 29 | 30 | def main() -> None: 31 | logger.info("Initializing service") 32 | init() 33 | logger.info("Service finished initializing") 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /part-09-async/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-09-async/app/core/__init__.py -------------------------------------------------------------------------------- /part-09-async/app/core/config.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from pydantic import AnyHttpUrl, BaseSettings, EmailStr, validator 4 | from typing import List, Optional, Union 5 | 6 | 7 | # Project Directories 8 | ROOT = pathlib.Path(__file__).resolve().parent.parent 9 | 10 | 11 | class Settings(BaseSettings): 12 | API_V1_STR: str = "/api/v1" 13 | # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins 14 | # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \ 15 | # "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]' 16 | BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] 17 | 18 | @validator("BACKEND_CORS_ORIGINS", pre=True) 19 | def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: 20 | if isinstance(v, str) and not v.startswith("["): 21 | return [i.strip() for i in v.split(",")] 22 | elif isinstance(v, (list, str)): 23 | return v 24 | raise ValueError(v) 25 | 26 | SQLALCHEMY_DATABASE_URI: Optional[str] = "sqlite:///example.db" 27 | FIRST_SUPERUSER: EmailStr = "admin@recipeapi.com" 28 | 29 | class Config: 30 | case_sensitive = True 31 | 32 | 33 | settings = Settings() 34 | -------------------------------------------------------------------------------- /part-09-async/app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_recipe import recipe 2 | from .crud_user import user 3 | -------------------------------------------------------------------------------- /part-09-async/app/crud/crud_recipe.py: -------------------------------------------------------------------------------- 1 | from app.crud.base import CRUDBase 2 | from app.models.recipe import Recipe 3 | from app.schemas.recipe import RecipeCreate, RecipeUpdate 4 | 5 | 6 | class CRUDRecipe(CRUDBase[Recipe, RecipeCreate, RecipeUpdate]): 7 | ... 8 | 9 | 10 | recipe = CRUDRecipe(Recipe) 11 | -------------------------------------------------------------------------------- /part-09-async/app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.user import User 7 | from app.schemas.user import UserCreate, UserUpdate 8 | 9 | 10 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 11 | def get_by_email(self, db: Session, *, email: str) -> Optional[User]: 12 | return db.query(User).filter(User.email == email).first() 13 | 14 | def update( 15 | self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] 16 | ) -> User: 17 | if isinstance(obj_in, dict): 18 | update_data = obj_in 19 | else: 20 | update_data = obj_in.dict(exclude_unset=True) 21 | 22 | return super().update(db, db_obj=db_obj, obj_in=update_data) 23 | 24 | def is_superuser(self, user: User) -> bool: 25 | return user.is_superuser 26 | 27 | 28 | user = CRUDUser(User) 29 | -------------------------------------------------------------------------------- /part-09-async/app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-09-async/app/db/__init__.py -------------------------------------------------------------------------------- /part-09-async/app/db/base.py: -------------------------------------------------------------------------------- 1 | # Import all the models, so that Base has them before being 2 | # imported by Alembic 3 | from app.db.base_class import Base # noqa 4 | from app.models.user import User # noqa 5 | from app.models.recipe import Recipe # noqa 6 | -------------------------------------------------------------------------------- /part-09-async/app/db/base_class.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 4 | 5 | 6 | class_registry: t.Dict = {} 7 | 8 | 9 | @as_declarative(class_registry=class_registry) 10 | class Base: 11 | id: t.Any 12 | __name__: str 13 | 14 | # Generate __tablename__ automatically 15 | @declared_attr 16 | def __tablename__(cls) -> str: 17 | return cls.__name__.lower() 18 | -------------------------------------------------------------------------------- /part-09-async/app/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from app.core.config import settings 5 | 6 | engine = create_engine( 7 | settings.SQLALCHEMY_DATABASE_URI, 8 | # required for sqlite 9 | connect_args={"check_same_thread": False}, 10 | ) 11 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 12 | -------------------------------------------------------------------------------- /part-09-async/app/initial_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app.db.init_db import init_db 4 | from app.db.session import SessionLocal 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def init() -> None: 11 | db = SessionLocal() 12 | init_db(db) 13 | 14 | 15 | def main() -> None: 16 | logger.info("Creating initial data") 17 | init() 18 | logger.info("Initial data created") 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /part-09-async/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-09-async/app/models/__init__.py -------------------------------------------------------------------------------- /part-09-async/app/models/recipe.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class Recipe(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | label = Column(String(256), nullable=False) 10 | url = Column(String(256), index=True, nullable=True) 11 | source = Column(String(256), nullable=True) 12 | submitter_id = Column(Integer, ForeignKey("user.id"), nullable=True) 13 | submitter = relationship("User", back_populates="recipes") 14 | -------------------------------------------------------------------------------- /part-09-async/app/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String, Column, Boolean 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class User(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | first_name = Column(String(256), nullable=True) 10 | surname = Column(String(256), nullable=True) 11 | email = Column(String, index=True, nullable=False) 12 | is_superuser = Column(Boolean, default=False) 13 | recipes = relationship( 14 | "Recipe", 15 | cascade="all,delete-orphan", 16 | back_populates="submitter", 17 | uselist=True, 18 | ) 19 | -------------------------------------------------------------------------------- /part-09-async/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .recipe import Recipe, RecipeCreate 2 | from .user import User, UserCreate 3 | -------------------------------------------------------------------------------- /part-09-async/app/schemas/recipe.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class RecipeBase(BaseModel): 7 | label: str 8 | source: str 9 | url: HttpUrl 10 | 11 | 12 | class RecipeCreate(RecipeBase): 13 | label: str 14 | source: str 15 | url: HttpUrl 16 | submitter_id: int 17 | 18 | 19 | class RecipeUpdate(RecipeBase): 20 | label: str 21 | 22 | 23 | # Properties shared by models stored in DB 24 | class RecipeInDBBase(RecipeBase): 25 | id: int 26 | submitter_id: int 27 | 28 | class Config: 29 | orm_mode = True 30 | 31 | 32 | # Properties to return to client 33 | class Recipe(RecipeInDBBase): 34 | pass 35 | 36 | 37 | # Properties properties stored in DB 38 | class RecipeInDB(RecipeInDBBase): 39 | pass 40 | 41 | 42 | class RecipeSearchResults(BaseModel): 43 | results: Sequence[Recipe] 44 | -------------------------------------------------------------------------------- /part-09-async/app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class UserBase(BaseModel): 7 | first_name: Optional[str] 8 | surname: Optional[str] 9 | email: Optional[EmailStr] = None 10 | is_superuser: bool = False 11 | 12 | 13 | # Properties to receive via API on creation 14 | class UserCreate(UserBase): 15 | email: EmailStr 16 | 17 | 18 | # Properties to receive via API on update 19 | class UserUpdate(UserBase): 20 | ... 21 | 22 | 23 | class UserInDBBase(UserBase): 24 | id: Optional[int] = None 25 | 26 | class Config: 27 | orm_mode = True 28 | 29 | 30 | # Additional properties to return via API 31 | class User(UserInDBBase): 32 | pass 33 | -------------------------------------------------------------------------------- /part-09-async/prestart.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | from alembic.config import Config 5 | from alembic import command 6 | 7 | from app.core.config import ROOT 8 | 9 | 10 | alembic_cfg = Config(ROOT.parent / "alembic.ini") 11 | 12 | subprocess.run([sys.executable, "./app/backend_pre_start.py"]) 13 | command.upgrade(alembic_cfg, "head") 14 | subprocess.run([sys.executable, "./app/initial_data.py"]) 15 | -------------------------------------------------------------------------------- /part-09-async/prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Let the DB start 4 | python ./app/backend_pre_start.py 5 | 6 | # Run migrations 7 | alembic upgrade head 8 | 9 | # Create initial data in DB 10 | python ./app/initial_data.py -------------------------------------------------------------------------------- /part-09-async/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | uvicorn = "~0.11.3" 10 | fastapi = "~0.68.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = "~1.8.1"} 13 | Jinja2 = "^3.0.1" 14 | SQLAlchemy = "^1.4.3" 15 | alembic = "^1.6.5" 16 | tenacity = "^8.0.1" 17 | httpx = "0.18.1" 18 | -------------------------------------------------------------------------------- /part-09-async/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-10-jwt-auth/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache -------------------------------------------------------------------------------- /part-10-jwt-auth/README.md: -------------------------------------------------------------------------------- 1 | ## Part 10 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. If continuing from a previous part of the series, delete your current project database because we 6 | have made breaking DB migration changes. `rm example.db`. If you're starting here, you can ignore this step. 7 | 4. Run the DB migrations via poetry `poetry run python app/prestart.py` (only required once) (Unix users can use 8 | the bash script if preferred) 9 | 5. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 10 | 6. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 11 | 7. Open http://localhost:8001/ -------------------------------------------------------------------------------- /part-10-jwt-auth/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /part-10-jwt-auth/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-10-jwt-auth/app/__init__.py -------------------------------------------------------------------------------- /part-10-jwt-auth/app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-10-jwt-auth/app/api/__init__.py -------------------------------------------------------------------------------- /part-10-jwt-auth/app/api/api_v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-10-jwt-auth/app/api/api_v1/__init__.py -------------------------------------------------------------------------------- /part-10-jwt-auth/app/api/api_v1/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.api.api_v1.endpoints import recipe, auth 4 | 5 | 6 | api_router = APIRouter() 7 | api_router.include_router(recipe.router, prefix="/recipes", tags=["recipes"]) 8 | api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) 9 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/api/api_v1/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-10-jwt-auth/app/api/api_v1/endpoints/__init__.py -------------------------------------------------------------------------------- /part-10-jwt-auth/app/backend_pre_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 4 | 5 | from app.db.session import SessionLocal 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | max_tries = 60 * 5 # 5 minutes 11 | wait_seconds = 1 12 | 13 | 14 | @retry( 15 | stop=stop_after_attempt(max_tries), 16 | wait=wait_fixed(wait_seconds), 17 | before=before_log(logger, logging.INFO), 18 | after=after_log(logger, logging.WARN), 19 | ) 20 | def init() -> None: 21 | try: 22 | db = SessionLocal() 23 | # Try to create session to check if DB is awake 24 | db.execute("SELECT 1") 25 | except Exception as e: 26 | logger.error(e) 27 | raise e 28 | 29 | 30 | def main() -> None: 31 | logger.info("Initializing service") 32 | init() 33 | logger.info("Service finished initializing") 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-10-jwt-auth/app/core/__init__.py -------------------------------------------------------------------------------- /part-10-jwt-auth/app/core/security.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | 4 | PWD_CONTEXT = CryptContext(schemes=["bcrypt"], deprecated="auto") 5 | 6 | 7 | def verify_password(plain_password: str, hashed_password: str) -> bool: 8 | return PWD_CONTEXT.verify(plain_password, hashed_password) 9 | 10 | 11 | def get_password_hash(password: str) -> str: 12 | return PWD_CONTEXT.hash(password) 13 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_recipe import recipe 2 | from .crud_user import user 3 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/crud/crud_recipe.py: -------------------------------------------------------------------------------- 1 | from app.crud.base import CRUDBase 2 | from app.models.recipe import Recipe 3 | from app.schemas.recipe import RecipeCreate, RecipeUpdate 4 | 5 | 6 | class CRUDRecipe(CRUDBase[Recipe, RecipeCreate, RecipeUpdate]): 7 | ... 8 | 9 | 10 | recipe = CRUDRecipe(Recipe) 11 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.user import User 7 | from app.schemas.user import UserCreate, UserUpdate 8 | from app.core.security import get_password_hash 9 | 10 | 11 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 12 | def get_by_email(self, db: Session, *, email: str) -> Optional[User]: 13 | return db.query(User).filter(User.email == email).first() 14 | 15 | def create(self, db: Session, *, obj_in: UserCreate) -> User: 16 | create_data = obj_in.dict() 17 | create_data.pop("password") 18 | db_obj = User(**create_data) 19 | db_obj.hashed_password = get_password_hash(obj_in.password) 20 | db.add(db_obj) 21 | db.commit() 22 | 23 | return db_obj 24 | 25 | def update( 26 | self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] 27 | ) -> User: 28 | if isinstance(obj_in, dict): 29 | update_data = obj_in 30 | else: 31 | update_data = obj_in.dict(exclude_unset=True) 32 | 33 | return super().update(db, db_obj=db_obj, obj_in=update_data) 34 | 35 | def is_superuser(self, user: User) -> bool: 36 | return user.is_superuser 37 | 38 | 39 | user = CRUDUser(User) 40 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-10-jwt-auth/app/db/__init__.py -------------------------------------------------------------------------------- /part-10-jwt-auth/app/db/base.py: -------------------------------------------------------------------------------- 1 | # Import all the models, so that Base has them before being 2 | # imported by Alembic 3 | from app.db.base_class import Base # noqa 4 | from app.models.user import User # noqa 5 | from app.models.recipe import Recipe # noqa 6 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/db/base_class.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 4 | 5 | 6 | class_registry: t.Dict = {} 7 | 8 | 9 | @as_declarative(class_registry=class_registry) 10 | class Base: 11 | id: t.Any 12 | __name__: str 13 | 14 | # Generate __tablename__ automatically 15 | @declared_attr 16 | def __tablename__(cls) -> str: 17 | return cls.__name__.lower() 18 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from app.core.config import settings 5 | 6 | engine = create_engine( 7 | settings.SQLALCHEMY_DATABASE_URI, 8 | # required for sqlite 9 | connect_args={"check_same_thread": False}, 10 | ) 11 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 12 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/initial_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app.db.init_db import init_db 4 | from app.db.session import SessionLocal 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def init() -> None: 11 | db = SessionLocal() 12 | init_db(db) 13 | 14 | 15 | def main() -> None: 16 | logger.info("Creating initial data") 17 | init() 18 | logger.info("Initial data created") 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-10-jwt-auth/app/models/__init__.py -------------------------------------------------------------------------------- /part-10-jwt-auth/app/models/recipe.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class Recipe(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | label = Column(String(256), nullable=False) 10 | url = Column(String(256), index=True, nullable=True) 11 | source = Column(String(256), nullable=True) 12 | submitter_id = Column(Integer, ForeignKey("user.id"), nullable=True) 13 | submitter = relationship("User", back_populates="recipes") 14 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String, Column, Boolean 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class User(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | first_name = Column(String(256), nullable=True) 10 | surname = Column(String(256), nullable=True) 11 | email = Column(String, index=True, nullable=False) 12 | is_superuser = Column(Boolean, default=False) 13 | recipes = relationship( 14 | "Recipe", 15 | cascade="all,delete-orphan", 16 | back_populates="submitter", 17 | uselist=True, 18 | ) 19 | 20 | # New addition 21 | hashed_password = Column(String, nullable=False) 22 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .recipe import Recipe, RecipeCreate 2 | from .user import User, UserCreate 3 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/schemas/recipe.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class RecipeBase(BaseModel): 7 | label: str 8 | source: str 9 | url: HttpUrl 10 | 11 | 12 | class RecipeCreate(RecipeBase): 13 | label: str 14 | source: str 15 | url: HttpUrl 16 | submitter_id: int 17 | 18 | 19 | class RecipeUpdate(RecipeBase): 20 | label: str 21 | 22 | 23 | # Properties shared by models stored in DB 24 | class RecipeInDBBase(RecipeBase): 25 | id: int 26 | submitter_id: int 27 | 28 | class Config: 29 | orm_mode = True 30 | 31 | 32 | # Properties to return to client 33 | class Recipe(RecipeInDBBase): 34 | pass 35 | 36 | 37 | # Properties properties stored in DB 38 | class RecipeInDB(RecipeInDBBase): 39 | pass 40 | 41 | 42 | class RecipeSearchResults(BaseModel): 43 | results: Sequence[Recipe] 44 | -------------------------------------------------------------------------------- /part-10-jwt-auth/app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class UserBase(BaseModel): 7 | first_name: Optional[str] 8 | surname: Optional[str] 9 | email: Optional[EmailStr] = None 10 | is_superuser: bool = False 11 | 12 | 13 | # Properties to receive via API on creation 14 | class UserCreate(UserBase): 15 | email: EmailStr 16 | password: str 17 | 18 | 19 | # Properties to receive via API on update 20 | class UserUpdate(UserBase): 21 | ... 22 | 23 | 24 | class UserInDBBase(UserBase): 25 | id: Optional[int] = None 26 | 27 | class Config: 28 | orm_mode = True 29 | 30 | 31 | # Additional properties stored in DB but not returned by API 32 | class UserInDB(UserInDBBase): 33 | hashed_password: str 34 | 35 | 36 | # Additional properties to return via API 37 | class User(UserInDBBase): 38 | ... 39 | -------------------------------------------------------------------------------- /part-10-jwt-auth/prestart.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | from alembic.config import Config 5 | from alembic import command 6 | 7 | from app.core.config import ROOT 8 | 9 | 10 | alembic_cfg = Config(ROOT.parent / "alembic.ini") 11 | 12 | subprocess.run([sys.executable, "./app/backend_pre_start.py"]) 13 | command.upgrade(alembic_cfg, "head") 14 | subprocess.run([sys.executable, "./app/initial_data.py"]) 15 | -------------------------------------------------------------------------------- /part-10-jwt-auth/prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Let the DB start 4 | python ./app/backend_pre_start.py 5 | 6 | # Run migrations 7 | alembic upgrade head 8 | 9 | # Create initial data in DB 10 | python ./app/initial_data.py -------------------------------------------------------------------------------- /part-10-jwt-auth/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | uvicorn = "~0.11.3" 10 | fastapi = "~0.68.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = "~1.8.1"} 13 | Jinja2 = "~3.0.1" 14 | SQLAlchemy = "~1.4.3" 15 | alembic = "~1.6.5" 16 | tenacity = "~8.0.1" 17 | httpx = "~0.18.1" 18 | passlib = {extras = ["bcrypt"], version = "^1.7.2"} 19 | python-jose = {extras = ["cryptography"], version = "^3.3.0"} 20 | cffi = "^1.15.0" 21 | -------------------------------------------------------------------------------- /part-10-jwt-auth/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-11-dependency-injection/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache -------------------------------------------------------------------------------- /part-11-dependency-injection/README.md: -------------------------------------------------------------------------------- 1 | ## Part 11 Local Setup 2 | 3 | 1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) 4 | 2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` 5 | 3. If continuing from a previous part of the series, delete your current project database because we 6 | have made breaking DB migration changes. `rm example.db`. If you're starting here, you can ignore this step. 7 | 4. Run the DB migrations via poetry `poetry run python app/prestart.py` (only required once) (Unix users can use 8 | the bash script if preferred) 9 | 5. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` 10 | 6. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` 11 | 7. Open http://localhost:8001/ 12 | -------------------------------------------------------------------------------- /part-11-dependency-injection/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /part-11-dependency-injection/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/api/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/api/api_v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/api/api_v1/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/api/api_v1/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.api.api_v1.endpoints import recipe, auth 4 | 5 | 6 | api_router = APIRouter() 7 | api_router.include_router(recipe.router, prefix="/recipes", tags=["recipes"]) 8 | api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) 9 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/api/api_v1/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/api/api_v1/endpoints/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/backend_pre_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 4 | 5 | from app.db.session import SessionLocal 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | max_tries = 60 * 5 # 5 minutes 11 | wait_seconds = 1 12 | 13 | 14 | @retry( 15 | stop=stop_after_attempt(max_tries), 16 | wait=wait_fixed(wait_seconds), 17 | before=before_log(logger, logging.INFO), 18 | after=after_log(logger, logging.WARN), 19 | ) 20 | def init() -> None: 21 | try: 22 | db = SessionLocal() 23 | # Try to create session to check if DB is awake 24 | db.execute("SELECT 1") 25 | except Exception as e: 26 | logger.error(e) 27 | raise e 28 | 29 | 30 | def main() -> None: 31 | logger.info("Initializing service") 32 | init() 33 | logger.info("Service finished initializing") 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/clients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/clients/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/core/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/core/security.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | 4 | PWD_CONTEXT = CryptContext(schemes=["bcrypt"], deprecated="auto") 5 | 6 | 7 | def verify_password(plain_password: str, hashed_password: str) -> bool: 8 | return PWD_CONTEXT.verify(plain_password, hashed_password) 9 | 10 | 11 | def get_password_hash(password: str) -> str: 12 | return PWD_CONTEXT.hash(password) 13 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_recipe import recipe 2 | from .crud_user import user 3 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/crud/crud_recipe.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.recipe import Recipe 7 | from app.schemas.recipe import RecipeCreate, RecipeUpdateRestricted, RecipeUpdate 8 | 9 | 10 | class CRUDRecipe(CRUDBase[Recipe, RecipeCreate, RecipeUpdate]): 11 | def update( 12 | self, 13 | db: Session, 14 | *, 15 | db_obj: Recipe, 16 | obj_in: Union[RecipeUpdate, RecipeUpdateRestricted] 17 | ) -> Recipe: 18 | db_obj = super().update(db, db_obj=db_obj, obj_in=obj_in) 19 | return db_obj 20 | 21 | 22 | recipe = CRUDRecipe(Recipe) 23 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.user import User 7 | from app.schemas.user import UserCreate, UserUpdate 8 | from app.core.security import get_password_hash 9 | 10 | 11 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 12 | def get_by_email(self, db: Session, *, email: str) -> Optional[User]: 13 | return db.query(User).filter(User.email == email).first() 14 | 15 | def create(self, db: Session, *, obj_in: UserCreate) -> User: 16 | create_data = obj_in.dict() 17 | create_data.pop("password") 18 | db_obj = User(**create_data) 19 | db_obj.hashed_password = get_password_hash(obj_in.password) 20 | db.add(db_obj) 21 | db.commit() 22 | 23 | return db_obj 24 | 25 | def update( 26 | self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] 27 | ) -> User: 28 | if isinstance(obj_in, dict): 29 | update_data = obj_in 30 | else: 31 | update_data = obj_in.dict(exclude_unset=True) 32 | 33 | return super().update(db, db_obj=db_obj, obj_in=update_data) 34 | 35 | def is_superuser(self, user: User) -> bool: 36 | return user.is_superuser 37 | 38 | 39 | user = CRUDUser(User) 40 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/db/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/db/base.py: -------------------------------------------------------------------------------- 1 | # Import all the models, so that Base has them before being 2 | # imported by Alembic 3 | from app.db.base_class import Base # noqa 4 | from app.models.user import User # noqa 5 | from app.models.recipe import Recipe # noqa 6 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/db/base_class.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 4 | 5 | 6 | class_registry: t.Dict = {} 7 | 8 | 9 | @as_declarative(class_registry=class_registry) 10 | class Base: 11 | id: t.Any 12 | __name__: str 13 | 14 | # Generate __tablename__ automatically 15 | @declared_attr 16 | def __tablename__(cls) -> str: 17 | return cls.__name__.lower() 18 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from app.core.config import settings 5 | 6 | engine = create_engine( 7 | settings.SQLALCHEMY_DATABASE_URI, 8 | # required for sqlite 9 | connect_args={"check_same_thread": False}, 10 | ) 11 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 12 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/initial_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app.db.init_db import init_db 4 | from app.db.session import SessionLocal 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def init() -> None: 11 | db = SessionLocal() 12 | init_db(db) 13 | 14 | 15 | def main() -> None: 16 | logger.info("Creating initial data") 17 | init() 18 | logger.info("Initial data created") 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/models/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/models/recipe.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class Recipe(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | label = Column(String(256), nullable=False) 10 | url = Column(String(256), index=True, nullable=True) 11 | source = Column(String(256), nullable=True) 12 | submitter_id = Column(Integer, ForeignKey("user.id"), nullable=True) 13 | submitter = relationship("User", back_populates="recipes") 14 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String, Column, Boolean 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class User(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | first_name = Column(String(256), nullable=True) 10 | surname = Column(String(256), nullable=True) 11 | email = Column(String, index=True, nullable=False) 12 | is_superuser = Column(Boolean, default=False) 13 | recipes = relationship( 14 | "Recipe", 15 | cascade="all,delete-orphan", 16 | back_populates="submitter", 17 | uselist=True, 18 | ) 19 | 20 | # New addition 21 | hashed_password = Column(String, nullable=False) 22 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .recipe import Recipe, RecipeCreate, RecipeUpdateRestricted, RecipeUpdate 2 | from .user import User, UserCreate 3 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/schemas/recipe.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class RecipeBase(BaseModel): 7 | label: str 8 | source: str 9 | url: HttpUrl 10 | 11 | 12 | class RecipeCreate(RecipeBase): 13 | label: str 14 | source: str 15 | url: HttpUrl 16 | submitter_id: int 17 | 18 | 19 | class RecipeUpdate(RecipeBase): 20 | id: int 21 | 22 | 23 | class RecipeUpdateRestricted(BaseModel): 24 | id: int 25 | label: str 26 | 27 | 28 | # Properties shared by models stored in DB 29 | class RecipeInDBBase(RecipeBase): 30 | id: int 31 | submitter_id: int 32 | 33 | class Config: 34 | orm_mode = True 35 | 36 | 37 | # Properties to return to client 38 | class Recipe(RecipeInDBBase): 39 | pass 40 | 41 | 42 | # Properties properties stored in DB 43 | class RecipeInDB(RecipeInDBBase): 44 | pass 45 | 46 | 47 | class RecipeSearchResults(BaseModel): 48 | results: Sequence[Recipe] 49 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class UserBase(BaseModel): 7 | first_name: Optional[str] 8 | surname: Optional[str] 9 | email: Optional[EmailStr] = None 10 | is_superuser: bool = False 11 | 12 | 13 | # Properties to receive via API on creation 14 | class UserCreate(UserBase): 15 | email: EmailStr 16 | password: str 17 | 18 | 19 | # Properties to receive via API on update 20 | class UserUpdate(UserBase): 21 | ... 22 | 23 | 24 | class UserInDBBase(UserBase): 25 | id: Optional[int] = None 26 | 27 | class Config: 28 | orm_mode = True 29 | 30 | 31 | # Additional properties stored in DB but not returned by API 32 | class UserInDB(UserInDBBase): 33 | hashed_password: str 34 | 35 | 36 | # Additional properties to return via API 37 | class User(UserInDBBase): 38 | ... 39 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-11-dependency-injection/app/tests/api/__init__.py -------------------------------------------------------------------------------- /part-11-dependency-injection/app/tests/api/test_recipe.py: -------------------------------------------------------------------------------- 1 | from app.core.config import settings 2 | 3 | 4 | def test_fetch_ideas_reddit_sync(client): 5 | # When 6 | response = client.get(f"{settings.API_V1_STR}/recipes/ideas/") 7 | data = response.json() 8 | 9 | # Then 10 | assert response.status_code == 200 11 | for key in data.keys(): 12 | assert key in ["recipes", "easyrecipes", "TopSecretRecipes"] 13 | -------------------------------------------------------------------------------- /part-11-dependency-injection/app/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | from unittest.mock import MagicMock 3 | 4 | import pytest 5 | from fastapi.testclient import TestClient 6 | 7 | from app.main import app 8 | from app.api import deps 9 | 10 | 11 | async def override_reddit_dependency() -> MagicMock: 12 | mock = MagicMock() 13 | reddit_stub = { 14 | "recipes": [ 15 | "2085: the best chicken wings ever!! (https://i.redd.it/5iabdxh1jq381.jpg)", 16 | ], 17 | "easyrecipes": [ 18 | "74: Instagram accounts that post easy recipes? (https://www.reddit.com/r/easyrecipes/comments/rcluhd/instagram_accounts_that_post_easy_recipes/)", 19 | ], 20 | "TopSecretRecipes": [ 21 | "238: Halal guys red sauce - looking for recipe. Tried a recipe from a google search and it wasn’t nearly spicy enough. (https://i.redd.it/516yb30q9u381.jpg)", 22 | "132: Benihana Diablo Sauce - THE AUTHENTIC RECIPE! (https://www.reddit.com/r/TopSecretRecipes/comments/rbcirf/benihana_diablo_sauce_the_authentic_recipe/)", 23 | ], 24 | } 25 | mock.get_reddit_top.return_value = reddit_stub 26 | return mock 27 | 28 | 29 | @pytest.fixture 30 | def client() -> Generator: 31 | with TestClient(app) as client: 32 | app.dependency_overrides[deps.get_reddit_client] = override_reddit_dependency 33 | yield client 34 | app.dependency_overrides = {} 35 | -------------------------------------------------------------------------------- /part-11-dependency-injection/di_demo/main.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | RECIPE_SUBREDDITS = ["recipes", "easyrecipes", "TopSecretRecipes"] 4 | 5 | 6 | def get_reddit_top(subreddit: str) -> None: 7 | response = httpx.get( 8 | f"https://www.reddit.com/r/{subreddit}/top.json?sort=top&t=day&limit=5", 9 | headers={"User-agent": "recipe bot 0.1"}, 10 | ) 11 | subreddit_recipes = response.json() 12 | subreddit_data = [] 13 | for entry in subreddit_recipes["data"]["children"]: 14 | score = entry["data"]["score"] 15 | title = entry["data"]["title"] 16 | link = entry["data"]["url"] 17 | subreddit_data.append(f"{str(score)}: {title} ({link})") 18 | return subreddit_data 19 | 20 | 21 | def fetch_ideas() -> dict: 22 | return {key: get_reddit_top(subreddit=key) for key in RECIPE_SUBREDDITS} 23 | -------------------------------------------------------------------------------- /part-11-dependency-injection/di_demo/main_with_di.py: -------------------------------------------------------------------------------- 1 | from reddit import RedditClient 2 | 3 | 4 | RECIPE_SUBREDDITS = ["recipes", "easyrecipes", "TopSecretRecipes"] 5 | 6 | 7 | def fetch_ideas(reddit_client: RedditClient) -> dict: 8 | return { 9 | key: reddit_client.get_reddit_top(subreddit=key) for key in RECIPE_SUBREDDITS 10 | } 11 | -------------------------------------------------------------------------------- /part-11-dependency-injection/di_demo/patterns/three_types.py: -------------------------------------------------------------------------------- 1 | from ..reddit import RedditClient 2 | 3 | RECIPE_SUBREDDITS = ["recipes", "easyrecipes", "TopSecretRecipes"] 4 | 5 | # 1. Constructor Injection 6 | class Ideas: 7 | def __init__(self, reddit_client: RedditClient): 8 | self.reddit_client = reddit_client 9 | 10 | def fetch_ideas(self) -> dict: 11 | return { 12 | key: self.reddit_client.get_reddit_top(subreddit=key) 13 | for key in RECIPE_SUBREDDITS 14 | } 15 | 16 | 17 | # 2. Setter Injection 18 | class Ideas: 19 | _client = None 20 | 21 | def fetch_ideas(self) -> dict: 22 | return { 23 | key: self.client.get_reddit_top(subreddit=key) for key in RECIPE_SUBREDDITS 24 | } 25 | 26 | @property 27 | def client(self): 28 | return self._client 29 | 30 | @client.setter 31 | def client(self, value: RedditClient): 32 | self._client = value 33 | 34 | 35 | # Interface Injection 36 | -------------------------------------------------------------------------------- /part-11-dependency-injection/di_demo/test_main.py: -------------------------------------------------------------------------------- 1 | from main import fetch_ideas 2 | 3 | from unittest.mock import patch 4 | 5 | REDDIT_RESPONSE_DATA = { 6 | "recipes": [ 7 | "2825: Air Fryer Juicy Steak Bites (https://i.redd.it/6zdbb0zvrag81.jpg)" 8 | ], 9 | "easyrecipes": [ 10 | "189: Meals for when you have absolutely no energy to cook? (https://www.reddit.com/r/easyrecipes/comments/smbbr5/meals_for_when_you_have_absolutely_no_energy_to/)" 11 | ], 12 | } 13 | 14 | 15 | def test_fetch_ideas(): 16 | # Given 17 | with patch("main.get_reddit_top") as mocked_get_reddit: 18 | mocked_get_reddit.return_value = REDDIT_RESPONSE_DATA 19 | 20 | # When 21 | subject = fetch_ideas() 22 | 23 | # Then 24 | assert subject["recipes"] 25 | assert subject["easyrecipes"] 26 | -------------------------------------------------------------------------------- /part-11-dependency-injection/di_demo/test_main_with_di.py: -------------------------------------------------------------------------------- 1 | from main_with_di import fetch_ideas 2 | 3 | import pytest 4 | 5 | 6 | class FakeClient: 7 | def get_reddit_top(self, subreddit: str) -> dict: 8 | return { 9 | "recipes": [ 10 | "2825: Air Fryer Juicy Steak Bites (https://i.redd.it/6zdbb0zvrag81.jpg)" 11 | ], 12 | "easyrecipes": [ 13 | "189: Meals for when you have absolutely no energy to cook? (https://www.reddit.com/r/easyrecipes/comments/smbbr5/meals_for_when_you_have_absolutely_no_energy_to/)" 14 | ], 15 | } 16 | 17 | 18 | @pytest.fixture 19 | def fake_reddit_client(): 20 | return FakeClient() 21 | 22 | 23 | def test_fetch_ideas(fake_reddit_client): 24 | # When 25 | subject = fetch_ideas(reddit_client=fake_reddit_client) 26 | 27 | # Then 28 | assert subject["recipes"] 29 | assert subject["easyrecipes"] 30 | -------------------------------------------------------------------------------- /part-11-dependency-injection/prestart.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | from alembic.config import Config 5 | from alembic import command 6 | 7 | from app.core.config import ROOT 8 | 9 | 10 | alembic_cfg = Config(ROOT.parent / "alembic.ini") 11 | 12 | subprocess.run([sys.executable, "./app/backend_pre_start.py"]) 13 | command.upgrade(alembic_cfg, "head") 14 | subprocess.run([sys.executable, "./app/initial_data.py"]) 15 | -------------------------------------------------------------------------------- /part-11-dependency-injection/prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Let the DB start 4 | python ./app/backend_pre_start.py 5 | 6 | # Run migrations 7 | alembic upgrade head 8 | 9 | # Create initial data in DB 10 | python ./app/initial_data.py -------------------------------------------------------------------------------- /part-11-dependency-injection/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | uvicorn = "~0.11.3" 10 | fastapi = "~0.68.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = "~1.8.1"} 13 | Jinja2 = "~3.0.1" 14 | SQLAlchemy = "~1.4.3" 15 | alembic = "~1.6.5" 16 | tenacity = "~8.0.1" 17 | httpx = "~0.18.1" 18 | passlib = {extras = ["bcrypt"], version = "^1.7.2"} 19 | python-jose = {extras = ["cryptography"], version = "^3.3.0"} 20 | 21 | [tool.poetry.dev-dependencies] 22 | pytest = "^6.2.5" 23 | requests = "^2.26.0" 24 | -------------------------------------------------------------------------------- /part-11-dependency-injection/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-12-react-frontend/README.md: -------------------------------------------------------------------------------- 1 | ## New: Setup & Run Frontend (requires backend to be running) 2 | 3 | 1. Install nodejs (>=v14) & npm 4 | 2. `cd frontend` 5 | 3. `npm install` 6 | 4. `npm start` 7 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache -------------------------------------------------------------------------------- /part-12-react-frontend/backend/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /part-12-react-frontend/backend/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/api/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/api/api_v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/api/api_v1/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/api/api_v1/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.api.api_v1.endpoints import recipe, auth 4 | 5 | 6 | api_router = APIRouter() 7 | api_router.include_router(recipe.router, prefix="/recipes", tags=["recipes"]) 8 | api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) 9 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/api/api_v1/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/api/api_v1/endpoints/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/backend_pre_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 4 | 5 | from app.db.session import SessionLocal 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | max_tries = 60 * 5 # 5 minutes 11 | wait_seconds = 1 12 | 13 | 14 | @retry( 15 | stop=stop_after_attempt(max_tries), 16 | wait=wait_fixed(wait_seconds), 17 | before=before_log(logger, logging.INFO), 18 | after=after_log(logger, logging.WARN), 19 | ) 20 | def init() -> None: 21 | try: 22 | db = SessionLocal() 23 | # Try to create session to check if DB is awake 24 | db.execute("SELECT 1") 25 | except Exception as e: 26 | logger.error(e) 27 | raise e 28 | 29 | 30 | def main() -> None: 31 | logger.info("Initializing service") 32 | init() 33 | logger.info("Service finished initializing") 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/clients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/clients/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/core/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/core/security.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | 4 | PWD_CONTEXT = CryptContext(schemes=["bcrypt"], deprecated="auto") 5 | 6 | 7 | def verify_password(plain_password: str, hashed_password: str) -> bool: 8 | return PWD_CONTEXT.verify(plain_password, hashed_password) 9 | 10 | 11 | def get_password_hash(password: str) -> str: 12 | return PWD_CONTEXT.hash(password) 13 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_recipe import recipe 2 | from .crud_user import user 3 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/crud/crud_recipe.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.recipe import Recipe 7 | from app.models.user import User 8 | from app.schemas.recipe import RecipeCreate, RecipeUpdateRestricted, RecipeUpdate 9 | 10 | 11 | class CRUDRecipe(CRUDBase[Recipe, RecipeCreate, RecipeUpdate]): 12 | def update( 13 | self, 14 | db: Session, 15 | *, 16 | db_obj: User, 17 | obj_in: Union[RecipeUpdate, RecipeUpdateRestricted] 18 | ) -> Recipe: 19 | db_obj = super().update(db, db_obj=db_obj, obj_in=obj_in) 20 | return db_obj 21 | 22 | 23 | recipe = CRUDRecipe(Recipe) 24 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.user import User 7 | from app.schemas.user import UserCreate, UserUpdate 8 | from app.core.security import get_password_hash 9 | 10 | 11 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 12 | def get_by_email(self, db: Session, *, email: str) -> Optional[User]: 13 | return db.query(User).filter(User.email == email).first() 14 | 15 | def create(self, db: Session, *, obj_in: UserCreate) -> User: 16 | create_data = obj_in.dict() 17 | create_data.pop("password") 18 | db_obj = User(**create_data) 19 | db_obj.hashed_password = get_password_hash(obj_in.password) 20 | db.add(db_obj) 21 | db.commit() 22 | 23 | return db_obj 24 | 25 | def update( 26 | self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] 27 | ) -> User: 28 | if isinstance(obj_in, dict): 29 | update_data = obj_in 30 | else: 31 | update_data = obj_in.dict(exclude_unset=True) 32 | 33 | return super().update(db, db_obj=db_obj, obj_in=update_data) 34 | 35 | def is_superuser(self, user: User) -> bool: 36 | return user.is_superuser 37 | 38 | 39 | user = CRUDUser(User) 40 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/db/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/db/base.py: -------------------------------------------------------------------------------- 1 | # Import all the models, so that Base has them before being 2 | # imported by Alembic 3 | from app.db.base_class import Base # noqa 4 | from app.models.user import User # noqa 5 | from app.models.recipe import Recipe # noqa 6 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/db/base_class.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 4 | 5 | 6 | class_registry: t.Dict = {} 7 | 8 | 9 | @as_declarative(class_registry=class_registry) 10 | class Base: 11 | id: t.Any 12 | __name__: str 13 | 14 | # Generate __tablename__ automatically 15 | @declared_attr 16 | def __tablename__(cls) -> str: 17 | return cls.__name__.lower() 18 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from app.core.config import settings 5 | 6 | engine = create_engine( 7 | settings.SQLALCHEMY_DATABASE_URI, 8 | # required for sqlite 9 | connect_args={"check_same_thread": False}, 10 | ) 11 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 12 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/initial_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app.db.init_db import init_db 4 | from app.db.session import SessionLocal 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def init() -> None: 11 | db = SessionLocal() 12 | init_db(db) 13 | 14 | 15 | def main() -> None: 16 | logger.info("Creating initial data") 17 | init() 18 | logger.info("Initial data created") 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/models/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/models/recipe.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class Recipe(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | label = Column(String(256), nullable=False) 10 | url = Column(String(256), index=True, nullable=True) 11 | source = Column(String(256), nullable=True) 12 | submitter_id = Column(Integer, ForeignKey("user.id"), nullable=True) 13 | submitter = relationship("User", back_populates="recipes") 14 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String, Column, Boolean 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.db.base_class import Base 5 | 6 | 7 | class User(Base): 8 | id = Column(Integer, primary_key=True, index=True) 9 | first_name = Column(String(256), nullable=True) 10 | surname = Column(String(256), nullable=True) 11 | email = Column(String, index=True, nullable=False) 12 | is_superuser = Column(Boolean, default=False) 13 | recipes = relationship( 14 | "Recipe", 15 | cascade="all,delete-orphan", 16 | back_populates="submitter", 17 | uselist=True, 18 | ) 19 | 20 | # New addition 21 | hashed_password = Column(String, nullable=False) 22 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .recipe import Recipe, RecipeCreate, RecipeUpdateRestricted, RecipeUpdate 2 | from .user import User, UserCreate 3 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/schemas/recipe.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | 3 | from typing import Sequence 4 | 5 | 6 | class RecipeBase(BaseModel): 7 | label: str 8 | source: str 9 | url: HttpUrl 10 | 11 | 12 | class RecipeCreate(RecipeBase): 13 | label: str 14 | source: str 15 | url: HttpUrl 16 | submitter_id: int 17 | 18 | 19 | class RecipeUpdate(RecipeBase): 20 | id: int 21 | 22 | 23 | class RecipeUpdateRestricted(BaseModel): 24 | id: int 25 | label: str 26 | 27 | 28 | # Properties shared by models stored in DB 29 | class RecipeInDBBase(RecipeBase): 30 | id: int 31 | submitter_id: int 32 | 33 | class Config: 34 | orm_mode = True 35 | 36 | 37 | # Properties to return to client 38 | class Recipe(RecipeInDBBase): 39 | pass 40 | 41 | 42 | # Properties properties stored in DB 43 | class RecipeInDB(RecipeInDBBase): 44 | pass 45 | 46 | 47 | class RecipeSearchResults(BaseModel): 48 | results: Sequence[Recipe] 49 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class UserBase(BaseModel): 7 | first_name: Optional[str] 8 | surname: Optional[str] 9 | email: Optional[EmailStr] = None 10 | is_superuser: bool = False 11 | 12 | 13 | # Properties to receive via API on creation 14 | class UserCreate(UserBase): 15 | email: EmailStr 16 | password: str 17 | 18 | 19 | # Properties to receive via API on update 20 | class UserUpdate(UserBase): 21 | ... 22 | 23 | 24 | class UserInDBBase(UserBase): 25 | id: Optional[int] = None 26 | 27 | class Config: 28 | orm_mode = True 29 | 30 | 31 | # Additional properties stored in DB but not returned by API 32 | class UserInDB(UserInDBBase): 33 | hashed_password: str 34 | 35 | 36 | # Additional properties to return via API 37 | class User(UserInDBBase): 38 | ... 39 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/backend/app/tests/api/__init__.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/app/tests/api/test_recipe.py: -------------------------------------------------------------------------------- 1 | from app.core.config import settings 2 | 3 | 4 | def test_fetch_ideas_reddit_sync(client): 5 | # When 6 | response = client.get(f"{settings.API_V1_STR}/recipes/ideas/") 7 | data = response.json() 8 | 9 | # Then 10 | assert response.status_code == 200 11 | for key in data.keys(): 12 | assert key in ["recipes", "easyrecipes", "TopSecretRecipes"] 13 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/prestart.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | from alembic.config import Config 4 | from alembic import command 5 | from app.core.config import ROOT 6 | 7 | alembic_cfg = Config(ROOT.parent / "alembic.ini") 8 | 9 | subprocess.run([sys.executable, "./app/backend_pre_start.py"]) 10 | command.upgrade(alembic_cfg, "head") 11 | subprocess.run([sys.executable, "./app/initial_data.py"]) 12 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Let the DB start 4 | python ./app/backend_pre_start.py 5 | 6 | # Run migrations 7 | alembic upgrade head 8 | 9 | # Create initial data in DB 10 | python ./app/initial_data.py -------------------------------------------------------------------------------- /part-12-react-frontend/backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "Ultimate FastAPI Tutorial Part 5" 5 | authors = ["ChristopherGS"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | uvicorn = "~0.11.3" 10 | fastapi = "~0.68.0" 11 | python-multipart = "~0.0.5" 12 | pydantic = {extras = ["email"], version = "~1.8.1"} 13 | Jinja2 = "~3.0.1" 14 | SQLAlchemy = "~1.4.3" 15 | alembic = "~1.6.5" 16 | tenacity = "~8.0.1" 17 | httpx = "~0.18.1" 18 | passlib = {extras = ["bcrypt"], version = "^1.7.2"} 19 | python-jose = {extras = ["cryptography"], version = "^3.3.0"} 20 | 21 | [tool.poetry.dev-dependencies] 22 | pytest = "^6.2.5" 23 | requests = "^2.26.0" 24 | -------------------------------------------------------------------------------- /part-12-react-frontend/backend/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export APP_MODULE=${APP_MODULE-app.main:app} 4 | export HOST=${HOST:-0.0.0.0} 5 | export PORT=${PORT:-8001} 6 | 7 | exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE" -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'node': true, 5 | 'es2021': true, 6 | }, 7 | 'extends': [ 8 | 'plugin:react/recommended', 9 | 'eslint:recommended', 10 | ], 11 | 'parserOptions': { 12 | 'ecmaFeatures': { 13 | 'jsx': true, 14 | }, 15 | 'ecmaVersion': 'latest', 16 | 'sourceType': 'module', 17 | }, 18 | 'plugins': [ 19 | 'react', 20 | ], 21 | 'rules': { 22 | 'react/jsx-uses-react': 'error', 23 | 'react/jsx-uses-vars': 'error', 24 | "react/prop-types": "off" 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/frontend/public/favicon.ico -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/frontend/public/logo192.png -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGS/ultimate-fastapi-tutorial/c8a0c69d895df57d3d77a21de817ed7ddc71f183/part-12-react-frontend/frontend/public/logo512.png -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | height: 100%; 4 | } 5 | 6 | .App-logo { 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | @media (prefers-reduced-motion: no-preference) { 12 | .App-logo { 13 | animation: App-logo-spin infinite 20s linear; 14 | } 15 | } 16 | 17 | .App-header { 18 | background-color: #282c34; 19 | min-height: 100vh; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | font-size: calc(10px + 2vmin); 25 | color: white; 26 | } 27 | 28 | .App-link { 29 | color: #61dafb; 30 | } 31 | 32 | @keyframes App-logo-spin { 33 | from { 34 | transform: rotate(0deg); 35 | } 36 | 37 | to { 38 | transform: rotate(360deg); 39 | } 40 | } -------------------------------------------------------------------------------- /part-12-react-frontend/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import {BrowserRouter, Routes, Route} from 'react-router-dom'; 4 | import Login from './pages/login'; 5 | import SignUp from './pages/sign-up'; 6 | import Home from './pages/home'; 7 | import RecipeDashboard from './pages/my-recipes'; 8 | import ErrorPage from './pages/error-page'; 9 | 10 | const App = () => { 11 | return ( 12 |
No Ideas found!
16 | )} 17 |21 | The page you’re looking for doesn’t exist. 22 |
23 | Go home 27 |No Ideas found!
16 | )} 17 |21 | The page you’re looking for doesn’t exist. 22 |
23 | Go home 27 |No Ideas found!
16 | )} 17 |