├── .gitignore ├── README.md ├── app ├── __init__.py ├── __init__test.py ├── base.py ├── config.py ├── db.py ├── fizz │ ├── __init__.py │ ├── controller.py │ ├── controller_test.py │ ├── model.py │ ├── model_test.py │ ├── schema.py │ ├── schema_test.py │ ├── service.py │ └── service_test.py ├── routes.py ├── shared │ ├── __init__.py │ └── query │ │ ├── __init__.py │ │ ├── service.py │ │ └── service_test.py ├── test │ ├── __init__.py │ └── fixtures.py └── widget │ ├── __init__.py │ ├── controller.py │ ├── controller_test.py │ ├── model.py │ ├── model_test.py │ ├── schema.py │ ├── schema_test.py │ ├── service.py │ └── service_test.py ├── docs └── site.png ├── requirements.txt ├── tasks.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | **/*.db 3 | *.pyc 4 | py3/ 5 | data/ 6 | build/ 7 | dist/ 8 | .pytest_cache/ 9 | .vscode/ 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | *.pkl 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | .idea 117 | 118 | *.pptx 119 | 120 | misc/ 121 | .Rproj.user 122 | 123 | app/app-test.db 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example of a scalable FastAPI 2 | 3 | ![The site](docs/site.png) 4 | 5 | A sample project showing how to build a scalable, maintainable, modular FastAPI with a heavy emphasis on testing. 6 | 7 | _This is an example project using the structure proposed in [this blog post](https://apryor6.github.io/2019-05-20-flask-api-example/)._, but with FastApi instead of Flask. 8 | 9 | 10 | ## Running the app 11 | 12 | Preferably, first create a virtualenv and activate it, perhaps with the following command: 13 | 14 | ``` 15 | virtualenv -p python3 venv 16 | source venv/bin/activate 17 | ``` 18 | 19 | Next, run 20 | 21 | ``` 22 | pip install -r requirements.txt 23 | ``` 24 | 25 | to get the dependencies. 26 | 27 | Next, initialize the database 28 | 29 | ``` 30 | invoke seed-db 31 | ``` 32 | 33 | Type "Y" to accept the message (which is just there to prevent you accidentally deleting things -- it's just a local SQLite database) 34 | 35 | Finally run the app with 36 | 37 | ``` 38 | python wsgi.py 39 | ``` 40 | 41 | Navigate to the posted URL in your terminal to be greeted with Swagger, where you can test out the API. 42 | 43 | 44 | 45 | 46 | ## Running tests 47 | 48 | To run the test suite, simply pip install it and run from the root directory like so 49 | 50 | ``` 51 | pip install pytest 52 | pytest 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from fastapi import FastAPI 4 | 5 | from .config import get_config 6 | from .routes import register_routes 7 | 8 | 9 | def create_app(config="dev"): 10 | settings = get_config(config=config) 11 | 12 | app = FastAPI(title="Fasterific API") 13 | 14 | register_routes(app) 15 | 16 | @app.get("/") 17 | def index(): 18 | return settings.CONFIG_NAME 19 | 20 | @app.get("/health") 21 | def health(): 22 | return {"status": "healthy"} 23 | 24 | return app 25 | -------------------------------------------------------------------------------- /app/__init__test.py: -------------------------------------------------------------------------------- 1 | from app.test.fixtures import app, client # noqa 2 | 3 | 4 | def test_app_creates(app): # noqa 5 | assert app 6 | 7 | 8 | def test_app_healthy(app, client): # noqa 9 | resp = client.get("/health") 10 | assert resp.status_code == 200 11 | assert resp.json() == {"status": "healthy"} 12 | -------------------------------------------------------------------------------- /app/base.py: -------------------------------------------------------------------------------- 1 | "Base Pydantic Schema" 2 | 3 | from pydantic import BaseModel 4 | from humps import camelize 5 | 6 | 7 | def to_camel(string): 8 | """ 9 | Func to create an alias from snake case variables 10 | """ 11 | return camelize(string) 12 | 13 | 14 | class CamelModel(BaseModel): 15 | """ 16 | Base model to auto create a camelCase alias. 17 | Also allows popualtion of Pydantic model via alias 18 | """ 19 | 20 | class Config: 21 | """Config""" 22 | 23 | alias_generator = to_camel 24 | allow_population_by_field_name = True 25 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Type 3 | 4 | from pydantic import BaseSettings 5 | 6 | basedir = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | 9 | class Settings(BaseSettings): 10 | CONFIG_NAME: str = "base" 11 | USE_MOCK_EQUIVALENCY: bool = False 12 | DEBUG: bool = False 13 | SQLALCHEMY_TRACK_MODIFICATIONS: bool = False 14 | 15 | 16 | class DevelopmentConfig(Settings): 17 | CONFIG_NAME: str = "dev" 18 | SECRET_KEY: str = os.getenv( 19 | "DEV_SECRET_KEY", "You can't see California without Marlon Widgeto's eyes" 20 | ) 21 | DEBUG: bool = True 22 | SQLALCHEMY_TRACK_MODIFICATIONS: bool = False 23 | TESTING: bool = False 24 | SQLALCHEMY_DATABASE_URL: str = "sqlite:///{0}/app-dev.db".format(basedir) 25 | 26 | 27 | class TestingConfig(Settings): 28 | CONFIG_NAME: str = "test" 29 | SECRET_KEY: str = os.getenv("TEST_SECRET_KEY", "Thanos did nothing wrong") 30 | DEBUG: bool = True 31 | SQLALCHEMY_TRACK_MODIFICATIONS: bool = False 32 | TESTING: bool = True 33 | SQLALCHEMY_DATABASE_URL: str = "sqlite:///{0}/app-test.db".format(basedir) 34 | 35 | 36 | class ProductionConfig(Settings): 37 | CONFIG_NAME: str = "prod" 38 | SECRET_KEY: str = os.getenv("PROD_SECRET_KEY", "I'm Ron Burgundy?") 39 | DEBUG: bool = False 40 | SQLALCHEMY_TRACK_MODIFICATIONS: bool = False 41 | TESTING: bool = False 42 | SQLALCHEMY_DATABASE_URL: str = "sqlite:///{0}/app-prod.db".format(basedir) 43 | 44 | 45 | def get_config(config): 46 | return config_by_name[config] 47 | 48 | 49 | EXPORT_CONFIGS: List[Type[Settings]] = [ 50 | DevelopmentConfig, 51 | TestingConfig, 52 | ProductionConfig, 53 | ] 54 | config_by_name = {cfg().CONFIG_NAME: cfg() for cfg in EXPORT_CONFIGS} 55 | -------------------------------------------------------------------------------- /app/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sqlalchemy import create_engine, MetaData 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import scoped_session, sessionmaker 6 | 7 | from .config import get_config 8 | 9 | settings = get_config(os.getenv("ENV") or "test") 10 | 11 | SQLALCHEMY_DATABASE_URL = settings.SQLALCHEMY_DATABASE_URL 12 | 13 | engine = create_engine( 14 | SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} 15 | ) 16 | db_session = scoped_session( 17 | sessionmaker(autocommit=False, autoflush=False, bind=engine) 18 | ) 19 | 20 | Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) 21 | Base = declarative_base() 22 | Base.metadata.bind = engine 23 | 24 | 25 | def get_db(): 26 | """ 27 | Generator function for dependency injection to fetch a new sesesion on a new request 28 | """ 29 | db = Session() 30 | try: 31 | yield db 32 | finally: 33 | db.close() 34 | 35 | -------------------------------------------------------------------------------- /app/fizz/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Fizz # noqa 2 | from .schema import FizzSchema # noqa 3 | 4 | BASE_ROUTE = "fizz" 5 | 6 | 7 | def register_routes(app, root="api"): 8 | from .controller import router as fizz_router 9 | 10 | app.include_router(fizz_router, prefix=f"/{root}/{BASE_ROUTE}") 11 | -------------------------------------------------------------------------------- /app/fizz/controller.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import Depends, APIRouter 4 | 5 | from app.db import Session, get_db 6 | from .schema import FizzSchema 7 | from .service import FizzService 8 | from .model import Fizz 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.get("/", response_model=List[FizzSchema]) 14 | async def get_fizz(session: Session = Depends(get_db)) -> List[Fizz]: 15 | """Get all Fizzs""" 16 | return await FizzService.get_all(session) 17 | 18 | 19 | @router.post( 20 | "/", response_model=FizzSchema, 21 | ) 22 | async def post_fizz( 23 | fizz: FizzSchema, session: Session = Depends(get_db), 24 | ) -> FizzSchema: 25 | """Create a new Fizz""" 26 | return await FizzService.create(fizz, session) 27 | 28 | 29 | @router.get("/", response_model=FizzSchema) 30 | async def get_fizz_by_id(fizz_id: int, session: Session = Depends(get_db)) -> Fizz: 31 | """Get Single Fizz""" 32 | 33 | return await FizzService.get_by_id(fizz_id, session) 34 | 35 | 36 | @router.delete("/") 37 | async def delete(fizz_id: int, session: Session = Depends(get_db)): 38 | """Delete Single Fizz""" 39 | _id = await FizzService.delete_by_id(fizz_id, session) 40 | return dict(status="Success", id=_id) 41 | 42 | 43 | @router.put("/") 44 | async def put(fizz: FizzSchema, session: Session = Depends(get_db)) -> FizzSchema: 45 | """Update Single Fizz""" 46 | cur_fizz = session.query(Fizz).get(fizz.fizz_id) 47 | return await FizzService.update(cur_fizz, fizz, session) 48 | -------------------------------------------------------------------------------- /app/fizz/controller_test.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | 6 | from app.test.fixtures import client, app # noqa 7 | from .service import FizzService 8 | from .schema import FizzSchema 9 | from .model import Fizz 10 | from . import BASE_ROUTE 11 | 12 | 13 | def make_fizz( 14 | id: int = 123, name: str = "Test fizz", purpose: str = "Test purpose" 15 | ) -> FizzSchema: 16 | return FizzSchema(fizz_id=id, name=name, purpose=purpose) 17 | 18 | 19 | async def fake_get_fizzs(session) -> List[FizzSchema]: 20 | return [ 21 | make_fizz(123, name="Test Fizz 1"), 22 | make_fizz(456, name="Test Fizz 2"), 23 | ] 24 | 25 | 26 | @patch.object(FizzService, "get_all", fake_get_fizzs) 27 | def test_get_fizz(client): # noqa 28 | results = [FizzSchema(**i) for i in client.get(f"/api/{BASE_ROUTE}").json()] 29 | print(results) 30 | expected: List[FizzSchema] = [ 31 | make_fizz(123, name="Test Fizz 1"), 32 | make_fizz(456, name="Test Fizz 2"), 33 | ] 34 | 35 | for r in results: 36 | assert r in expected 37 | 38 | -------------------------------------------------------------------------------- /app/fizz/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app.db import Base 3 | 4 | from .schema import FizzSchema 5 | 6 | 7 | class Fizz(Base): # type: ignore 8 | """A snazzy Fizz""" 9 | 10 | __tablename__ = "fizz" 11 | 12 | fizz_id = Column(Integer(), primary_key=True) 13 | name = Column(String(255)) 14 | purpose = Column(String(255)) 15 | 16 | def update(self, changes: FizzSchema): 17 | for key, val in changes.dict().items(): 18 | setattr(self, key, val) 19 | return self 20 | -------------------------------------------------------------------------------- /app/fizz/model_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from app.test.fixtures import app, session # noqa 3 | from app.db import Session 4 | from .model import Fizz 5 | from .schema import FizzSchema 6 | 7 | 8 | @fixture 9 | def fizz() -> FizzSchema: 10 | fizz = FizzSchema(fizz_id=1, name="Test fizz", purpose="Test purpose") 11 | return Fizz(**fizz.dict()) 12 | 13 | 14 | def test_Fizz_create(fizz: FizzSchema): 15 | assert fizz 16 | 17 | 18 | def test_Fizz_retrieve(fizz: Fizz, session: Session): # noqa 19 | session.add(fizz) 20 | session.commit() 21 | s = session.query(Fizz).first() 22 | assert s.__dict__ == fizz.__dict__ 23 | -------------------------------------------------------------------------------- /app/fizz/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from app.base import CamelModel 4 | 5 | 6 | class FizzSchema(CamelModel): 7 | """Fizz schema""" 8 | 9 | fizz_id: Optional[int] 10 | name: str 11 | purpose: str 12 | -------------------------------------------------------------------------------- /app/fizz/schema_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import Fizz 4 | from .schema import FizzSchema 5 | 6 | 7 | def test_FizzSchema_works(): 8 | fizz: FizzSchema = FizzSchema( 9 | **{"fizz_id": 123, "name": "Test fizz", "purpose": "Test purpose"} 10 | ) 11 | 12 | assert fizz.fizz_id == 123 13 | assert fizz.name == "Test fizz" 14 | assert fizz.purpose == "Test purpose" 15 | -------------------------------------------------------------------------------- /app/fizz/service.py: -------------------------------------------------------------------------------- 1 | from app.db import get_db, Session 2 | from typing import List 3 | 4 | from fastapi import Depends 5 | 6 | from .model import Fizz 7 | from .schema import FizzSchema 8 | 9 | 10 | class FizzService: 11 | @staticmethod 12 | async def get_all(session: Session) -> List[FizzSchema]: 13 | resp = session.query(Fizz).all() 14 | return [FizzSchema(**i.__dict__) for i in resp] 15 | 16 | @staticmethod 17 | async def get_by_id(fizz_id: int, session: Session) -> FizzSchema: 18 | resp = session.query(Fizz).get(fizz_id) 19 | return FizzSchema(**resp.__dict__) 20 | 21 | @staticmethod 22 | async def update(fizz: Fizz, updates: FizzSchema, session: Session,) -> FizzSchema: 23 | fizz.update(updates) 24 | session.commit() 25 | session.refresh(fizz) 26 | return fizz 27 | 28 | @staticmethod 29 | async def delete_by_id(fizz_id: int, session: Session) -> List[int]: 30 | fizz = session.query(Fizz).filter(Fizz.fizz_id == fizz_id).first() 31 | if not fizz: 32 | return [] 33 | session.delete(fizz) 34 | session.commit() 35 | return [fizz_id] 36 | 37 | @staticmethod 38 | async def create(new_attrs: FizzSchema, session: Session) -> FizzSchema: 39 | new_fizz = Fizz(**new_attrs.dict()) 40 | 41 | session.add(new_fizz) 42 | session.commit() 43 | session.refresh(new_fizz) 44 | return FizzSchema(**new_fizz.__dict__) 45 | 46 | -------------------------------------------------------------------------------- /app/fizz/service_test.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from app.test.fixtures import app, session # noqa 6 | from app.db import Session 7 | from .model import Fizz 8 | from .service import FizzService # noqa 9 | from .schema import FizzSchema 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_get_all(session: Session): # noqa 14 | yin: FizzSchema = FizzSchema(fizz_id=1, name="Yin", purpose="thing 1") 15 | yang: FizzSchema = FizzSchema(fizz_id=2, name="Yang", purpose="thing 2") 16 | session.add(Fizz(**yin.dict())) 17 | session.add(Fizz(**yang.dict())) 18 | session.commit() 19 | 20 | results: List[FizzSchema] = await FizzService.get_all(session) 21 | 22 | assert len(results) == 2 23 | assert yin in results and yang in results 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_update(session: Session): # noqa 28 | yin: FizzSchema = FizzSchema(fizz_id=1, name="Yin", purpose="thing 1") 29 | yin_orm = Fizz(**yin.dict()) 30 | session.add(yin_orm) 31 | updates: FizzSchema = FizzSchema(fizz_id=1, name="New Fizz name", purpose="thing 1") 32 | 33 | await FizzService.update(yin_orm, updates, session) 34 | result: FizzSchema = session.query(Fizz).get(yin.fizz_id) 35 | assert result.name == "New Fizz name" 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_delete_by_id(session: Session): # noqa 40 | yin: FizzSchema = FizzSchema(fizz_id=1, name="Yin", purpose="thing 1") 41 | yang: FizzSchema = FizzSchema(fizz_id=2, name="Yang", purpose="thing 2") 42 | 43 | yin_orm = Fizz(**yin.dict()) 44 | yang_orm = Fizz(**yang.dict()) 45 | 46 | session.add(yin_orm) 47 | session.add(yang_orm) 48 | session.commit() 49 | 50 | await FizzService.delete_by_id(1, session) 51 | session.commit() 52 | 53 | results: List[Fizz] = session.query(Fizz).all() 54 | 55 | assert len(results) == 1 56 | assert yin_orm not in results and yang_orm in results 57 | 58 | 59 | @pytest.mark.asyncio 60 | async def test_create(session: Session): # noqa 61 | 62 | yin: FizzSchema = FizzSchema(fizz_id=1, name="Yin", purpose="thing 1") 63 | await FizzService.create(yin, session) 64 | results: List[Fizz] = session.query(Fizz).all() 65 | 66 | assert len(results) == 1 67 | 68 | for k in yin.dict().keys(): 69 | assert getattr(results[0], k) == getattr(yin, k) 70 | -------------------------------------------------------------------------------- /app/routes.py: -------------------------------------------------------------------------------- 1 | def register_routes(app, root="api"): 2 | from app.widget import register_routes as attach_widget 3 | from app.fizz import register_routes as attach_fizz 4 | 5 | # Add routes 6 | attach_widget(app) 7 | attach_fizz(app) 8 | -------------------------------------------------------------------------------- /app/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/fastapi_example/14368b74440e72b073360c6e05db30f0e337957a/app/shared/__init__.py -------------------------------------------------------------------------------- /app/shared/query/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/fastapi_example/14368b74440e72b073360c6e05db30f0e337957a/app/shared/query/__init__.py -------------------------------------------------------------------------------- /app/shared/query/service.py: -------------------------------------------------------------------------------- 1 | class QueryService: 2 | """An example of a service that is shared""" 3 | 4 | @staticmethod 5 | def execute(query): 6 | return "Success" 7 | -------------------------------------------------------------------------------- /app/shared/query/service_test.py: -------------------------------------------------------------------------------- 1 | from .service import QueryService 2 | 3 | 4 | def test_execute(): 5 | result = QueryService.execute("a complicated query") 6 | 7 | assert result == "Success" 8 | -------------------------------------------------------------------------------- /app/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/fastapi_example/14368b74440e72b073360c6e05db30f0e337957a/app/test/__init__.py -------------------------------------------------------------------------------- /app/test/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from app import create_app 4 | from app.db import Base 5 | from fastapi.testclient import TestClient 6 | 7 | 8 | @pytest.fixture 9 | def app(): 10 | return create_app("test") 11 | 12 | 13 | @pytest.fixture 14 | def client(app): 15 | client = TestClient(app) 16 | return client 17 | 18 | 19 | @pytest.fixture 20 | def session(app): 21 | from app.db import get_db 22 | 23 | session = next(get_db()) 24 | Base.metadata.drop_all() 25 | Base.metadata.create_all() 26 | yield session 27 | Base.metadata.drop_all() 28 | session.commit() 29 | -------------------------------------------------------------------------------- /app/widget/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Widget # noqa 2 | from .schema import WidgetSchema # noqa 3 | 4 | BASE_ROUTE = "widget" 5 | 6 | 7 | def register_routes(app, root="api"): 8 | from .controller import router as widget_router 9 | 10 | app.include_router(widget_router, prefix=f"/{root}/{BASE_ROUTE}") 11 | -------------------------------------------------------------------------------- /app/widget/controller.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import Depends, APIRouter 4 | 5 | from app.db import Session, get_db 6 | from .schema import WidgetSchema 7 | from .service import WidgetService 8 | from .model import Widget 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.get("/", response_model=List[WidgetSchema]) 14 | async def get_widget(session: Session = Depends(get_db)) -> List[Widget]: 15 | """Get all Widgets""" 16 | return await WidgetService.get_all(session) 17 | 18 | 19 | @router.post( 20 | "/", response_model=WidgetSchema, 21 | ) 22 | async def post_widget( 23 | widget: WidgetSchema, session: Session = Depends(get_db), 24 | ) -> WidgetSchema: 25 | """Create a new Widget""" 26 | return await WidgetService.create(widget, session) 27 | 28 | 29 | @router.get("/", response_model=WidgetSchema) 30 | async def get_widget_by_id( 31 | widget_id: int, session: Session = Depends(get_db) 32 | ) -> Widget: 33 | """Get Single Widget""" 34 | 35 | return await WidgetService.get_by_id(widget_id, session) 36 | 37 | 38 | @router.delete("/") 39 | async def delete(widget_id: int, session: Session = Depends(get_db)): 40 | """Delete Single Widget""" 41 | _id = await WidgetService.delete_by_id(widget_id, session) 42 | return dict(status="Success", id=_id) 43 | 44 | 45 | @router.put("/") 46 | async def put(widget: WidgetSchema, session: Session = Depends(get_db)) -> WidgetSchema: 47 | """Update Single Widget""" 48 | cur_widget = session.query(Widget).get(widget.widget_id) 49 | return await WidgetService.update(cur_widget, widget, session) 50 | -------------------------------------------------------------------------------- /app/widget/controller_test.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | 6 | from app.test.fixtures import client, app # noqa 7 | from .service import WidgetService 8 | from .schema import WidgetSchema 9 | from .model import Widget 10 | from . import BASE_ROUTE 11 | 12 | 13 | def make_widget( 14 | id: int = 123, name: str = "Test widget", purpose: str = "Test purpose" 15 | ) -> WidgetSchema: 16 | return WidgetSchema(widget_id=id, name=name, purpose=purpose) 17 | 18 | 19 | async def fake_get_widgets(session) -> List[WidgetSchema]: 20 | return [ 21 | make_widget(123, name="Test Widget 1"), 22 | make_widget(456, name="Test Widget 2"), 23 | ] 24 | 25 | 26 | @patch.object(WidgetService, "get_all", fake_get_widgets) 27 | def test_get_widget(client): # noqa 28 | results = [WidgetSchema(**i) for i in client.get(f"/api/{BASE_ROUTE}").json()] 29 | print(results) 30 | expected: List[WidgetSchema] = [ 31 | make_widget(123, name="Test Widget 1"), 32 | make_widget(456, name="Test Widget 2"), 33 | ] 34 | 35 | for r in results: 36 | assert r in expected 37 | 38 | -------------------------------------------------------------------------------- /app/widget/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app.db import Base 3 | 4 | from .schema import WidgetSchema 5 | 6 | 7 | class Widget(Base): # type: ignore 8 | """A snazzy Widget""" 9 | 10 | __tablename__ = "widget" 11 | 12 | widget_id = Column(Integer(), primary_key=True) 13 | name = Column(String(255)) 14 | purpose = Column(String(255)) 15 | 16 | def update(self, changes: WidgetSchema): 17 | for key, val in changes.dict().items(): 18 | setattr(self, key, val) 19 | return self 20 | -------------------------------------------------------------------------------- /app/widget/model_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from app.test.fixtures import app, session # noqa 3 | from app.db import Session 4 | from .model import Widget 5 | from .schema import WidgetSchema 6 | 7 | 8 | @fixture 9 | def widget() -> WidgetSchema: 10 | widget = WidgetSchema(widget_id=1, name="Test widget", purpose="Test purpose") 11 | return Widget(**widget.dict()) 12 | 13 | 14 | def test_Widget_create(widget: WidgetSchema): 15 | assert widget 16 | 17 | 18 | def test_Widget_retrieve(widget: Widget, session: Session): # noqa 19 | session.add(widget) 20 | session.commit() 21 | s = session.query(Widget).first() 22 | assert s.__dict__ == widget.__dict__ 23 | -------------------------------------------------------------------------------- /app/widget/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from app.base import CamelModel 4 | 5 | 6 | class WidgetSchema(CamelModel): 7 | """Widget schema""" 8 | 9 | widget_id: Optional[int] 10 | name: str 11 | purpose: str 12 | -------------------------------------------------------------------------------- /app/widget/schema_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import Widget 4 | from .schema import WidgetSchema 5 | 6 | 7 | def test_WidgetSchema_works(): 8 | widget: WidgetSchema = WidgetSchema( 9 | **{"widget_id": 123, "name": "Test widget", "purpose": "Test purpose"} 10 | ) 11 | 12 | assert widget.widget_id == 123 13 | assert widget.name == "Test widget" 14 | assert widget.purpose == "Test purpose" 15 | -------------------------------------------------------------------------------- /app/widget/service.py: -------------------------------------------------------------------------------- 1 | from app.db import get_db, Session 2 | from typing import List 3 | 4 | from fastapi import Depends 5 | 6 | from .model import Widget 7 | from .schema import WidgetSchema 8 | 9 | 10 | class WidgetService: 11 | @staticmethod 12 | async def get_all(session: Session) -> List[WidgetSchema]: 13 | resp = session.query(Widget).all() 14 | return [WidgetSchema(**i.__dict__) for i in resp] 15 | 16 | @staticmethod 17 | async def get_by_id(widget_id: int, session: Session) -> WidgetSchema: 18 | resp = session.query(Widget).get(widget_id) 19 | return WidgetSchema(**resp.__dict__) 20 | 21 | @staticmethod 22 | async def update( 23 | widget: Widget, updates: WidgetSchema, session: Session, 24 | ) -> WidgetSchema: 25 | widget.update(updates) 26 | session.commit() 27 | session.refresh(widget) 28 | return widget 29 | 30 | @staticmethod 31 | async def delete_by_id(widget_id: int, session: Session) -> List[int]: 32 | widget = session.query(Widget).filter(Widget.widget_id == widget_id).first() 33 | if not widget: 34 | return [] 35 | session.delete(widget) 36 | session.commit() 37 | return [widget_id] 38 | 39 | @staticmethod 40 | async def create(new_attrs: WidgetSchema, session: Session) -> WidgetSchema: 41 | new_widget = Widget(**new_attrs.dict()) 42 | 43 | session.add(new_widget) 44 | session.commit() 45 | session.refresh(new_widget) 46 | return WidgetSchema(**new_widget.__dict__) 47 | 48 | -------------------------------------------------------------------------------- /app/widget/service_test.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from app.test.fixtures import app, session # noqa 6 | from app.db import Session 7 | from .model import Widget 8 | from .service import WidgetService # noqa 9 | from .schema import WidgetSchema 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_get_all(session: Session): # noqa 14 | yin: WidgetSchema = WidgetSchema(widget_id=1, name="Yin", purpose="thing 1") 15 | yang: WidgetSchema = WidgetSchema(widget_id=2, name="Yang", purpose="thing 2") 16 | session.add(Widget(**yin.dict())) 17 | session.add(Widget(**yang.dict())) 18 | session.commit() 19 | 20 | results: List[WidgetSchema] = await WidgetService.get_all(session) 21 | 22 | assert len(results) == 2 23 | assert yin in results and yang in results 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_update(session: Session): # noqa 28 | yin: WidgetSchema = WidgetSchema(widget_id=1, name="Yin", purpose="thing 1") 29 | yin_orm = Widget(**yin.dict()) 30 | session.add(yin_orm) 31 | updates: WidgetSchema = WidgetSchema( 32 | widget_id=1, name="New Widget name", purpose="thing 1" 33 | ) 34 | 35 | await WidgetService.update(yin_orm, updates, session) 36 | result: WidgetSchema = session.query(Widget).get(yin.widget_id) 37 | assert result.name == "New Widget name" 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_delete_by_id(session: Session): # noqa 42 | yin: WidgetSchema = WidgetSchema(widget_id=1, name="Yin", purpose="thing 1") 43 | yang: WidgetSchema = WidgetSchema(widget_id=2, name="Yang", purpose="thing 2") 44 | 45 | yin_orm = Widget(**yin.dict()) 46 | yang_orm = Widget(**yang.dict()) 47 | 48 | session.add(yin_orm) 49 | session.add(yang_orm) 50 | session.commit() 51 | 52 | await WidgetService.delete_by_id(1, session) 53 | session.commit() 54 | 55 | results: List[Widget] = session.query(Widget).all() 56 | 57 | assert len(results) == 1 58 | assert yin_orm not in results and yang_orm in results 59 | 60 | 61 | @pytest.mark.asyncio 62 | async def test_create(session: Session): # noqa 63 | 64 | yin: WidgetSchema = WidgetSchema(widget_id=1, name="Yin", purpose="thing 1") 65 | await WidgetService.create(yin, session) 66 | results: List[Widget] = session.query(Widget).all() 67 | 68 | assert len(results) == 1 69 | 70 | for k in yin.dict().keys(): 71 | assert getattr(results[0], k) == getattr(yin, k) 72 | -------------------------------------------------------------------------------- /docs/site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/fastapi_example/14368b74440e72b073360c6e05db30f0e337957a/docs/site.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | astroid==2.4.2 3 | attrs==19.3.0 4 | black==19.10b0 5 | certifi==2020.6.20 6 | chardet==3.0.4 7 | click==7.1.2 8 | fastapi==0.58.1 9 | Flask==1.1.2 10 | h11==0.9.0 11 | httptools==0.1.1 12 | idna==2.10 13 | importlib-metadata==1.7.0 14 | invoke==1.4.1 15 | isort==4.3.21 16 | itsdangerous==1.1.0 17 | Jinja2==2.11.3 18 | lazy-object-proxy==1.4.3 19 | MarkupSafe==1.1.1 20 | mccabe==0.6.1 21 | more-itertools==8.4.0 22 | mypy==0.782 23 | mypy-extensions==0.4.3 24 | packaging==20.4 25 | pathspec==0.8.0 26 | pluggy==0.13.1 27 | py==1.10.0 28 | pydantic==1.6.2 29 | pyhumps==1.3.1 30 | pylint==2.5.3 31 | pyparsing==2.4.7 32 | pytest==5.4.3 33 | pytest-asyncio==0.14.0 34 | regex==2020.6.8 35 | requests==2.24.0 36 | six==1.15.0 37 | SQLAlchemy==1.3.18 38 | starlette==0.13.4 39 | toml==0.10.1 40 | typed-ast==1.4.1 41 | typing-extensions==3.7.4.2 42 | urllib3==1.26.5 43 | uvicorn==0.11.7 44 | uvloop==0.14.0 45 | wcwidth==0.2.5 46 | websockets==8.1 47 | Werkzeug==1.0.1 48 | wrapt==1.12.1 49 | zipp==3.1.0 50 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from invoke import task 4 | 5 | from app.widget.model import Widget 6 | from app.fizz.model import Fizz 7 | 8 | from app.db import get_db, Base, engine 9 | from wsgi import app 10 | 11 | 12 | @task 13 | def init_db(ctx): 14 | print("Creating all resources.") 15 | 16 | Base.metadata.create_all() 17 | engine.execute("insert into widget values (1, 'hey', 'there');") 18 | print(engine.execute("select * from widget;")) 19 | 20 | 21 | @task 22 | def drop_all(ctx): 23 | if input("Are you sure you want to drop all tables? (y/N)\n").lower() == "y": 24 | print("Dropping tables...") 25 | Base.metadata.drop_all() 26 | 27 | 28 | def seed_things(): 29 | classes = [Widget, Fizz] 30 | for klass in classes: 31 | seed_thing(klass) 32 | 33 | 34 | def seed_thing(cls): 35 | session = next(get_db()) 36 | things = [ 37 | {"name": "Pizza Slicer", "purpose": "Cut delicious pizza"}, 38 | {"name": "Rolling Pin", "purpose": "Roll delicious pizza"}, 39 | {"name": "Pizza Oven", "purpose": "Bake delicious pizza"}, 40 | ] 41 | session.bulk_insert_mappings(cls, things) 42 | session.commit() 43 | 44 | 45 | @task 46 | def seed_db(ctx): 47 | if ( 48 | input("Are you sure you want to drop all tables and recreate? (y/N)\n").lower() 49 | == "y" 50 | ): 51 | print("Dropping tables...") 52 | Base.metadata.drop_all() 53 | Base.metadata.create_all() 54 | seed_things() 55 | print("DB successfully seeded.") 56 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from app import create_app 4 | 5 | app = create_app(config=os.getenv("ENV") or "test") 6 | 7 | --------------------------------------------------------------------------------