├── .gitignore ├── README.md ├── app ├── __init__.py ├── __init__test.py ├── config.py ├── fizz │ ├── __init__.py │ ├── fizzbar │ │ ├── __init__.py │ │ ├── controller.py │ │ ├── controller_test.py │ │ ├── interface.py │ │ ├── interface_test.py │ │ ├── model.py │ │ ├── model_test.py │ │ ├── schema.py │ │ ├── schema_test.py │ │ ├── service.py │ │ └── service_test.py │ └── fizzbaz │ │ ├── __init__.py │ │ ├── controller.py │ │ ├── controller_test.py │ │ ├── interface.py │ │ ├── interface_test.py │ │ ├── model.py │ │ ├── model_test.py │ │ ├── schema.py │ │ ├── schema_test.py │ │ ├── service.py │ │ └── service_test.py ├── other_api │ ├── __init__.py │ ├── doodad │ │ ├── __init__.py │ │ ├── controller.py │ │ ├── controller_test.py │ │ ├── interface.py │ │ ├── interface_test.py │ │ ├── model.py │ │ ├── model_test.py │ │ ├── schema.py │ │ ├── schema_test.py │ │ ├── service.py │ │ └── service_test.py │ └── whatsit │ │ ├── __init__.py │ │ ├── controller.py │ │ ├── controller_test.py │ │ ├── interface.py │ │ ├── interface_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 ├── third_party │ ├── __init__.py │ └── app │ │ ├── __init__.py │ │ ├── app-test.db │ │ ├── config.py │ │ └── routes.py └── widget │ ├── __init__.py │ ├── controller.py │ ├── controller_test.py │ ├── interface.py │ ├── interface_test.py │ ├── model.py │ ├── model_test.py │ ├── schema.py │ ├── schema_test.py │ ├── service.py │ └── service_test.py ├── commands ├── __init__.py └── seed_command.py ├── docs └── site.png ├── manage.py ├── requirements.txt └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | py3/ 4 | data/ 5 | build/ 6 | dist/ 7 | .pytest_cache/ 8 | .vscode/ 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | *.pkl 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | .idea 116 | 117 | *.pptx 118 | 119 | misc/ 120 | .Rproj.user 121 | 122 | app/app-test.db 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example of a scalable Flask API 2 | 3 | ![The site](docs/site.png) 4 | 5 | A sample project showing how to build a scalable, maintainable, modular Flask API 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/)._ 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 | python manage.py 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 | from flask import Flask, jsonify 2 | from flask_sqlalchemy import SQLAlchemy 3 | from flask_restx import Api 4 | 5 | db = SQLAlchemy() 6 | 7 | 8 | def create_app(env=None): 9 | from app.config import config_by_name 10 | from app.routes import register_routes 11 | 12 | app = Flask(__name__) 13 | app.config.from_object(config_by_name[env or "test"]) 14 | api = Api(app, title="Flaskerific API", version="0.1.0") 15 | 16 | register_routes(api, app) 17 | db.init_app(app) 18 | 19 | @app.route("/health") 20 | def health(): 21 | return jsonify("healthy") 22 | 23 | return app 24 | -------------------------------------------------------------------------------- /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 | with client: 10 | resp = client.get("/health") 11 | assert resp.status_code == 200 12 | assert resp.is_json 13 | assert resp.json == "healthy" 14 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Type 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class BaseConfig: 8 | CONFIG_NAME = "base" 9 | USE_MOCK_EQUIVALENCY = False 10 | DEBUG = False 11 | SQLALCHEMY_TRACK_MODIFICATIONS = False 12 | 13 | 14 | class DevelopmentConfig(BaseConfig): 15 | CONFIG_NAME = "dev" 16 | SECRET_KEY = os.getenv( 17 | "DEV_SECRET_KEY", "You can't see California without Marlon Widgeto's eyes" 18 | ) 19 | DEBUG = True 20 | SQLALCHEMY_TRACK_MODIFICATIONS = False 21 | TESTING = False 22 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-dev.db".format(basedir) 23 | 24 | 25 | class TestingConfig(BaseConfig): 26 | CONFIG_NAME = "test" 27 | SECRET_KEY = os.getenv("TEST_SECRET_KEY", "Thanos did nothing wrong") 28 | DEBUG = True 29 | SQLALCHEMY_TRACK_MODIFICATIONS = False 30 | TESTING = True 31 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-test.db".format(basedir) 32 | 33 | 34 | class ProductionConfig(BaseConfig): 35 | CONFIG_NAME = "prod" 36 | SECRET_KEY = os.getenv("PROD_SECRET_KEY", "I'm Ron Burgundy?") 37 | DEBUG = False 38 | SQLALCHEMY_TRACK_MODIFICATIONS = False 39 | TESTING = False 40 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-prod.db".format(basedir) 41 | 42 | 43 | EXPORT_CONFIGS: List[Type[BaseConfig]] = [ 44 | DevelopmentConfig, 45 | TestingConfig, 46 | ProductionConfig, 47 | ] 48 | config_by_name = {cfg.CONFIG_NAME: cfg for cfg in EXPORT_CONFIGS} 49 | -------------------------------------------------------------------------------- /app/fizz/__init__.py: -------------------------------------------------------------------------------- 1 | BASE_ROUTE = "fizz" 2 | 3 | 4 | def register_routes(api, app, root="api"): 5 | from .fizzbar.controller import api as fizzbar_api 6 | from .fizzbaz.controller import api as fizzbaz_api 7 | 8 | api.add_namespace(fizzbar_api, path=f"/{root}/{BASE_ROUTE}/fizzbar") 9 | api.add_namespace(fizzbaz_api, path=f"/{root}/{BASE_ROUTE}/fizzbaz") 10 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Fizzbar # noqa 2 | from .schema import FizzbarSchema # noqa 3 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_accepts import accepts, responds 3 | from flask_restx import Namespace, Resource 4 | from flask.wrappers import Response 5 | from typing import List 6 | 7 | from .schema import FizzbarSchema 8 | from .service import FizzbarService 9 | from .model import Fizzbar 10 | from .interface import FizzbarInterface 11 | 12 | api = Namespace("Fizzbar", description="A modular namespace within fizz") # noqa 13 | 14 | 15 | @api.route("/") 16 | class FizzbarResource(Resource): 17 | """Fizzbars""" 18 | 19 | @responds(schema=FizzbarSchema, many=True) 20 | def get(self) -> List[Fizzbar]: 21 | """Get all Fizzbars""" 22 | 23 | return FizzbarService.get_all() 24 | 25 | @accepts(schema=FizzbarSchema, api=api) 26 | @responds(schema=FizzbarSchema) 27 | def post(self) -> Fizzbar: 28 | """Create a Single Fizzbar""" 29 | 30 | return FizzbarService.create(request.parsed_obj) 31 | 32 | 33 | @api.route("/") 34 | @api.param("fizzbarId", "Fizzbar database ID") 35 | class FizzbarIdResource(Resource): 36 | @responds(schema=FizzbarSchema) 37 | def get(self, fizzbarId: int) -> Fizzbar: 38 | """Get Single Fizzbar""" 39 | 40 | return FizzbarService.get_by_id(fizzbarId) 41 | 42 | def delete(self, fizzbarId: int) -> Response: 43 | """Delete Single Fizzbar""" 44 | from flask import jsonify 45 | 46 | print("fizzbarId = ", fizzbarId) 47 | id = FizzbarService.delete_by_id(fizzbarId) 48 | return jsonify(dict(status="Success", id=id)) 49 | 50 | @accepts(schema=FizzbarSchema, api=api) 51 | @responds(schema=FizzbarSchema) 52 | def put(self, fizzbarId: int) -> Fizzbar: 53 | """Update Single Fizzbar""" 54 | 55 | changes: FizzbarInterface = request.parsed_obj 56 | Fizzbar = FizzbarService.get_by_id(fizzbarId) 57 | return FizzbarService.update(Fizzbar, changes) 58 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/controller_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | from flask.testing import FlaskClient 3 | 4 | from app.test.fixtures import client, app # noqa 5 | from .service import FizzbarService 6 | from .schema import FizzbarSchema 7 | from .model import Fizzbar 8 | from .interface import FizzbarInterface 9 | from .. import BASE_ROUTE 10 | 11 | 12 | def make_fizzbar( 13 | id: int = 123, name: str = "Test fizzbar", purpose: str = "Test purpose" 14 | ) -> Fizzbar: 15 | return Fizzbar(fizzbar_id=id, name=name, purpose=purpose) 16 | 17 | 18 | class TestFizzbarResource: 19 | @patch.object( 20 | FizzbarService, 21 | "get_all", 22 | lambda: [ 23 | make_fizzbar(123, name="Test Fizzbar 1"), 24 | make_fizzbar(456, name="Test Fizzbar 2"), 25 | ], 26 | ) 27 | def test_get(self, client: FlaskClient): # noqa 28 | with client: 29 | results = client.get( 30 | f"/api/{BASE_ROUTE}/fizzbar", follow_redirects=True 31 | ).get_json() 32 | expected = ( 33 | FizzbarSchema(many=True) 34 | .dump( 35 | [ 36 | make_fizzbar(123, name="Test Fizzbar 1"), 37 | make_fizzbar(456, name="Test Fizzbar 2"), 38 | ] 39 | ) 40 | 41 | ) 42 | for r in results: 43 | assert r in expected 44 | 45 | @patch.object( 46 | FizzbarService, "create", lambda create_request: Fizzbar(**create_request) 47 | ) 48 | def test_post(self, client: FlaskClient): # noqa 49 | with client: 50 | 51 | payload = dict(name="Test fizzbar", purpose="Test purpose") 52 | result = client.post(f"/api/{BASE_ROUTE}/fizzbar/", json=payload).get_json() 53 | expected = ( 54 | FizzbarSchema() 55 | .dump(Fizzbar(name=payload["name"], purpose=payload["purpose"])) 56 | 57 | ) 58 | assert result == expected 59 | 60 | 61 | def fake_update(fizzbar: Fizzbar, changes: FizzbarInterface) -> Fizzbar: 62 | # To fake an update, just return a new object 63 | updated_Fizzbar = Fizzbar( 64 | fizzbar_id=fizzbar.fizzbar_id, name=changes["name"], purpose=changes["purpose"] 65 | ) 66 | return updated_Fizzbar 67 | 68 | 69 | class TestFizzbarIdResource: 70 | @patch.object(FizzbarService, "get_by_id", lambda id: make_fizzbar(id=id)) 71 | def test_get(self, client: FlaskClient): # noqa 72 | with client: 73 | result = client.get(f"/api/{BASE_ROUTE}/fizzbar/123").get_json() 74 | expected = Fizzbar(fizzbar_id=123) 75 | assert result["fizzbarId"] == expected.fizzbar_id 76 | 77 | @patch.object(FizzbarService, "delete_by_id", lambda id: [id]) 78 | def test_delete(self, client: FlaskClient): # noqa 79 | with client: 80 | result = client.delete(f"/api/{BASE_ROUTE}/fizzbar/123").get_json() 81 | expected = dict(status="Success", id=[123]) 82 | assert result == expected 83 | 84 | @patch.object(FizzbarService, "get_by_id", lambda id: make_fizzbar(id=id)) 85 | @patch.object(FizzbarService, "update", fake_update) 86 | def test_put(self, client: FlaskClient): # noqa 87 | with client: 88 | result = client.put( 89 | f"/api/{BASE_ROUTE}/fizzbar/123", 90 | json={"name": "New Fizzbar", "purpose": "New purpose"}, 91 | ).get_json() 92 | expected = ( 93 | FizzbarSchema() 94 | .dump( 95 | Fizzbar(fizzbar_id=123, name="New Fizzbar", purpose="New purpose") 96 | ) 97 | 98 | ) 99 | assert result == expected 100 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/interface.py: -------------------------------------------------------------------------------- 1 | from mypy_extensions import TypedDict 2 | 3 | 4 | class FizzbarInterface(TypedDict, total=False): 5 | fizzbar_id: int 6 | name: str 7 | purpose: str 8 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/interface_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from .model import Fizzbar 3 | from .interface import FizzbarInterface 4 | 5 | 6 | @fixture 7 | def interface() -> FizzbarInterface: 8 | return FizzbarInterface(fizzbar_id=1, name="Test fizzbar", purpose="Test purpose") 9 | 10 | 11 | def test_FizzbarInterface_create(interface: FizzbarInterface): 12 | assert interface 13 | 14 | 15 | def test_FizzbarInterface_works(interface: FizzbarInterface): 16 | fizzbar = Fizzbar(**interface) 17 | assert fizzbar 18 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app import db # noqa 3 | from .interface import FizzbarInterface 4 | from typing import Any 5 | 6 | 7 | class Fizzbar(db.Model): # type: ignore 8 | """A snazzy Fizzbar""" 9 | 10 | __tablename__ = "fizzbar" 11 | 12 | fizzbar_id = Column(Integer(), primary_key=True) 13 | name = Column(String(255)) 14 | purpose = Column(String(255)) 15 | 16 | def update(self, changes: FizzbarInterface): 17 | for key, val in changes.items(): 18 | setattr(self, key, val) 19 | return self 20 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/model_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from flask_sqlalchemy import SQLAlchemy 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Fizzbar 5 | 6 | 7 | @fixture 8 | def fizzbar() -> Fizzbar: 9 | return Fizzbar(fizzbar_id=1, name="Test fizzbar", purpose="Test purpose") 10 | 11 | 12 | def test_Fizzbar_create(fizzbar: Fizzbar): 13 | assert fizzbar 14 | 15 | 16 | def test_Fizzbar_retrieve(fizzbar: Fizzbar, db: SQLAlchemy): # noqa 17 | db.session.add(fizzbar) 18 | db.session.commit() 19 | s = Fizzbar.query.first() 20 | assert s.__dict__ == fizzbar.__dict__ 21 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, Schema 2 | 3 | 4 | class FizzbarSchema(Schema): 5 | """Fizzbar schema""" 6 | 7 | fizzbarId = fields.Number(attribute="fizzbar_id") 8 | name = fields.String(attribute="name") 9 | purpose = fields.String(attribute="purpose") 10 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/schema_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import Fizzbar 4 | from .schema import FizzbarSchema 5 | from .interface import FizzbarInterface 6 | 7 | 8 | @fixture 9 | def schema() -> FizzbarSchema: 10 | return FizzbarSchema() 11 | 12 | 13 | def test_FizzbarSchema_create(schema: FizzbarSchema): 14 | assert schema 15 | 16 | 17 | def test_FizzbarSchema_works(schema: FizzbarSchema): 18 | params: FizzbarInterface = schema.load( 19 | {"fizzbarId": "123", "name": "Test fizzbar", "purpose": "Test purpose"} 20 | ) 21 | fizzbar = Fizzbar(**params) 22 | 23 | assert fizzbar.fizzbar_id == 123 24 | assert fizzbar.name == "Test fizzbar" 25 | assert fizzbar.purpose == "Test purpose" 26 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/service.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from typing import List 3 | from .model import Fizzbar 4 | from .interface import FizzbarInterface 5 | 6 | 7 | class FizzbarService: 8 | @staticmethod 9 | def get_all() -> List[Fizzbar]: 10 | return Fizzbar.query.all() 11 | 12 | @staticmethod 13 | def get_by_id(fizzbar_id: int) -> Fizzbar: 14 | return Fizzbar.query.get(fizzbar_id) 15 | 16 | @staticmethod 17 | def update(fizzbar: Fizzbar, Fizzbar_change_updates: FizzbarInterface) -> Fizzbar: 18 | fizzbar.update(Fizzbar_change_updates) 19 | db.session.commit() 20 | return fizzbar 21 | 22 | @staticmethod 23 | def delete_by_id(fizzbar_id: int) -> List[int]: 24 | fizzbar = Fizzbar.query.filter(Fizzbar.fizzbar_id == fizzbar_id).first() 25 | if not fizzbar: 26 | return [] 27 | db.session.delete(fizzbar) 28 | db.session.commit() 29 | return [fizzbar_id] 30 | 31 | @staticmethod 32 | def create(new_attrs: FizzbarInterface) -> Fizzbar: 33 | new_fizzbar = Fizzbar(name=new_attrs["name"], purpose=new_attrs["purpose"]) 34 | 35 | db.session.add(new_fizzbar) 36 | db.session.commit() 37 | 38 | return new_fizzbar 39 | -------------------------------------------------------------------------------- /app/fizz/fizzbar/service_test.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from typing import List 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Fizzbar 5 | from .service import FizzbarService # noqa 6 | from .interface import FizzbarInterface 7 | 8 | 9 | def test_get_all(db: SQLAlchemy): # noqa 10 | yin: Fizzbar = Fizzbar(fizzbar_id=1, name="Yin", purpose="thing 1") 11 | yang: Fizzbar = Fizzbar(fizzbar_id=2, name="Yang", purpose="thing 2") 12 | db.session.add(yin) 13 | db.session.add(yang) 14 | db.session.commit() 15 | 16 | results: List[Fizzbar] = FizzbarService.get_all() 17 | 18 | assert len(results) == 2 19 | assert yin in results and yang in results 20 | 21 | 22 | def test_update(db: SQLAlchemy): # noqa 23 | yin: Fizzbar = Fizzbar(fizzbar_id=1, name="Yin", purpose="thing 1") 24 | 25 | db.session.add(yin) 26 | db.session.commit() 27 | updates: FizzbarInterface = dict(name="New Fizzbar name") 28 | 29 | FizzbarService.update(yin, updates) 30 | 31 | result: Fizzbar = Fizzbar.query.get(yin.fizzbar_id) 32 | assert result.name == "New Fizzbar name" 33 | 34 | 35 | def test_delete_by_id(db: SQLAlchemy): # noqa 36 | yin: Fizzbar = Fizzbar(fizzbar_id=1, name="Yin", purpose="thing 1") 37 | yang: Fizzbar = Fizzbar(fizzbar_id=2, name="Yang", purpose="thing 2") 38 | db.session.add(yin) 39 | db.session.add(yang) 40 | db.session.commit() 41 | 42 | FizzbarService.delete_by_id(1) 43 | db.session.commit() 44 | 45 | results: List[Fizzbar] = Fizzbar.query.all() 46 | 47 | assert len(results) == 1 48 | assert yin not in results and yang in results 49 | 50 | 51 | def test_create(db: SQLAlchemy): # noqa 52 | 53 | yin: FizzbarInterface = dict(name="Fancy new fizzbar", purpose="Fancy new purpose") 54 | FizzbarService.create(yin) 55 | results: List[Fizzbar] = Fizzbar.query.all() 56 | 57 | assert len(results) == 1 58 | 59 | for k in yin.keys(): 60 | assert getattr(results[0], k) == yin[k] 61 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Fizzbaz # noqa 2 | from .schema import FizzbazSchema # noqa 3 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_accepts import accepts, responds 3 | from flask_restx import Namespace, Resource 4 | from flask.wrappers import Response 5 | from typing import List 6 | 7 | from .schema import FizzbazSchema 8 | from .service import FizzbazService 9 | from .model import Fizzbaz 10 | from .interface import FizzbazInterface 11 | 12 | api = Namespace("Fizzbaz", description="A modular namespace within fizz") # noqa 13 | 14 | 15 | @api.route("/") 16 | class FizzbazResource(Resource): 17 | """Fizzbaz""" 18 | 19 | @responds(schema=FizzbazSchema, many=True) 20 | def get(self) -> List[Fizzbaz]: 21 | """Get all Fizzbaz""" 22 | 23 | return FizzbazService.get_all() 24 | 25 | @accepts(schema=FizzbazSchema, api=api) 26 | @responds(schema=FizzbazSchema) 27 | def post(self) -> Fizzbaz: 28 | """Create a Single Fizzbaz""" 29 | 30 | return FizzbazService.create(request.parsed_obj) 31 | 32 | 33 | @api.route("/") 34 | @api.param("fizzbazId", "Fizzbaz database ID") 35 | class FizzbazIdResource(Resource): 36 | @responds(schema=FizzbazSchema) 37 | def get(self, fizzbazId: int) -> Fizzbaz: 38 | """Get Single Fizzbaz""" 39 | 40 | return FizzbazService.get_by_id(fizzbazId) 41 | 42 | def delete(self, fizzbazId: int) -> Response: 43 | """Delete Single Fizzbaz""" 44 | from flask import jsonify 45 | 46 | id = FizzbazService.delete_by_id(fizzbazId) 47 | return jsonify(dict(status="Success", id=id)) 48 | 49 | @accepts(schema=FizzbazSchema, api=api) 50 | @responds(schema=FizzbazSchema) 51 | def put(self, fizzbazId: int) -> Fizzbaz: 52 | """Update Single Fizzbaz""" 53 | 54 | changes: FizzbazInterface = request.parsed_obj 55 | Fizzbaz = FizzbazService.get_by_id(fizzbazId) 56 | return FizzbazService.update(Fizzbaz, changes) 57 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/controller_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | from flask.testing import FlaskClient 3 | 4 | from app.test.fixtures import client, app # noqa 5 | from .service import FizzbazService 6 | from .schema import FizzbazSchema 7 | from .model import Fizzbaz 8 | from .interface import FizzbazInterface 9 | from .. import BASE_ROUTE 10 | 11 | 12 | def make_fizzbaz( 13 | id: int = 123, name: str = "Test fizzbaz", purpose: str = "Test purpose" 14 | ) -> Fizzbaz: 15 | return Fizzbaz(fizzbaz_id=id, name=name, purpose=purpose) 16 | 17 | 18 | class TestFizzbazResource: 19 | @patch.object( 20 | FizzbazService, 21 | "get_all", 22 | lambda: [ 23 | make_fizzbaz(123, name="Test Fizzbaz 1"), 24 | make_fizzbaz(456, name="Test Fizzbaz 2"), 25 | ], 26 | ) 27 | def test_get(self, client: FlaskClient): # noqa 28 | with client: 29 | results = client.get( 30 | f"/api/{BASE_ROUTE}/fizzbaz", follow_redirects=True 31 | ).get_json() 32 | expected = ( 33 | FizzbazSchema(many=True) 34 | .dump( 35 | [ 36 | make_fizzbaz(123, name="Test Fizzbaz 1"), 37 | make_fizzbaz(456, name="Test Fizzbaz 2"), 38 | ] 39 | ) 40 | 41 | ) 42 | for r in results: 43 | assert r in expected 44 | 45 | @patch.object( 46 | FizzbazService, "create", lambda create_request: Fizzbaz(**create_request) 47 | ) 48 | def test_post(self, client: FlaskClient): # noqa 49 | with client: 50 | 51 | payload = dict(name="Test fizzbaz", purpose="Test purpose") 52 | result = client.post(f"/api/{BASE_ROUTE}/fizzbaz/", json=payload).get_json() 53 | expected = ( 54 | FizzbazSchema() 55 | .dump(Fizzbaz(name=payload["name"], purpose=payload["purpose"])) 56 | 57 | ) 58 | assert result == expected 59 | 60 | 61 | def fake_update(fizzbaz: Fizzbaz, changes: FizzbazInterface) -> Fizzbaz: 62 | # To fake an update, just return a new object 63 | updated_Fizzbaz = Fizzbaz( 64 | fizzbaz_id=fizzbaz.fizzbaz_id, name=changes["name"], purpose=changes["purpose"] 65 | ) 66 | return updated_Fizzbaz 67 | 68 | 69 | class TestFizzbazIdResource: 70 | @patch.object(FizzbazService, "get_by_id", lambda id: make_fizzbaz(id=id)) 71 | def test_get(self, client: FlaskClient): # noqa 72 | with client: 73 | result = client.get(f"/api/{BASE_ROUTE}/fizzbaz/123").get_json() 74 | expected = Fizzbaz(fizzbaz_id=123) 75 | assert result["fizzbazId"] == expected.fizzbaz_id 76 | 77 | @patch.object(FizzbazService, "delete_by_id", lambda id: [id]) 78 | def test_delete(self, client: FlaskClient): # noqa 79 | with client: 80 | result = client.delete(f"/api/{BASE_ROUTE}/fizzbaz/123").get_json() 81 | expected = dict(status="Success", id=[123]) 82 | assert result == expected 83 | 84 | @patch.object(FizzbazService, "get_by_id", lambda id: make_fizzbaz(id=id)) 85 | @patch.object(FizzbazService, "update", fake_update) 86 | def test_put(self, client: FlaskClient): # noqa 87 | with client: 88 | result = client.put( 89 | f"/api/{BASE_ROUTE}/fizzbaz/123", 90 | json={"name": "New Fizzbaz", "purpose": "New purpose"}, 91 | ).get_json() 92 | expected = ( 93 | FizzbazSchema() 94 | .dump( 95 | Fizzbaz(fizzbaz_id=123, name="New Fizzbaz", purpose="New purpose") 96 | ) 97 | 98 | ) 99 | assert result == expected 100 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/interface.py: -------------------------------------------------------------------------------- 1 | from mypy_extensions import TypedDict 2 | 3 | 4 | class FizzbazInterface(TypedDict, total=False): 5 | fizzbaz_id: int 6 | name: str 7 | purpose: str 8 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/interface_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from .model import Fizzbaz 3 | from .interface import FizzbazInterface 4 | 5 | 6 | @fixture 7 | def interface() -> FizzbazInterface: 8 | return FizzbazInterface(fizzbaz_id=1, name="Test fizzbaz", purpose="Test purpose") 9 | 10 | 11 | def test_FizzbazInterface_create(interface: FizzbazInterface): 12 | assert interface 13 | 14 | 15 | def test_FizzbazInterface_works(interface: FizzbazInterface): 16 | fizzbaz = Fizzbaz(**interface) 17 | assert fizzbaz 18 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app import db # noqa 3 | from .interface import FizzbazInterface 4 | from typing import Any 5 | 6 | 7 | class Fizzbaz(db.Model): # type: ignore 8 | """A snazzy Fizzbaz""" 9 | 10 | __tablename__ = "fizzbaz" 11 | 12 | fizzbaz_id = Column(Integer(), primary_key=True) 13 | name = Column(String(255)) 14 | purpose = Column(String(255)) 15 | 16 | def update(self, changes: FizzbazInterface): 17 | for key, val in changes.items(): 18 | setattr(self, key, val) 19 | return self 20 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/model_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from flask_sqlalchemy import SQLAlchemy 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Fizzbaz 5 | 6 | 7 | @fixture 8 | def fizzbaz() -> Fizzbaz: 9 | return Fizzbaz(fizzbaz_id=1, name="Test fizzbaz", purpose="Test purpose") 10 | 11 | 12 | def test_Fizzbaz_create(fizzbaz: Fizzbaz): 13 | assert fizzbaz 14 | 15 | 16 | def test_Fizzbaz_retrieve(fizzbaz: Fizzbaz, db: SQLAlchemy): # noqa 17 | db.session.add(fizzbaz) 18 | db.session.commit() 19 | s = Fizzbaz.query.first() 20 | assert s.__dict__ == fizzbaz.__dict__ 21 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, Schema 2 | 3 | 4 | class FizzbazSchema(Schema): 5 | """Fizzbaz schema""" 6 | 7 | fizzbazId = fields.Number(attribute="fizzbaz_id") 8 | name = fields.String(attribute="name") 9 | purpose = fields.String(attribute="purpose") 10 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/schema_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import Fizzbaz 4 | from .schema import FizzbazSchema 5 | from .interface import FizzbazInterface 6 | 7 | 8 | @fixture 9 | def schema() -> FizzbazSchema: 10 | return FizzbazSchema() 11 | 12 | 13 | def test_FizzbazSchema_create(schema: FizzbazSchema): 14 | assert schema 15 | 16 | 17 | def test_FizzbazSchema_works(schema: FizzbazSchema): 18 | params: FizzbazInterface = schema.load( 19 | {"fizzbazId": "123", "name": "Test fizzbaz", "purpose": "Test purpose"} 20 | ) 21 | fizzbaz = Fizzbaz(**params) 22 | 23 | assert fizzbaz.fizzbaz_id == 123 24 | assert fizzbaz.name == "Test fizzbaz" 25 | assert fizzbaz.purpose == "Test purpose" 26 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/service.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from typing import List 3 | from .model import Fizzbaz 4 | from .interface import FizzbazInterface 5 | 6 | 7 | class FizzbazService(): 8 | @staticmethod 9 | def get_all() -> List[Fizzbaz]: 10 | return Fizzbaz.query.all() 11 | 12 | @staticmethod 13 | def get_by_id(fizzbaz_id: int) -> Fizzbaz: 14 | return Fizzbaz.query.get(fizzbaz_id) 15 | 16 | @staticmethod 17 | def update(fizzbaz: Fizzbaz, Fizzbaz_change_updates: FizzbazInterface) -> Fizzbaz: 18 | fizzbaz.update(Fizzbaz_change_updates) 19 | db.session.commit() 20 | return fizzbaz 21 | 22 | @staticmethod 23 | def delete_by_id(fizzbaz_id: int) -> List[int]: 24 | fizzbaz = Fizzbaz.query.filter(Fizzbaz.fizzbaz_id == fizzbaz_id).first() 25 | if not fizzbaz: 26 | return [] 27 | db.session.delete(fizzbaz) 28 | db.session.commit() 29 | return [fizzbaz_id] 30 | 31 | @staticmethod 32 | def create(new_attrs: FizzbazInterface) -> Fizzbaz: 33 | new_fizzbaz = Fizzbaz( 34 | name=new_attrs['name'], 35 | purpose=new_attrs['purpose'] 36 | ) 37 | 38 | db.session.add(new_fizzbaz) 39 | db.session.commit() 40 | 41 | return new_fizzbaz 42 | -------------------------------------------------------------------------------- /app/fizz/fizzbaz/service_test.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from typing import List 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Fizzbaz 5 | from .service import FizzbazService # noqa 6 | from .interface import FizzbazInterface 7 | 8 | 9 | def test_get_all(db: SQLAlchemy): # noqa 10 | yin: Fizzbaz = Fizzbaz(fizzbaz_id=1, name='Yin', purpose='thing 1') 11 | yang: Fizzbaz = Fizzbaz(fizzbaz_id=2, name='Yang', purpose='thing 2') 12 | db.session.add(yin) 13 | db.session.add(yang) 14 | db.session.commit() 15 | 16 | results: List[Fizzbaz] = FizzbazService.get_all() 17 | 18 | assert len(results) == 2 19 | assert yin in results and yang in results 20 | 21 | 22 | def test_update(db: SQLAlchemy): # noqa 23 | yin: Fizzbaz = Fizzbaz(fizzbaz_id=1, name='Yin', purpose='thing 1') 24 | 25 | db.session.add(yin) 26 | db.session.commit() 27 | updates: FizzbazInterface = dict(name='New Fizzbaz name') 28 | 29 | FizzbazService.update(yin, updates) 30 | 31 | result: Fizzbaz = Fizzbaz.query.get(yin.fizzbaz_id) 32 | assert result.name == 'New Fizzbaz name' 33 | 34 | 35 | def test_delete_by_id(db: SQLAlchemy): # noqa 36 | yin: Fizzbaz = Fizzbaz(fizzbaz_id=1, name='Yin', purpose='thing 1') 37 | yang: Fizzbaz = Fizzbaz(fizzbaz_id=2, name='Yang', purpose='thing 2') 38 | db.session.add(yin) 39 | db.session.add(yang) 40 | db.session.commit() 41 | 42 | FizzbazService.delete_by_id(1) 43 | db.session.commit() 44 | 45 | results: List[Fizzbaz] = Fizzbaz.query.all() 46 | 47 | assert len(results) == 1 48 | assert yin not in results and yang in results 49 | 50 | 51 | def test_create(db: SQLAlchemy): # noqa 52 | 53 | yin: FizzbazInterface = dict(name='Fancy new fizzbaz', purpose='Fancy new purpose') 54 | FizzbazService.create(yin) 55 | results: List[Fizzbaz] = Fizzbaz.query.all() 56 | 57 | assert len(results) == 1 58 | 59 | for k in yin.keys(): 60 | assert getattr(results[0], k) == yin[k] 61 | -------------------------------------------------------------------------------- /app/other_api/__init__.py: -------------------------------------------------------------------------------- 1 | BASE_ROUTE = "other_api" 2 | 3 | 4 | def register_routes(api, app, root="api"): 5 | from flask import Blueprint 6 | from flask_restx import Api 7 | 8 | bp = Blueprint("other_api", __name__) 9 | api = Api(bp, title="Another API with separate Swagger docs", version="0.1.0") 10 | 11 | from .doodad.controller import api as doodad_api 12 | from .whatsit.controller import api as whatsit_api 13 | 14 | api.add_namespace(doodad_api, path=f"/doodad") 15 | api.add_namespace(whatsit_api, path=f"/whatsit") 16 | app.register_blueprint(bp, url_prefix=f"/{root}/{BASE_ROUTE}") 17 | -------------------------------------------------------------------------------- /app/other_api/doodad/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Doodad # noqa 2 | from .schema import DoodadSchema # noqa 3 | -------------------------------------------------------------------------------- /app/other_api/doodad/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_accepts import accepts, responds 3 | from flask_restx import Namespace, Resource 4 | from flask.wrappers import Response 5 | from typing import List 6 | 7 | from .schema import DoodadSchema 8 | from .service import DoodadService 9 | from .model import Doodad 10 | from .interface import DoodadInterface 11 | 12 | api = Namespace("Doodad", description="A modular namespace within Other API") # noqa 13 | 14 | 15 | @api.route("/") 16 | class DoodadResource(Resource): 17 | """Doodads""" 18 | 19 | @responds(schema=DoodadSchema, many=True) 20 | def get(self) -> List[Doodad]: 21 | """Get all Doodads""" 22 | 23 | return DoodadService.get_all() 24 | 25 | @accepts(schema=DoodadSchema, api=api) 26 | @responds(schema=DoodadSchema) 27 | def post(self) -> Doodad: 28 | """Create a Single Doodad""" 29 | 30 | return DoodadService.create(request.parsed_obj) 31 | 32 | 33 | @api.route("/") 34 | @api.param("doodadId", "Doodad database ID") 35 | class DoodadIdResource(Resource): 36 | @responds(schema=DoodadSchema) 37 | def get(self, doodadId: int) -> Doodad: 38 | """Get Single Doodad""" 39 | 40 | return DoodadService.get_by_id(doodadId) 41 | 42 | def delete(self, doodadId: int) -> Response: 43 | """Delete Single Doodad""" 44 | from flask import jsonify 45 | 46 | print("doodadId = ", doodadId) 47 | id = DoodadService.delete_by_id(doodadId) 48 | return jsonify(dict(status="Success", id=id)) 49 | 50 | @accepts(schema=DoodadSchema, api=api) 51 | @responds(schema=DoodadSchema) 52 | def put(self, doodadId: int) -> Doodad: 53 | """Update Single Doodad""" 54 | 55 | changes: DoodadInterface = request.parsed_obj 56 | Doodad = DoodadService.get_by_id(doodadId) 57 | return DoodadService.update(Doodad, changes) 58 | -------------------------------------------------------------------------------- /app/other_api/doodad/controller_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | from flask.testing import FlaskClient 3 | 4 | from app.test.fixtures import client, app # noqa 5 | from .service import DoodadService 6 | from .schema import DoodadSchema 7 | from .model import Doodad 8 | from .interface import DoodadInterface 9 | from .. import BASE_ROUTE 10 | 11 | 12 | def make_doodad( 13 | id: int = 123, name: str = "Test doodad", purpose: str = "Test purpose" 14 | ) -> Doodad: 15 | return Doodad(doodad_id=id, name=name, purpose=purpose) 16 | 17 | 18 | class TestDoodadResource: 19 | @patch.object( 20 | DoodadService, 21 | "get_all", 22 | lambda: [ 23 | make_doodad(123, name="Test Doodad 1"), 24 | make_doodad(456, name="Test Doodad 2"), 25 | ], 26 | ) 27 | def test_get(self, client: FlaskClient): # noqa 28 | with client: 29 | results = client.get( 30 | f"/api/{BASE_ROUTE}/doodad", follow_redirects=True 31 | ).get_json() 32 | expected = ( 33 | DoodadSchema(many=True) 34 | .dump( 35 | [ 36 | make_doodad(123, name="Test Doodad 1"), 37 | make_doodad(456, name="Test Doodad 2"), 38 | ] 39 | ) 40 | 41 | ) 42 | for r in results: 43 | assert r in expected 44 | 45 | @patch.object( 46 | DoodadService, "create", lambda create_request: Doodad(**create_request) 47 | ) 48 | def test_post(self, client: FlaskClient): # noqa 49 | with client: 50 | 51 | payload = dict(name="Test doodad", purpose="Test purpose") 52 | result = client.post(f"/api/{BASE_ROUTE}/doodad/", json=payload).get_json() 53 | expected = ( 54 | DoodadSchema() 55 | .dump(Doodad(name=payload["name"], purpose=payload["purpose"])) 56 | 57 | ) 58 | assert result == expected 59 | 60 | 61 | def fake_update(doodad: Doodad, changes: DoodadInterface) -> Doodad: 62 | # To fake an update, just return a new object 63 | updated_Doodad = Doodad( 64 | doodad_id=doodad.doodad_id, name=changes["name"], purpose=changes["purpose"] 65 | ) 66 | return updated_Doodad 67 | 68 | 69 | class TestDoodadIdResource: 70 | @patch.object(DoodadService, "get_by_id", lambda id: make_doodad(id=id)) 71 | def test_get(self, client: FlaskClient): # noqa 72 | with client: 73 | result = client.get(f"/api/{BASE_ROUTE}/doodad/123").get_json() 74 | expected = Doodad(doodad_id=123) 75 | assert result["doodadId"] == expected.doodad_id 76 | 77 | @patch.object(DoodadService, "delete_by_id", lambda id: [id]) 78 | def test_delete(self, client: FlaskClient): # noqa 79 | with client: 80 | result = client.delete(f"/api/{BASE_ROUTE}/doodad/123").get_json() 81 | expected = dict(status="Success", id=[123]) 82 | assert result == expected 83 | 84 | @patch.object(DoodadService, "get_by_id", lambda id: make_doodad(id=id)) 85 | @patch.object(DoodadService, "update", fake_update) 86 | def test_put(self, client: FlaskClient): # noqa 87 | with client: 88 | result = client.put( 89 | f"/api/{BASE_ROUTE}/doodad/123", 90 | json={"name": "New Doodad", "purpose": "New purpose"}, 91 | ).get_json() 92 | expected = ( 93 | DoodadSchema() 94 | .dump(Doodad(doodad_id=123, name="New Doodad", purpose="New purpose")) 95 | 96 | ) 97 | assert result == expected 98 | -------------------------------------------------------------------------------- /app/other_api/doodad/interface.py: -------------------------------------------------------------------------------- 1 | from mypy_extensions import TypedDict 2 | 3 | 4 | class DoodadInterface(TypedDict, total=False): 5 | doodad_id: int 6 | name: str 7 | purpose: str 8 | -------------------------------------------------------------------------------- /app/other_api/doodad/interface_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from .model import Doodad 3 | from .interface import DoodadInterface 4 | 5 | 6 | @fixture 7 | def interface() -> DoodadInterface: 8 | return DoodadInterface(doodad_id=1, name="Test doodad", purpose="Test purpose") 9 | 10 | 11 | def test_DoodadInterface_create(interface: DoodadInterface): 12 | assert interface 13 | 14 | 15 | def test_DoodadInterface_works(interface: DoodadInterface): 16 | doodad = Doodad(**interface) 17 | assert doodad 18 | -------------------------------------------------------------------------------- /app/other_api/doodad/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app import db # noqa 3 | from .interface import DoodadInterface 4 | from typing import Any 5 | 6 | 7 | class Doodad(db.Model): # type: ignore 8 | """A snazzy Doodad""" 9 | 10 | __tablename__ = "doodad" 11 | 12 | doodad_id = Column(Integer(), primary_key=True) 13 | name = Column(String(255)) 14 | purpose = Column(String(255)) 15 | 16 | def update(self, changes: DoodadInterface): 17 | for key, val in changes.items(): 18 | setattr(self, key, val) 19 | return self 20 | -------------------------------------------------------------------------------- /app/other_api/doodad/model_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from flask_sqlalchemy import SQLAlchemy 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Doodad 5 | 6 | 7 | @fixture 8 | def doodad() -> Doodad: 9 | return Doodad(doodad_id=1, name="Test doodad", purpose="Test purpose") 10 | 11 | 12 | def test_Doodad_create(doodad: Doodad): 13 | assert doodad 14 | 15 | 16 | def test_Doodad_retrieve(doodad: Doodad, db: SQLAlchemy): # noqa 17 | db.session.add(doodad) 18 | db.session.commit() 19 | s = Doodad.query.first() 20 | assert s.__dict__ == doodad.__dict__ 21 | -------------------------------------------------------------------------------- /app/other_api/doodad/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, Schema 2 | 3 | 4 | class DoodadSchema(Schema): 5 | """Doodad schema""" 6 | 7 | doodadId = fields.Number(attribute="doodad_id") 8 | name = fields.String(attribute="name") 9 | purpose = fields.String(attribute="purpose") 10 | -------------------------------------------------------------------------------- /app/other_api/doodad/schema_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import Doodad 4 | from .schema import DoodadSchema 5 | from .interface import DoodadInterface 6 | 7 | 8 | @fixture 9 | def schema() -> DoodadSchema: 10 | return DoodadSchema() 11 | 12 | 13 | def test_DoodadSchema_create(schema: DoodadSchema): 14 | assert schema 15 | 16 | 17 | def test_DoodadSchema_works(schema: DoodadSchema): 18 | params: DoodadInterface = schema.load( 19 | {"doodadId": "123", "name": "Test doodad", "purpose": "Test purpose"} 20 | ) 21 | doodad = Doodad(**params) 22 | 23 | assert doodad.doodad_id == 123 24 | assert doodad.name == "Test doodad" 25 | assert doodad.purpose == "Test purpose" 26 | -------------------------------------------------------------------------------- /app/other_api/doodad/service.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from typing import List 3 | from .model import Doodad 4 | from .interface import DoodadInterface 5 | 6 | 7 | class DoodadService: 8 | @staticmethod 9 | def get_all() -> List[Doodad]: 10 | return Doodad.query.all() 11 | 12 | @staticmethod 13 | def get_by_id(doodad_id: int) -> Doodad: 14 | return Doodad.query.get(doodad_id) 15 | 16 | @staticmethod 17 | def update(doodad: Doodad, Doodad_change_updates: DoodadInterface) -> Doodad: 18 | doodad.update(Doodad_change_updates) 19 | db.session.commit() 20 | return doodad 21 | 22 | @staticmethod 23 | def delete_by_id(doodad_id: int) -> List[int]: 24 | doodad = Doodad.query.filter(Doodad.doodad_id == doodad_id).first() 25 | if not doodad: 26 | return [] 27 | db.session.delete(doodad) 28 | db.session.commit() 29 | return [doodad_id] 30 | 31 | @staticmethod 32 | def create(new_attrs: DoodadInterface) -> Doodad: 33 | new_doodad = Doodad(name=new_attrs["name"], purpose=new_attrs["purpose"]) 34 | 35 | db.session.add(new_doodad) 36 | db.session.commit() 37 | 38 | return new_doodad 39 | -------------------------------------------------------------------------------- /app/other_api/doodad/service_test.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from typing import List 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Doodad 5 | from .service import DoodadService # noqa 6 | from .interface import DoodadInterface 7 | 8 | 9 | def test_get_all(db: SQLAlchemy): # noqa 10 | yin: Doodad = Doodad(doodad_id=1, name="Yin", purpose="thing 1") 11 | yang: Doodad = Doodad(doodad_id=2, name="Yang", purpose="thing 2") 12 | db.session.add(yin) 13 | db.session.add(yang) 14 | db.session.commit() 15 | 16 | results: List[Doodad] = DoodadService.get_all() 17 | 18 | assert len(results) == 2 19 | assert yin in results and yang in results 20 | 21 | 22 | def test_update(db: SQLAlchemy): # noqa 23 | yin: Doodad = Doodad(doodad_id=1, name="Yin", purpose="thing 1") 24 | 25 | db.session.add(yin) 26 | db.session.commit() 27 | updates: DoodadInterface = dict(name="New Doodad name") 28 | 29 | DoodadService.update(yin, updates) 30 | 31 | result: Doodad = Doodad.query.get(yin.doodad_id) 32 | assert result.name == "New Doodad name" 33 | 34 | 35 | def test_delete_by_id(db: SQLAlchemy): # noqa 36 | yin: Doodad = Doodad(doodad_id=1, name="Yin", purpose="thing 1") 37 | yang: Doodad = Doodad(doodad_id=2, name="Yang", purpose="thing 2") 38 | db.session.add(yin) 39 | db.session.add(yang) 40 | db.session.commit() 41 | 42 | DoodadService.delete_by_id(1) 43 | db.session.commit() 44 | 45 | results: List[Doodad] = Doodad.query.all() 46 | 47 | assert len(results) == 1 48 | assert yin not in results and yang in results 49 | 50 | 51 | def test_create(db: SQLAlchemy): # noqa 52 | 53 | yin: DoodadInterface = dict(name="Fancy new doodad", purpose="Fancy new purpose") 54 | DoodadService.create(yin) 55 | results: List[Doodad] = Doodad.query.all() 56 | 57 | assert len(results) == 1 58 | 59 | for k in yin.keys(): 60 | assert getattr(results[0], k) == yin[k] 61 | -------------------------------------------------------------------------------- /app/other_api/whatsit/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Whatsit # noqa 2 | from .schema import WhatsitSchema # noqa 3 | -------------------------------------------------------------------------------- /app/other_api/whatsit/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_accepts import accepts, responds 3 | from flask_restx import Namespace, Resource 4 | from flask.wrappers import Response 5 | from typing import List 6 | 7 | from .schema import WhatsitSchema 8 | from .service import WhatsitService 9 | from .model import Whatsit 10 | from .interface import WhatsitInterface 11 | 12 | api = Namespace('Whatsit', description='A modular namespace within Other API') # noqa 13 | 14 | 15 | @api.route('/') 16 | class WhatsitResource(Resource): 17 | '''Whatsits''' 18 | 19 | @responds(schema=WhatsitSchema, many=True) 20 | def get(self) -> List[Whatsit]: 21 | '''Get all Whatsits''' 22 | 23 | return WhatsitService.get_all() 24 | 25 | @accepts(schema=WhatsitSchema, api=api) 26 | @responds(schema=WhatsitSchema) 27 | def post(self) -> Whatsit: 28 | '''Create a Single Whatsit''' 29 | 30 | return WhatsitService.create(request.parsed_obj) 31 | 32 | 33 | @api.route('/') 34 | @api.param('whatsitId', 'Whatsit database ID') 35 | class WhatsitIdResource(Resource): 36 | @responds(schema=WhatsitSchema) 37 | def get(self, whatsitId: int) -> Whatsit: 38 | '''Get Single Whatsit''' 39 | 40 | return WhatsitService.get_by_id(whatsitId) 41 | 42 | def delete(self, whatsitId: int) -> Response: 43 | '''Delete Single Whatsit''' 44 | from flask import jsonify 45 | 46 | id = WhatsitService.delete_by_id(whatsitId) 47 | return jsonify(dict(status='Success', id=id)) 48 | 49 | @accepts(schema=WhatsitSchema, api=api) 50 | @responds(schema=WhatsitSchema) 51 | def put(self, whatsitId: int) -> Whatsit: 52 | '''Update Single Whatsit''' 53 | 54 | changes: WhatsitInterface = request.parsed_obj 55 | Whatsit = WhatsitService.get_by_id(whatsitId) 56 | return WhatsitService.update(Whatsit, changes) 57 | -------------------------------------------------------------------------------- /app/other_api/whatsit/controller_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | from flask.testing import FlaskClient 3 | 4 | from app.test.fixtures import client, app # noqa 5 | from .service import WhatsitService 6 | from .schema import WhatsitSchema 7 | from .model import Whatsit 8 | from .interface import WhatsitInterface 9 | from .. import BASE_ROUTE 10 | 11 | 12 | def make_whatsit( 13 | id: int = 123, name: str = "Test whatsit", purpose: str = "Test purpose" 14 | ) -> Whatsit: 15 | return Whatsit(whatsit_id=id, name=name, purpose=purpose) 16 | 17 | 18 | class TestWhatsitResource: 19 | @patch.object( 20 | WhatsitService, 21 | "get_all", 22 | lambda: [ 23 | make_whatsit(123, name="Test Whatsit 1"), 24 | make_whatsit(456, name="Test Whatsit 2"), 25 | ], 26 | ) 27 | def test_get(self, client: FlaskClient): # noqa 28 | with client: 29 | results = client.get( 30 | f"/api/{BASE_ROUTE}/whatsit", follow_redirects=True 31 | ).get_json() 32 | expected = ( 33 | WhatsitSchema(many=True) 34 | .dump( 35 | [ 36 | make_whatsit(123, name="Test Whatsit 1"), 37 | make_whatsit(456, name="Test Whatsit 2"), 38 | ] 39 | ) 40 | 41 | ) 42 | for r in results: 43 | assert r in expected 44 | 45 | @patch.object( 46 | WhatsitService, "create", lambda create_request: Whatsit(**create_request) 47 | ) 48 | def test_post(self, client: FlaskClient): # noqa 49 | with client: 50 | 51 | payload = dict(name="Test whatsit", purpose="Test purpose") 52 | result = client.post(f"/api/{BASE_ROUTE}/whatsit/", json=payload).get_json() 53 | expected = ( 54 | WhatsitSchema() 55 | .dump(Whatsit(name=payload["name"], purpose=payload["purpose"])) 56 | 57 | ) 58 | assert result == expected 59 | 60 | 61 | def fake_update(whatsit: Whatsit, changes: WhatsitInterface) -> Whatsit: 62 | # To fake an update, just return a new object 63 | updated_Whatsit = Whatsit( 64 | whatsit_id=whatsit.whatsit_id, name=changes["name"], purpose=changes["purpose"] 65 | ) 66 | return updated_Whatsit 67 | 68 | 69 | class TestWhatsitIdResource: 70 | @patch.object(WhatsitService, "get_by_id", lambda id: make_whatsit(id=id)) 71 | def test_get(self, client: FlaskClient): # noqa 72 | with client: 73 | result = client.get(f"/api/{BASE_ROUTE}/whatsit/123").get_json() 74 | expected = Whatsit(whatsit_id=123) 75 | assert result["whatsitId"] == expected.whatsit_id 76 | 77 | @patch.object(WhatsitService, "delete_by_id", lambda id: [id]) 78 | def test_delete(self, client: FlaskClient): # noqa 79 | with client: 80 | result = client.delete(f"/api/{BASE_ROUTE}/whatsit/123").get_json() 81 | expected = dict(status="Success", id=[123]) 82 | assert result == expected 83 | 84 | @patch.object(WhatsitService, "get_by_id", lambda id: make_whatsit(id=id)) 85 | @patch.object(WhatsitService, "update", fake_update) 86 | def test_put(self, client: FlaskClient): # noqa 87 | with client: 88 | result = client.put( 89 | f"/api/{BASE_ROUTE}/whatsit/123", 90 | json={"name": "New Whatsit", "purpose": "New purpose"}, 91 | ).get_json() 92 | expected = ( 93 | WhatsitSchema() 94 | .dump( 95 | Whatsit(whatsit_id=123, name="New Whatsit", purpose="New purpose") 96 | ) 97 | 98 | ) 99 | assert result == expected 100 | -------------------------------------------------------------------------------- /app/other_api/whatsit/interface.py: -------------------------------------------------------------------------------- 1 | from mypy_extensions import TypedDict 2 | 3 | 4 | class WhatsitInterface(TypedDict, total=False): 5 | whatsit_id: int 6 | name: str 7 | purpose: str 8 | -------------------------------------------------------------------------------- /app/other_api/whatsit/interface_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from .model import Whatsit 3 | from .interface import WhatsitInterface 4 | 5 | 6 | @fixture 7 | def interface() -> WhatsitInterface: 8 | return WhatsitInterface(whatsit_id=1, name="Test whatsit", purpose="Test purpose") 9 | 10 | 11 | def test_WhatsitInterface_create(interface: WhatsitInterface): 12 | assert interface 13 | 14 | 15 | def test_WhatsitInterface_works(interface: WhatsitInterface): 16 | whatsit = Whatsit(**interface) 17 | assert whatsit 18 | -------------------------------------------------------------------------------- /app/other_api/whatsit/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app import db # noqa 3 | from .interface import WhatsitInterface 4 | from typing import Any 5 | 6 | 7 | class Whatsit(db.Model): # type: ignore 8 | """A snazzy Whatsit""" 9 | 10 | __tablename__ = "whatsit" 11 | 12 | whatsit_id = Column(Integer(), primary_key=True) 13 | name = Column(String(255)) 14 | purpose = Column(String(255)) 15 | 16 | def update(self, changes: WhatsitInterface): 17 | for key, val in changes.items(): 18 | setattr(self, key, val) 19 | return self 20 | -------------------------------------------------------------------------------- /app/other_api/whatsit/model_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from flask_sqlalchemy import SQLAlchemy 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Whatsit 5 | 6 | 7 | @fixture 8 | def whatsit() -> Whatsit: 9 | return Whatsit(whatsit_id=1, name="Test whatsit", purpose="Test purpose") 10 | 11 | 12 | def test_Whatsit_create(whatsit: Whatsit): 13 | assert whatsit 14 | 15 | 16 | def test_Whatsit_retrieve(whatsit: Whatsit, db: SQLAlchemy): # noqa 17 | db.session.add(whatsit) 18 | db.session.commit() 19 | s = Whatsit.query.first() 20 | assert s.__dict__ == whatsit.__dict__ 21 | -------------------------------------------------------------------------------- /app/other_api/whatsit/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, Schema 2 | 3 | 4 | class WhatsitSchema(Schema): 5 | """Whatsit schema""" 6 | 7 | whatsitId = fields.Number(attribute="whatsit_id") 8 | name = fields.String(attribute="name") 9 | purpose = fields.String(attribute="purpose") 10 | -------------------------------------------------------------------------------- /app/other_api/whatsit/schema_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import Whatsit 4 | from .schema import WhatsitSchema 5 | from .interface import WhatsitInterface 6 | 7 | 8 | @fixture 9 | def schema() -> WhatsitSchema: 10 | return WhatsitSchema() 11 | 12 | 13 | def test_WhatsitSchema_create(schema: WhatsitSchema): 14 | assert schema 15 | 16 | 17 | def test_WhatsitSchema_works(schema: WhatsitSchema): 18 | params: WhatsitInterface = schema.load( 19 | {"whatsitId": "123", "name": "Test whatsit", "purpose": "Test purpose"} 20 | ) 21 | whatsit = Whatsit(**params) 22 | 23 | assert whatsit.whatsit_id == 123 24 | assert whatsit.name == "Test whatsit" 25 | assert whatsit.purpose == "Test purpose" 26 | -------------------------------------------------------------------------------- /app/other_api/whatsit/service.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from typing import List 3 | from .model import Whatsit 4 | from .interface import WhatsitInterface 5 | 6 | 7 | class WhatsitService: 8 | @staticmethod 9 | def get_all() -> List[Whatsit]: 10 | return Whatsit.query.all() 11 | 12 | @staticmethod 13 | def get_by_id(whatsit_id: int) -> Whatsit: 14 | return Whatsit.query.get(whatsit_id) 15 | 16 | @staticmethod 17 | def update(whatsit: Whatsit, Whatsit_change_updates: WhatsitInterface) -> Whatsit: 18 | whatsit.update(Whatsit_change_updates) 19 | db.session.commit() 20 | return whatsit 21 | 22 | @staticmethod 23 | def delete_by_id(whatsit_id: int) -> List[int]: 24 | whatsit = Whatsit.query.filter(Whatsit.whatsit_id == whatsit_id).first() 25 | if not whatsit: 26 | return [] 27 | db.session.delete(whatsit) 28 | db.session.commit() 29 | return [whatsit_id] 30 | 31 | @staticmethod 32 | def create(new_attrs: WhatsitInterface) -> Whatsit: 33 | new_whatsit = Whatsit(name=new_attrs["name"], purpose=new_attrs["purpose"]) 34 | 35 | db.session.add(new_whatsit) 36 | db.session.commit() 37 | 38 | return new_whatsit 39 | -------------------------------------------------------------------------------- /app/other_api/whatsit/service_test.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from typing import List 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Whatsit 5 | from .service import WhatsitService # noqa 6 | from .interface import WhatsitInterface 7 | 8 | 9 | def test_get_all(db: SQLAlchemy): # noqa 10 | yin: Whatsit = Whatsit(whatsit_id=1, name="Yin", purpose="thing 1") 11 | yang: Whatsit = Whatsit(whatsit_id=2, name="Yang", purpose="thing 2") 12 | db.session.add(yin) 13 | db.session.add(yang) 14 | db.session.commit() 15 | 16 | results: List[Whatsit] = WhatsitService.get_all() 17 | 18 | assert len(results) == 2 19 | assert yin in results and yang in results 20 | 21 | 22 | def test_update(db: SQLAlchemy): # noqa 23 | yin: Whatsit = Whatsit(whatsit_id=1, name="Yin", purpose="thing 1") 24 | 25 | db.session.add(yin) 26 | db.session.commit() 27 | updates: WhatsitInterface = dict(name="New Whatsit name") 28 | 29 | WhatsitService.update(yin, updates) 30 | 31 | result: Whatsit = Whatsit.query.get(yin.whatsit_id) 32 | assert result.name == "New Whatsit name" 33 | 34 | 35 | def test_delete_by_id(db: SQLAlchemy): # noqa 36 | yin: Whatsit = Whatsit(whatsit_id=1, name="Yin", purpose="thing 1") 37 | yang: Whatsit = Whatsit(whatsit_id=2, name="Yang", purpose="thing 2") 38 | db.session.add(yin) 39 | db.session.add(yang) 40 | db.session.commit() 41 | 42 | WhatsitService.delete_by_id(1) 43 | db.session.commit() 44 | 45 | results: List[Whatsit] = Whatsit.query.all() 46 | 47 | assert len(results) == 1 48 | assert yin not in results and yang in results 49 | 50 | 51 | def test_create(db: SQLAlchemy): # noqa 52 | 53 | yin: WhatsitInterface = dict(name="Fancy new whatsit", purpose="Fancy new purpose") 54 | WhatsitService.create(yin) 55 | results: List[Whatsit] = Whatsit.query.all() 56 | 57 | assert len(results) == 1 58 | 59 | for k in yin.keys(): 60 | assert getattr(results[0], k) == yin[k] 61 | -------------------------------------------------------------------------------- /app/routes.py: -------------------------------------------------------------------------------- 1 | def register_routes(api, app, root="api"): 2 | from app.widget import register_routes as attach_widget 3 | from app.fizz import register_routes as attach_fizz 4 | from app.other_api import register_routes as attach_other_api 5 | from app.third_party.app import create_bp 6 | 7 | # Add routes 8 | attach_widget(api, app) 9 | attach_fizz(api, app) 10 | attach_other_api(api, app) 11 | app.register_blueprint(create_bp(), url_prefix="/third_party") 12 | -------------------------------------------------------------------------------- /app/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flask_api_example/744239535f1b4a7d122491693ac49590ef761fef/app/shared/__init__.py -------------------------------------------------------------------------------- /app/shared/query/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flask_api_example/744239535f1b4a7d122491693ac49590ef761fef/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/flask_api_example/744239535f1b4a7d122491693ac49590ef761fef/app/test/__init__.py -------------------------------------------------------------------------------- /app/test/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from app import create_app 4 | 5 | 6 | @pytest.fixture 7 | def app(): 8 | return create_app("test") 9 | 10 | 11 | @pytest.fixture 12 | def client(app): 13 | return app.test_client() 14 | 15 | 16 | @pytest.fixture 17 | def db(app): 18 | from app import db 19 | 20 | with app.app_context(): 21 | db.drop_all() 22 | db.create_all() 23 | yield db 24 | db.drop_all() 25 | db.session.commit() 26 | -------------------------------------------------------------------------------- /app/third_party/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flask_api_example/744239535f1b4a7d122491693ac49590ef761fef/app/third_party/__init__.py -------------------------------------------------------------------------------- /app/third_party/app/__init__.py: -------------------------------------------------------------------------------- 1 | def create_bp(env=None): 2 | from flask import Blueprint, jsonify 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_restx import Api, Resource, Namespace 5 | 6 | bp = Blueprint("Example third party API", __name__) 7 | api = Api(bp, title="Flaskerific API", version="0.1.0") 8 | ns = Namespace("Third party hello world API") 9 | 10 | @ns.route("/") 11 | class ExampleResource(Resource): 12 | def get(self): 13 | return "I'm a third party API!" 14 | 15 | api.add_namespace(ns, path="/hello_world") 16 | return bp 17 | -------------------------------------------------------------------------------- /app/third_party/app/app-test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flask_api_example/744239535f1b4a7d122491693ac49590ef761fef/app/third_party/app/app-test.db -------------------------------------------------------------------------------- /app/third_party/app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Type 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class BaseConfig: 8 | CONFIG_NAME = "base" 9 | USE_MOCK_EQUIVALENCY = False 10 | DEBUG = False 11 | SQLALCHEMY_TRACK_MODIFICATIONS = False 12 | 13 | 14 | class DevelopmentConfig(BaseConfig): 15 | CONFIG_NAME = "dev" 16 | SECRET_KEY = os.getenv( 17 | "DEV_SECRET_KEY", "You can't see California without Marlon Widgeto's eyes" 18 | ) 19 | DEBUG = True 20 | SQLALCHEMY_TRACK_MODIFICATIONS = False 21 | TESTING = False 22 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-dev.db".format(basedir) 23 | 24 | 25 | class TestingConfig(BaseConfig): 26 | CONFIG_NAME = "test" 27 | SECRET_KEY = os.getenv("TEST_SECRET_KEY", "Thanos did nothing wrong") 28 | DEBUG = True 29 | SQLALCHEMY_TRACK_MODIFICATIONS = False 30 | TESTING = True 31 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-test.db".format(basedir) 32 | 33 | 34 | class ProductionConfig(BaseConfig): 35 | CONFIG_NAME = "prod" 36 | SECRET_KEY = os.getenv("PROD_SECRET_KEY", "I'm Ron Burgundy?") 37 | DEBUG = False 38 | SQLALCHEMY_TRACK_MODIFICATIONS = False 39 | TESTING = False 40 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-prod.db".format(basedir) 41 | 42 | 43 | EXPORT_CONFIGS: List[Type[BaseConfig]] = [ 44 | DevelopmentConfig, 45 | TestingConfig, 46 | ProductionConfig, 47 | ] 48 | config_by_name = {cfg.CONFIG_NAME: cfg for cfg in EXPORT_CONFIGS} 49 | -------------------------------------------------------------------------------- /app/third_party/app/routes.py: -------------------------------------------------------------------------------- 1 | def register_routes(api, app, root="api"): 2 | from app.widget import register_routes as attach_widget 3 | from app.fizz import register_routes as attach_fizz 4 | from app.other_api import register_routes as attach_other_api 5 | 6 | # Add routes 7 | attach_widget(api, app) 8 | attach_fizz(api, app) 9 | attach_other_api(api, app) 10 | -------------------------------------------------------------------------------- /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(api, app, root="api"): 8 | from .controller import api as widget_api 9 | 10 | api.add_namespace(widget_api, path=f"/{root}/{BASE_ROUTE}") 11 | -------------------------------------------------------------------------------- /app/widget/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_accepts import accepts, responds 3 | from flask_restx import Namespace, Resource 4 | from flask.wrappers import Response 5 | from typing import List 6 | 7 | from .schema import WidgetSchema 8 | from .service import WidgetService 9 | from .model import Widget 10 | from .interface import WidgetInterface 11 | 12 | api = Namespace("Widget", description="Single namespace, single entity") # noqa 13 | 14 | 15 | @api.route("/") 16 | class WidgetResource(Resource): 17 | """Widgets""" 18 | 19 | @responds(schema=WidgetSchema, many=True) 20 | def get(self) -> List[Widget]: 21 | """Get all Widgets""" 22 | 23 | return WidgetService.get_all() 24 | 25 | @accepts(schema=WidgetSchema, api=api) 26 | @responds(schema=WidgetSchema) 27 | def post(self) -> Widget: 28 | """Create a Single Widget""" 29 | 30 | return WidgetService.create(request.parsed_obj) 31 | 32 | 33 | @api.route("/") 34 | @api.param("widgetId", "Widget database ID") 35 | class WidgetIdResource(Resource): 36 | @responds(schema=WidgetSchema) 37 | def get(self, widgetId: int) -> Widget: 38 | """Get Single Widget""" 39 | 40 | return WidgetService.get_by_id(widgetId) 41 | 42 | def delete(self, widgetId: int) -> Response: 43 | """Delete Single Widget""" 44 | from flask import jsonify 45 | 46 | id = WidgetService.delete_by_id(widgetId) 47 | return jsonify(dict(status="Success", id=id)) 48 | 49 | @accepts(schema=WidgetSchema, api=api) 50 | @responds(schema=WidgetSchema) 51 | def put(self, widgetId: int) -> Widget: 52 | """Update Single Widget""" 53 | 54 | changes: WidgetInterface = request.parsed_obj 55 | Widget = WidgetService.get_by_id(widgetId) 56 | return WidgetService.update(Widget, changes) 57 | -------------------------------------------------------------------------------- /app/widget/controller_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | from flask.testing import FlaskClient 3 | 4 | from app.test.fixtures import client, app # noqa 5 | from .service import WidgetService 6 | from .schema import WidgetSchema 7 | from .model import Widget 8 | from .interface import WidgetInterface 9 | from . import BASE_ROUTE 10 | 11 | 12 | def make_widget( 13 | id: int = 123, name: str = "Test widget", purpose: str = "Test purpose" 14 | ) -> Widget: 15 | return Widget(widget_id=id, name=name, purpose=purpose) 16 | 17 | 18 | class TestWidgetResource: 19 | @patch.object( 20 | WidgetService, 21 | "get_all", 22 | lambda: [ 23 | make_widget(123, name="Test Widget 1"), 24 | make_widget(456, name="Test Widget 2"), 25 | ], 26 | ) 27 | def test_get(self, client: FlaskClient): # noqa 28 | with client: 29 | results = client.get(f"/api/{BASE_ROUTE}", follow_redirects=True).get_json() 30 | expected = ( 31 | WidgetSchema(many=True) 32 | .dump( 33 | [ 34 | make_widget(123, name="Test Widget 1"), 35 | make_widget(456, name="Test Widget 2"), 36 | ] 37 | ) 38 | 39 | ) 40 | for r in results: 41 | assert r in expected 42 | 43 | @patch.object( 44 | WidgetService, "create", lambda create_request: Widget(**create_request) 45 | ) 46 | def test_post(self, client: FlaskClient): # noqa 47 | with client: 48 | 49 | payload = dict(name="Test widget", purpose="Test purpose") 50 | result = client.post(f"/api/{BASE_ROUTE}/", json=payload).get_json() 51 | expected = ( 52 | WidgetSchema() 53 | .dump(Widget(name=payload["name"], purpose=payload["purpose"])) 54 | 55 | ) 56 | assert result == expected 57 | 58 | 59 | def fake_update(widget: Widget, changes: WidgetInterface) -> Widget: 60 | # To fake an update, just return a new object 61 | updated_Widget = Widget( 62 | widget_id=widget.widget_id, name=changes["name"], purpose=changes["purpose"] 63 | ) 64 | return updated_Widget 65 | 66 | 67 | class TestWidgetIdResource: 68 | @patch.object(WidgetService, "get_by_id", lambda id: make_widget(id=id)) 69 | def test_get(self, client: FlaskClient): # noqa 70 | with client: 71 | result = client.get(f"/api/{BASE_ROUTE}/123").get_json() 72 | expected = make_widget(id=123) 73 | print(f"result = ", result) 74 | assert result["widgetId"] == expected.widget_id 75 | 76 | @patch.object(WidgetService, "delete_by_id", lambda id: id) 77 | def test_delete(self, client: FlaskClient): # noqa 78 | with client: 79 | result = client.delete(f"/api/{BASE_ROUTE}/123").get_json() 80 | expected = dict(status="Success", id=123) 81 | assert result == expected 82 | 83 | @patch.object(WidgetService, "get_by_id", lambda id: make_widget(id=id)) 84 | @patch.object(WidgetService, "update", fake_update) 85 | def test_put(self, client: FlaskClient): # noqa 86 | with client: 87 | result = client.put( 88 | f"/api/{BASE_ROUTE}/123", 89 | json={"name": "New Widget", "purpose": "New purpose"}, 90 | ).get_json() 91 | expected = ( 92 | WidgetSchema() 93 | .dump(Widget(widget_id=123, name="New Widget", purpose="New purpose")) 94 | 95 | ) 96 | assert result == expected 97 | -------------------------------------------------------------------------------- /app/widget/interface.py: -------------------------------------------------------------------------------- 1 | from mypy_extensions import TypedDict 2 | 3 | 4 | class WidgetInterface(TypedDict, total=False): 5 | widget_id: int 6 | name: str 7 | purpose: str 8 | -------------------------------------------------------------------------------- /app/widget/interface_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from .model import Widget 3 | from .interface import WidgetInterface 4 | 5 | 6 | @fixture 7 | def interface() -> WidgetInterface: 8 | return WidgetInterface(widget_id=1, name="Test widget", purpose="Test purpose") 9 | 10 | 11 | def test_WidgetInterface_create(interface: WidgetInterface): 12 | assert interface 13 | 14 | 15 | def test_WidgetInterface_works(interface: WidgetInterface): 16 | widget = Widget(**interface) 17 | assert widget 18 | -------------------------------------------------------------------------------- /app/widget/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app import db # noqa 3 | from .interface import WidgetInterface 4 | 5 | 6 | class Widget(db.Model): # type: ignore 7 | """A snazzy Widget""" 8 | 9 | __tablename__ = "widget" 10 | 11 | widget_id = Column(Integer(), primary_key=True) 12 | name = Column(String(255)) 13 | purpose = Column(String(255)) 14 | 15 | def update(self, changes: WidgetInterface): 16 | for key, val in changes.items(): 17 | setattr(self, key, val) 18 | return self 19 | -------------------------------------------------------------------------------- /app/widget/model_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from flask_sqlalchemy import SQLAlchemy 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Widget 5 | 6 | 7 | @fixture 8 | def widget() -> Widget: 9 | return Widget(widget_id=1, name="Test widget", purpose="Test purpose") 10 | 11 | 12 | def test_Widget_create(widget: Widget): 13 | assert widget 14 | 15 | 16 | def test_Widget_retrieve(widget: Widget, db: SQLAlchemy): # noqa 17 | db.session.add(widget) 18 | db.session.commit() 19 | s = Widget.query.first() 20 | assert s.__dict__ == widget.__dict__ 21 | -------------------------------------------------------------------------------- /app/widget/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, Schema 2 | 3 | 4 | class WidgetSchema(Schema): 5 | """Widget schema""" 6 | 7 | widgetId = fields.Number(attribute="widget_id") 8 | name = fields.String(attribute="name") 9 | purpose = fields.String(attribute="purpose") 10 | -------------------------------------------------------------------------------- /app/widget/schema_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import Widget 4 | from .schema import WidgetSchema 5 | from .interface import WidgetInterface 6 | 7 | 8 | @fixture 9 | def schema() -> WidgetSchema: 10 | return WidgetSchema() 11 | 12 | 13 | def test_WidgetSchema_create(schema: WidgetSchema): 14 | assert schema 15 | 16 | 17 | def test_WidgetSchema_works(schema: WidgetSchema): 18 | params: WidgetInterface = schema.load( 19 | {"widgetId": "123", "name": "Test widget", "purpose": "Test purpose"} 20 | ) 21 | widget = Widget(**params) 22 | 23 | assert widget.widget_id == 123 24 | assert widget.name == "Test widget" 25 | assert widget.purpose == "Test purpose" 26 | -------------------------------------------------------------------------------- /app/widget/service.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from typing import List 3 | from .model import Widget 4 | from .interface import WidgetInterface 5 | 6 | 7 | class WidgetService: 8 | @staticmethod 9 | def get_all() -> List[Widget]: 10 | return Widget.query.all() 11 | 12 | @staticmethod 13 | def get_by_id(widget_id: int) -> Widget: 14 | return Widget.query.get(widget_id) 15 | 16 | @staticmethod 17 | def update(widget: Widget, Widget_change_updates: WidgetInterface) -> Widget: 18 | widget.update(Widget_change_updates) 19 | db.session.commit() 20 | return widget 21 | 22 | @staticmethod 23 | def delete_by_id(widget_id: int) -> List[int]: 24 | widget = Widget.query.filter(Widget.widget_id == widget_id).first() 25 | if not widget: 26 | return [] 27 | db.session.delete(widget) 28 | db.session.commit() 29 | return [widget_id] 30 | 31 | @staticmethod 32 | def create(new_attrs: WidgetInterface) -> Widget: 33 | new_widget = Widget(name=new_attrs["name"], purpose=new_attrs["purpose"]) 34 | 35 | db.session.add(new_widget) 36 | db.session.commit() 37 | 38 | return new_widget 39 | -------------------------------------------------------------------------------- /app/widget/service_test.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from typing import List 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Widget 5 | from .service import WidgetService # noqa 6 | from .interface import WidgetInterface 7 | 8 | 9 | def test_get_all(db: SQLAlchemy): # noqa 10 | yin: Widget = Widget(widget_id=1, name="Yin", purpose="thing 1") 11 | yang: Widget = Widget(widget_id=2, name="Yang", purpose="thing 2") 12 | db.session.add(yin) 13 | db.session.add(yang) 14 | db.session.commit() 15 | 16 | results: List[Widget] = WidgetService.get_all() 17 | 18 | assert len(results) == 2 19 | assert yin in results and yang in results 20 | 21 | 22 | def test_update(db: SQLAlchemy): # noqa 23 | yin: Widget = Widget(widget_id=1, name="Yin", purpose="thing 1") 24 | 25 | db.session.add(yin) 26 | db.session.commit() 27 | updates: WidgetInterface = dict(name="New Widget name") 28 | 29 | WidgetService.update(yin, updates) 30 | 31 | result: Widget = Widget.query.get(yin.widget_id) 32 | assert result.name == "New Widget name" 33 | 34 | 35 | def test_delete_by_id(db: SQLAlchemy): # noqa 36 | yin: Widget = Widget(widget_id=1, name="Yin", purpose="thing 1") 37 | yang: Widget = Widget(widget_id=2, name="Yang", purpose="thing 2") 38 | db.session.add(yin) 39 | db.session.add(yang) 40 | db.session.commit() 41 | 42 | WidgetService.delete_by_id(1) 43 | db.session.commit() 44 | 45 | results: List[Widget] = Widget.query.all() 46 | 47 | assert len(results) == 1 48 | assert yin not in results and yang in results 49 | 50 | 51 | def test_create(db: SQLAlchemy): # noqa 52 | 53 | yin: WidgetInterface = dict(name="Fancy new widget", purpose="Fancy new purpose") 54 | WidgetService.create(yin) 55 | results: List[Widget] = Widget.query.all() 56 | 57 | assert len(results) == 1 58 | 59 | for k in yin.keys(): 60 | assert getattr(results[0], k) == yin[k] 61 | -------------------------------------------------------------------------------- /commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .seed_command import SeedCommand 2 | -------------------------------------------------------------------------------- /commands/seed_command.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import pandas as pd 3 | import numpy as np 4 | from flask_script import Command 5 | 6 | from app import db 7 | from app.widget import Widget 8 | from app.fizz.fizzbaz import Fizzbaz 9 | from app.fizz.fizzbar import Fizzbar 10 | from app.other_api.doodad import Doodad 11 | from app.other_api.whatsit import Whatsit 12 | 13 | 14 | def seed_things(): 15 | classes = [Widget, Fizzbaz, Fizzbar, Doodad, Whatsit] 16 | for klass in classes: 17 | seed_thing(klass) 18 | 19 | 20 | def seed_thing(cls): 21 | things = [ 22 | {"name": "Pizza Slicer", "purpose": "Cut delicious pizza"}, 23 | {"name": "Rolling Pin", "purpose": "Roll delicious pizza"}, 24 | {"name": "Pizza Oven", "purpose": "Bake delicious pizza"}, 25 | ] 26 | db.session.bulk_insert_mappings(cls, things) 27 | 28 | 29 | class SeedCommand(Command): 30 | """ Seed the DB.""" 31 | 32 | def run(self): 33 | if ( 34 | input( 35 | "Are you sure you want to drop all tables and recreate? (y/N)\n" 36 | ).lower() 37 | == "y" 38 | ): 39 | print("Dropping tables...") 40 | db.drop_all() 41 | db.create_all() 42 | seed_things() 43 | db.session.commit() 44 | print("DB successfully seeded.") 45 | -------------------------------------------------------------------------------- /docs/site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flask_api_example/744239535f1b4a7d122491693ac49590ef761fef/docs/site.png -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask_script import Manager 3 | 4 | from app import create_app, db 5 | from commands.seed_command import SeedCommand 6 | 7 | env = os.getenv("FLASK_ENV") or "test" 8 | print(f"Active environment: * {env} *") 9 | app = create_app(env) 10 | 11 | manager = Manager(app) 12 | app.app_context().push() 13 | manager.add_command("seed_db", SeedCommand) 14 | 15 | 16 | @manager.command 17 | def run(): 18 | app.run() 19 | 20 | 21 | @manager.command 22 | def init_db(): 23 | print("Creating all resources.") 24 | db.create_all() 25 | 26 | 27 | @manager.command 28 | def drop_all(): 29 | if input("Are you sure you want to drop all tables? (y/N)\n").lower() == "y": 30 | print("Dropping tables...") 31 | db.drop_all() 32 | 33 | 34 | if __name__ == "__main__": 35 | manager.run() 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==7.0.0 2 | attrs==19.1.0 3 | Click==7.0 4 | Flask==1.1.1 5 | flask-accepts==0.10.0 6 | Flask-RESTful==0.3.7 7 | flask-restplus==0.12.1 8 | Flask-Script==2.0.6 9 | Flask-SQLAlchemy==2.4.0 10 | itsdangerous==1.1.0 11 | Jinja2==2.10.1 12 | jsonschema==3.0.2 13 | MarkupSafe==1.1.1 14 | marshmallow==3.2.0 15 | mypy-extensions==0.4.1 16 | numpy==1.17.0 17 | pandas==0.25.0 18 | pyrsistent==0.15.4 19 | python-dateutil==2.8.0 20 | pytz==2019.2 21 | six==1.12.0 22 | SQLAlchemy==1.3.6 23 | Werkzeug==0.15.5 24 | flask-restx==0.1.0 25 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from app import create_app 4 | 5 | app = create_app(os.getenv("FLASK_ENV") or "test") 6 | if __name__ == "__main__": 7 | app.run(debug=True) 8 | --------------------------------------------------------------------------------