|
16 | 16 | weight: 1 # You can add weight to some posts to override the default sorting (date descending)
|
17 | 17 | ---
|
18 | 18 |
|
| 19 | +In the [previous blog](../fastapi), I mentioned how I was able to make simple endpoints with FastAPI and how I was even able to use it to deploy a pretrained ML model. Moving forward, I will be discussing about how to make our FastAPI application more robust and efficient with some commonly used practices I found around the internet. Note that these are completely optional and for just deploying a model, even the previous blog will suffice. It's just that I thought while I am at it, might just learn a bit more the best practices regarding FastAPI. |
| 20 | + |
19 | 21 | ## Advanced Features with FastAPI
|
20 | 22 | After learning how to be able to serve my models using FastAPI, I decided to extend it further in order to enhance the functionality, security and scalability of my API. For this, I read about additional topics like input validation, error handling and authentication, which although won't be help me much for the purpose for which I initially started learning FastAPI, i.e., to make demo apps for my ML models, but they are crucial for developing production-ready APIs and so I thought about giving them a read too.
|
21 | 23 |
|
@@ -54,46 +56,64 @@ def read_item(item_id : int):
|
54 | 56 | Securing APIs with authentication and authorization mechanisms is essential for protecting sensitive data and restricting access to authorized users. FastAPI supports various authentication methods, including OAuth2, JWT (JSON Web Tokens), and basic authentication. Here’s a simple example of implementing JWT authentication in FastAPI:
|
55 | 57 |
|
56 | 58 | ```python
|
57 |
| -from fastapi import Depends, HTTPException, status |
58 |
| -from fastapi.security import OAuth2PasswordBearer |
59 |
| - |
60 |
| -security = OAuth2PasswordBearer(tokenurl='/token') |
61 |
| - |
62 |
| -# Mock user database |
63 |
| -fake_users_db = { |
64 |
| - "johndoe": { |
65 |
| - "username": "johndoe", |
66 |
| - "hashed_password": "$2b$12$V2qBpWn5GDK/9QrF3l7AyO6x9BdFssEcVbOeYURVn8t62MzK4IO5u", # hashed version of password 'secret' |
67 |
| - "disabled": False, |
68 |
| - } |
69 |
| -} |
70 |
| - |
71 |
| -def verify_password(username: str, password: str): |
72 |
| - user = fake_users_db.get(username) |
73 |
| - if not user or not password: |
74 |
| - return False |
75 |
| - if password == 'secret': |
76 |
| - return True |
77 |
| - |
78 |
| - |
79 |
| -def get_current_user(token: str = Depends(security)): |
80 |
| - username, _ = token.split(":") |
81 |
| - user = fake_users_db.get(username) |
82 |
| - |
83 |
| - if not user: |
84 |
| - raise HTTPException( |
85 |
| - status_code = status.HTTP_401_UNAUTHORIZED, |
86 |
| - detail = 'Invalid Credentials', |
87 |
| - headers = {"WWW-Authenticate": "Bearer"} |
88 |
| - ) |
89 |
| - return user |
90 |
| - |
91 |
| -@app.get('users/me') |
92 |
| -def read_current_user(current_user : dict = Depends(get_current_user)): |
93 |
| - return current_user |
| 59 | +# Pydantic models |
| 60 | +class User(BaseModel): |
| 61 | + username: str |
| 62 | + email: Optional[str] = None |
| 63 | + full_name: Optional[str] = None |
| 64 | + |
| 65 | +class UserInDB(User): |
| 66 | + hashed_password: str |
| 67 | + |
| 68 | +# OAuth2 scheme using JWT tokens |
| 69 | +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") |
| 70 | + |
| 71 | +# Function to verify JWT token and extract user information |
| 72 | +def verify_token(token: str = Depends(oauth2_scheme)): |
| 73 | + try: |
| 74 | + payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) |
| 75 | + username: str = payload.get("sub") |
| 76 | + if username is None: |
| 77 | + raise HTTPException(status_code=401, detail="Invalid credentials") |
| 78 | + token_data = {"username": username} |
| 79 | + except jwt.ExpiredSignatureError: |
| 80 | + raise HTTPException(status_code=401, detail="Token has expired") |
| 81 | + except jwt.JWTError: |
| 82 | + raise HTTPException(status_code=401, detail="Invalid token") |
| 83 | + return token_data |
| 84 | + |
| 85 | +# Route to generate JWT token |
| 86 | +@app.post("/token") |
| 87 | +async def login(form_data: OAuth2PasswordRequestForm = Depends()): |
| 88 | + user_dict = fake_users_db.get(form_data.username) |
| 89 | + if user_dict and form_data.password == "fakepassword": |
| 90 | + # Generate JWT token |
| 91 | + expiration = datetime.utcnow() + timedelta(minutes=TOKEN_EXPIRATION) |
| 92 | + token_data = {"sub": form_data.username, "exp": expiration} |
| 93 | + token = jwt.encode(token_data, SECRET_KEY, algorithm="HS256") |
| 94 | + return {"access_token": token, "token_type": "bearer"} |
| 95 | + raise HTTPException(status_code=401, detail="Incorrect username or password") |
| 96 | + |
| 97 | +# Example protected route |
| 98 | +@app.get("/users/me", response_model=User) |
| 99 | +async def read_users_me(current_user: User = Depends(verify_token)): |
| 100 | + return fake_users_db[current_user["username"]] |
94 | 101 | ```
|
95 | 102 |
|
96 |
| -- **Explanation:** In this example, `OAuth2PasswordBearer` is used to define an authentication scheme using OAuth2 with password flow. The `get_current_user()` function verifies the JWT token and retrieves the current user from the mock database `(fake_users_db)`. The `read_current_user()` endpoint demonstrates accessing user information with authentication. |
| 103 | +- **User Database:** Here, `fake_users_db` simulates a user database with a single user for demonstration. |
| 104 | +- **Pydantic models:** `User` and `UserInDB` are Pydantic models used for type checking. |
| 105 | +- **OAuth2 Scheme:** `oauth2_scheme` is configured using `OAuth2PasswordBearer`, specifying the token URL `(/token)` for token retrieval. |
| 106 | +- **Token Verification:** `verify_token` function is a dependency that verifies and decodes the JWT token sent in the Authorization header of requests. |
| 107 | +- **Token Generation:** The `/token` endpoint (login function) handles user authentication. If the credentials are valid (fakepassword is the hardcoded password for demonstration), it generates a JWT token using jwt.encode. |
| 108 | +- **Protected Route:** The `/users/me` endpoint (read_users_me function) demonstrates a protected route that requires JWT token authentication (verify_token dependency). It returns user information based on the decoded JWT token and remains protected if try to access it without logging in or logging in with some other username or password. |
| 109 | + |
| 110 | +Now, if open up `Swagger UI`, and try to access the `/users/me` endpoint, it won't show anything and give an error message saying we are not authenticated. |
| 111 | + |
| 112 | + |
| 113 | + |
| 114 | +However, if we login (Use `Autheticate` at the top right hand side of Swagger) with the credentials, `johndoe` and `fakepasword`, we are able to generate our JWT token and thus are logged in and can now access our protected endpoint. |
| 115 | + |
| 116 | + |
97 | 117 |
|
98 | 118 | ## Deployment Strategies for FASTApi
|
99 | 119 | Moving further, after building the app, it is also essential to be able to deploy it. FastAPI applications can be deployed using various deployment options, depending on scalability requirements, infrastructure preferences, and operational constraints. Here are some common deployment strategies.
|
@@ -121,24 +141,66 @@ For my learning purposes, I did not go in depth but still learned how to contain
|
121 | 141 |
|
122 | 142 | 1. **Dockerfile:** Create a `Dockerfile` in your FastAPI project directory.
|
123 | 143 | ```Dockerfile
|
124 |
| -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 |
| 144 | +# Use an official Python runtime as a parent image |
| 145 | +FROM python:3.9-slim |
| 146 | + |
| 147 | +# Set environment variables |
| 148 | +ENV PYTHONDONTWRITEBYTECODE 1 |
| 149 | +ENV PYTHONUNBUFFERED 1 |
| 150 | + |
| 151 | +# Set the working directory in the container |
| 152 | +WORKDIR /app |
| 153 | + |
| 154 | +# Install system dependencies |
| 155 | +RUN apt-get update \ |
| 156 | + && apt-get install -y --no-install-recommends netcat-traditional \ |
| 157 | + && apt-get clean \ |
| 158 | + && rm -rf /var/lib/apt/lists/* |
| 159 | + |
| 160 | +# Install Python dependencies |
| 161 | +COPY requirements.txt /app/ |
| 162 | +RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt |
| 163 | + |
| 164 | +# Copy the FastAPI app code into the container |
| 165 | +COPY . /app/ |
| 166 | + |
| 167 | +# Expose the port that FastAPI runs on |
| 168 | +EXPOSE 8000 |
| 169 | + |
| 170 | +# Command to run the FastAPI application |
| 171 | +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] |
125 | 172 |
|
126 |
| -COPY ./app /app |
127 | 173 | ```
|
128 | 174 |
|
129 |
| -Replace `./app` with the path to your FastAPI application directory. |
| 175 | +2. Add a `requirements.txt` file in the FastAPI directory |
| 176 | +```requirements.txt |
| 177 | +fastapi |
| 178 | +uvicorn |
| 179 | +pyjwt |
| 180 | +prometheus-client |
| 181 | +``` |
130 | 182 |
|
131 | 183 | 2. **Build Docker Image:** Build the docker image
|
132 | 184 | ```bash
|
133 |
| -docker build -t fastapi-app |
| 185 | +docker build -t my-fastapi-app . |
134 | 186 | ```
|
135 | 187 |
|
136 | 188 | 3. **Run Docker Container:** Run the Docker container.
|
137 | 189 | ```bash
|
138 |
| -docker run -d -p 80:80 fastapi-app |
| 190 | +docker run -d --name my-fastapi-container -p 8000:8000 my-fastapi-app |
| 191 | +``` |
| 192 | + |
| 193 | +4. The container should run without any issue and we can even monitor its health using: |
| 194 | +```bash |
| 195 | +sudo docker logs my-fastapi-container |
139 | 196 | ```
|
140 | 197 |
|
141 |
| -Adjust `-p 80:80` to map the container port to a desired host port. |
| 198 | + |
| 199 | + |
| 200 | +5. To stop the container use: |
| 201 | +```bash |
| 202 | +docker stop my-fastapi-container |
| 203 | +``` |
142 | 204 |
|
143 | 205 | ## Continuous Integration and Delivery (CI/CD) for FastAPI
|
144 | 206 | Phewwww! Now that we've come all the way to deployment, I thought why not also add some CI/CD pipelines to it as it helps automate the building, testing and deployment processes, ensuring reliable and efficient software delivery.
|
@@ -250,37 +312,39 @@ REQUEST_COUNT = Counter("request_count", "Total count of requests", ["method", "
|
250 | 312 | REQUEST_LATENCY = Histogram("request_latency_seconds", "Request latency in seconds", ["method", "endpoint"])
|
251 | 313 |
|
252 | 314 | class PrometheusMiddleware(BaseHTTPMiddleware):
|
253 |
| - async def dispatch(self, request, call_next): |
254 |
| - path = request.url.path |
255 |
| - method = request.method |
| 315 | + async def dispatch(self, request : Response, call_next): |
| 316 | + start_time = time.time() |
256 | 317 | try:
|
257 | 318 | response = await call_next(request)
|
258 | 319 | status_code = response.status_code
|
259 | 320 | return response
|
| 321 | + except HTTPException as http_exc: |
| 322 | + # Capture HTTPException to get status_code |
| 323 | + status_code = http_exc.status_code |
| 324 | + raise http_exc |
| 325 | + |
260 | 326 | finally:
|
261 |
| - REQUEST_COUNT.labels(method=method, endpoint=path, status_code=status_code).inc() |
262 |
| - latency = time.time() - request.scope["start_time"] |
263 |
| - REQUEST_LATENCY.labels(method=method, endpoint=path).observe(latency) |
| 327 | + REQUEST_COUNT.labels(method=request.method, endpoint=request.url.path, status_code=status_code).inc() |
| 328 | + latency = time.time() - start_time |
| 329 | + REQUEST_LATENCY.labels(method=request.method, endpoint=request.url.path).observe(latency) |
264 | 330 |
|
265 | 331 | app.add_middleware(PrometheusMiddleware)
|
266 |
| -``` |
267 |
| - |
268 |
| -3. Expose Metrics Endpoint: |
269 |
| -```python |
270 |
| -from fastapi.responses import Response |
271 | 332 |
|
272 | 333 | @app.get("/metrics")
|
273 | 334 | def get_metrics():
|
274 | 335 | return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
275 | 336 | ```
|
276 | 337 |
|
277 |
| -4. Instrument Endpoints: |
278 |
| -```python |
279 |
| -@app.get("/items/") |
280 |
| -async def read_items(): |
281 |
| - # Endpoint logic |
282 |
| - return {"message": "Items retrieved successfully"} |
283 |
| -``` |
| 338 | + |
| 339 | + |
| 340 | +After this, we will be able to access our FastAPI metrics at the given endpoint. However, in order to understand it more efficiently, one can even use services like `Prometheus` and `Grafana` |
284 | 341 |
|
285 | 342 | ## Conclusion
|
286 |
| -And that's a wrap. Phewww.. While there is a lot to cover in FastAPI and certainly it can't be done in a single blog, I still attempted to atleast log some of the things which I learnt during this time of learning FastAPI. After all, this is not a book lol. Just a reference blog for me to look back to when I get stuck with something working with FastAPI again. I think it covers most of the basic stuff and just in case I missed out something, I can always go back and have a look at the FastAPI docs. For now, I'd say that FastAPI is truly amazing as for how easy it is to make and manage endpoints and deploying my ML apps to it. And the best part? It's fairly easy to learn too. It took me just 2 days to go through all this and I believe someone who works with Python will find it fairly easy to use. With that being said, I think that's all from my side regarding FastAPI for now. Until Next Time. Adios! |
| 343 | +And that's a wrap. Phewww.. While there is a lot to cover in FastAPI and certainly it can't be done in a single blog, I still attempted to atleast log some of the things which I learnt during this time of learning FastAPI. After all, this is not a book lol. Just a reference blog for me to look back to when I get stuck with something working with FastAPI again. I think it covers most of the basic stuff and just in case I missed out something, I can always go back and have a look at the FastAPI docs. For now, I'd say that FastAPI is truly amazing as for how easy it is to make and manage endpoints and deploying my ML apps to it. And the best part? It's fairly easy to learn too. It took me just 2 days to go through all this and I believe someone who works with Python will find it fairly easy to use. With that being said, I think that's all from my side regarding FastAPI for now. Until Next Time. Adios! |
| 344 | + |
| 345 | +## Appendix |
| 346 | +- [Code for this blog](main.py) |
| 347 | +- [Dockerfile Code](Dockerfile) |
| 348 | +- [GitHub Actions Code](fastapi.yaml) |
| 349 | + |
| 350 | +> Photo by [Data Scientist](https://datascientest.com/en/fastapi-everything-you-need-to-know-about-the-most-widely-used-python-web-framework-for-machine-learning) |
0 commit comments