├── logs └── README.md ├── runtime.txt ├── scripts ├── formatter.sh └── launch.sh ├── Procfile ├── requirements.txt ├── docker-compose.yml ├── apis ├── models │ ├── base.py │ ├── iris.py │ └── house.py └── v1 │ ├── boston.py │ └── iris.py ├── Dockerfile ├── .github └── workflows │ ├── dockerimage.yml │ └── pythonapp.yml ├── core ├── datasets.py └── trainer.py ├── main.py ├── README.md └── .gitignore /logs/README.md: -------------------------------------------------------------------------------- 1 | # Logs -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.6 -------------------------------------------------------------------------------- /scripts/formatter.sh: -------------------------------------------------------------------------------- 1 | black --line-length 80 . -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn main:app --host=0.0.0.0 --port=${PORT:-5000} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.63.0 2 | flake8==3.8.4 3 | loguru==0.5.3 4 | scikit-learn==0.24.1 5 | uvicorn==0.13.4 6 | -------------------------------------------------------------------------------- /scripts/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Avoid shifting paramters in second line, incompatible with Windows WSL2, Docker on windows 3 | uvicorn main:app --host=0.0.0.0 --port=8000 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | katana: 4 | build: . 5 | container_name : katana-fastapi 6 | restart : always 7 | ports: 8 | - "8000:8000" -------------------------------------------------------------------------------- /apis/models/base.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class TrainingStatusResponse(BaseModel): 5 | trainingId: str = "056b5d3d-f983-4cd3-8fbd-20b8dad24e0f" 6 | status: str = "Training queued" 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | 8 | COPY . . 9 | 10 | CMD [ "bash", "./scripts/launch.sh" ] 11 | -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Build the Docker image 18 | run: docker build . --file Dockerfile --tag shaz13/katana:$(date +%s) -------------------------------------------------------------------------------- /core/datasets.py: -------------------------------------------------------------------------------- 1 | from sklearn import datasets 2 | 3 | 4 | class IrisDatasetLoader: 5 | def __init__(self): 6 | self.data = datasets.load_iris() 7 | 8 | def load_data(self): 9 | X = self.data.data 10 | y = self.data.target 11 | return X, y 12 | 13 | 14 | class BostonDatasetLoader: 15 | def __init__(self): 16 | self.X = None 17 | self.y = None 18 | 19 | def load_data(self): 20 | self.X, self.y = datasets.load_boston(return_X_y=True) 21 | return self.X, self.y 22 | -------------------------------------------------------------------------------- /apis/models/iris.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | 4 | class IrisFlowerRequestModel(BaseModel): 5 | sepalLength: float = Field(example=12, description="Sepal length in cms") 6 | sepalWidth: float = Field(example=4, description="Sepal width in cms") 7 | petalLength: float = Field(example=17, description="Petal length in cms") 8 | petalWidth: float = Field(example=7, description="Sepal width in cms") 9 | 10 | 11 | class IrisPredictionResponseModel(BaseModel): 12 | predictionId: str = "f75ef3b8-f414-422c-87b1-1e21e684661c" 13 | classification: str = "virginica" 14 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | from fastapi import FastAPI 3 | from fastapi.responses import RedirectResponse 4 | from apis.v1.iris import router as iris_ns 5 | from apis.v1.boston import router as boston_ns 6 | 7 | # Initialize logging 8 | logger.add("./logs/katana.log", rotation="500 MB") 9 | logger.info("Initializing application : Katana") 10 | 11 | app = FastAPI( 12 | title="Katana ML FastAPI Serving ⚡", 13 | version=1.0, 14 | description="FastAPI based template for ASAP ML API development 👩‍💻", 15 | ) 16 | logger.info("Adding Iris namespace route") 17 | app.include_router(iris_ns) 18 | logger.info("Adding Boston namespace route") 19 | app.include_router(boston_ns) 20 | 21 | 22 | @app.get("/", include_in_schema=False) 23 | async def redirect(): 24 | return RedirectResponse("/docs") 25 | -------------------------------------------------------------------------------- /core/trainer.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | from core.datasets import IrisDatasetLoader, BostonDatasetLoader 3 | from sklearn import svm 4 | from sklearn.linear_model import LinearRegression 5 | 6 | 7 | class IrisTrainerInstance: 8 | def __init__(self): 9 | pass 10 | 11 | def load_data(self): 12 | self.data = IrisDatasetLoader() 13 | 14 | def train(self): 15 | try: 16 | classifier = svm.SVC() 17 | logger.info("Fetching dataset") 18 | X, y = self.data.load_data() 19 | logger.info("Training SVC Model") 20 | classifier.fit(X, y) 21 | except Exception as e: 22 | raise (e) 23 | return classifier 24 | 25 | 26 | class BostonHousePriceTrainerInstance: 27 | def __init__(self): 28 | self.dataloder = BostonDatasetLoader() 29 | 30 | def train(self): 31 | self.X, self.y = self.dataloder.load_data() 32 | model = LinearRegression() 33 | model.fit(self.X, self.y) 34 | return model 35 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Katana API Setup Build 5 | 6 | on: 7 | push: 8 | branches: [ develop ] 9 | pull_request: 10 | branches: [ master] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.x 20 | uses: actions/setup-python@v1 21 | with: 22 | python-version: 3.x 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install -r requirements.txt 27 | - name: Lint with flake8 28 | run: | 29 | pip install flake8 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 -------------------------------------------------------------------------------- /apis/v1/boston.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from loguru import logger 3 | from fastapi.routing import APIRouter 4 | from apis.models.base import TrainingStatusResponse 5 | from apis.models.house import BostonHouseRequestModel, BostonHouseResponseModel 6 | from core.trainer import BostonHousePriceTrainerInstance 7 | 8 | router = APIRouter(prefix="/boston") 9 | # Load trained model. Dummy model being trained on startup... 10 | logger.info("Training/Loading iris classification model") 11 | trainer = BostonHousePriceTrainerInstance() 12 | boston_model = trainer.train() 13 | logger.info("Training completed") 14 | 15 | 16 | @router.post( 17 | "/trainModel", tags=["boston"], response_model=TrainingStatusResponse 18 | ) 19 | async def boston_train(): 20 | training_id = uuid.uuid1() 21 | # Queue training / start training via RabbitMQ, Queue, etc.. 22 | # Add task here 23 | # Track the id in a database 24 | return { 25 | "trainingId": str(training_id), 26 | "status": "Training started", 27 | } 28 | 29 | 30 | @router.post( 31 | "/predictPrice", tags=["boston"], response_model=BostonHouseResponseModel 32 | ) 33 | async def boston_price_prediction(body: BostonHouseRequestModel): 34 | request = body.dict() 35 | payload = [x for x in request.values()] 36 | prediction = boston_model.predict([payload]) 37 | result = {"predictionId": str(uuid.uuid1()), "predictedPrice": prediction} 38 | return result 39 | -------------------------------------------------------------------------------- /apis/v1/iris.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from loguru import logger 3 | from fastapi.routing import APIRouter 4 | from apis.models.iris import IrisFlowerRequestModel 5 | from apis.models.iris import IrisPredictionResponseModel 6 | from apis.models.base import TrainingStatusResponse 7 | from core.trainer import IrisTrainerInstance 8 | 9 | router = APIRouter(prefix="/iris") 10 | labels = ["Setosa", "Versicolor", "Virginica"] 11 | # Load trained model. Dummy model being trained on startup... 12 | logger.info("Training/Loading iris classification model") 13 | trainer = IrisTrainerInstance() 14 | trainer.load_data() 15 | iris_model = trainer.train() 16 | logger.info("Training completed") 17 | 18 | 19 | @router.post( 20 | "/trainModel", tags=["iris"], response_model=TrainingStatusResponse 21 | ) 22 | async def iris_train(): 23 | training_id = uuid.uuid1() 24 | # Queue training / start training via RabbitMQ, Queue, etc.. 25 | # Add task here 26 | # Track the id in a database 27 | return { 28 | "trainingId": str(training_id), 29 | "status": "Training started", 30 | } 31 | 32 | 33 | @router.post( 34 | "/predictFlower", tags=["iris"], response_model=IrisPredictionResponseModel 35 | ) 36 | async def iris_prediction(iris: IrisFlowerRequestModel): 37 | payload = [ 38 | iris.sepalLength, 39 | iris.sepalWidth, 40 | iris.petalLength, 41 | iris.petalWidth, 42 | ] 43 | prediction = iris_model.predict([payload]) 44 | target = labels[prediction[0]] 45 | result = {"predictionId": str(uuid.uuid1()), "classification": target} 46 | return result 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Katana Cover 2 | 3 | ## Introduction 🌻 4 | > **Katana** project is a template for ASAP 🚀 ML application deployment 5 | > 6 | 7 | ### Features 🎉 8 | 1. FastAPI inbuilt 9 | 2. Swagger UI and uvicorn integration 10 | 3. Docker ready configuration 11 | 4. Integrated GitHub actions 12 | 5. Production ready code 🚀 13 | 14 | ## Set-up Instructions 🔧 15 | We recommend using flask default serving for development and uvicorn server for production 16 | 17 | We included following setup instructions; 18 | 19 | 1. Local development 20 | 2. Docker supported deployment 21 | 22 | 23 | ### Local Development 👨🏻‍💻 24 | 1. Clone this repo with `git@github.com:shaz13/katana.git` 25 | 2. Set up environment using `python3 -m venv .env` 26 | 3. Activate envrionment using 27 | ``` 28 | # Linux / Mac / Unix 29 | $ source .env/bin/activate 30 | 31 | # Windows 32 | $ \.env\Scripts\activate 33 | ``` 34 | 4. Install requirements using `pip install -r requirements.txt` 35 | 5. For debugging run from root - `python main.py` 36 | 6. Deploy using `Procfile` or `bash scripts/launch.sh` 37 | 7. Your API is being served at `localhost:9000` 38 | 39 | ### Docker Setup ⛴ 40 | 1. Clone this repo with `git@github.com:shaz13/katana.git` 41 | 2. Install docker in your system 42 | 3. Run `docker-compose up` 43 | 4. Your local port is mapped and being served at `localhost:9000` 44 | 45 | ![Capture](https://user-images.githubusercontent.com/24438869/111058914-bd9d0d00-84b7-11eb-9d3c-ecd2e4331013.PNG) 46 | 47 | 48 | 49 | ## Contributors 😎 50 | 1. Mohammad Shahebaz - @shaz13 51 | 2. Aditya Soni - @AdityaSoni19031997 52 | 53 | ## License 👩🏻‍💼 54 | MIT License 55 | -------------------------------------------------------------------------------- /apis/models/house.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | 4 | class BostonHouseRequestModel(BaseModel): 5 | """ 6 | Dataset description 7 | 8 | 1. CRIM per capita crime rate by town 9 | 2. ZN proportion of residential land zoned for lots over 10 | 25,000 sq.ft. 11 | 3. INDUS proportion of non-retail business acres per town 12 | 4. CHAS Charles River dummy variable (= 1 if tract bounds 13 | river; 0 otherwise) 14 | 5. NOX nitric oxides concentration (parts per 10 million) 15 | 6. RM average number of rooms per dwelling 16 | 7. AGE proportion of owner-occupied units built prior to 1940 17 | 8. DIS weighted distances to five Boston employment centres 18 | 9. RAD index of accessibility to radial highways 19 | 10. TAX full-value property-tax rate per $10,000 20 | 11. PTRATIO pupil-teacher ratio by town 21 | 12. B 1000(Bk - 0.63)^2 where Bk is the proportion of blacks 22 | by town 23 | 13. LSTAT % lower status of the population 24 | """ 25 | 26 | # prediction_id: str = "387ef3d8-84a5-11eb-a8fc-84c5a6c1c8e2" 27 | crimeRateByTown: float = Field( 28 | example=0.00632, description="Per capita crime rate by town" 29 | ) 30 | residentZoneProportion: float = Field( 31 | example=18.00, 32 | description="Proportion of residential land \ 33 | zoned for lots over 25,000 sq.ft.", 34 | ) 35 | nonRetailBusinessArea: float = Field( 36 | example=2.310, 37 | description="Proportion of non-retail business acres per town", 38 | ) 39 | charlesRiverVar: float = Field( 40 | example=0, 41 | description="Charles River var (= 1 if tract bounds river; else 0 )", 42 | ) 43 | nitricOxidePpm: float = Field( 44 | example=0.5380, 45 | description="Nitric oxides concentration (parts per 10 million)", 46 | ) 47 | averageRooms: float = Field( 48 | example=6.5750, 49 | description="Average number of rooms per dwelling", 50 | ) 51 | ageOfHouse: float = Field( 52 | example=65.20, 53 | description="Proportion of owner-occupied units built prior to 1940", 54 | ) 55 | distFromCentre: float = Field( 56 | example=4.0900, 57 | description="Weighted distances to five Boston employment centres", 58 | ) 59 | idxRadial: float = Field( 60 | example=0, 61 | description="Index of accessibility to radial highways", 62 | ) 63 | taxRate: float = Field( 64 | example=296, 65 | description="Full-value property-tax rate per $10,000", 66 | ) 67 | pupilTeacherRatio: float = Field( 68 | example=15.30, 69 | description="Pupil-teacher ratio by town", 70 | ) 71 | discriminateProportion: float = Field( 72 | example=396.30, 73 | description="1000(Bk - 0.63)^2 where Bk is the proportion \ 74 | of colored by town", 75 | ) 76 | percentLowerStatPopulation: float = Field( 77 | example=4.30, 78 | description="Percentage of lower status of the population", 79 | ) 80 | 81 | 82 | class BostonHouseResponseModel(BaseModel): 83 | predictionId: str = "387ef3d8-84a5-11eb-a8fc-84c5a6c1c8e2" 84 | predictedPrice: float = "3349030" 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/linux,macos,flask,python,windows 2 | # Edit at https://www.gitignore.io/?templates=linux,macos,flask,python,windows 3 | 4 | ### Flask ### 5 | instance/* 6 | !instance/.gitignore 7 | .webassets-cache 8 | 9 | ### Flask.Python Stack ### 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | *.logs 15 | *.log 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | .vscode 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # pipenv 80 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 81 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 82 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 83 | # install all needed dependencies. 84 | #Pipfile.lock 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # Mr Developer 100 | .mr.developer.cfg 101 | .project 102 | .pydevproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | .dmypy.json 110 | dmypy.json 111 | 112 | # Pyre type checker 113 | .pyre/ 114 | 115 | ### Linux ### 116 | *~ 117 | 118 | # temporary files which can be created if a process still has a handle open of a deleted file 119 | .fuse_hidden* 120 | 121 | # KDE directory preferences 122 | .directory 123 | 124 | # Linux trash folder which might appear on any partition or disk 125 | .Trash-* 126 | 127 | # .nfs files are created when an open file is removed but is still being accessed 128 | .nfs* 129 | 130 | ### macOS ### 131 | # General 132 | .DS_Store 133 | .AppleDouble 134 | .LSOverride 135 | 136 | # Icon must end with two \r 137 | Icon 138 | 139 | # Thumbnails 140 | ._* 141 | 142 | # Files that might appear in the root of a volume 143 | .DocumentRevisions-V100 144 | .fseventsd 145 | .Spotlight-V100 146 | .TemporaryItems 147 | .Trashes 148 | .VolumeIcon.icns 149 | .com.apple.timemachine.donotpresent 150 | 151 | # Directories potentially created on remote AFP share 152 | .AppleDB 153 | .AppleDesktop 154 | Network Trash Folder 155 | Temporary Items 156 | .apdisk 157 | 158 | ### Python ### 159 | # Byte-compiled / optimized / DLL files 160 | 161 | # C extensions 162 | 163 | # Distribution / packaging 164 | 165 | # PyInstaller 166 | # Usually these files are written by a python script from a template 167 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 168 | 169 | # Installer logs 170 | 171 | # Unit test / coverage reports 172 | 173 | # Translations 174 | 175 | # Scrapy stuff: 176 | 177 | # Sphinx documentation 178 | 179 | # PyBuilder 180 | 181 | # pyenv 182 | 183 | # pipenv 184 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 185 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 186 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 187 | # install all needed dependencies. 188 | 189 | # celery beat schedule file 190 | 191 | # SageMath parsed files 192 | 193 | # Spyder project settings 194 | 195 | # Rope project settings 196 | 197 | # Mr Developer 198 | 199 | # mkdocs documentation 200 | 201 | # mypy 202 | 203 | # Pyre type checker 204 | 205 | ### Windows ### 206 | # Windows thumbnail cache files 207 | Thumbs.db 208 | Thumbs.db:encryptable 209 | ehthumbs.db 210 | ehthumbs_vista.db 211 | 212 | # Dump file 213 | *.stackdump 214 | 215 | # Folder config file 216 | [Dd]esktop.ini 217 | 218 | # Recycle Bin used on file shares 219 | $RECYCLE.BIN/ 220 | 221 | # Windows Installer files 222 | *.cab 223 | *.msi 224 | *.msix 225 | *.msm 226 | *.msp 227 | 228 | # Windows shortcuts 229 | *.lnk 230 | 231 | # Folders 232 | .env 233 | 234 | # End of https://www.gitignore.io/api/linux,macos,flask,python,windows 235 | 236 | *.pkl --------------------------------------------------------------------------------