val2e commited on
Commit
9f34d76
·
verified ·
1 Parent(s): 2fa3a0e

Upload 8 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exclude files not needed for Docker build
2
+ *__pycache__/
3
+ *.pyc
4
+ *.pyo
5
+
6
+ # Exclude log files but keep the logs directory
7
+ logs/*
8
+ !logs/.gitkeep
9
+
10
+ # Exclude notebook checkpoints
11
+ *.ipynb_checkpoints/
12
+
13
+ # Environments
14
+ .env
15
+ *.env
16
+ .venv
17
+ env/
18
+ venv/
19
+ ENV/
20
+ env.bak/
21
+ venv.bak/
Dockerfile-api ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11.9-slim
2
+
3
+ # Copy requirements file
4
+ COPY requirements_docker_api.txt .
5
+
6
+ # Update pip
7
+ RUN pip --timeout=30000 install --no-cache-dir --upgrade pip
8
+
9
+ # Install dependecies
10
+ RUN pip --timeout=30000 install --no-cache-dir -r requirements_docker_api.txt
11
+
12
+ # Copy API
13
+ RUN mkdir /src/
14
+ COPY ./src /src
15
+
16
+ # Set workdir
17
+ WORKDIR /
18
+
19
+ # Expose app port
20
+ EXPOSE 9000
21
+
22
+ # Start application
23
+ CMD ["uvicorn", "src.api.api:app", "--host", "0.0.0.0", "--port", "9000", "--reload"]
requirements_docker_api.txt ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohttp==3.9.5
2
+ aiosignal==1.3.1
3
+ annotated-types==0.7.0
4
+ anyio==4.4.0
5
+ asttokens==2.4.1
6
+ attrs==23.2.0
7
+ certifi==2024.7.4
8
+ click==8.1.7
9
+ colorama==0.4.6
10
+ comm==0.2.2
11
+ contourpy==1.2.1
12
+ cycler==0.12.1
13
+ debugpy==1.6.7
14
+ decorator==5.1.1
15
+ dnspython==2.6.1
16
+ email_validator==2.2.0
17
+ exceptiongroup==1.2.0
18
+ executing==2.0.1
19
+ fastapi==0.111.0
20
+ fastapi-cache2==0.2.1
21
+ fastapi-cli==0.0.4
22
+ fonttools==4.53.1
23
+ frozenlist==1.4.1
24
+ h11==0.14.0
25
+ httpcore==1.0.5
26
+ httptools==0.6.1
27
+ httpx==0.27.0
28
+ idna==3.7
29
+ imbalanced-learn==0.12.3
30
+ imblearn==0.0
31
+ importlib_metadata==8.0.0
32
+ jedi==0.19.1
33
+ Jinja2==3.1.4
34
+ joblib==1.4.2
35
+ mdurl==0.1.2
36
+ multidict==6.0.5
37
+ nest_asyncio==1.6.0
38
+ numpy==2.0.0
39
+ orjson==3.10.6
40
+ packaging==24.1
41
+ pandas==2.2.2
42
+ parso==0.8.4
43
+ pendulum==3.0.0
44
+ pickleshare==0.7.5
45
+ pillow==10.4.0
46
+ pip==24.0
47
+ platformdirs==4.2.2
48
+ prompt_toolkit==3.0.47
49
+ pure-eval==0.2.2
50
+ pydantic==2.8.2
51
+ pydantic_core==2.20.1
52
+ Pygments==2.18.0
53
+ pyparsing==3.1.2
54
+ python-dateutil==2.9.0
55
+ python-dotenv==1.0.1
56
+ python-multipart==0.0.9
57
+ pytz==2024.1
58
+ PyYAML==6.0.1
59
+ pyzmq==25.1.2
60
+ redis==5.0.7
61
+ rich==13.7.1
62
+ scikit-learn==1.5.1
63
+ scipy==1.14.0
64
+ shellingham==1.5.4
65
+ six==1.16.0
66
+ sniffio==1.3.1
67
+ stack-data==0.6.2
68
+ starlette==0.37.2
69
+ tenacity==8.5.0
70
+ threadpoolctl==3.5.0
71
+ time-machine==2.14.2
72
+ tornado==6.4.1
73
+ traitlets==5.14.3
74
+ typer==0.12.3
75
+ typing_extensions==4.12.2
76
+ tzdata==2024.1
77
+ ujson==5.10.0
78
+ uvicorn==0.30.1
79
+ watchfiles==0.22.0
80
+ wcwidth==0.2.13
81
+ websockets==12.0
82
+ wheel==0.43.0
83
+ xgboost==2.1.0
84
+ yarl==1.9.4
85
+ zipp==3.19.2
src/api/.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ REDIS_URL=redis://redis-14458.c14.us-east-1-2.ec2.redns.redis-cloud.com:14458/
2
+ REDIS_USERNAME=default
3
+ REDIS_PASSWORD=lkiTwONUyGZdOlg216hwOAV3L82OJuRM
src/api/__pycache__/api.cpython-311.pyc ADDED
Binary file (8.11 kB). View file
 
src/api/__pycache__/config.cpython-311.pyc ADDED
Binary file (842 Bytes). View file
 
src/api/api.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ from collections.abc import AsyncIterator
5
+ from contextlib import asynccontextmanager
6
+
7
+ from fastapi import FastAPI
8
+ from fastapi_cache import FastAPICache
9
+ from fastapi_cache.backends.redis import RedisBackend
10
+ from fastapi_cache.coder import PickleCoder
11
+ from fastapi_cache.decorator import cache
12
+
13
+ from redis import asyncio as aioredis
14
+
15
+ from pydantic import BaseModel
16
+ from typing import Tuple, Dict, Union
17
+
18
+ from imblearn.pipeline import Pipeline as imbPipeline
19
+ from sklearn.preprocessing._label import LabelEncoder
20
+ import joblib
21
+ import pandas as pd
22
+ from urllib.request import urlopen
23
+
24
+
25
+ from src.api.config import ONE_DAY_SEC, ONE_WEEK_SEC, XGBOOST_URL, RANDOM_FOREST_URL, ENCODER_URL, ENV_PATH
26
+
27
+ load_dotenv(ENV_PATH)
28
+
29
+
30
+ @asynccontextmanager
31
+ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
32
+ url = os.getenv("REDIS_URL")
33
+ username = os.getenv("REDIS_USERNAME")
34
+ password = os.getenv("REDIS_PASSWORD")
35
+ redis = aioredis.from_url(url=url, username=username,
36
+ password=password, encoding="utf8", decode_responses=True)
37
+ FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
38
+ yield
39
+
40
+
41
+ # FastAPI Object
42
+ app = FastAPI(
43
+ title='Sepsis classification',
44
+ version='0.0.1',
45
+ description='Identify ICU patients at risk of developing sepsis',
46
+ lifespan=lifespan,
47
+ )
48
+
49
+
50
+ # API input features
51
+ class SepsisFeatures(BaseModel):
52
+ PRG: float
53
+ PL: float
54
+ PR: float
55
+ SK: float
56
+ TS: float
57
+ M11: float
58
+ BD2: float
59
+ Age: float
60
+ Insurance: float
61
+
62
+
63
+ class Url(BaseModel):
64
+ pipeline_url: str
65
+ encoder_url: str
66
+
67
+
68
+ class ResultData(BaseModel):
69
+ prediction: str
70
+ probability: float
71
+
72
+
73
+ class PredictionResponse(BaseModel):
74
+ execution_msg: str
75
+ execution_code: int
76
+ result: ResultData
77
+
78
+
79
+ class ErrorResponse(BaseModel):
80
+ execution_msg: Union[str, None]
81
+ execution_code: Union[int, None]
82
+ result: Union[Dict[str, Union[str, int]], Union[Dict[str, None], None]]
83
+
84
+
85
+ # Load the model pipelines and encoder
86
+ # Cache for 1 day
87
+ @cache(expire=ONE_DAY_SEC, namespace='pipeline_resource', coder=PickleCoder)
88
+ async def load_pipeline(pipeline_url: Url, encoder_url: Url) -> Tuple[imbPipeline, LabelEncoder]:
89
+ pipeline, encoder = None, None
90
+ try:
91
+ pipeline: imbPipeline = joblib.load(urlopen(pipeline_url))
92
+ encoder: LabelEncoder = joblib.load(urlopen(encoder_url))
93
+ except Exception:
94
+ # Log exception
95
+ pass
96
+ finally:
97
+ return pipeline, encoder
98
+
99
+
100
+ # Endpoints
101
+
102
+ # Status endpoint: check if api is online
103
+ @app.get('/')
104
+ @cache(expire=ONE_WEEK_SEC, namespace='status_check') # Cache for 1 week
105
+ async def status_check():
106
+ return {"Status": "API is online..."}
107
+
108
+
109
+ @cache(expire=ONE_DAY_SEC, namespace='pipeline_classifier') # Cache for 1 day
110
+ async def pipeline_classifier(pipeline: imbPipeline, encoder: LabelEncoder, data: SepsisFeatures) -> ErrorResponse | PredictionResponse:
111
+ output = ErrorResponse(**{'execution_msg': None,
112
+ 'execution_code': None, 'result': None})
113
+ try:
114
+ # Create dataframe
115
+ df = pd.DataFrame([data.model_dump()])
116
+
117
+ # Make prediction
118
+ prediction = pipeline.predict(df)
119
+
120
+ pred_int = int(prediction[0])
121
+
122
+ prediction = encoder.inverse_transform([pred_int])[0]
123
+
124
+ # Get the probability of the predicted class
125
+ probability = round(
126
+ float(pipeline.predict_proba(df)[0][pred_int] * 100), 2)
127
+
128
+ msg = 'Execution was successful'
129
+ code = 1
130
+ result = {"prediction": prediction, "probability": probability}
131
+
132
+ output = PredictionResponse(
133
+ **{'execution_msg': msg,
134
+ 'execution_code': code, 'result': result}
135
+ )
136
+
137
+ except Exception as e:
138
+ msg = 'Execution failed'
139
+ code = 0
140
+ result = {'error': f"Omg, pipeline classsifier failure{e}"}
141
+ output = ErrorResponse(**{'execution_msg': msg,
142
+ 'execution_code': code, 'result': result})
143
+
144
+ finally:
145
+ return output
146
+
147
+
148
+ # Xgboost endpoint: classify sepsis with xgboost
149
+ @app.post('/xgboost_prediction')
150
+ async def xgboost_classifier(data: SepsisFeatures) -> ErrorResponse | PredictionResponse:
151
+ xgboost_pipeline, encoder = await load_pipeline(XGBOOST_URL, ENCODER_URL)
152
+ output = await pipeline_classifier(xgboost_pipeline, encoder, data)
153
+ return output
154
+
155
+
156
+ # Random forest endpoint: classify sepsis with random forest
157
+ @app.post('/random_forest_prediction')
158
+ async def random_forest_classifier(data: SepsisFeatures) -> ErrorResponse | PredictionResponse:
159
+ random_forest_pipeline, encoder = await load_pipeline(RANDOM_FOREST_URL, ENCODER_URL)
160
+ output = await pipeline_classifier(random_forest_pipeline, encoder, data)
161
+ return output
src/api/config.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ ONE_DAY_SEC = 24*60*60
4
+
5
+ ONE_WEEK_SEC = ONE_DAY_SEC*7
6
+
7
+ XGBOOST_URL = "https://raw.githubusercontent.com/valiantezabuku/Building-Machine-Learning-API-s-with-FastAPI-Collabo/develop/dev/models/xgboost.joblib"
8
+
9
+ RANDOM_FOREST_URL = "https://raw.githubusercontent.com/valiantezabuku/Building-Machine-Learning-API-s-with-FastAPI-Collabo/develop/dev/models/random_forest.joblib"
10
+
11
+ ENCODER_URL = "https://raw.githubusercontent.com/valiantezabuku/Building-Machine-Learning-API-s-with-FastAPI-Collabo/develop/dev/models/encoder.joblib"
12
+
13
+ BASE_DIR = './' # Where Unicorn server runs from
14
+
15
+ ENV_PATH = os.path.join(BASE_DIR, 'src/api/.env')