├── .gitignore ├── CODEOWNERS ├── LICENSE ├── Procfile ├── README.md ├── app ├── __init__.py ├── api.py ├── database.py ├── generators.py ├── seeds.py ├── utilities.py └── validation.py ├── pull_request_template.md ├── requirements.txt └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | venv/ 3 | .venv 4 | env/ 5 | ENV/ 6 | env.bak/ 7 | venv.bak/ 8 | *.yml 9 | .idea/ 10 | Pipfile 11 | Pipfile.lock 12 | .vscode/ 13 | 14 | # Python generated files 15 | **/*.py[cod] 16 | **/*$py.class 17 | **/__pycache__/ 18 | 19 | # Elastic Beanstalk Files 20 | .elasticbeanstalk/* 21 | !.elasticbeanstalk/*.cfg.yml 22 | !.elasticbeanstalk/*.global.yml 23 | 24 | **/.DS_Store 25 | /notebooks/.ipynb_checkpoints/ 26 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @BrokenShell 2 | @paulstgermain 3 | @ashtilawat23 4 | @jinjahninjah 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bloom Institute of Technology 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app.api:API -w 4 -k uvicorn.workers.UvicornWorker 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BloomTech Labs Data Science API Template 2 | 3 | ## Tech Stack 4 | - Logic: Python 5 | - API Framework: FastAPI 6 | - Validation: Pydantic 7 | - Database: MongoDB 8 | - Unit Testing: DocTest 9 | 10 | 11 | ## API Structure 12 | All endpoints must return JSON compatible data. 13 | 14 | - API Root `/` Swagger Docs 15 | - API Version `/version` () -> String 16 | - HTTP Method: GET 17 | - Create User `/create-user` (User) -> Bool 18 | - HTTP Method: POST 19 | - Read Users `/read-users` (Query) -> Array[User] 20 | - HTTP Method: PUT 21 | - Update Users `/update-users` (Query, Update) -> Bool 22 | - HTTP Method: PATCH 23 | - Delete Users `/delete-users` (Query) -> Bool 24 | - HTTP Method: DELETE 25 | 26 | 27 | ## App Structure 28 | - `/app/` Application Package 29 | - `__init__` 30 | - `api.py` API File 31 | - `database.py` Database Interface 32 | - `generators.py` Random Generators 33 | - `seeds.py` DB Seed Script 34 | - `utilities.py` General Tools 35 | - `validation.py` Data Validation Schema 36 | - `.env` Environment Variables 37 | - `Procfile` Server Run Script 38 | - `requirements.txt` Dependencies 39 | - `run.sh` Local Run Script 40 | 41 | 42 | ## Data Schemas 43 | The following classes are used to validate incoming data to the API. 44 | ### User 45 | - `name` Required String (maxLength: 128 minLength: 3) 46 | - `age` Required Integer (maximum: 120, minimum: 1) 47 | - `email` Required String(EmailStr) 48 | - `active` Optional Boolean 49 | - `score` Required Float (maximum: 1, minimum: 0) 50 | 51 | ### UserQuery 52 | - `name` Optional String (maxLength: 128 minLength: 3) 53 | - `age` Optional Integer (maximum: 120, minimum: 1) 54 | - `email` Optional String(EmailStr) 55 | - `active` Optional Boolean 56 | - `score` Optional Float (maximum: 1, minimum: 0) 57 | 58 | ### UserUpdate 59 | - `name` Optional String (maxLength: 128 minLength: 3) 60 | - `age` Optional Integer (maximum: 120, minimum: 1) 61 | - `email` Optional String(EmailStr) 62 | - `active` Optional Boolean 63 | - `score` Optional Float (maximum: 1, minimum: 0) 64 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BloomTech-Labs/labs-ds-starter/57417b9674f078877bb5ae35f4bccf6a2a7b9296/app/__init__.py -------------------------------------------------------------------------------- /app/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from app.database import MongoDB 5 | from app.validation import User, UserQuery, UserUpdate 6 | from app.validation import default_query, default_update, default_user 7 | 8 | 9 | API = FastAPI( 10 | title="BloomTech Labs DS API Template", 11 | version="0.0.1", 12 | docs_url="/", 13 | ) 14 | API.db = MongoDB() 15 | API.add_middleware( 16 | CORSMiddleware, 17 | allow_origins=["*"], 18 | allow_credentials=True, 19 | allow_methods=["*"], 20 | allow_headers=["*"], 21 | ) 22 | 23 | 24 | @API.get("/version") 25 | async def api_version(): 26 | """ Returns current API version 27 | @return: String Version """ 28 | return API.version 29 | 30 | 31 | @API.post("/create-user") 32 | async def create_user(user: User = default_user): 33 | """ Creates one user 34 | @param user: User 35 | @return: Boolean Success """ 36 | return API.db.create(user.dict(exclude_none=True)) 37 | 38 | 39 | @API.put("/read-users") 40 | async def read_users(user_query: UserQuery = default_query): 41 | """ Returns array of all matched users 42 | @param user_query: UserQuery 43 | @return: Array[User] """ 44 | return API.db.read(user_query.dict(exclude_none=True)) 45 | 46 | 47 | @API.patch("/update-users") 48 | async def update_users(user_query: UserQuery = default_query, 49 | user_update: UserUpdate = default_update): 50 | """ Updates all matched users 51 | @param user_query: UserQuery 52 | @param user_update: UserUpdate 53 | @return: Boolean Success """ 54 | return API.db.update( 55 | user_query.dict(exclude_none=True), 56 | user_update.dict(exclude_none=True), 57 | ) 58 | 59 | 60 | @API.delete("/delete-users") 61 | async def delete_users(user_query: UserQuery = default_query): 62 | """ Deletes all matched users 63 | @param user_query: UserQuery 64 | @return: Boolean Success """ 65 | return API.db.delete(user_query.dict(exclude_none=True)) 66 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | """ MongoDB Interface """ 2 | from os import getenv 3 | from typing import Optional, List, Dict, Iterable 4 | 5 | from pymongo import MongoClient 6 | from pymongo.collection import Collection 7 | from dotenv import load_dotenv 8 | 9 | 10 | class MongoDB: 11 | """ DocTests 12 | >>> db = MongoDB() 13 | >>> db.create({"Test": True}) 14 | True 15 | >>> db.read({"Test": True}) 16 | [{'Test': True}] 17 | >>> db.update({"Test": True}, {"New Field": True}) 18 | True 19 | >>> db.read({"Test": True}) 20 | [{'Test': True, 'New Field': True}] 21 | >>> db.delete({"Test": True}) 22 | True 23 | >>> db.read({"Test": True}) 24 | [] 25 | """ 26 | load_dotenv() 27 | 28 | def _collection(self) -> Collection: 29 | """ Connects to the MongoDB Collection 30 | @return: Collection """ 31 | return MongoClient( 32 | getenv("MONGO_URL") 33 | )[getenv("MONGO_DB")][getenv("MONGO_COLLECTION")] 34 | 35 | def create(self, data: Dict) -> bool: 36 | """ Creates one record in the Collection 37 | @param data: Dict 38 | @return: Boolean Success """ 39 | return self._collection().insert_one(dict(data)).acknowledged 40 | 41 | def create_many(self, data: Iterable[Dict]) -> bool: 42 | """ Creates many records in the Collection 43 | @param data: Iterable[Dict] 44 | @return: Boolean Success """ 45 | return self._collection().insert_many(map(dict, data)).acknowledged 46 | 47 | def read(self, query: Optional[Dict] = None) -> List[Dict]: 48 | """ Returns a list of records from the collection 49 | @param query: Dict 50 | @return: List[Dict] """ 51 | return list(self._collection().find(query, {"_id": False})) 52 | 53 | def update(self, query: Dict, update_data: Dict) -> bool: 54 | """ Updates the matched records with new data 55 | @param query: Dict 56 | @param update_data: Dict 57 | @return: Boolean Success """ 58 | return self._collection().update_many( 59 | query, {"$set": update_data} 60 | ).acknowledged 61 | 62 | def delete(self, query: Dict) -> bool: 63 | """ Deletes the matched records 64 | @param query: Dict 65 | @return: Boolean Success """ 66 | return self._collection().delete_many(query).acknowledged 67 | -------------------------------------------------------------------------------- /app/generators.py: -------------------------------------------------------------------------------- 1 | """ Random Data Generators """ 2 | from random import normalvariate 3 | from math import ceil 4 | 5 | from app.utilities import percent_true, clamp 6 | from app.utilities import random_name, random_email 7 | from app.validation import User 8 | 9 | 10 | class RandomUser: 11 | """ Random User Generator Class 12 | 13 | DocTests - Asserts 1000 RandomUser() are valid Users() 14 | >>> assert all(User(**vars(RandomUser())) for _ in range(1000)) 15 | """ 16 | 17 | def __init__(self): 18 | """ Creates a Random User """ 19 | self.name = random_name(percent_male=66) 20 | self.age = clamp(ceil(normalvariate(24, 5)), 1, 120) 21 | self.email = random_email(self.name) 22 | self.active = percent_true(43) 23 | self.score = clamp(normalvariate(0.5, 0.03125), 0.0, 1.0) 24 | -------------------------------------------------------------------------------- /app/seeds.py: -------------------------------------------------------------------------------- 1 | """ Run this script to reset and reseed the database """ 2 | from app.database import MongoDB 3 | from app.generators import RandomUser 4 | 5 | 6 | def reset_database(mongo: MongoDB) -> bool: 7 | """ Resets the database by deleting all records in the collection 8 | @param mongo: MongoDB Interface 9 | @return: Boolean Success """ 10 | return mongo.delete({}) 11 | 12 | 13 | def seed_database(mongo: MongoDB, count: int) -> bool: 14 | """ Seeds the collection with random data 15 | @param mongo: MongoDB Interface 16 | @param count: Integer, number of records to create 17 | @return: Boolean Success """ 18 | return mongo.create_many(vars(RandomUser()) for _ in range(count)) 19 | 20 | 21 | if __name__ == '__main__': 22 | db = MongoDB() 23 | reset_database(db) 24 | seed_database(db, 1000) 25 | -------------------------------------------------------------------------------- /app/utilities.py: -------------------------------------------------------------------------------- 1 | """ General Utilities """ 2 | from typing import Union 3 | from random import randint, choice 4 | 5 | male_first_names = [ 6 | 'Brycen', 'Cash', 'Deandre', 'Kase', 'Lochlan', 'Ramon', 'Darian', 'Shiloh', 7 | 'Keith', 'Finnley', 'Kellan', 'Erik', 'Lance', 'Jadiel', 'Ray', 'Izaiah', 8 | 'Dennis', 'Aries', 'Leo', 'Elliot', 'Jorge', 'Edwin', 'Phillip', 'Richard', 9 | 'Benjamin', 'Tucker', 'Pierce', 'Dax', 'Kameron', 'George', 'Kiaan', 10 | 'Camilo', 'Kody', 'Colin', 'Alberto', 'Finley', 'Kamden', 'Ace', 'Zyair', 11 | 'Byron', 'Rory', 'Cassius', 'Felix', 'Jesus', 'Aarav', 'Aaron', 'Giovanni', 12 | 'Blaze', 'Korbyn', 'Jonathan', 'Robin', 'Christopher', 'Muhammad', 'Cullen', 13 | ] 14 | 15 | female_first_names = [ 16 | 'Ezra', 'Penny', 'Madison', 'Elaina', 'Lennox', 'Zola', 'Briella', 'Opal', 17 | 'Colette', 'Jaelynn', 'Ivory', 'Gemma', 'Loretta', 'Francesca', 'Sylvia', 18 | 'Madeline', 'Yara', 'Mabel', 'Sloane', 'Vera', 'Dorothy', 'Scarlette', 19 | 'Amira', 'June', 'Raelyn', 'Drew', 'Analia', 'Madelyn', 'Haisley', 'Alaina', 20 | 'Cecilia', 'Megan', 'Veda', 'Kinsley', 'Oakley', 'Cataleya', 'Zaria', 21 | 'Kinley', 'Kailey', 'Maeve', 'Hallie', 'Giana', 'Henley', 'Matilda', 22 | 'Jazmin', 'Rosalyn', 'Ellie', 'Ana', 'Savanna', 'Kimber', 'Reign', 'Flora', 23 | ] 24 | 25 | last_names = [ 26 | 'Stewart', 'Rogers', 'Miller', 'Robinson', 'Martinez', 'Wood', 'Kim', 27 | 'Myers', 'Smith', 'Evans', 'James', 'Campbell', 'Turner', 'Perez', 28 | 'Williams', 'Long', 'Edwards', 'Peterson', 'Alvarez', 'Ortiz', 'Nguyen', 29 | 'Roberts', 'Wilson', 'Lewis', 'Howard', 'Cook', 'Jackson', 'Walker', 30 | 'Clark', 'Martin', 'Brown', 'Johnson', 'Gomez', 'Patel', 'Cruz', 'Reyes', 31 | 'Richardson', 'Thompson', 'King', 'Ramirez', 'Adams', 'White', 'Parker', 32 | 'Sanchez', 'Brooks', 'Scott', 'Sanders', 'Castillo', 'Jones', 'Anderson', 33 | ] 34 | 35 | Number = Union[int, float] 36 | 37 | 38 | def clamp(target: Number, low_limit: Number, hi_limit: Number) -> Number: 39 | """ Clamps input target into the range [low_limit, high_limit] 40 | 41 | @param target: Number 42 | @param low_limit: Number, must be <= hi_limit 43 | @param hi_limit: Number, must be >= low_limit 44 | @return: Number in range [low_limit, high_limit] 45 | 46 | DocTests 47 | >>> clamp(10, 1, 100) 48 | 10 49 | >>> clamp(1, 10, 20) 50 | 10 51 | >>> clamp(100, 1, 10) 52 | 10 53 | """ 54 | return min(max(target, low_limit), hi_limit) 55 | 56 | 57 | def percent_true(percent: int) -> bool: 58 | return randint(1, 100) <= percent 59 | 60 | 61 | def random_name(percent_male: int = 50) -> str: 62 | if percent_true(percent_male): 63 | first_name = choice(male_first_names) 64 | else: 65 | first_name = choice(female_first_names) 66 | last_name = choice(last_names) 67 | return f"{first_name} {last_name}" 68 | 69 | 70 | def random_email(name: str) -> str: 71 | return f"{name.replace(' ', '.').lower()}@gmail.com" 72 | -------------------------------------------------------------------------------- /app/validation.py: -------------------------------------------------------------------------------- 1 | """ Data Validation Schema """ 2 | from typing import Optional 3 | 4 | from pydantic import BaseModel, Extra, constr, conint, confloat, EmailStr 5 | 6 | 7 | class User(BaseModel): 8 | name: constr(min_length=3, max_length=128) 9 | age: conint(ge=1, le=120) 10 | email: EmailStr 11 | active: Optional[bool] 12 | score: confloat(ge=0, le=1) 13 | 14 | class Config: 15 | extra = Extra.forbid 16 | 17 | 18 | class UserQuery(BaseModel): 19 | name: Optional[constr(max_length=128)] 20 | age: Optional[conint(ge=1, le=120)] 21 | email: Optional[EmailStr] 22 | active: Optional[bool] 23 | score: Optional[confloat(ge=0, le=1)] 24 | 25 | class Config: 26 | extra = Extra.forbid 27 | 28 | 29 | class UserUpdate(BaseModel): 30 | name: Optional[constr(max_length=128)] 31 | age: Optional[conint(ge=1, le=120)] 32 | email: Optional[EmailStr] 33 | active: Optional[bool] 34 | score: Optional[confloat(ge=0, le=1)] 35 | 36 | class Config: 37 | extra = Extra.forbid 38 | 39 | 40 | default_user = User( 41 | name="John Smith", 42 | age=42, 43 | email="john.smith@gmail.com", 44 | active=False, 45 | score=0.5, 46 | ) 47 | default_query = UserQuery( 48 | email="john.smith@gmail.com", 49 | ) 50 | default_update = UserUpdate( 51 | active=True, 52 | score=0.125, 53 | ) 54 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix 12 | - [ ] New feature 13 | - [ ] This change requires a documentation update 14 | 15 | ## Checklist: 16 | 17 | - [ ] My code follows PEP8 style guide 18 | - [ ] I have removed unnecessary print statements from my code 19 | - [ ] I have made corresponding changes to the documentation if necessary 20 | - [ ] My changes generate no errors 21 | - [ ] No commented-out code 22 | - [ ] Size of pull request kept to a minimum 23 | - [ ] Pull request description clearly describes changes made & motivations for said changes 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dnspython 2 | fastapi 3 | gunicorn 4 | pymongo[srv] 5 | pydantic[email] 6 | python-dotenv 7 | python-multipart 8 | uvicorn[standard] 9 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | python -m uvicorn app.api:API --reload 2 | --------------------------------------------------------------------------------