├── Chapter01 ├── bookstore │ ├── requirements.txt │ ├── models.py │ ├── test_main.py │ └── main.py └── fastapi_start │ ├── router_example.py │ └── main.py ├── Chapter02 ├── uploads_and_downloads │ ├── requirements.txt │ ├── uploads │ │ └── content.txt │ └── main.py ├── sql_example │ ├── requirements.txt │ ├── database.py │ └── main.py ├── async_example │ ├── requirements.txt │ ├── main.py │ └── timing_api_calls.py └── nosql_example │ ├── requirements.txt │ ├── database.py │ └── main.py ├── Chapter08 └── trip_platform │ ├── pytest.ini │ ├── requirements.txt │ ├── app │ ├── rate_limiter.py │ ├── background_task.py │ ├── middleware.py │ ├── profiler.py │ ├── internationalization.py │ ├── main.py │ └── dependencies.py │ ├── tests │ └── test_main.py │ ├── profiling_application.py │ └── test_workers.py ├── Chapter12 ├── live_application │ ├── Procfile │ ├── requirements.txt │ ├── Dockerfile.dev │ └── app │ │ └── main.py ├── fca-server │ ├── src │ │ └── fca_server │ │ │ ├── __about__.py │ │ │ ├── __init__.py │ │ │ └── main.py │ ├── tests │ │ └── __init__.py │ ├── README.md │ ├── LICENSE.txt │ └── pyproject.toml └── import-fca-server │ └── main.py ├── Chapter11 └── middleware_project │ ├── requirements.txt │ ├── middleware │ ├── request_middleware.py │ ├── response_middlaware.py │ ├── asgi_middleware.py │ └── webhook.py │ ├── cors_page.html │ ├── http_server.py │ └── main.py ├── Chapter06 └── ticketing_system │ ├── alembic │ ├── README │ ├── script.py.mako │ ├── versions │ │ ├── 9a9cfeb65ea4_add_ticket_sold_information.py │ │ ├── c89449e81bb3_start_database.py │ │ ├── 3d195160b2e3_add_ticket_details_one_to_one_.py │ │ ├── 4149f66b5194_create_events_and_many_to_one_.py │ │ └── 9891059574b3_create_sponsorships_for_many_to_many_.py │ └── env.py │ ├── pytest.ini │ ├── requirements.txt │ ├── app │ ├── db_connection.py │ ├── security.py │ ├── database.py │ └── main.py │ ├── tests │ ├── test_security.py │ ├── test_client.py │ └── conftest.py │ └── alembic.ini ├── Chapter10 ├── graphql │ ├── requirements.txt │ ├── main.py │ ├── graphql_utils.py │ └── database.py ├── grpc_gateway │ ├── requirements.txt │ ├── grpcserver.proto │ ├── app │ │ └── main.py │ ├── grpc_server.py │ ├── grpcserver_pb2.py │ └── grpcserver_pb2_grpc.py ├── chef_ai │ ├── requirements.txt │ ├── handlers.py │ └── main.py ├── ai_doctor │ ├── requirements.txt │ └── app │ │ ├── main.py │ │ └── utils.py └── ecotech_RAG │ ├── requirements.txt │ ├── model.py │ ├── documents.py │ ├── prompting.py │ ├── main.py │ └── docs │ └── faq_ecotech.txt ├── Chapter09 └── chat_platform │ ├── requirements.txt │ ├── app │ ├── templating.py │ ├── ws_manager.py │ ├── ws_password_bearer.py │ ├── chat.py │ ├── main.py │ ├── exclusive_chatroom.py │ └── security.py │ ├── benchmark_websocket.py │ └── templates │ ├── chatroom.html │ ├── exclusivechatroom.html │ └── login.html ├── Chapter03 └── task_manager_app │ ├── requirements.txt │ ├── tasks.csv │ ├── models.py │ ├── conftest.py │ ├── security.py │ ├── test_operations.py │ ├── test_main.py │ ├── operations.py │ └── main.py ├── Chapter05 └── protoapp │ ├── pytest.ini │ ├── requirements.txt │ ├── run_server.py │ ├── locustfile.py │ ├── protoapp │ ├── logging.py │ ├── database.py │ └── main.py │ └── tests │ ├── conftest.py │ └── test_main.py ├── Chapter07 └── streaming_platform │ ├── requirements.txt │ ├── app │ ├── third_party_endpoint.py │ ├── database.py │ ├── db_connection.py │ ├── es_queries.py │ ├── main_search.py │ └── main.py │ ├── fill_mongo_db_database.py │ ├── fill_elasticsearch_index.py │ ├── queries_used_in_elasticsearch.md │ ├── create_aggregation_and_user_data_view.py │ └── tests │ ├── conftest.py │ └── test_main.py ├── Chapter04 └── saas_app │ ├── requirements.txt │ ├── responses.py │ ├── db_connection.py │ ├── api_key.py │ ├── models.py │ ├── user_session.py │ ├── test_security.py │ ├── third_party_login.py │ ├── operations.py │ ├── test_operations.py │ ├── premium_access.py │ ├── github_login.py │ ├── test_main.py │ ├── conftest.py │ ├── mfa.py │ ├── rbac.py │ ├── main.py │ └── security.py ├── .gitignore ├── LICENSE └── README.md /Chapter01/bookstore/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn -------------------------------------------------------------------------------- /Chapter02/uploads_and_downloads/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] -------------------------------------------------------------------------------- /Chapter08/trip_platform/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath=. -------------------------------------------------------------------------------- /Chapter02/sql_example/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | sqlalchemy -------------------------------------------------------------------------------- /Chapter12/live_application/Procfile: -------------------------------------------------------------------------------- 1 | web: fastapi run --port $PORT -------------------------------------------------------------------------------- /Chapter02/async_example/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx -------------------------------------------------------------------------------- /Chapter11/middleware_project/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | -------------------------------------------------------------------------------- /Chapter12/live_application/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi~=0.111.0 2 | gunicorn -------------------------------------------------------------------------------- /Chapter06/ticketing_system/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /Chapter02/nosql_example/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | pymongo 3 | pydantic[email] -------------------------------------------------------------------------------- /Chapter10/graphql/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | strawberry-graphql[fastapi] -------------------------------------------------------------------------------- /Chapter10/grpc_gateway/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | grpcio 4 | grpcio-tools -------------------------------------------------------------------------------- /Chapter09/chat_platform/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | websockets 3 | jinja2 4 | pydantic -------------------------------------------------------------------------------- /Chapter10/chef_ai/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | 4 | python-dotenv 5 | cohere 6 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = . app 3 | asyncio_mode = auto 4 | -------------------------------------------------------------------------------- /Chapter10/ai_doctor/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | huggingface_hub 3 | joblib 4 | scikit-learn -------------------------------------------------------------------------------- /Chapter03/task_manager_app/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | sqlalchemy 3 | 4 | # testing packages 5 | pytest -------------------------------------------------------------------------------- /Chapter05/protoapp/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = protoapp . 3 | markers = 4 | integration: marks tests as integration -------------------------------------------------------------------------------- /Chapter05/protoapp/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | sqlalchemy>=2.0.0 3 | 4 | pytest 5 | httpx 6 | pytest-asyncio 7 | pytest-cov 8 | 9 | locust -------------------------------------------------------------------------------- /Chapter08/trip_platform/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | 4 | pytest 5 | httpx 6 | 7 | babel 8 | pyinstrument 9 | slowapi 10 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/app/templating.py: -------------------------------------------------------------------------------- 1 | from fastapi.templating import Jinja2Templates 2 | 3 | templates = Jinja2Templates(directory="templates") 4 | -------------------------------------------------------------------------------- /Chapter03/task_manager_app/tasks.csv: -------------------------------------------------------------------------------- 1 | id,title,description,status,priority 2 | 1,Task One,Description One,Incomplete 3 | 2,Task Two,Description Two,Ongoing,high -------------------------------------------------------------------------------- /Chapter06/ticketing_system/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | 3 | sqlalchemy >= 2.0.0 4 | aiosqlite 5 | cryptography 6 | 7 | pytest 8 | pytest-asyncio 9 | alembic -------------------------------------------------------------------------------- /Chapter05/protoapp/run_server.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | # from protoapp.main import app 3 | 4 | if __name__ == "__main__": 5 | uvicorn.run("protoapp.main:app", reload=True) 6 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | motor 3 | elasticsearch>=8,<9 4 | 5 | aiohttp 6 | redis 7 | fastapi-cache2[redis] 8 | 9 | 10 | pytest -------------------------------------------------------------------------------- /Chapter10/graphql/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from graphql_utils import graphql_app 4 | 5 | app = FastAPI() 6 | app.include_router(graphql_app, prefix="/graphql") 7 | -------------------------------------------------------------------------------- /Chapter12/fca-server/src/fca_server/__about__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024-present U.N. Owen 2 | # 3 | # SPDX-License-Identifier: MIT 4 | __version__ = "0.0.1" 5 | -------------------------------------------------------------------------------- /Chapter12/fca-server/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024-present U.N. Owen 2 | # 3 | # SPDX-License-Identifier: MIT 4 | def test_prova(): 5 | assert True 6 | -------------------------------------------------------------------------------- /Chapter04/saas_app/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all] 2 | sqlalchemy>=2.0.0 3 | passlib 4 | bcrypt==3.2.2 5 | python-jose[cryptography] 6 | httpx 7 | pyotp 8 | 9 | # testing packages 10 | pytest -------------------------------------------------------------------------------- /Chapter12/fca-server/src/fca_server/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024-present U.N. Owen 2 | # 3 | # SPDX-License-Identifier: MIT 4 | from fca_server.main import router 5 | -------------------------------------------------------------------------------- /Chapter10/ecotech_RAG/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | python-dotenv 4 | 5 | 6 | chromadb 7 | unstructured 8 | 9 | 10 | langchain 11 | langchain-community 12 | langchain-cohere 13 | -------------------------------------------------------------------------------- /Chapter12/import-fca-server/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fca_server import router 3 | 4 | app = FastAPI(title="Import FCA Server Application") 5 | 6 | app.include_router(router) 7 | -------------------------------------------------------------------------------- /Chapter02/uploads_and_downloads/uploads/content.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 2 | Proin ultrices laoreet purus, nec vehicula arcu posuere a. 3 | Sed placerat accumsan. 4 | 5 | -------------------------------------------------------------------------------- /Chapter01/fastapi_start/router_example.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter() 4 | 5 | 6 | @router.get("/items/{item_id}") 7 | async def read_item(item_id: int): 8 | return {"item_id": item_id} 9 | -------------------------------------------------------------------------------- /Chapter08/trip_platform/app/rate_limiter.py: -------------------------------------------------------------------------------- 1 | from slowapi import Limiter 2 | from slowapi.util import get_remote_address 3 | 4 | limiter = Limiter( 5 | key_func=get_remote_address, 6 | default_limits=["5/minute"], 7 | ) 8 | -------------------------------------------------------------------------------- /Chapter01/fastapi_start/main.py: -------------------------------------------------------------------------------- 1 | import router_example 2 | from fastapi import FastAPI 3 | 4 | app = FastAPI() 5 | 6 | app.include_router(router_example.router) 7 | 8 | 9 | @app.get("/") 10 | async def read_root(): 11 | return {"Hello": "World"} 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | .env 4 | .coverage 5 | htmlcov/ 6 | node_modules 7 | dist/ 8 | 9 | __pychache__/ 10 | .venv/ 11 | env/ 12 | ENV/ 13 | env.bak/ 14 | venv.bak/ 15 | *.py[cod] 16 | *$py.class 17 | *.pem 18 | 19 | .DS_Store 20 | Thumbs.db 21 | -------------------------------------------------------------------------------- /Chapter12/fca-server/src/fca_server/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter() 4 | 5 | 6 | @router.get("/home") 7 | def read_root(): 8 | return { 9 | "message": "Welcome to the FastAPI Cookbook Application!" 10 | } 11 | -------------------------------------------------------------------------------- /Chapter02/nosql_example/database.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | 3 | client = MongoClient() 4 | # equivalent to 5 | # client = MongoClient("mongodb://localhost:27017") 6 | 7 | 8 | database = client.mydatabase 9 | 10 | user_collection = database["users"] 11 | -------------------------------------------------------------------------------- /Chapter01/bookstore/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | 4 | class Book(BaseModel): 5 | title: str = Field( 6 | ..., min_length=1, max_length=100 7 | ) 8 | author: str = Field( 9 | ..., min_length=1, max_length=50 10 | ) 11 | year: int = Field(..., gt=1900, lt=2100) 12 | -------------------------------------------------------------------------------- /Chapter05/protoapp/locustfile.py: -------------------------------------------------------------------------------- 1 | from locust import HttpUser, task 2 | 3 | 4 | class ProtoappUser(HttpUser): 5 | host = "http://localhost:8000" 6 | 7 | @task 8 | def hello_world(self): 9 | self.client.get("/home") 10 | 11 | @task 12 | def get_item(self): 13 | self.client.get("/item") 14 | -------------------------------------------------------------------------------- /Chapter10/grpc_gateway/grpcserver.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | 4 | service GrpcServer{ 5 | 6 | rpc GetServerResponse(Message) 7 | returns (MessageResponse) {} 8 | 9 | } 10 | 11 | message Message{ 12 | string message = 1; 13 | } 14 | 15 | message MessageResponse{ 16 | string message = 1; 17 | bool received = 2; 18 | } -------------------------------------------------------------------------------- /Chapter08/trip_platform/app/background_task.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | logger = logging.getLogger("uvicorn.error") 5 | 6 | 7 | async def store_query_to_external_db(message: str): 8 | logger.info(f"Storing message '{message}'.") 9 | await asyncio.sleep(2) 10 | logger.info(f"Message '{message}' stored!") 11 | -------------------------------------------------------------------------------- /Chapter10/ecotech_RAG/model.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain.schema import StrOutputParser 3 | from langchain_cohere import ChatCohere 4 | 5 | from prompting import ( 6 | chat_prompt_template, 7 | ) 8 | 9 | load_dotenv() 10 | 11 | model = ChatCohere(model="command-r-plus") 12 | 13 | chain = ( 14 | chat_prompt_template | model | StrOutputParser() 15 | ) 16 | -------------------------------------------------------------------------------- /Chapter03/task_manager_app/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Task(BaseModel): 5 | title: str 6 | description: str 7 | status: str 8 | 9 | 10 | class TaskWithID(Task): 11 | id: int 12 | 13 | 14 | class TaskV2(BaseModel): 15 | title: str 16 | description: str 17 | status: str 18 | priority: str | None = "low" 19 | 20 | 21 | class TaskV2WithID(TaskV2): 22 | id: int 23 | -------------------------------------------------------------------------------- /Chapter01/bookstore/test_main.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from .main import app 4 | 5 | client = TestClient(app) 6 | 7 | 8 | def test_read_book_by_id(): 9 | response = client.get("/books/9999") 10 | 11 | assert response.status_code == 200 12 | 13 | assert response.json() == { 14 | "book_id": 9999, 15 | "title": "The Great Gatsby", 16 | "author": "F. Scott Fitzgerald", 17 | } 18 | -------------------------------------------------------------------------------- /Chapter02/async_example/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | from fastapi import FastAPI 5 | 6 | app = FastAPI() 7 | 8 | 9 | @app.get("/sync") 10 | def read_sync(): 11 | time.sleep(2) 12 | return { 13 | "message": "Synchrounouns blocking endpoint" 14 | } 15 | 16 | 17 | @app.get("/async") 18 | async def read_async(): 19 | await asyncio.sleep(2) 20 | return { 21 | "message": "Asynchronous non-blocking endpoint" 22 | } 23 | -------------------------------------------------------------------------------- /Chapter04/saas_app/responses.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from pydantic import BaseModel, EmailStr, Field 4 | 5 | 6 | class UserCreateBody(BaseModel): 7 | username: str 8 | email: EmailStr 9 | password: str 10 | 11 | 12 | class UserCreateResponse(BaseModel): 13 | username: str 14 | email: EmailStr 15 | 16 | 17 | class ResponseCreateUser(BaseModel): 18 | message: Annotated[ 19 | str, Field(default="user created") 20 | ] 21 | user: UserCreateResponse 22 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/app/third_party_endpoint.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | 3 | from app.database import mongo_database 4 | 5 | router = APIRouter( 6 | prefix="/thirdparty", 7 | tags=["third party"], 8 | ) 9 | 10 | 11 | @router.get("/users/actions") 12 | async def get_users_with_actions( 13 | db=Depends(mongo_database), 14 | ): 15 | users = [ 16 | user 17 | async for user in db.users_data_view.find( 18 | {}, {"_id": 0} 19 | ) 20 | ] 21 | 22 | return users 23 | -------------------------------------------------------------------------------- /Chapter08/trip_platform/tests/test_main.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from fastapi.testclient import TestClient 4 | 5 | from app.dependencies import time_range 6 | from app.main import app 7 | 8 | 9 | def test_get_v1_trips_endpoint(): 10 | client = TestClient(app) 11 | app.dependency_overrides[time_range] = lambda: ( 12 | date.fromisoformat("2024-02-01"), 13 | None, 14 | ) 15 | response = client.get("/v1/trips") 16 | assert ( 17 | response.json() 18 | == "Request trips from 2024-02-01" 19 | ) 20 | -------------------------------------------------------------------------------- /Chapter12/live_application/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | 4 | WORKDIR /code 5 | 6 | 7 | COPY ./requirements.txt /code/requirements.txt 8 | 9 | 10 | RUN pip install --no-cache-dir \ 11 | --upgrade -r /code/requirements.txt 12 | 13 | 14 | COPY ./app /code/app 15 | 16 | 17 | CMD ["fastapi", "run", "app/main.py", "--port", "80"] 18 | 19 | CMD ["gunicorn",\ 20 | "app.main:app",\ 21 | "--bind", "0.0.0.0:80",\ 22 | "--workers", "4",\ 23 | "--worker-class",\ 24 | "uvicorn.workers.UvicornWorker",\ 25 | "--log-level", "debug"] -------------------------------------------------------------------------------- /Chapter12/fca-server/README.md: -------------------------------------------------------------------------------- 1 | # FCA Server 2 | 3 | [![PyPI - Version](https://img.shields.io/pypi/v/fca-server.svg)](https://pypi.org/project/fca-server) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fca-server.svg)](https://pypi.org/project/fca-server) 5 | 6 | ----- 7 | 8 | ## Table of Contents 9 | 10 | - [Installation](#installation) 11 | - [License](#license) 12 | 13 | ## Installation 14 | 15 | ```console 16 | pip install fca-server 17 | ``` 18 | 19 | ## License 20 | 21 | `fca-server` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. 22 | -------------------------------------------------------------------------------- /Chapter04/saas_app/db_connection.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | 6 | SQLALCHEMY_DATABASE_URL = "sqlite:///database.db" 7 | 8 | 9 | @lru_cache 10 | def get_engine(): 11 | return create_engine( 12 | SQLALCHEMY_DATABASE_URL, 13 | ) 14 | 15 | def get_session(): 16 | Session = sessionmaker( 17 | autocommit=False, 18 | autoflush=False, 19 | bind=get_engine(), 20 | ) 21 | try: 22 | session = Session() 23 | yield session 24 | finally: 25 | session.close() 26 | -------------------------------------------------------------------------------- /Chapter12/live_application/app/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from os import getpid 3 | 4 | from fastapi import FastAPI 5 | 6 | """ 7 | Uncomment the following lines to 8 | enable HTTPS redirection 9 | only works when the server is running with HTTPS 10 | """ 11 | 12 | # from fastapi.middleware.httpsredirect import ( 13 | # HTTPSRedirectMiddleware, 14 | # ) 15 | 16 | logger = logging.getLogger("uvicorn") 17 | 18 | app = FastAPI(title="FastAPI Live Application") 19 | 20 | # app.add_middleware(HTTPSRedirectMiddleware) 21 | 22 | 23 | @app.get("/") 24 | def read_root(): 25 | logger.info(f"Processd by worker {getpid()}") 26 | return {"Hello": "World"} 27 | -------------------------------------------------------------------------------- /Chapter08/trip_platform/app/middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import Request 4 | from starlette.middleware.base import BaseHTTPMiddleware 5 | 6 | logger = logging.getLogger("uvicorn") 7 | 8 | 9 | class ClientInfoMiddleware(BaseHTTPMiddleware): 10 | async def dispatch(self, request: Request, call_next): 11 | host_client = request.client.host 12 | requested_path = request.url.path 13 | method = request.method 14 | 15 | logger.info( 16 | f"host client {host_client} " 17 | f"requested {method} {requested_path} " 18 | "endpoint" 19 | ) 20 | 21 | return await call_next(request) 22 | -------------------------------------------------------------------------------- /Chapter04/saas_app/api_key.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import APIRouter, Depends, HTTPException 4 | 5 | VALID_API_KEYS = [ 6 | "verysecureapikey", 7 | "anothersecureapi", 8 | "onemoresecureapi", 9 | ] 10 | 11 | 12 | async def get_api_key(api_key: Optional[str]): 13 | if api_key not in VALID_API_KEYS: 14 | raise HTTPException( 15 | status_code=403, detail="Invalid API Key" 16 | ) 17 | return api_key 18 | 19 | 20 | router = APIRouter() 21 | 22 | 23 | @router.get("/secure-data") 24 | async def get_secure_data( 25 | api_key: str = Depends(get_api_key), 26 | ): 27 | return {"message": "Access to secure data granted"} 28 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/app/db_connection.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import ( 2 | AsyncSession, 3 | create_async_engine, 4 | ) 5 | from sqlalchemy.orm import sessionmaker 6 | 7 | SQLALCHEMY_DATABASE_URL = ( 8 | "sqlite+aiosqlite:///.database.db" 9 | ) 10 | 11 | 12 | def get_engine(): 13 | return create_async_engine( 14 | SQLALCHEMY_DATABASE_URL, echo=True 15 | ) 16 | 17 | 18 | # sessionmaker for async sessions 19 | AsyncSessionLocal = sessionmaker( 20 | autocommit=False, 21 | autoflush=False, 22 | bind=get_engine(), 23 | class_=AsyncSession, 24 | ) 25 | 26 | 27 | async def get_db_session(): 28 | async with AsyncSessionLocal() as session: 29 | yield session 30 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/app/database.py: -------------------------------------------------------------------------------- 1 | from app.db_connection import es_client, mongo_client 2 | 3 | database = mongo_client.beat_streaming 4 | 5 | 6 | def mongo_database(): 7 | return database 8 | 9 | 10 | songs_index_mapping = { 11 | "mappings": { 12 | "properties": { 13 | "artist": {"type": "keyword"}, 14 | "views_per_country": { 15 | "type": "object", 16 | "dynamic": True, 17 | }, 18 | } 19 | } 20 | } 21 | 22 | 23 | async def create_es_index(): 24 | await es_client.options( 25 | ignore_status=[400, 404] 26 | ).indices.create( 27 | index="songs_index", 28 | body=songs_index_mapping, 29 | ) 30 | -------------------------------------------------------------------------------- /Chapter10/grpc_gateway/app/main.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | from fastapi import FastAPI 3 | from pydantic import BaseModel 4 | 5 | from grpcserver_pb2 import Message 6 | from grpcserver_pb2_grpc import GrpcServerStub 7 | 8 | app = FastAPI() 9 | 10 | 11 | class GRPCResponse(BaseModel): 12 | message: str 13 | received: bool 14 | 15 | 16 | grpc_channel = grpc.aio.insecure_channel( 17 | "localhost:50051" 18 | ) 19 | 20 | 21 | @app.get("/grpc") 22 | async def call_grpc(message: str) -> GRPCResponse: 23 | async with grpc_channel as channel: 24 | grpc_stub = GrpcServerStub(channel) 25 | response = await grpc_stub.GetServerResponse( 26 | Message(message=message) 27 | ) 28 | return response 29 | -------------------------------------------------------------------------------- /Chapter02/sql_example/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | create_engine, 3 | ) 4 | from sqlalchemy.orm import ( 5 | DeclarativeBase, 6 | Mapped, 7 | mapped_column, 8 | sessionmaker, 9 | ) 10 | 11 | DATABASE_URL = "sqlite:///./test.db" 12 | 13 | engine = create_engine(DATABASE_URL) 14 | 15 | 16 | class Base(DeclarativeBase): 17 | pass 18 | 19 | 20 | class User(Base): 21 | __tablename__ = "user" 22 | id: Mapped[int] = mapped_column( 23 | primary_key=True, 24 | ) 25 | name: Mapped[str] 26 | email: Mapped[str] 27 | 28 | 29 | Base.metadata.create_all(bind=engine) 30 | 31 | 32 | SessionLocal = sessionmaker( 33 | autocommit=False, 34 | autoflush=False, 35 | bind=engine, 36 | ) 37 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | ${upgrades if upgrades else "pass"} 23 | 24 | 25 | def downgrade() -> None: 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /Chapter10/graphql/graphql_utils.py: -------------------------------------------------------------------------------- 1 | import strawberry 2 | from strawberry.fastapi import GraphQLRouter 3 | 4 | from database import users_db 5 | 6 | 7 | @strawberry.type 8 | class User: 9 | username: str 10 | phone_number: str 11 | country: str 12 | 13 | 14 | @strawberry.type 15 | class Query: 16 | @strawberry.field 17 | def users( 18 | self, country: str | None 19 | ) -> list[User]: 20 | return [ 21 | User( 22 | username=user.username, 23 | phone_number=user.phone_number, 24 | country=user.country, 25 | ) 26 | for user in users_db 27 | if user.country == country 28 | ] 29 | 30 | 31 | schema = strawberry.Schema(Query) 32 | 33 | graphql_app = GraphQLRouter(schema) 34 | -------------------------------------------------------------------------------- /Chapter10/ecotech_RAG/documents.py: -------------------------------------------------------------------------------- 1 | from langchain.text_splitter import ( 2 | CharacterTextSplitter, 3 | ) 4 | from langchain_community.document_loaders import ( 5 | DirectoryLoader, 6 | ) 7 | from langchain_community.vectorstores import Chroma 8 | 9 | 10 | async def load_documents( 11 | db: Chroma, 12 | ): 13 | text_splitter = CharacterTextSplitter( 14 | chunk_size=100, chunk_overlap=0 15 | ) 16 | 17 | raw_documents = DirectoryLoader( 18 | "docs", "*.txt" 19 | ).load() 20 | 21 | chunks = text_splitter.split_documents( 22 | raw_documents 23 | ) 24 | await db.aadd_documents(chunks) 25 | 26 | 27 | def get_context(user_query: str, db: Chroma) -> str: 28 | docs = db.similarity_search(user_query) 29 | return "\n\n".join( 30 | doc.page_content for doc in docs 31 | ) 32 | -------------------------------------------------------------------------------- /Chapter05/protoapp/protoapp/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import TimedRotatingFileHandler 3 | 4 | from uvicorn.logging import ColourizedFormatter 5 | 6 | client_logger = logging.getLogger("client.logger") 7 | client_logger.setLevel(logging.INFO) 8 | 9 | console_handler = logging.StreamHandler() 10 | 11 | console_formatter = ColourizedFormatter( 12 | "%(levelprefix)s CLIENT CALL - %(message)s", 13 | use_colors=True, 14 | ) 15 | console_handler.setFormatter(console_formatter) 16 | client_logger.addHandler(console_handler) 17 | 18 | file_handler = TimedRotatingFileHandler("app.log") 19 | 20 | file_formatter = logging.Formatter( 21 | "time %(asctime)s, %(levelname)s: %(message)s", 22 | datefmt="%Y-%m-%d %H:%M:%S", 23 | ) 24 | 25 | file_handler.setFormatter(file_formatter) 26 | client_logger.addHandler(file_handler) 27 | -------------------------------------------------------------------------------- /Chapter04/saas_app/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from sqlalchemy.orm import ( 4 | DeclarativeBase, 5 | Mapped, 6 | mapped_column, 7 | ) 8 | 9 | 10 | class Base(DeclarativeBase): 11 | pass 12 | 13 | 14 | class Role(str, Enum): 15 | basic = "basic" 16 | premium = "premium" 17 | 18 | 19 | class User(Base): 20 | __tablename__ = "users" 21 | id: Mapped[int] = mapped_column( 22 | primary_key=True, index=True 23 | ) 24 | username: Mapped[str] = mapped_column( 25 | unique=True, index=True 26 | ) 27 | email: Mapped[str] = mapped_column( 28 | unique=True, index=True 29 | ) 30 | hashed_password: Mapped[str] 31 | role: Mapped[Role] = mapped_column( 32 | default=Role.basic 33 | ) 34 | totp_secret: Mapped[str] = mapped_column( 35 | nullable=True 36 | ) 37 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/fill_mongo_db_database.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from app.db_connection import ( 5 | mongo_client, 6 | ping_mongo_db_server, 7 | ) 8 | from songs_info import songs_info 9 | 10 | logging.basicConfig(level=logging.DEBUG) 11 | db = mongo_client.beat_streaming 12 | collection = db["songs"] 13 | 14 | 15 | async def insert_songs(): 16 | # Generate JSON files 17 | for i, song_info in enumerate(songs_info, start=1): 18 | # Connect to MongoDB 19 | 20 | # Insert song info into MongoDB 21 | await collection.insert_one(song_info) 22 | logging.info(f"{song_info['title']} inserted") 23 | 24 | # Close the MongoDB connection 25 | 26 | 27 | async def main(): 28 | await ping_mongo_db_server() 29 | await insert_songs() 30 | 31 | 32 | if __name__ == "__main__": 33 | asyncio.run(main()) 34 | -------------------------------------------------------------------------------- /Chapter05/protoapp/protoapp/database.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import ( 5 | DeclarativeBase, 6 | Mapped, 7 | mapped_column, 8 | sessionmaker, 9 | ) 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Base(DeclarativeBase): 15 | pass 16 | 17 | 18 | class Item(Base): 19 | __tablename__ = "items" 20 | id: Mapped[int] = mapped_column( 21 | primary_key=True, index=True 22 | ) 23 | name: Mapped[str] = mapped_column(index=True) 24 | color: Mapped[str] 25 | 26 | 27 | DATABASE_URL = "sqlite:///./production.db" 28 | 29 | 30 | engine = create_engine(DATABASE_URL) 31 | 32 | 33 | logger.debug("Binding the engine to the database") 34 | 35 | 36 | Base.metadata.create_all(bind=engine) 37 | 38 | SessionLocal = sessionmaker( 39 | autocommit=False, autoflush=False, bind=engine 40 | ) 41 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/app/ws_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from fastapi import WebSocket 4 | 5 | 6 | class ConnectionManager: 7 | def __init__(self): 8 | self.active_connections: list[WebSocket] = [] 9 | 10 | async def connect(self, websocket: WebSocket): 11 | await websocket.accept() 12 | self.active_connections.append(websocket) 13 | 14 | def disconnect(self, websocket: WebSocket): 15 | self.active_connections.remove(websocket) 16 | 17 | async def send_personal_message( 18 | self, message: dict, websocket: WebSocket 19 | ): 20 | await websocket.send_json(message) 21 | 22 | async def broadcast( 23 | self, message: dict, exclude: WebSocket = None 24 | ): 25 | tasks = [ 26 | connection.send_json(message) 27 | for connection in self.active_connections 28 | if connection != exclude 29 | ] 30 | await asyncio.gather(*tasks) 31 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/app/ws_password_bearer.py: -------------------------------------------------------------------------------- 1 | from fastapi import ( 2 | WebSocket, 3 | WebSocketException, 4 | status, 5 | ) 6 | from fastapi.security import OAuth2PasswordBearer 7 | 8 | 9 | class OAuth2WebSocketPasswordBearer( 10 | OAuth2PasswordBearer 11 | ): 12 | async def __call__( 13 | self, websocket: WebSocket 14 | ) -> str: 15 | authorization: str = websocket.headers.get( 16 | "authorization" 17 | ) 18 | if not authorization: 19 | raise WebSocketException( 20 | code=status.WS_1008_POLICY_VIOLATION, 21 | reason="Not authenticated", 22 | ) 23 | scheme, param = authorization.split() 24 | if scheme.lower() != "bearer": 25 | raise WebSocketException( 26 | code=status.WS_1008_POLICY_VIOLATION, 27 | reason="Invalid authentication credentials", 28 | ) 29 | return param 30 | -------------------------------------------------------------------------------- /Chapter02/uploads_and_downloads/main.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | 4 | from fastapi import ( 5 | FastAPI, 6 | File, 7 | HTTPException, 8 | UploadFile, 9 | ) 10 | from fastapi.responses import FileResponse 11 | 12 | app = FastAPI() 13 | 14 | 15 | @app.post("/uploadfile") 16 | async def upload_file( 17 | file: UploadFile = File(...), 18 | ): 19 | with open( 20 | f"uploads/{file.filename}", "wb" 21 | ) as buffer: 22 | shutil.copyfileobj(file.file, buffer) 23 | 24 | return {"filename": file.filename} 25 | 26 | 27 | @app.get( 28 | "/downloadfile/{filename}", 29 | response_class=FileResponse, 30 | ) 31 | async def download_file(filename: str): 32 | if not Path(f"uploads/{filename}").exists(): 33 | raise HTTPException( 34 | status_code=404, 35 | detail=f"file {filename} not found", 36 | ) 37 | 38 | return FileResponse( 39 | path=f"uploads/{filename}", filename=filename 40 | ) 41 | -------------------------------------------------------------------------------- /Chapter05/protoapp/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | from sqlalchemy import create_engine, inspect 4 | from sqlalchemy.orm import sessionmaker 5 | from sqlalchemy.pool import StaticPool 6 | 7 | from protoapp.database import Base 8 | from protoapp.main import app, get_db_session 9 | 10 | engine = create_engine( 11 | "sqlite:///:memory:", 12 | connect_args={"check_same_thread": False}, 13 | poolclass=StaticPool, 14 | ) 15 | 16 | Base.metadata.create_all(bind=engine) # Create tables 17 | 18 | TestingSessionLocal = sessionmaker( 19 | autocommit=False, autoflush=False, bind=engine 20 | ) 21 | 22 | @pytest.fixture 23 | def test_db_session(): 24 | db = TestingSessionLocal() 25 | try: 26 | yield db 27 | finally: 28 | db.close() 29 | 30 | 31 | @pytest.fixture 32 | def test_client(test_db_session): 33 | client = TestClient(app) 34 | app.dependency_overrides[get_db_session] = ( 35 | lambda: test_db_session 36 | ) 37 | 38 | return client 39 | -------------------------------------------------------------------------------- /Chapter04/saas_app/user_session.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Response 2 | from sqlalchemy.orm import Session 3 | 4 | from db_connection import get_session 5 | from operations import get_user 6 | from rbac import get_current_user 7 | from responses import UserCreateResponse 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.post("/login") 13 | async def login( 14 | response: Response, 15 | user: UserCreateResponse = Depends( 16 | get_current_user 17 | ), 18 | session: Session = Depends(get_session), 19 | ): 20 | user = get_user(session, user.username) 21 | 22 | response.set_cookie( 23 | key="fakesession", value=f"{user.id}" 24 | ) 25 | return {"message": "User logged in successfully"} 26 | 27 | 28 | @router.post("/logout") 29 | async def logout( 30 | response: Response, 31 | user: UserCreateResponse = Depends( 32 | get_current_user 33 | ), 34 | ): 35 | response.delete_cookie( 36 | "fakesession" 37 | ) # Clear session data 38 | return {"message": "User logged out successfully"} 39 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/benchmark_websocket.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import multiprocessing 3 | 4 | import uvicorn 5 | from websockets import connect 6 | 7 | from app.main import app 8 | 9 | 10 | def run_server(): 11 | uvicorn.run(app) 12 | 13 | 14 | async def connect_client( 15 | n: int, n_messages: int = 3 16 | ): 17 | async with connect( 18 | f"ws://localhost:8000/chatroom/user{n}", 19 | ) as client: 20 | for _ in range(n_messages): 21 | await client.send( 22 | f"Hello World from user{n}" 23 | ) 24 | await asyncio.sleep(n * 0.1) 25 | await asyncio.sleep(2) 26 | 27 | 28 | async def main(n_clients: int = 10): 29 | p = multiprocessing.Process(target=run_server) 30 | p.start() 31 | await asyncio.sleep(1) 32 | 33 | connections = [ 34 | connect_client(n) for n in range(n_clients) 35 | ] 36 | 37 | await asyncio.gather(*connections) 38 | 39 | await asyncio.sleep(1) 40 | p.terminate() 41 | 42 | 43 | if __name__ == "__main__": 44 | asyncio.run(main()) 45 | -------------------------------------------------------------------------------- /Chapter12/fca-server/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present U.N. Owen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Packt 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 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/alembic/versions/9a9cfeb65ea4_add_ticket_sold_information.py: -------------------------------------------------------------------------------- 1 | """Add ticket sold information 2 | 3 | Revision ID: 9a9cfeb65ea4 4 | Revises: c89449e81bb3 5 | Create Date: 2024-03-06 18:11:32.072355 6 | 7 | """ 8 | 9 | from typing import Sequence, Union 10 | 11 | import sqlalchemy as sa 12 | 13 | from alembic import op 14 | 15 | # revision identifiers, used by Alembic. 16 | revision: str = "9a9cfeb65ea4" 17 | down_revision: Union[str, None] = "c89449e81bb3" 18 | branch_labels: Union[str, Sequence[str], None] = None 19 | depends_on: Union[str, Sequence[str], None] = None 20 | 21 | 22 | def upgrade() -> None: 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.add_column( 25 | "tickets", 26 | sa.Column( 27 | "sold", 28 | sa.Boolean(), 29 | nullable=False, 30 | default=False, 31 | ), 32 | ) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade() -> None: 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_column("tickets", "sold") 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /Chapter04/saas_app/test_security.py: -------------------------------------------------------------------------------- 1 | from security import authenticate_user 2 | 3 | 4 | def test_authenticate_user_with_username( 5 | fill_database_session, 6 | ): 7 | user = authenticate_user( 8 | fill_database_session, "johndoe", "pass1234" 9 | ) 10 | assert user.username == "johndoe" 11 | assert user.email == "johndoe@email.com" 12 | 13 | 14 | def test_authenticate_user_with_email( 15 | fill_database_session, 16 | ): 17 | user = authenticate_user( 18 | fill_database_session, 19 | "johndoe@email.com", 20 | "pass1234", 21 | ) 22 | assert user.username == "johndoe" 23 | assert user.email == "johndoe@email.com" 24 | 25 | 26 | def test_authenticate_user_dot_find_username(session): 27 | assert ( 28 | authenticate_user( 29 | session, "non_existing_user", "pass1234" 30 | ) 31 | is None 32 | ) 33 | 34 | 35 | def test_authenticate_user_incorrect_password(session): 36 | assert ( 37 | authenticate_user( 38 | session, 39 | "johndoe@email.com", 40 | "incorrect_password", 41 | ) 42 | is None 43 | ) 44 | -------------------------------------------------------------------------------- /Chapter10/grpc_gateway/grpc_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | import grpc 5 | 6 | from grpcserver_pb2 import MessageResponse 7 | from grpcserver_pb2_grpc import ( 8 | GrpcServerServicer, 9 | add_GrpcServerServicer_to_server, 10 | ) 11 | 12 | 13 | class Service(GrpcServerServicer): 14 | async def GetServerResponse( 15 | self, request, context 16 | ): 17 | message = request.message 18 | logging.info(f"Received message: {message}") 19 | result = ( 20 | f"Hello I am up and running received: {message}" 21 | ) 22 | result = { 23 | "message": result, 24 | "received": True, 25 | } 26 | return MessageResponse(**result) 27 | 28 | 29 | async def serve(): 30 | server = grpc.aio.server() 31 | add_GrpcServerServicer_to_server( 32 | Service(), server 33 | ) 34 | server.add_insecure_port("[::]:50051") 35 | logging.info("Starting server on port 50051") 36 | await server.start() 37 | await server.wait_for_termination() 38 | 39 | 40 | if __name__ == "__main__": 41 | logging.basicConfig(level=logging.INFO) 42 | asyncio.run(serve()) 43 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/alembic/versions/c89449e81bb3_start_database.py: -------------------------------------------------------------------------------- 1 | """Start database 2 | 3 | Revision ID: c89449e81bb3 4 | Revises: 5 | Create Date: 2024-03-06 17:59:44.979602 6 | 7 | """ 8 | 9 | from typing import Sequence, Union 10 | 11 | import sqlalchemy as sa 12 | 13 | from alembic import op 14 | 15 | # revision identifiers, used by Alembic. 16 | revision: str = "c89449e81bb3" 17 | down_revision: Union[str, None] = None 18 | branch_labels: Union[str, Sequence[str], None] = None 19 | depends_on: Union[str, Sequence[str], None] = None 20 | 21 | 22 | def upgrade() -> None: 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.create_table( 25 | "tickets", 26 | sa.Column("id", sa.Integer(), nullable=False), 27 | sa.Column("price", sa.Float(), nullable=True), 28 | sa.Column("show", sa.String(), nullable=True), 29 | sa.Column("user", sa.String(), nullable=True), 30 | sa.PrimaryKeyConstraint("id"), 31 | ) 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade() -> None: 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | op.drop_table("tickets") 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/fill_elasticsearch_index.py: -------------------------------------------------------------------------------- 1 | from app.db_connection import es_client 2 | from songs_info import songs_info 3 | 4 | mapping = { 5 | "mappings": { 6 | "properties": { 7 | "artist": {"type": "keyword"}, 8 | "views_per_country": { 9 | "type": "object", 10 | "dynamic": True, 11 | }, 12 | } 13 | } 14 | } 15 | 16 | 17 | async def create_index(): 18 | await es_client.options( 19 | ignore_status=[400, 404] 20 | ).indices.create( 21 | index="songs_index", 22 | body=mapping, 23 | ) 24 | await es_client.close() 25 | 26 | 27 | async def fill_elastichsearch(): 28 | for song in songs_info: 29 | await es_client.index( 30 | index="songs_index", body=song 31 | ) 32 | await es_client.close() 33 | 34 | 35 | async def delete_all_indexes(): 36 | await es_client.options( 37 | ignore_status=[400, 404] 38 | ).indices.delete(index="*") 39 | await es_client.close() 40 | 41 | 42 | async def main(): 43 | await delete_all_indexes() 44 | await create_index() 45 | await fill_elastichsearch() 46 | 47 | 48 | if __name__ == "__main__": 49 | import asyncio 50 | 51 | asyncio.run(main()) 52 | -------------------------------------------------------------------------------- /Chapter11/middleware_project/middleware/request_middleware.py: -------------------------------------------------------------------------------- 1 | from starlette.types import ( 2 | ASGIApp, 3 | Scope, 4 | Receive, 5 | Send, 6 | Message, 7 | ) 8 | from hashlib import sha1 9 | 10 | 11 | class HashBodyContentMiddleWare: 12 | def __init__( 13 | self, app: ASGIApp, allowed_paths: list[str] 14 | ): 15 | self.app = app 16 | self.allowed_paths = allowed_paths 17 | 18 | async def __call__( 19 | self, 20 | scope: Scope, 21 | receive: Receive, 22 | send: Send, 23 | ): 24 | if ( 25 | scope["type"] != "http" 26 | or scope["path"] 27 | not in self.allowed_paths 28 | ): 29 | await self.app(scope, receive, send) 30 | return 31 | 32 | async def receive_with_new_body() -> Message: 33 | message = await receive() 34 | assert message["type"] == "http.request" 35 | 36 | body = message.get("body", b"") 37 | message["body"] = ( 38 | f'"{sha1(body).hexdigest()}"'.encode() 39 | ) 40 | 41 | return message 42 | 43 | await self.app( 44 | scope, 45 | receive_with_new_body, 46 | send, 47 | ) 48 | -------------------------------------------------------------------------------- /Chapter08/trip_platform/profiling_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from contextlib import contextmanager 4 | from multiprocessing import Process 5 | 6 | import uvicorn 7 | from httpx import AsyncClient 8 | 9 | from app.main import app 10 | from app.profiler import ProfileEndpointsMiddleWare 11 | 12 | 13 | def run_server(): 14 | app.add_middleware(ProfileEndpointsMiddleWare) 15 | uvicorn.run(app, port=8000) 16 | 17 | 18 | @contextmanager 19 | def run_server_in_process(): 20 | p = Process(target=run_server) 21 | p.start() 22 | time.sleep(2) # Give the server a second to start 23 | yield 24 | p.terminate() 25 | 26 | 27 | async def make_requests(n: int): 28 | async with AsyncClient( 29 | base_url="http://localhost:8000" 30 | ) as client: 31 | tasks = ( 32 | client.get( 33 | "/sleeping_sync", timeout=float("inf") 34 | ) 35 | for _ in range(n) 36 | ) 37 | 38 | await asyncio.gather(*tasks) 39 | 40 | 41 | async def main(): 42 | with run_server_in_process(): 43 | # Make requests to the server 44 | print("Server is running in a separate process") 45 | await make_requests(2) 46 | 47 | 48 | if __name__ == "__main__": 49 | asyncio.run(main()) 50 | -------------------------------------------------------------------------------- /Chapter04/saas_app/third_party_login.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from fastapi import Depends, HTTPException 3 | from fastapi.security import OAuth2 4 | from sqlalchemy.orm import Session 5 | 6 | from db_connection import get_session 7 | from models import User 8 | from operations import get_user 9 | 10 | GITHUB_CLIENT_ID = "your_github_client_id" 11 | GITHUB_CLIENT_SECRET = "your_github_client_secret" 12 | GITHUB_REDIRECT_URI = ( 13 | "http://localhost:8000/github/auth/token" 14 | ) 15 | GITHUB_AUTHORIZATION_URL = ( 16 | "https://github.com/login/oauth/authorize" 17 | ) 18 | 19 | 20 | def resolve_github_token( 21 | access_token: str = Depends(OAuth2()), 22 | session: Session = Depends(get_session), 23 | ) -> User: 24 | user_response = httpx.get( 25 | "https://api.github.com/user", 26 | headers={"Authorization": access_token}, 27 | ).json() 28 | username = user_response.get("login", " ") 29 | user = get_user(session, username) 30 | if not user: 31 | email = user_response.get("email", " ") 32 | user = get_user(session, email) 33 | # Process user_response to log 34 | # the user in or create a new account 35 | if not user: 36 | raise HTTPException( 37 | status_code=403, detail="Token not valid" 38 | ) 39 | return user 40 | -------------------------------------------------------------------------------- /Chapter10/ecotech_RAG/prompting.py: -------------------------------------------------------------------------------- 1 | from langchain.prompts import ( 2 | ChatPromptTemplate, 3 | HumanMessagePromptTemplate, 4 | SystemMessagePromptTemplate, 5 | ) 6 | 7 | template: str = """ 8 | You are a customer support Chatbot. 9 | You assist users with general inquiries 10 | and technical issues. 11 | You will answer to the question: 12 | {question} 13 | Your answer will only be based on the knowledge 14 | of the context below you are trained on. 15 | ----------- 16 | {context} 17 | ----------- 18 | if you don't know the answer, 19 | you will ask the user to rephrase the question or 20 | redirect the user the support@ecotech.com 21 | always be friendly and helpful 22 | at the end of the conversation, 23 | ask the user if they are satisfied with the answer 24 | if yes, say goodbye and end the conversation 25 | """ 26 | 27 | system_message_prompt = ( 28 | SystemMessagePromptTemplate.from_template( 29 | template 30 | ) 31 | ) 32 | 33 | human_message_prompt = ( 34 | HumanMessagePromptTemplate.from_template( 35 | template="{question}", 36 | ) 37 | ) 38 | 39 | chat_prompt_template = ( 40 | ChatPromptTemplate.from_messages( 41 | [system_message_prompt, human_message_prompt] 42 | ) 43 | ) 44 | -------------------------------------------------------------------------------- /Chapter10/chef_ai/handlers.py: -------------------------------------------------------------------------------- 1 | from cohere import AsyncClient, ChatMessage 2 | from cohere.core.api_error import ApiError 3 | from dotenv import load_dotenv 4 | from fastapi import HTTPException 5 | 6 | load_dotenv() 7 | 8 | SYSTEM_MESSAGE = ( 9 | "You are a skilled Italian top chef " 10 | "expert in Italian cuisine tradition " 11 | "that suggest the best recipes unveiling " 12 | "tricks and tips from Grandma's Kitchen" 13 | "shortly and concisely." 14 | ) 15 | 16 | 17 | client = AsyncClient() 18 | 19 | 20 | async def generate_chat_completion( 21 | user_query=" ", messages=[] 22 | ) -> str: 23 | try: 24 | response = await client.chat( 25 | message=user_query, 26 | model="command-r-plus", 27 | preamble=SYSTEM_MESSAGE, 28 | chat_history=messages, 29 | ) 30 | messages.extend( 31 | [ 32 | ChatMessage( 33 | role="USER", message=user_query 34 | ), 35 | ChatMessage( 36 | role="CHATBOT", 37 | message=response.text, 38 | ), 39 | ] 40 | ) 41 | return response.text 42 | 43 | except ApiError as e: 44 | raise HTTPException( 45 | status_code=e.status_code, detail=e.body 46 | ) 47 | -------------------------------------------------------------------------------- /Chapter11/middleware_project/middleware/response_middlaware.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | 3 | from starlette.datastructures import MutableHeaders 4 | from starlette.types import ( 5 | ASGIApp, 6 | Receive, 7 | Scope, 8 | Send, 9 | Message, 10 | ) 11 | 12 | 13 | class ExtraHeadersResponseMiddleware: 14 | def __init__( 15 | self, 16 | app: ASGIApp, 17 | headers: Sequence[tuple[str, str]], 18 | ): 19 | self.app = app 20 | self.headers = headers 21 | 22 | async def __call__( 23 | self, 24 | scope: Scope, 25 | receive: Receive, 26 | send: Send, 27 | ): 28 | if scope["type"] != "http": 29 | return await self.app( 30 | scope, receive, send 31 | ) 32 | 33 | async def send_with_extra_headers( 34 | message: Message, 35 | ): 36 | if ( 37 | message["type"] 38 | == "http.response.start" 39 | ): 40 | headers = MutableHeaders( 41 | scope=message 42 | ) 43 | for key, value in self.headers: 44 | headers.append(key, value) 45 | 46 | await send(message) 47 | 48 | await self.app( 49 | scope, receive, send_with_extra_headers 50 | ) 51 | -------------------------------------------------------------------------------- /Chapter04/saas_app/operations.py: -------------------------------------------------------------------------------- 1 | from email_validator import ( 2 | EmailNotValidError, 3 | validate_email, 4 | ) 5 | from passlib.context import CryptContext 6 | from sqlalchemy.exc import IntegrityError 7 | from sqlalchemy.orm import Session 8 | 9 | from models import Role, User 10 | 11 | pwd_context = CryptContext( 12 | schemes=["bcrypt"], deprecated="auto" 13 | ) 14 | 15 | 16 | def add_user( 17 | session: Session, 18 | username: str, 19 | password: str, 20 | email: str, 21 | role: Role = Role.basic, 22 | ) -> User | None: 23 | hashed_password = pwd_context.hash(password) 24 | db_user = User( 25 | username=username, 26 | email=email, 27 | hashed_password=hashed_password, 28 | role=role, 29 | ) 30 | session.add(db_user) 31 | try: 32 | session.commit() 33 | session.refresh(db_user) 34 | except IntegrityError: 35 | session.rollback() 36 | return 37 | return db_user 38 | 39 | 40 | def get_user( 41 | session: Session, username_or_email: str 42 | ) -> User | None: 43 | try: 44 | validate_email(username_or_email) 45 | query_filter = User.email 46 | except EmailNotValidError: 47 | query_filter = User.username 48 | user = ( 49 | session.query(User) 50 | .filter(query_filter == username_or_email) 51 | .first() 52 | ) 53 | return user 54 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/app/db_connection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from elasticsearch import ( 4 | AsyncElasticsearch, 5 | TransportError, 6 | ) 7 | from motor.motor_asyncio import AsyncIOMotorClient 8 | from redis import asyncio as aioredis 9 | 10 | logger = logging.getLogger("uvicorn") 11 | 12 | 13 | mongo_client = AsyncIOMotorClient( 14 | "mongodb://localhost:27017" 15 | ) 16 | 17 | es_client = AsyncElasticsearch("http://localhost:9200") 18 | 19 | redis_client = aioredis.from_url("redis://localhost") 20 | 21 | 22 | async def ping_mongo_db_server(): 23 | try: 24 | await mongo_client.admin.command("ping") 25 | logger.info("Connected to MongoDB") 26 | except Exception as e: 27 | logger.error( 28 | f"Error connecting to MongoDB: {e}" 29 | ) 30 | raise e 31 | 32 | 33 | async def ping_elasticsearch_server(): 34 | try: 35 | await es_client.info() 36 | logger.info( 37 | "Elasticsearch connection successful" 38 | ) 39 | except TransportError as e: 40 | logger.error( 41 | f"Elasticsearch connection failed: {e}" 42 | ) 43 | raise e 44 | 45 | 46 | async def ping_redis_server(): 47 | try: 48 | await redis_client.ping() 49 | logger.info("Connected to Redis") 50 | except Exception as e: 51 | logger.error(f"Error connecting to Redis: {e}") 52 | raise e 53 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/queries_used_in_elasticsearch.md: -------------------------------------------------------------------------------- 1 | # top ten songs by country 2 | GET songs_index/_search?pretty=true 3 | ```json 4 | { 5 | "query": { 6 | "bool": { 7 | "must": { 8 | "match_all": {} 9 | }, 10 | "filter": [ 11 | { 12 | "exists": { 13 | "field": "views_per_country.Italy" 14 | } 15 | } 16 | ] 17 | } 18 | }, 19 | "_source": [ 20 | "title", 21 | "views_per_country.Italy", 22 | "album.title", 23 | "artist" 24 | ], 25 | "sort": { 26 | "views_per_country.Italy": { 27 | "order": "desc" 28 | } 29 | }, 30 | "size": 10 31 | } 32 | ``` 33 | 34 | 35 | # top ten artists by country 36 | GET songs_index/_search?pretty=true 37 | ```json 38 | { 39 | "query": { 40 | "bool": { 41 | "filter": [ 42 | { 43 | "exists": { 44 | "field": "views_per_country.Italy" 45 | } 46 | } 47 | ] 48 | } 49 | }, 50 | "size": 0, 51 | "aggs": { 52 | "top_artists": { 53 | "terms": { 54 | "field": "artist", 55 | "order": { 56 | "views": "desc" 57 | }, 58 | "size": 10 59 | }, 60 | "aggs": { 61 | "views": { 62 | "sum": { 63 | "field": "views_per_country.Italy", 64 | "missing": 0 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | ``` -------------------------------------------------------------------------------- /Chapter10/ai_doctor/app/main.py: -------------------------------------------------------------------------------- 1 | from huggingface_hub import hf_hub_download 2 | import joblib 3 | from typing import Annotated 4 | from pydantic import create_model 5 | from fastapi import FastAPI, Depends 6 | from contextlib import asynccontextmanager 7 | from app.utils import symptoms_list 8 | 9 | ml_model = {} 10 | REPO_ID = "AWeirdDev/human-disease-prediction" 11 | FILENAME = "sklearn_model.joblib" 12 | 13 | 14 | @asynccontextmanager 15 | async def lifespan(app: FastAPI): 16 | ml_model["doctor"] = joblib.load( 17 | hf_hub_download( 18 | repo_id=REPO_ID, filename=FILENAME 19 | ) 20 | ) 21 | 22 | yield 23 | ml_model.clear() 24 | 25 | 26 | app = FastAPI(title="AI Doctor", lifespan=lifespan) 27 | 28 | 29 | query_parameters = { 30 | symp: (bool, False) 31 | for symp in symptoms_list[:10] 32 | } 33 | 34 | Symptoms = create_model("Symptoms", **query_parameters) 35 | 36 | 37 | @app.get("/diagnosis") 38 | async def get_diagnosis( 39 | symptoms: Annotated[Symptoms, Depends()], 40 | ): 41 | array = [ 42 | int(value) 43 | for _, value in symptoms.model_dump().items() 44 | ] 45 | array.extend( # adapt the array to the model's input shape 46 | [0] * (len(symptoms_list) - len(array)) 47 | ) 48 | len(symptoms_list) 49 | diseases = ml_model["doctor"].predict([array]) 50 | return { 51 | "diseases": [disease for disease in diseases] 52 | } 53 | -------------------------------------------------------------------------------- /Chapter04/saas_app/test_operations.py: -------------------------------------------------------------------------------- 1 | from operations import add_user, get_user 2 | from models import User, Role 3 | 4 | 5 | def test_add_user_into_the_database(session): 6 | user = add_user( 7 | session=session, 8 | username="sheldonsonny", 9 | password="difficultpassword", 10 | email="sheldonsonny@email.com", 11 | ) 12 | 13 | assert ( 14 | session.query(User) 15 | .filter(User.id == user.id) 16 | .first() 17 | == user 18 | ) 19 | 20 | 21 | def test_add_premium_user_into_the_database(session): 22 | user = add_user( 23 | session=session, 24 | username="mariorossi", 25 | password="difficultpassword", 26 | email="mariorossi@email.com", 27 | role=Role.premium, 28 | ) 29 | 30 | premium_user = ( 31 | session.query(User) 32 | .filter(User.id == user.id) 33 | .first() 34 | ) 35 | 36 | assert premium_user.role == Role.premium 37 | 38 | 39 | def test_get_user_by_username(fill_database_session): 40 | user = get_user(fill_database_session, "johndoe") 41 | 42 | assert user.username == "johndoe" 43 | assert user.email == "johndoe@email.com" 44 | 45 | 46 | def test_get_user_by_email(fill_database_session): 47 | user = get_user( 48 | fill_database_session, "johndoe@email.com" 49 | ) 50 | 51 | assert user.username == "johndoe" 52 | assert user.email == "johndoe@email.com" 53 | -------------------------------------------------------------------------------- /Chapter04/saas_app/premium_access.py: -------------------------------------------------------------------------------- 1 | from fastapi import ( 2 | APIRouter, 3 | Depends, 4 | HTTPException, 5 | status, 6 | ) 7 | from sqlalchemy.orm import Session 8 | 9 | from db_connection import get_session 10 | from models import Role 11 | from operations import add_user 12 | from responses import ( 13 | ResponseCreateUser, 14 | UserCreateBody, 15 | UserCreateResponse, 16 | ) 17 | 18 | router = APIRouter() 19 | 20 | 21 | @router.post( 22 | "/register/premium-user", 23 | status_code=status.HTTP_201_CREATED, 24 | response_model=ResponseCreateUser, 25 | responses={ 26 | status.HTTP_409_CONFLICT: { 27 | "description": "The user already exists" 28 | }, 29 | status.HTTP_201_CREATED: { 30 | "description": "User created" 31 | }, 32 | }, 33 | ) 34 | def register_premium_user( 35 | user: UserCreateBody, 36 | session: Session = Depends(get_session), 37 | ): 38 | user = add_user( 39 | session=session, 40 | **user.model_dump(), 41 | role=Role.premium, 42 | ) 43 | if not user: 44 | raise HTTPException( 45 | status.HTTP_409_CONFLICT, 46 | "username or email already exists", 47 | ) 48 | user_response = UserCreateResponse( 49 | username=user.username, 50 | email=user.email, 51 | ) 52 | return { 53 | "message": "user created", 54 | "user": user_response, 55 | } 56 | -------------------------------------------------------------------------------- /Chapter05/protoapp/tests/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import ASGITransport, AsyncClient 3 | 4 | from protoapp.database import Item 5 | from protoapp.main import app 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_read_main(): 10 | client = AsyncClient( 11 | transport=ASGITransport(app=app), 12 | base_url="http://test", 13 | ) 14 | response = await client.get("/home") 15 | assert response.status_code == 200 16 | assert response.json() == {"message": "Hello World"} 17 | 18 | 19 | def test_read_main_client(test_client): 20 | response = test_client.get("/home") 21 | 22 | assert response.status_code == 200 23 | assert response.json() == {"message": "Hello World"} 24 | 25 | 26 | @pytest.mark.integration 27 | def test_client_can_add_read_the_item_from_database( 28 | test_client, test_db_session 29 | ): 30 | response = test_client.get("/item/1") 31 | assert response.status_code == 404 32 | 33 | response = test_client.post( 34 | "/item", json={"name": "ball", "color": "red"} 35 | ) 36 | assert response.status_code == 201 37 | # Verify the user was added to the database 38 | item_id = response.json() 39 | item = ( 40 | test_db_session.query(Item) 41 | .filter(Item.id == item_id) 42 | .first() 43 | ) 44 | assert item is not None 45 | 46 | response = test_client.get("item/1") 47 | assert response.status_code == 200 48 | assert response.json() == { 49 | "name": "ball", 50 | "color": "red", 51 | } 52 | -------------------------------------------------------------------------------- /Chapter10/grpc_gateway/grpcserver_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: grpcserver.proto 4 | # Protobuf Python Version: 5.26.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10grpcserver.proto\"\x1a\n\x07Message\x12\x0f\n\x07message\x18\x01 \x01(\t\"4\n\x0fMessageResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x10\n\x08received\x18\x02 \x01(\x08\x32?\n\nGrpcServer\x12\x31\n\x11GetServerResponse\x12\x08.Message\x1a\x10.MessageResponse\"\x00\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpcserver_pb2', _globals) 22 | if not _descriptor._USE_C_DESCRIPTORS: 23 | DESCRIPTOR._loaded_options = None 24 | _globals['_MESSAGE']._serialized_start=20 25 | _globals['_MESSAGE']._serialized_end=46 26 | _globals['_MESSAGERESPONSE']._serialized_start=48 27 | _globals['_MESSAGERESPONSE']._serialized_end=100 28 | _globals['_GRPCSERVER']._serialized_start=102 29 | _globals['_GRPCSERVER']._serialized_end=165 30 | # @@protoc_insertion_point(module_scope) 31 | -------------------------------------------------------------------------------- /Chapter03/task_manager_app/conftest.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from pathlib import Path 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | TEST_DATABASE_FILE = "test_tasks.csv" 9 | 10 | TEST_TASKS_CSV = [ 11 | { 12 | "id": "1", 13 | "title": "Test Task One", 14 | "description": "Test Description One", 15 | "status": "Incomplete", 16 | }, 17 | { 18 | "id": "2", 19 | "title": "Test Task Two", 20 | "description": "Test Description Two", 21 | "status": "Ongoing", 22 | }, 23 | ] 24 | 25 | TEST_TASKS = [ 26 | {**task_json, "id": int(task_json["id"])} 27 | for task_json in TEST_TASKS_CSV 28 | ] 29 | 30 | 31 | @pytest.fixture(autouse=True) 32 | def create_test_database(): 33 | database_file_location = str( 34 | Path(__file__).parent / TEST_DATABASE_FILE 35 | ) 36 | with patch( 37 | "operations.DATABASE_FILENAME", 38 | database_file_location, 39 | ) as csv_test: 40 | with open( 41 | database_file_location, mode="w", newline="" 42 | ) as csvfile: 43 | writer = csv.DictWriter( 44 | csvfile, 45 | fieldnames=[ 46 | "id", 47 | "title", 48 | "description", 49 | "status", 50 | ], 51 | ) 52 | writer.writeheader() 53 | writer.writerows(TEST_TASKS_CSV) 54 | print("") 55 | yield csv_test 56 | os.remove(database_file_location) 57 | -------------------------------------------------------------------------------- /Chapter10/chef_ai/main.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import Annotated, Tuple 3 | 4 | from cohere import ChatMessage 5 | from fastapi import Body, FastAPI, Request 6 | from pydantic import BaseModel 7 | 8 | from handlers import generate_chat_completion 9 | 10 | 11 | @asynccontextmanager 12 | async def lifespan(app: FastAPI): 13 | yield {"messages": []} 14 | 15 | 16 | app = FastAPI( 17 | title="Chef Cuisine Chatbot App", 18 | lifespan=lifespan, 19 | ) 20 | 21 | 22 | @app.post("/query") 23 | async def query_chat_bot( 24 | request: Request, 25 | query: Annotated[str, Body(min_length=1)], 26 | ) -> str: 27 | answer = await generate_chat_completion( 28 | query, request.state.messages 29 | ) 30 | return answer 31 | 32 | 33 | class MessagesResponse(BaseModel): 34 | messages: list[Tuple[str, str]] 35 | 36 | @classmethod 37 | def from_chat_messages( 38 | cls, messages: list[ChatMessage] 39 | ): 40 | return cls( 41 | messages=[ 42 | (message.role, message.message) 43 | for message in messages 44 | ] 45 | ) 46 | 47 | 48 | @app.get("/messages") 49 | def get_conversation_history( 50 | request: Request, 51 | ) -> MessagesResponse: 52 | return MessagesResponse.from_chat_messages( 53 | messages=request.state.messages 54 | ) 55 | 56 | 57 | @app.post("/restart-conversation") 58 | def restart_conversation(request: Request): 59 | request.state.messages = [] 60 | return {"message": "Conversation restarted"} 61 | -------------------------------------------------------------------------------- /Chapter11/middleware_project/middleware/asgi_middleware.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | 4 | from starlette.types import ( 5 | ASGIApp, 6 | Receive, 7 | Scope, 8 | Send, 9 | ) 10 | 11 | logger = logging.getLogger("uvicorn") 12 | 13 | 14 | class ASGIMiddleware: 15 | def __init__( 16 | self, 17 | app: ASGIApp, 18 | parameter: str = "default", 19 | ): 20 | self.app = app 21 | self.parameter = parameter 22 | 23 | async def __call__( 24 | self, 25 | scope: Scope, 26 | receive: Receive, 27 | send: Send, 28 | ): 29 | logger.info("Entering ASGI middleware") 30 | logger.info( 31 | f"The parameter is: {self.parameter}" 32 | ) 33 | logger.info( 34 | f"event scope: {scope.get('type')}" 35 | ) 36 | await self.app(scope, receive, send) 37 | logger.info("Exiting ASGI middleware") 38 | 39 | 40 | def asgi_middleware( 41 | app: ASGIApp, parameter: str = "default" 42 | ): 43 | @functools.wraps(app) 44 | async def wrapped_app( 45 | scope: Scope, receive: Receive, send: Send 46 | ): 47 | logger.info( 48 | "Entering second ASGI middleware" 49 | ) 50 | logger.info( 51 | f"The parameter you proved is: {parameter}" 52 | ) 53 | logger.info( 54 | f"event scope: {scope.get('type')}" 55 | ) 56 | await app(scope, receive, send) 57 | logger.info("Exiting second ASGI middleware") 58 | 59 | return wrapped_app 60 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/create_aggregation_and_user_data_view.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | 3 | client = MongoClient("mongodb://localhost:27017/") 4 | 5 | 6 | pipeline_redact = { 7 | "$redact": { 8 | "$cond": { 9 | "if": { 10 | "$eq": ["$consent_to_share_data", True] 11 | }, 12 | "then": "$$KEEP", 13 | "else": "$$PRUNE", 14 | } 15 | } 16 | } 17 | 18 | 19 | pipeline_remove_email_and_name = {"$unset": ["email", "name"]} 20 | 21 | 22 | obfuscate_day_of_date = { 23 | "$concat": [ 24 | { 25 | "$substrCP": [ 26 | "$$action.date", 27 | 0, 28 | 7, 29 | ] 30 | }, 31 | "-XX", 32 | ] 33 | } 34 | 35 | rebuild_actions_elements = { 36 | "input": "$actions", 37 | "as": "action", 38 | "in": { 39 | "$mergeObjects": [ 40 | "$$action", 41 | {"date": obfuscate_day_of_date}, 42 | ] 43 | }, 44 | } 45 | 46 | 47 | pipeline_set_actions = { 48 | "$set": { 49 | "actions": {"$map": rebuild_actions_elements}, 50 | } 51 | } 52 | 53 | 54 | pipeline = [ 55 | pipeline_redact, 56 | pipeline_remove_email_and_name, 57 | pipeline_set_actions, 58 | ] 59 | 60 | 61 | if __name__ == "__main__": 62 | client["beat_streaming"].drop_collection( 63 | "users_data_view" 64 | ) 65 | 66 | client["beat_streaming"].create_collection( 67 | "users_data_view", 68 | viewOn="users", 69 | pipeline=pipeline, 70 | ) 71 | -------------------------------------------------------------------------------- /Chapter04/saas_app/github_login.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from fastapi import APIRouter, HTTPException, status 3 | 4 | from security import Token 5 | from third_party_login import ( 6 | GITHUB_AUTHORIZATION_URL, 7 | GITHUB_CLIENT_ID, 8 | GITHUB_CLIENT_SECRET, 9 | GITHUB_REDIRECT_URI, 10 | ) 11 | 12 | router = APIRouter() 13 | 14 | 15 | @router.get("/auth/url") 16 | def github_login(): 17 | return { 18 | "auth_url": GITHUB_AUTHORIZATION_URL 19 | + f"?client_id={GITHUB_CLIENT_ID}" 20 | } 21 | 22 | 23 | @router.get( 24 | "/github/auth/token", 25 | response_model=Token, 26 | responses={ 27 | status.HTTP_401_UNAUTHORIZED: { 28 | "description": "User not registered" 29 | } 30 | }, 31 | ) 32 | async def github_callback(code: str): 33 | token_response = httpx.post( 34 | "https://github.com/login/oauth/access_token", 35 | data={ 36 | "client_id": GITHUB_CLIENT_ID, 37 | "client_secret": GITHUB_CLIENT_SECRET, 38 | "code": code, 39 | "redirect_uri": GITHUB_REDIRECT_URI, 40 | }, 41 | headers={"Accept": "application/json"}, 42 | ).json() 43 | access_token = token_response.get("access_token") 44 | if not access_token: 45 | raise HTTPException( 46 | status_code=401, 47 | detail="User not registered", 48 | ) 49 | token_type = token_response.get( 50 | "token_type", "bearer" 51 | ) 52 | 53 | return { 54 | "access_token": access_token, 55 | "token_type": token_type, 56 | } 57 | -------------------------------------------------------------------------------- /Chapter11/middleware_project/cors_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | CORS Request Example 10 | 11 | 12 |

CORS Request Example

13 | 16 |

17 | 
18 |     
52 |   
53 | 
54 | 


--------------------------------------------------------------------------------
/Chapter06/ticketing_system/alembic/versions/3d195160b2e3_add_ticket_details_one_to_one_.py:
--------------------------------------------------------------------------------
 1 | """add ticket details one to one relationship
 2 | 
 3 | Revision ID: 3d195160b2e3
 4 | Revises: 9a9cfeb65ea4
 5 | Create Date: 2024-03-07 10:02:25.781652
 6 | 
 7 | """
 8 | 
 9 | from typing import Sequence, Union
10 | 
11 | import sqlalchemy as sa
12 | 
13 | from alembic import op
14 | 
15 | # revision identifiers, used by Alembic.
16 | revision: str = "3d195160b2e3"
17 | down_revision: Union[str, None] = "9a9cfeb65ea4"
18 | branch_labels: Union[str, Sequence[str], None] = None
19 | depends_on: Union[str, Sequence[str], None] = None
20 | 
21 | 
22 | def upgrade() -> None:
23 |     # ### commands auto generated by Alembic - please adjust! ###
24 |     op.create_table(
25 |         "ticket_details",
26 |         sa.Column("id", sa.Integer(), nullable=False),
27 |         sa.Column(
28 |             "ticket_id", sa.Integer(), nullable=False
29 |         ),
30 |         sa.Column("seat", sa.String(), nullable=True),
31 |         sa.Column(
32 |             "ticket_type", sa.String(), nullable=True
33 |         ),
34 |         sa.ForeignKeyConstraint(
35 |             ["ticket_id"],
36 |             ["tickets.id"],
37 |         ),
38 |         sa.PrimaryKeyConstraint("id"),
39 |     )
40 |     op.execute("""
41 |         INSERT INTO ticket_details (ticket_id)
42 |         SELECT id FROM tickets
43 |     """)
44 |     # ### end Alembic commands ###
45 | 
46 | 
47 | def downgrade() -> None:
48 |     # ### commands auto generated by Alembic - please adjust! ###
49 |     op.drop_table("ticket_details")
50 |     # ### end Alembic commands ###
51 | 


--------------------------------------------------------------------------------
/Chapter07/streaming_platform/app/es_queries.py:
--------------------------------------------------------------------------------
 1 | def top_ten_artists_query(country) -> dict:
 2 |     views_field = f"views_per_country.{country}"
 3 | 
 4 |     query = {
 5 |         "bool": {
 6 |             "filter": [
 7 |                 {"exists": {"field": views_field}}
 8 |             ],
 9 |         }
10 |     }
11 | 
12 |     aggs = {
13 |         "top_ten_artists": {
14 |             "terms": {
15 |                 "field": "artist",
16 |                 "size": 10,
17 |                 "order": {"views": "desc"},
18 |             },
19 |             "aggs": {
20 |                 "views": {
21 |                     "sum": {
22 |                         "field": views_field,
23 |                         "missing": 0,
24 |                     }
25 |                 }
26 |             },
27 |         }
28 |     }
29 | 
30 |     return {
31 |         "index": "songs_index",
32 |         "size": 0,
33 |         "query": query,
34 |         "aggs": aggs,
35 |     }
36 | 
37 | 
38 | def top_ten_songs_query(country) -> dict:
39 |     views_field = f"views_per_country.{country}"
40 |     query = {
41 |         "bool": {
42 |             "must": {"match_all": {}},
43 |             "filter": [
44 |                 {"exists": {"field": views_field}}
45 |             ],
46 |         }
47 |     }
48 |     sort = {views_field: {"order": "desc"}}
49 |     source = [
50 |         "title",
51 |         views_field,
52 |         "album.title",
53 |         "artist",
54 |     ]
55 |     return {
56 |         "index": "songs_index",
57 |         "query": query,
58 |         "size": 10,
59 |         "sort": sort,
60 |         "source": source,
61 |     }
62 | 


--------------------------------------------------------------------------------
/Chapter02/async_example/timing_api_calls.py:
--------------------------------------------------------------------------------
 1 | import asyncio
 2 | import time
 3 | from contextlib import contextmanager
 4 | from multiprocessing import Process
 5 | 
 6 | import uvicorn
 7 | from httpx import AsyncClient
 8 | 
 9 | from main import app
10 | 
11 | 
12 | def run_server():
13 |     uvicorn.run(app, port=8000, log_level="error")
14 | 
15 | 
16 | @contextmanager
17 | def run_server_in_process():
18 |     p = Process(target=run_server)
19 |     p.start()
20 |     time.sleep(2)  # Give the server a second to start
21 |     print("Server is running in a separate process")
22 |     yield
23 |     p.terminate()
24 | 
25 | 
26 | async def make_requests_to_the_endpoint(
27 |     n: int, path: str
28 | ):
29 |     async with AsyncClient(
30 |         base_url="http://localhost:8000"
31 |     ) as client:
32 |         tasks = (
33 |             client.get(path, timeout=float("inf"))
34 |             for _ in range(n)
35 |         )
36 | 
37 |         await asyncio.gather(*tasks)
38 | 
39 | 
40 | async def main(n: int = 10):
41 |     with run_server_in_process():
42 |         begin = time.time()
43 |         await make_requests_to_the_endpoint(n, "/sync")
44 |         end = time.time()
45 |         print(
46 |             f"Time taken to make {n} requests "
47 |             f"to sync endpoint: {end - begin} seconds"
48 |         )
49 | 
50 |         begin = time.time()
51 |         await make_requests_to_the_endpoint(n, "/async")
52 |         end = time.time()
53 |         print(
54 |             f"Time taken to make {n} requests "
55 |             f"to async endpoint: {end - begin} seconds"
56 |         )
57 | 
58 | 
59 | if __name__ == "__main__":
60 |     asyncio.run(main(n=100))
61 | 


--------------------------------------------------------------------------------
/Chapter08/trip_platform/test_workers.py:
--------------------------------------------------------------------------------
 1 | import asyncio
 2 | import multiprocessing
 3 | import time
 4 | 
 5 | import uvicorn
 6 | from httpx import AsyncClient
 7 | 
 8 | from app.main import app
 9 | 
10 | 
11 | def run_app(workers: int):
12 |     uvicorn.run(app, port=8000, workers=workers)
13 | 
14 | 
15 | async def make_requests(n: int):
16 |     async with AsyncClient(
17 |         base_url="http://localhost:8000"
18 |     ) as client:
19 |         tasks = (
20 |             client.get(
21 |                 "/sleeping_sync", timeout=float("inf")
22 |             )
23 |             for _ in range(n)
24 |         )
25 |         start_time = time.time()
26 |         await asyncio.gather(*tasks)
27 |         end_time = time.time()
28 |         print(
29 |             f"Time taken with {n} requests: {end_time - start_time} seconds"
30 |         )
31 |         return end_time - start_time
32 | 
33 | 
34 | # To stop the thread, you can use the `thread.join()` method.
35 | # This will block the main thread until the `run_app` thread completes.
36 | async def main():
37 |     p = multiprocessing.Process(
38 |         target=run_app, args=(1,)
39 |     )
40 |     p.start()
41 |     time.sleep(5)  # Give the server a second to start
42 |     time_one_worker = await make_requests(10)
43 |     p.terminate()
44 | 
45 |     p = multiprocessing.Process(
46 |         target=run_app, args=(5,)
47 |     )
48 |     p.start()
49 |     time.sleep(5)  # Give the server a second to start
50 |     time_3_workers = await make_requests(10)
51 |     p.terminate()
52 | 
53 |     print("With one worker:", time_one_worker)
54 |     print("With three workers:", time_3_workers)
55 | 
56 | 
57 | if __name__ == "__main__":
58 |     asyncio.run(main())
59 | 


--------------------------------------------------------------------------------
/Chapter02/nosql_example/main.py:
--------------------------------------------------------------------------------
 1 | from bson import ObjectId
 2 | from fastapi import FastAPI, HTTPException
 3 | from pydantic import (
 4 |     BaseModel,
 5 |     EmailStr,
 6 |     field_validator,
 7 | )
 8 | 
 9 | from database import user_collection
10 | 
11 | app = FastAPI()
12 | 
13 | 
14 | class Tweet(BaseModel):
15 |     content: str
16 |     hashtags: list[str]
17 | 
18 | 
19 | class User(BaseModel):
20 |     name: str
21 |     email: EmailStr
22 |     age: int
23 |     tweets: list[Tweet] | None = None
24 | 
25 |     @field_validator("age")
26 |     def validate_age(cls, value):
27 |         if value < 18 or value > 100:
28 |             raise ValueError(
29 |                 "Age must be between 18 and 100"
30 |             )
31 |         return value
32 | 
33 | 
34 | @app.get("/users")
35 | def read_users() -> list[User]:
36 |     return [user for user in user_collection.find()]
37 | 
38 | 
39 | class UserResponse(User):
40 |     id: str
41 | 
42 | 
43 | @app.post("/user")
44 | def create_user(user: User) -> UserResponse:
45 |     result = user_collection.insert_one(
46 |         user.model_dump(exclude_none=True)
47 |     )
48 |     user_response = UserResponse(
49 |         id=str(result.inserted_id), **user.model_dump()
50 |     )
51 |     return user_response
52 | 
53 | 
54 | @app.get("/user")
55 | def get_user(user_id: str) -> UserResponse:
56 |     db_user = user_collection.find_one(
57 |         {
58 |             "_id": ObjectId(user_id)
59 |             if ObjectId.is_valid(user_id)
60 |             else None
61 |         }
62 |     )
63 |     if db_user is None:
64 |         raise HTTPException(
65 |             status_code=404, detail="User not found"
66 |         )
67 |     db_user["id"] = str(db_user["_id"])
68 |     return db_user
69 | 


--------------------------------------------------------------------------------
/Chapter03/task_manager_app/security.py:
--------------------------------------------------------------------------------
 1 | from fastapi.security import OAuth2PasswordBearer
 2 | from fastapi import Depends, HTTPException, status
 3 | from pydantic import BaseModel
 4 | 
 5 | fake_users_db = {
 6 |     "johndoe": {
 7 |         "username": "johndoe",
 8 |         "hashed_password": "hashedsecret",
 9 |     },
10 |     "janedoe": {
11 |         "username": "janedoe",
12 |         "hashed_password": "hashedsecret2",
13 |     },
14 | }
15 | 
16 | 
17 | def fakely_hash_password(password: str):
18 |     return f"hashed{password}"
19 | 
20 | 
21 | class User(BaseModel):
22 |     username: str
23 | 
24 | 
25 | class UserInDB(User):
26 |     hashed_password: str
27 | 
28 | 
29 | def get_user(db, username: str) -> UserInDB | None:
30 |     if username in db:
31 |         user_dict = db[username]
32 |         return UserInDB(**user_dict)
33 | 
34 | 
35 | def fake_token_generator(user: UserInDB) -> str:
36 |     # This doesn't provide any security at all
37 |     return f"tokenized{user.username}"
38 | 
39 | 
40 | def fake_token_resolver(
41 |     token: str,
42 | ) -> UserInDB | None:
43 |     if token.startswith("tokenized"):
44 |         user_id = token.removeprefix("tokenized")
45 |         user = get_user(fake_users_db, user_id)
46 |         return user
47 | 
48 | 
49 | oauth2_scheme = OAuth2PasswordBearer(
50 |     tokenUrl="token"
51 | )
52 | 
53 | 
54 | def get_user_from_token(
55 |     token: str = Depends(oauth2_scheme),
56 | ) -> UserInDB:
57 |     user = fake_token_resolver(token)
58 |     if not user:
59 |         raise HTTPException(
60 |             status_code=status.HTTP_401_UNAUTHORIZED,
61 |             detail="Invalid authentication credentials",
62 |             headers={"WWW-Authenticate": "Bearer"},
63 |         )
64 |     return user
65 | 


--------------------------------------------------------------------------------
/Chapter12/fca-server/pyproject.toml:
--------------------------------------------------------------------------------
 1 | [build-system]
 2 | requires = ["hatchling"]
 3 | build-backend = "hatchling.build"
 4 | 
 5 | [project]
 6 | name = "fca-server"
 7 | dynamic = ["version"]
 8 | description = ''
 9 | readme = "README.md"
10 | requires-python = ">=3.8"
11 | license = "MIT"
12 | keywords = []
13 | authors = [{ name = "U.N. Owen", email = "void@some.where" }]
14 | classifiers = [
15 |   "Development Status :: 4 - Beta",
16 |   "Programming Language :: Python",
17 |   "Programming Language :: Python :: 3.8",
18 |   "Programming Language :: Python :: 3.9",
19 |   "Programming Language :: Python :: 3.10",
20 |   "Programming Language :: Python :: 3.11",
21 |   "Programming Language :: Python :: 3.12",
22 |   "Programming Language :: Python :: Implementation :: CPython",
23 |   "Programming Language :: Python :: Implementation :: PyPy",
24 | ]
25 | dependencies = ["fastapi"]
26 | 
27 | [project.urls]
28 | Documentation = "https://github.com/U.N. Owen/fca-server#readme"
29 | Issues = "https://github.com/U.N. Owen/fca-server/issues"
30 | Source = "https://github.com/U.N. Owen/fca-server"
31 | 
32 | 
33 | [tool.hatch.version]
34 | path = "src/fca_server/__about__.py"
35 | 
36 | [tool.hatch.envs.types]
37 | extra-dependencies = ["mypy>=1.0.0"]
38 | [tool.hatch.envs.types.scripts]
39 | check = "mypy --install-types --non-interactive {args:src/fca_server tests}"
40 | 
41 | [tool.coverage.run]
42 | source_pkgs = ["fca_server", "tests"]
43 | branch = true
44 | parallel = true
45 | omit = ["src/fca_server/__about__.py"]
46 | 
47 | [tool.coverage.paths]
48 | fca_server = ["src/fca_server", "*/fca-server/src/fca_server"]
49 | tests = ["tests", "*/fca-server/tests"]
50 | 
51 | [tool.coverage.report]
52 | exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
53 | 


--------------------------------------------------------------------------------
/Chapter06/ticketing_system/alembic/versions/4149f66b5194_create_events_and_many_to_one_.py:
--------------------------------------------------------------------------------
 1 | """create events and many to one relationship
 2 | 
 3 | Revision ID: 4149f66b5194
 4 | Revises: 3d195160b2e3
 5 | Create Date: 2024-03-07 14:55:10.852676
 6 | 
 7 | """
 8 | 
 9 | from typing import Sequence, Union
10 | 
11 | import sqlalchemy as sa
12 | 
13 | from alembic import op
14 | 
15 | # revision identifiers, used by Alembic.
16 | revision: str = "4149f66b5194"
17 | down_revision: Union[str, None] = "3d195160b2e3"
18 | branch_labels: Union[str, Sequence[str], None] = None
19 | depends_on: Union[str, Sequence[str], None] = None
20 | 
21 | 
22 | def upgrade() -> None:
23 |     # ### commands auto generated by Alembic - please adjust! ###
24 |     op.create_table(
25 |         "events",
26 |         sa.Column("id", sa.Integer(), nullable=False),
27 |         sa.Column("name", sa.String(), nullable=False),
28 |         sa.PrimaryKeyConstraint("id"),
29 |     )
30 |     with op.batch_alter_table("tickets") as batch_op:
31 |         batch_op.add_column(
32 |             sa.Column(
33 |                 "event_id", sa.Integer(), nullable=True
34 |             )
35 |         )
36 |         batch_op.create_foreign_key(
37 |             "fk_tickets_event_id_events",
38 |             "events",
39 |             ["event_id"],
40 |             ["id"],
41 |         )
42 |     # ### end Alembic commands ###
43 | 
44 | 
45 | def downgrade() -> None:
46 |     # ### commands auto generated by Alembic - please adjust! ###
47 |     with op.batch_alter_table("tickets") as batch_op:
48 |         batch_op.drop_constraint(
49 |             "fk_tickets_event_id_events",
50 |             type_="foreignkey",
51 |         )
52 |         batch_op.drop_column("event_id")
53 |     op.drop_table("events")
54 |     # ### end Alembic commands ###
55 | 


--------------------------------------------------------------------------------
/Chapter06/ticketing_system/tests/test_security.py:
--------------------------------------------------------------------------------
 1 | import pytest
 2 | from sqlalchemy import select
 3 | 
 4 | from app.database import CreditCard
 5 | from app.security import (
 6 |     retrieve_credit_card_info,
 7 |     store_credit_card_info,
 8 | )
 9 | 
10 | 
11 | async def test_store_credit_card_info(db_session_test):
12 |     # Store encrypted credit card information in the database
13 |     credit_card_id = await store_credit_card_info(
14 |         db_session_test,
15 |         card_number="1234567812345678",
16 |         card_holder_name="John Doe",
17 |         expiration_date="12/23",
18 |         cvv="123",
19 |     )
20 |     assert credit_card_id is not None
21 | 
22 | 
23 | @pytest.fixture
24 | async def get_credit_card_id(db_session_test):
25 |     credit_card_id = await store_credit_card_info(
26 |         db_session_test,
27 |         card_number="1234567812345678",
28 |         card_holder_name="John Doe",
29 |         expiration_date="12/23",
30 |         cvv="123",
31 |     )
32 |     return credit_card_id
33 | 
34 | 
35 | async def test_retrieve_credit_card_info(
36 |     db_session_test, get_credit_card_id
37 | ):
38 |     credit_card = await retrieve_credit_card_info(
39 |         db_session_test, get_credit_card_id
40 |     )
41 |     assert (
42 |         credit_card["card_number"] == "1234567812345678"
43 |     )
44 |     assert credit_card["card_holder_name"] == "John Doe"
45 |     assert credit_card["expiration_date"] == "12/23"
46 | 
47 |     query = select(CreditCard).where(
48 |         CreditCard.id == get_credit_card_id
49 |     )
50 | 
51 |     async with db_session_test as session:
52 |         result = await session.execute(query)
53 |         credit_card = result.scalars().first()
54 |     assert credit_card.cvv != "123"
55 |     assert credit_card.number != "1234567812345678"
56 | 


--------------------------------------------------------------------------------
/Chapter03/task_manager_app/test_operations.py:
--------------------------------------------------------------------------------
 1 | from conftest import TEST_TASKS_CSV
 2 | from models import Task, TaskWithID
 3 | from operations import (
 4 |     create_task,
 5 |     get_next_id,
 6 |     modify_task,
 7 |     read_all_tasks,
 8 |     read_task,
 9 |     remove_task,
10 |     write_task_into_csv,
11 | )
12 | 
13 | 
14 | def test_read_all_tasks():
15 |     assert read_all_tasks() == [
16 |         TaskWithID(**task) for task in TEST_TASKS_CSV
17 |     ]
18 | 
19 | 
20 | def test_get_next_id():
21 |     assert get_next_id() == 3
22 | 
23 | 
24 | def test_write_task_into_csv():
25 |     task = TaskWithID(
26 |         id=3,
27 |         title="New Task",
28 |         description="New Desc",
29 |         status="Open",
30 |     )
31 |     write_task_into_csv(task)
32 |     assert read_all_tasks() == list(
33 |         map(
34 |             lambda task: TaskWithID(**task),
35 |             TEST_TASKS_CSV,
36 |         )
37 |     ) + [
38 |         TaskWithID(
39 |             id=3,
40 |             title="New Task",
41 |             description="New Desc",
42 |             status="Open",
43 |         )
44 |     ]
45 | 
46 | 
47 | def test_create_task():
48 |     task = Task(
49 |         title="New Task",
50 |         description="New Desc",
51 |         status="Open",
52 |     )
53 |     result = create_task(task)
54 | 
55 |     assert result.id == 3
56 | 
57 |     assert read_task(3) == TaskWithID(
58 |         id=3, **task.model_dump()
59 |     )
60 | 
61 | 
62 | def test_read_task():
63 |     assert read_task(1) == TaskWithID(
64 |         **TEST_TASKS_CSV[0]
65 |     )
66 | 
67 | 
68 | def test_modify_task():
69 |     updated_task = {"title": "New Title"}
70 |     modify_task(1, updated_task)
71 |     assert read_task(1).title == "New Title"
72 | 
73 | 
74 | def test_remove_task():
75 |     remove_task(1)
76 |     assert read_task(1) is None
77 | 


--------------------------------------------------------------------------------
/Chapter06/ticketing_system/alembic/versions/9891059574b3_create_sponsorships_for_many_to_many_.py:
--------------------------------------------------------------------------------
 1 | """create sponsorships for many to many relationships
 2 | 
 3 | Revision ID: 9891059574b3
 4 | Revises: 4149f66b5194
 5 | Create Date: 2024-03-07 17:23:29.911339
 6 | 
 7 | """
 8 | 
 9 | from typing import Sequence, Union
10 | 
11 | import sqlalchemy as sa
12 | 
13 | from alembic import op
14 | 
15 | # revision identifiers, used by Alembic.
16 | revision: str = "9891059574b3"
17 | down_revision: Union[str, None] = "4149f66b5194"
18 | branch_labels: Union[str, Sequence[str], None] = None
19 | depends_on: Union[str, Sequence[str], None] = None
20 | 
21 | 
22 | def upgrade() -> None:
23 |     # ### commands auto generated by Alembic - please adjust! ###
24 |     op.create_table(
25 |         "sponsors",
26 |         sa.Column("id", sa.Integer(), nullable=False),
27 |         sa.Column("name", sa.String(), nullable=False),
28 |         sa.PrimaryKeyConstraint("id"),
29 |         sa.UniqueConstraint("name"),
30 |     )
31 |     op.create_table(
32 |         "sponsorships",
33 |         sa.Column(
34 |             "event_id", sa.Integer(), nullable=False
35 |         ),
36 |         sa.Column(
37 |             "sponsor_id", sa.Integer(), nullable=False
38 |         ),
39 |         sa.Column("amount", sa.Float(), nullable=False),
40 |         sa.ForeignKeyConstraint(
41 |             ["event_id"],
42 |             ["events.id"],
43 |         ),
44 |         sa.ForeignKeyConstraint(
45 |             ["sponsor_id"],
46 |             ["sponsors.id"],
47 |         ),
48 |         sa.PrimaryKeyConstraint(
49 |             "event_id", "sponsor_id"
50 |         ),
51 |     )
52 | 
53 | 
54 | def downgrade() -> None:
55 |     # ### commands auto generated by Alembic - please adjust! ###
56 |     op.drop_table("sponsorships")
57 |     op.drop_table("sponsors")
58 |     # ### end Alembic commands ###
59 | 


--------------------------------------------------------------------------------
/Chapter05/protoapp/protoapp/main.py:
--------------------------------------------------------------------------------
 1 | from fastapi import (
 2 |     Depends,
 3 |     FastAPI,
 4 |     HTTPException,
 5 |     Request,
 6 |     status,
 7 | )
 8 | from pydantic import BaseModel
 9 | from sqlalchemy.orm import Session
10 | 
11 | from protoapp.database import Item, SessionLocal
12 | from protoapp.logging import client_logger
13 | 
14 | app = FastAPI()
15 | 
16 | 
17 | @app.get("/home")
18 | def read_main():
19 |     return {"message": "Hello World"}
20 | 
21 | 
22 | class ItemSchema(BaseModel):
23 |     name: str
24 |     color: str
25 | 
26 | 
27 | def get_db_session():
28 |     db = SessionLocal()
29 |     try:
30 |         yield db
31 |     finally:
32 |         db.close()
33 | 
34 | 
35 | @app.post(
36 |     "/item",
37 |     response_model=int,
38 |     status_code=status.HTTP_201_CREATED,
39 | )
40 | def add_item(
41 |     item: ItemSchema,
42 |     db_session: Session = Depends(get_db_session),
43 | ):
44 |     db_item = Item(name=item.name, color=item.color)
45 |     db_session.add(db_item)
46 |     db_session.commit()
47 |     db_session.refresh(db_item)
48 |     return db_item.id
49 | 
50 | 
51 | @app.get("/item/{item_id}", response_model=ItemSchema)
52 | def get_item(
53 |     item_id: int,
54 |     db_session: Session = Depends(get_db_session),
55 | ):
56 |     item_db = (
57 |         db_session.query(Item)
58 |         .filter(Item.id == item_id)
59 |         .first()
60 |     )
61 |     if item_db is None:
62 |         raise HTTPException(
63 |             status_code=404, detail="Item not found"
64 |         )
65 | 
66 |     return item_db
67 | 
68 | 
69 | @app.middleware("http")
70 | async def log_requests(request: Request, call_next):
71 |     client_logger.info(
72 |         f"method: {request.method}, "
73 |         f"call: {request.url.path}, "
74 |         f"ip: {request.client.host}"
75 |     )
76 |     response = await call_next(request)
77 |     return response
78 | 


--------------------------------------------------------------------------------
/Chapter04/saas_app/test_main.py:
--------------------------------------------------------------------------------
 1 | import pytest
 2 | from fastapi.testclient import TestClient
 3 | 
 4 | from db_connection import get_session
 5 | from main import app
 6 | 
 7 | 
 8 | @pytest.fixture
 9 | def client(session):
10 |     app.dependency_overrides |= {
11 |         get_session: lambda: session
12 |     }
13 |     testclient = TestClient(app)
14 |     return testclient
15 | 
16 | 
17 | def test_endpoint_add_basic_user(client):
18 |     user = {
19 |         "username": "lyampayet",
20 |         "password": "difficultpassword",
21 |         "email": "lyampayet@email.com",
22 |     }
23 | 
24 |     response = client.post("/register/user", json=user)
25 |     assert response.status_code == 201
26 |     assert response.json() == {
27 |         "message": "user created",
28 |         "user": {
29 |             "username": "lyampayet",
30 |             "email": "lyampayet@email.com",
31 |         },
32 |     }
33 | 
34 | 
35 | def test_endpoint_add_user_conflict_existing_user(
36 |     client, fill_database_session
37 | ):
38 |     user = {
39 |         "username": "johndoe",
40 |         "password": "strongpassword",
41 |         "email": "johndoe@email.com",
42 |     }
43 |     response = client.post("/register/user", json=user)
44 |     assert response.status_code == 409
45 |     assert response.json() == {
46 |         "detail": "username or email already exists"
47 |     }
48 | 
49 | 
50 | def test_endpoint_add_premium_user(client):
51 |     user = {
52 |         "username": "mariorossi",
53 |         "password": "difficultpassword",
54 |         "email": "mariorossi@email.com",
55 |     }
56 | 
57 |     response = client.post("/register/user", json=user)
58 |     assert response.status_code == 201
59 |     assert response.json() == {
60 |         "message": "user created",
61 |         "user": {
62 |             "username": "mariorossi",
63 |             "email": "mariorossi@email.com",
64 |         },
65 |     }
66 | 
67 | 
68 | 


--------------------------------------------------------------------------------
/Chapter04/saas_app/conftest.py:
--------------------------------------------------------------------------------
 1 | import pytest
 2 | from passlib.context import CryptContext
 3 | from sqlalchemy import QueuePool, create_engine
 4 | from sqlalchemy.orm import sessionmaker
 5 | 
 6 | from models import Base, Role, User
 7 | 
 8 | pwd_context = CryptContext(
 9 |     schemes=["bcrypt"], deprecated="auto"
10 | )
11 | 
12 | 
13 | @pytest.fixture
14 | def session():
15 |     engine = create_engine(
16 |         "sqlite:///:memory:",
17 |         connect_args={"check_same_thread": False},
18 |         poolclass=QueuePool,
19 |     )
20 | 
21 |     session_local = sessionmaker(engine)
22 | 
23 |     db_session = session_local()
24 | 
25 |     Base.metadata.create_all(bind=engine)
26 | 
27 |     yield db_session
28 | 
29 |     Base.metadata.drop_all(bind=engine)
30 | 
31 |     db_session.close()
32 | 
33 | 
34 | @pytest.fixture(scope="function")
35 | def fill_database_session(session):
36 |     (
37 |         session.add(
38 |             User(
39 |                 username="johndoe",
40 |                 email="johndoe@email.com",
41 |                 hashed_password=pwd_context.hash(
42 |                     "pass1234"
43 |                 ),
44 |                 role=Role.basic,
45 |             )
46 |         ),
47 |     )
48 |     (
49 |         session.add(
50 |             User(
51 |                 username="chrissophia",
52 |                 email="chrissophia@email.com",
53 |                 hashed_password=pwd_context.hash(
54 |                     "hardpass"
55 |                 ),
56 |                 role=Role.basic,
57 |             )
58 |         ),
59 |     )
60 |     (
61 |         session.add(
62 |             User(
63 |                 username="manucourtney",
64 |                 email="mcourtney@email.com",
65 |                 hashed_password=pwd_context.hash(
66 |                     "harderpass"
67 |                 ),
68 |                 role=Role.premium,
69 |             )
70 |         ),
71 |     )
72 |     session.commit()
73 |     yield session
74 | 


--------------------------------------------------------------------------------
/Chapter09/chat_platform/app/chat.py:
--------------------------------------------------------------------------------
 1 | import logging
 2 | 
 3 | from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect
 4 | from fastapi.responses import HTMLResponse
 5 | 
 6 | from app.templating import templates
 7 | from app.ws_manager import ConnectionManager
 8 | 
 9 | conn_manager = ConnectionManager()
10 | logger = logging.getLogger("uvicorn")
11 | 
12 | 
13 | router = APIRouter()
14 | 
15 | 
16 | @router.websocket("/chatroom/{username}")
17 | async def chatroom_endpoint(
18 |     websocket: WebSocket, username: str
19 | ):
20 |     await conn_manager.connect(websocket)
21 |     await conn_manager.broadcast(
22 |         {
23 |             "sender": "system",
24 |             "message": f"{username} joined the chat",
25 |         },
26 |         exclude=websocket,
27 |     )
28 |     
29 |     logger.info(f"{username} joined the chat")
30 | 
31 |     try:
32 |         while True:
33 |             data = await websocket.receive_text()
34 |             await conn_manager.broadcast(
35 |                 {"sender": username, "message": data},
36 |                 exclude=websocket,
37 |             )
38 |             await conn_manager.send_personal_message(
39 |                 {"sender": "You", "message": data},
40 |                 websocket,
41 |             )
42 |             logger.info(
43 |                 f"{username} says: {data}"
44 |             )
45 |     except WebSocketDisconnect:
46 |         conn_manager.disconnect(websocket)
47 |         await conn_manager.broadcast(
48 |             {
49 |                 "sender": "system",
50 |                 "message": f"{username} "
51 |                 "left the chat",
52 |             }
53 |         )
54 |         logger.info(f"{username} left the chat")
55 | 
56 | 
57 | 
58 | @router.get("/chatroom/{username}")
59 | async def chatroom_page_endpoint(
60 |     request: Request, username: str
61 | ) -> HTMLResponse:
62 |     return templates.TemplateResponse(
63 |         request=request,
64 |         name="chatroom.html",
65 |         context={"username": username},
66 |     )
67 | 


--------------------------------------------------------------------------------
/Chapter08/trip_platform/app/profiler.py:
--------------------------------------------------------------------------------
 1 | import asyncio
 2 | import os
 3 | from contextlib import asynccontextmanager
 4 | from time import sleep
 5 | 
 6 | from fastapi import APIRouter, FastAPI, Request
 7 | from pyinstrument import Profiler
 8 | from pyinstrument.renderers.speedscope import (
 9 |     SpeedscopeRenderer,
10 | )
11 | from starlette.middleware.base import BaseHTTPMiddleware
12 | 
13 | profiler = Profiler(
14 |     interval=0.0001, async_mode="enabled"
15 | )
16 | 
17 | 
18 | @asynccontextmanager
19 | async def lifespan(app: FastAPI):
20 |     profiler.start()
21 |     yield
22 |     profiler.stop()
23 |     profiler.write_html(os.getcwd() + "/profiler.html")
24 | 
25 | 
26 | class ProfileEndpointsMiddleWare(BaseHTTPMiddleware):
27 |     async def dispatch(
28 |         self, request: Request, call_next
29 |     ):
30 |         if not profiler.is_running:
31 |             profiler.start()
32 |         response = await call_next(request)
33 |         if profiler.is_running:
34 |             profiler.stop()
35 |             with open(
36 |                 os.getcwd() + "/profiler2.json", "w"
37 |             ) as file:
38 |                 file.write(
39 |                     profiler.output(
40 |                         SpeedscopeRenderer()
41 |                     )
42 |                 )
43 |             profiler.write_html(
44 |                 os.getcwd() + "/profiler.html"
45 |             )
46 |             profiler.start()
47 |         return response
48 | 
49 | 
50 | router = APIRouter(tags=["Profiler Endpoints"])
51 | 
52 | 
53 | @router.get("/sleeping")
54 | async def sleeping_endpoint():
55 |     await asyncio.sleep(5)
56 |     print("after sleep 5 seconds")
57 |     return {"message": "I slept for 5 seconds"}
58 | 
59 | 
60 | @router.get("/sleeping1")
61 | async def sleeping_endpoint1():
62 |     await asyncio.sleep(2)
63 |     print("after sleep 2")
64 |     return {"message": "I slept for 2 seconds"}
65 | 
66 | 
67 | @router.get("/sleeping_sync")
68 | async def sleeping_sync():
69 |     sleep(10)
70 |     return {"message": "I slept for 2 seconds"}
71 | 


--------------------------------------------------------------------------------
/Chapter04/saas_app/mfa.py:
--------------------------------------------------------------------------------
 1 | import pyotp
 2 | from fastapi import (
 3 |     APIRouter,
 4 |     Depends,
 5 |     HTTPException,
 6 |     status,
 7 | )
 8 | from sqlalchemy.orm import Session
 9 | 
10 | from db_connection import get_session
11 | from operations import get_user
12 | from rbac import get_current_user
13 | from responses import UserCreateResponse
14 | 
15 | 
16 | def generate_totp_secret():
17 |     return pyotp.random_base32()
18 | 
19 | 
20 | def generate_totp_uri(secret, user_email):
21 |     return pyotp.totp.TOTP(secret).provisioning_uri(
22 |         name=user_email, issuer_name="YourAppName"
23 |     )
24 | 
25 | 
26 | router = APIRouter()
27 | 
28 | 
29 | @router.post("/user/enable-mfa")
30 | def enable_mfa(
31 |     user: UserCreateResponse = Depends(
32 |         get_current_user
33 |     ),
34 |     db_session: Session = Depends(get_session),
35 | ):
36 |     secret = generate_totp_secret()
37 |     db_user = get_user(db_session, user.username)
38 |     db_user.totp_secret = secret
39 |     db_session.add(db_user)
40 |     db_session.commit()
41 |     totp_uri = generate_totp_uri(secret, user.email)
42 | 
43 |     # Return the TOTP URI
44 |     # for QR code generation in the frontend
45 |     return {
46 |         "totp_uri": totp_uri,
47 |         "secret_numbers": pyotp.TOTP(secret).now(),
48 |     }
49 | 
50 | 
51 | @router.post("/verify-totp")
52 | def verify_totp(
53 |     code: str,
54 |     username: str,
55 |     session: Session = Depends(get_session),
56 | ):
57 |     user = get_user(session, username)
58 |     if not user.totp_secret:
59 |         raise HTTPException(
60 |             status_code=status.HTTP_400_BAD_REQUEST,
61 |             detail="MFA not activated",
62 |         )
63 | 
64 |     totp = pyotp.TOTP(user.totp_secret)
65 |     if not totp.verify(code):
66 |         raise HTTPException(
67 |             status_code=status.HTTP_401_UNAUTHORIZED,
68 |             detail="Invalid TOTP token",
69 |         )
70 |     # Proceed with granting access
71 |     # or performing the sensitive operation
72 |     return {
73 |         "message": "TOTP token verified successfully"
74 |     }
75 | 


--------------------------------------------------------------------------------
/Chapter10/ecotech_RAG/main.py:
--------------------------------------------------------------------------------
 1 | from contextlib import asynccontextmanager
 2 | from typing import Annotated
 3 | 
 4 | from fastapi import (
 5 |     Body,
 6 |     FastAPI,
 7 |     HTTPException,
 8 |     Request,
 9 |     UploadFile,
10 | )
11 | from langchain.text_splitter import (
12 |     CharacterTextSplitter,
13 | )
14 | from langchain_cohere import CohereEmbeddings
15 | from langchain_community.vectorstores import Chroma
16 | from langchain_core.documents import Document
17 | 
18 | from documents import get_context, load_documents
19 | from model import chain
20 | 
21 | 
22 | @asynccontextmanager
23 | async def lifespan(app: FastAPI):
24 |     db = Chroma(
25 |         embedding_function=CohereEmbeddings()
26 |     )
27 |     await load_documents(db)
28 |     yield {"db": db}
29 | 
30 | 
31 | app = FastAPI(
32 |     title="Ecotech AI Assistant", lifespan=lifespan
33 | )
34 | 
35 | 
36 | @app.post("/message")
37 | async def query_assistant(
38 |     request: Request,
39 |     question: Annotated[str, Body()],
40 | ) -> str:
41 |     context = get_context(question, request.state.db)
42 |     response = await chain.ainvoke(
43 |         {
44 |             "question": question,
45 |             "context": context,
46 |         }
47 |     )
48 |     return response
49 | 
50 | 
51 | @app.post("/add_document")
52 | async def add_document(
53 |     request: Request, file: UploadFile
54 | ):
55 |     # check file extension
56 |     if file.content_type != "text/plain":
57 |         raise HTTPException(
58 |             status_code=400,
59 |             detail="File must be a text file",
60 |         )
61 | 
62 |     db = request.state.db
63 | 
64 |     text_splitter = CharacterTextSplitter(
65 |         chunk_size=100, chunk_overlap=0
66 |     )
67 | 
68 |     content_file = file.file.read().decode()
69 |     document = Document(content_file)
70 | 
71 |     chunks = text_splitter.split_documents(
72 |         [document]
73 |     )
74 |     await db.aadd_documents(chunks)
75 | 
76 |     with open(
77 |         f"docs/{file.filename}", "w"
78 |     ) as buffer:
79 |         buffer.write(content_file)
80 | 
81 |     return {"filename": file.filename}
82 | 


--------------------------------------------------------------------------------
/Chapter09/chat_platform/templates/chatroom.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     Chat
 5 |   
 6 | 
 7 |   
 8 |     

WebSocket Chat

9 |

Your ID:

10 |
11 | 16 | 17 |
18 |
    19 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/app/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Annotated 3 | 4 | from fastapi import ( 5 | Depends, 6 | FastAPI, 7 | WebSocket, 8 | WebSocketException, 9 | status, 10 | ) 11 | from fastapi.websockets import WebSocketDisconnect 12 | 13 | from app.chat import router as chat_router 14 | from app.exclusive_chatroom import ( 15 | router as exclusive_chatroom_router, 16 | ) 17 | from app.security import get_username_from_token 18 | from app.security import router as security_router 19 | 20 | app = FastAPI() 21 | app.include_router(security_router) 22 | app.include_router(exclusive_chatroom_router) 23 | app.include_router(chat_router) 24 | 25 | 26 | logger = logging.getLogger("uvicorn") 27 | 28 | 29 | @app.websocket("/ws") 30 | async def ws_endpoint(websocket: WebSocket): 31 | await websocket.accept() 32 | await websocket.send_text( 33 | "Welcome to the chat room!" 34 | ) 35 | try: 36 | while True: 37 | data = await websocket.receive_text() 38 | logger.info(f"Message received: {data}") 39 | await websocket.send_text("Message received!") 40 | if data == "disconnect": 41 | logger.warn("Disconnecting...") 42 | return await websocket.close( 43 | code=status.WS_1000_NORMAL_CLOSURE, 44 | reason="Disconnecting...", 45 | ) 46 | if "bad message" in data: 47 | raise WebSocketException( 48 | code=status.WS_1008_POLICY_VIOLATION, 49 | reason="Inappropriate message", 50 | ) 51 | except WebSocketDisconnect: 52 | logger.warning( 53 | "Connection closed by the client" 54 | ) 55 | 56 | 57 | @app.websocket("/secured-ws") 58 | async def secured_websocket( 59 | websocket: WebSocket, 60 | username: Annotated[ 61 | get_username_from_token, Depends() 62 | ], 63 | ): 64 | await websocket.accept() 65 | await websocket.send_text(f"Welcome {username}!") 66 | async for data in websocket.iter_text(): 67 | await websocket.send_text( 68 | f"You wrote: {data}" 69 | ) 70 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/app/security.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet 2 | from sqlalchemy import select 3 | from sqlalchemy.ext.asyncio import AsyncSession 4 | 5 | from app.database import CreditCard 6 | 7 | key = Fernet.generate_key() 8 | cypher_suite = Fernet(key) 9 | 10 | 11 | def encrypt_credit_card_info(card_info: str) -> str: 12 | return cypher_suite.encrypt( 13 | card_info.encode() 14 | ).decode() 15 | 16 | 17 | def decrypt_credit_card_info( 18 | encrypted_card_info: str, 19 | ) -> str: 20 | return cypher_suite.decrypt( 21 | encrypted_card_info.encode() 22 | ).decode() 23 | 24 | 25 | async def store_credit_card_info( 26 | db_session: AsyncSession, 27 | card_number: str, 28 | card_holder_name: str, 29 | expiration_date: str, 30 | cvv: str, 31 | ): 32 | encrypted_card_number = encrypt_credit_card_info( 33 | card_number 34 | ) 35 | encrypted_cvv = encrypt_credit_card_info(cvv) 36 | 37 | # Store encrypted credit card information in the database 38 | credit_card = CreditCard( 39 | number=encrypted_card_number, 40 | card_holder_name=card_holder_name, 41 | expiration_date=expiration_date, 42 | cvv=encrypted_cvv, 43 | ) 44 | 45 | async with db_session.begin(): 46 | db_session.add(credit_card) 47 | await db_session.flush() 48 | credit_card_id = credit_card.id 49 | await db_session.commit() 50 | return credit_card_id 51 | 52 | 53 | async def retrieve_credit_card_info( 54 | db_session: AsyncSession, credit_card_id: int 55 | ): 56 | query = select(CreditCard).where( 57 | CreditCard.id == credit_card_id 58 | ) 59 | 60 | async with db_session as session: 61 | result = await session.execute(query) 62 | credit_card = result.scalars().first() 63 | 64 | credit_card_number = decrypt_credit_card_info( 65 | credit_card.number 66 | ) 67 | cvv = decrypt_credit_card_info(credit_card.cvv) 68 | 69 | return { 70 | "card_number": credit_card_number, 71 | "card_holder_name": credit_card.card_holder_name, 72 | "expiration_date": credit_card.expiration_date, 73 | "cvv": cvv, 74 | } 75 | -------------------------------------------------------------------------------- /Chapter04/saas_app/rbac.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import ( 4 | APIRouter, 5 | Depends, 6 | HTTPException, 7 | status, 8 | ) 9 | from pydantic import BaseModel, EmailStr 10 | from sqlalchemy.orm import Session 11 | 12 | from db_connection import get_session 13 | from models import Role 14 | from security import decode_access_token, oauth2_scheme 15 | 16 | 17 | class UserCreateRequestWithRole(BaseModel): 18 | username: str 19 | email: EmailStr 20 | role: Role 21 | 22 | 23 | def get_current_user( 24 | token: str = Depends(oauth2_scheme), 25 | session: Session = Depends(get_session), 26 | ) -> UserCreateRequestWithRole: 27 | user = decode_access_token(token, session) 28 | if not user: 29 | raise HTTPException( 30 | status_code=status.HTTP_401_UNAUTHORIZED, 31 | detail="User not authorized", 32 | ) 33 | 34 | return UserCreateRequestWithRole( 35 | username=user.username, 36 | email=user.email, 37 | role=user.role, 38 | ) 39 | 40 | 41 | def get_premium_user( 42 | current_user: Annotated[ 43 | get_current_user, Depends() 44 | ], 45 | ): 46 | if current_user.role != Role.premium: 47 | raise HTTPException( 48 | status_code=status.HTTP_401_UNAUTHORIZED, 49 | detail="User not authorized", 50 | ) 51 | return current_user 52 | 53 | 54 | router = APIRouter() 55 | 56 | 57 | @router.get( 58 | "/welcome/all-users", 59 | responses={ 60 | status.HTTP_401_UNAUTHORIZED: { 61 | "description": "User not authorized" 62 | } 63 | }, 64 | ) 65 | def all_user_can_access( 66 | user: Annotated[get_current_user, Depends()], 67 | ): 68 | return { 69 | f"Hello {user.username}, welcome to your space" 70 | } 71 | 72 | 73 | @router.get( 74 | "/welcome/premium-user", 75 | responses={ 76 | status.HTTP_401_UNAUTHORIZED: { 77 | "description": "User not authorized" 78 | } 79 | }, 80 | ) 81 | def only_premium_user_can_access( 82 | user: Annotated[get_premium_user, Depends()], 83 | ): 84 | return { 85 | f"Hello {user.username}, " 86 | "welcome to your premium space" 87 | } 88 | -------------------------------------------------------------------------------- /Chapter02/sql_example/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, FastAPI, HTTPException 2 | from pydantic import BaseModel 3 | from sqlalchemy.orm import Session 4 | 5 | from database import SessionLocal, User 6 | 7 | 8 | def get_db(): 9 | db = SessionLocal() 10 | try: 11 | yield db 12 | finally: 13 | db.close() 14 | 15 | 16 | app = FastAPI() 17 | 18 | 19 | class UserBody(BaseModel): 20 | name: str 21 | email: str 22 | 23 | 24 | @app.get("/users") 25 | def read_users(db: Session = Depends(get_db)): 26 | users = db.query(User).all() 27 | return users 28 | 29 | 30 | @app.post("/user") 31 | def add_new_user( 32 | user: UserBody, db: Session = Depends(get_db) 33 | ): 34 | new_user = User(name=user.name, email=user.email) 35 | db.add(new_user) 36 | db.commit() 37 | db.refresh(new_user) 38 | return new_user 39 | 40 | 41 | @app.get("/user") 42 | def get_user( 43 | user_id: int, db: Session = Depends(get_db) 44 | ): 45 | user = ( 46 | db.query(User) 47 | .filter(User.id == user_id) 48 | .first() 49 | ) 50 | if user is None: 51 | raise HTTPException( 52 | status_code=404, detail="User not found" 53 | ) 54 | 55 | return user 56 | 57 | 58 | @app.post("/user/{user_id}") 59 | def update_user( 60 | user_id: int, 61 | user: UserBody, 62 | db: Session = Depends(get_db), 63 | ): 64 | db_user = ( 65 | db.query(User) 66 | .filter(User.id == user_id) 67 | .first() 68 | ) 69 | if db_user is None: 70 | raise HTTPException( 71 | status_code=404, detail="User not found" 72 | ) 73 | db_user.name = user.name 74 | db_user.email = user.email 75 | db.commit() 76 | db.refresh(db_user) 77 | return db_user 78 | 79 | 80 | @app.delete("/user") 81 | def delete_user( 82 | user_id: int, db: Session = Depends(get_db) 83 | ): 84 | db_user = ( 85 | db.query(User) 86 | .filter(User.id == user_id) 87 | .first() 88 | ) 89 | if db_user is None: 90 | raise HTTPException( 91 | status_code=404, detail="User not found" 92 | ) 93 | db.delete(db_user) 94 | db.commit() 95 | return {"detail": "User deleted"} 96 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import AsyncMock, MagicMock 2 | 3 | import pytest 4 | from bson import ObjectId 5 | from fastapi.testclient import TestClient 6 | 7 | from app.database import mongo_database 8 | from app.main import app 9 | from app.main_search import ( 10 | get_elasticsearch_client as es_client, 11 | ) 12 | 13 | 14 | def mock_find_songs_list(): 15 | to_return = [ 16 | { 17 | "_id": ObjectId("89abcdef0123456789abcdef"), 18 | "title": "Song 1", 19 | "artist": "Artist 1", 20 | }, 21 | { 22 | "_id": ObjectId("23456789abcdef0123456789"), 23 | "title": "Song 2", 24 | "artist": "Artist 2", 25 | }, 26 | ] 27 | song_list = MagicMock() 28 | print(song_list.id) 29 | song_list.to_list = AsyncMock( 30 | return_value=to_return 31 | ) 32 | return song_list 33 | 34 | 35 | @pytest.fixture 36 | def mongo_db_mock(): 37 | database = MagicMock() 38 | database.songs.insert_one = AsyncMock() 39 | 40 | song = { 41 | "_id": ObjectId("0123456789abcdef01234567"), 42 | "title": "Test Song", 43 | "artist": "Test Artist", 44 | } 45 | database.songs.find_one = AsyncMock( 46 | return_value=song 47 | ) 48 | 49 | database.songs.find = MagicMock( 50 | return_value=mock_find_songs_list() 51 | ) 52 | 53 | result_mock = MagicMock() 54 | result_mock.modified_count = 1 55 | database.songs.update_one = AsyncMock( 56 | return_value=result_mock 57 | ) 58 | 59 | result_mock.deleted_count = 1 60 | database.songs.delete_one = AsyncMock( 61 | return_value=result_mock 62 | ) 63 | 64 | return database 65 | 66 | 67 | @pytest.fixture 68 | def es_client_mock(): 69 | client = MagicMock() 70 | client.index = AsyncMock() 71 | client.search = AsyncMock() 72 | return client 73 | 74 | 75 | @pytest.fixture 76 | def test_client(mongo_db_mock, es_client_mock): 77 | client = TestClient(app) 78 | client.app.dependency_overrides[mongo_database] = ( 79 | lambda: mongo_db_mock 80 | ) 81 | client.app.dependency_overrides[es_client] = ( 82 | lambda: es_client_mock 83 | ) 84 | 85 | return client 86 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/templates/exclusivechatroom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exclusive Chat 5 | 6 | 7 | 8 |

    Exclusive Chat

    9 |

    Your ID:

    10 |
    11 | 16 | 17 |
    18 |
      19 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Chapter03/task_manager_app/test_main.py: -------------------------------------------------------------------------------- 1 | from conftest import TEST_TASKS 2 | from fastapi.testclient import TestClient 3 | 4 | from main import app 5 | from operations import read_all_tasks, read_task 6 | 7 | client = TestClient(app) 8 | 9 | 10 | def test_endpoint_read_all_tasks(): 11 | response = client.get("/tasks") 12 | assert response.status_code == 200 13 | assert response.json() == TEST_TASKS 14 | 15 | 16 | def test_endpoint_get_task(): 17 | response = client.get("/task/1") 18 | 19 | assert response.status_code == 200 20 | assert response.json() == TEST_TASKS[0] 21 | 22 | response = client.get("/task/5") 23 | 24 | assert response.status_code == 404 25 | 26 | 27 | def test_endpoint_create_task(): 28 | task = { 29 | "title": "To Define", 30 | "description": "will be done", 31 | "status": "Ready", 32 | } 33 | response = client.post("/task", json=task) 34 | 35 | assert response.status_code == 200 36 | assert response.json() == {**task, "id": 3} 37 | assert len(read_all_tasks()) == 3 38 | 39 | 40 | def test_endpoint_modify_task(): 41 | updated_fields = {"status": "Finished"} 42 | response = client.put("/task/2", json=updated_fields) 43 | 44 | assert response.status_code == 200 45 | assert response.json() == { 46 | **TEST_TASKS[1], 47 | **updated_fields, 48 | } 49 | 50 | response = client.put("/task/3", json=updated_fields) 51 | 52 | assert response.status_code == 404 53 | 54 | 55 | def test_endpoint_delete_task(): 56 | response = client.delete("/task/2") 57 | assert response.status_code == 200 58 | 59 | expected_response = TEST_TASKS[1] 60 | del expected_response["id"] 61 | 62 | assert response.json() == expected_response 63 | assert read_task(2) is None 64 | 65 | 66 | def test_endpoint_get_tasks_with_filters(): 67 | response = client.get( 68 | "/tasks", 69 | params={"status": "Ongoing", "title": "Task Two"}, 70 | ) 71 | 72 | assert response.status_code == 200 73 | response.json() == [TEST_TASKS[1]] 74 | 75 | 76 | def test_endpoint_search_task_by_keyword(): 77 | response = client.get("/tasks/search", params={"keyword": "ONE"}) 78 | 79 | assert response.status_code == 200 80 | assert response.json() == [TEST_TASKS[0]] 81 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/app/exclusive_chatroom.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import ( 4 | APIRouter, 5 | Depends, 6 | Request, 7 | WebSocket, 8 | WebSocketDisconnect, 9 | ) 10 | from fastapi.responses import RedirectResponse 11 | from fastapi.templating import Jinja2Templates 12 | 13 | from app.security import ( 14 | fake_token_resolver, 15 | get_username_from_token, 16 | ) 17 | from app.ws_manager import ConnectionManager 18 | 19 | router = APIRouter() 20 | 21 | templates = Jinja2Templates(directory="templates") 22 | 23 | 24 | @router.get("/exclusive-chatroom") 25 | async def exclusive_chatroom( 26 | request: Request, 27 | ): 28 | token = request.cookies.get("chatroomtoken", "") 29 | user = fake_token_resolver(token) 30 | if not user: 31 | return RedirectResponse( 32 | url="/login?redirecturl=http://localhost:8000/exclusive-chatroom", 33 | ) 34 | return templates.TemplateResponse( 35 | request=request, 36 | name="chatroom.html", 37 | context={"username": user.username}, 38 | ) 39 | 40 | 41 | connection_manager = ConnectionManager() 42 | 43 | 44 | @router.websocket("/ws-eclusive") 45 | async def websocket_chatroom( 46 | websocket: WebSocket, 47 | username: Annotated[ 48 | get_username_from_token, Depends() 49 | ], 50 | ): 51 | await connection_manager.connect(websocket) 52 | await connection_manager.broadcast( 53 | { 54 | "sender": "system", 55 | "message": f"{username} joined the chat", 56 | }, 57 | exclude=websocket, 58 | ) 59 | try: 60 | while True: 61 | data = await websocket.receive_text() 62 | await connection_manager.broadcast( 63 | { 64 | "sender": username, 65 | "message": data, 66 | }, 67 | exclude=websocket, 68 | ) 69 | await connection_manager.send_personal_message( 70 | {"sender": "You", "message": data}, 71 | websocket, 72 | ) 73 | except WebSocketDisconnect: 74 | connection_manager.disconnect(websocket) 75 | await connection_manager.broadcast( 76 | f"Client #{username} left the chat" 77 | ) 78 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import create_engine 4 | 5 | from alembic import context 6 | from app.database import Base 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | if config.config_file_name is not None: 15 | fileConfig(config.config_file_name) 16 | 17 | # add your model's MetaData object here 18 | # for 'autogenerate' support 19 | # from myapp import mymodel 20 | # target_metadata = mymodel.Base.metadata 21 | target_metadata = Base.metadata 22 | 23 | # other values from the config, defined by the needs of env.py, 24 | # can be acquired: 25 | # my_important_option = config.get_main_option("my_important_option") 26 | # ... etc. 27 | 28 | 29 | def run_migrations_offline() -> None: 30 | """Run migrations in 'offline' mode. 31 | 32 | This configures the context with just a URL 33 | and not an Engine, though an Engine is acceptable 34 | here as well. By skipping the Engine creation 35 | we don't even need a DBAPI to be available. 36 | 37 | Calls to context.execute() here emit the given string to the 38 | script output. 39 | 40 | """ 41 | url = config.get_main_option("sqlalchemy.url") 42 | context.configure( 43 | url=url, 44 | target_metadata=target_metadata, 45 | literal_binds=True, 46 | dialect_opts={"paramstyle": "named"}, 47 | ) 48 | 49 | with context.begin_transaction(): 50 | context.run_migrations() 51 | 52 | 53 | def run_migrations_online() -> None: 54 | """Run migrations in 'online' mode. 55 | 56 | In this scenario we need to create an Engine 57 | and associate a connection with the context. 58 | 59 | """ 60 | connectable = create_engine( 61 | "sqlite:///.database.db" 62 | ) 63 | 64 | with connectable.connect() as connection: 65 | context.configure( 66 | connection=connection, 67 | target_metadata=target_metadata, 68 | ) 69 | 70 | with context.begin_transaction(): 71 | context.run_migrations() 72 | 73 | 74 | if context.is_offline_mode(): 75 | run_migrations_offline() 76 | else: 77 | run_migrations_online() 78 | -------------------------------------------------------------------------------- /Chapter04/saas_app/main.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | 3 | from fastapi import ( 4 | Depends, 5 | FastAPI, 6 | HTTPException, 7 | status, 8 | ) 9 | from sqlalchemy.orm import Session 10 | 11 | import api_key 12 | import github_login 13 | import mfa 14 | import premium_access 15 | import rbac 16 | import security 17 | import user_session 18 | from db_connection import get_engine, get_session 19 | from models import Base 20 | from operations import add_user 21 | from responses import ( 22 | ResponseCreateUser, 23 | UserCreateBody, 24 | UserCreateResponse, 25 | ) 26 | from third_party_login import resolve_github_token 27 | 28 | 29 | @asynccontextmanager 30 | async def lifespan(app: FastAPI): 31 | Base.metadata.create_all(bind=get_engine()) 32 | yield 33 | 34 | 35 | app = FastAPI( 36 | title="Saas application", lifespan=lifespan 37 | ) 38 | 39 | app.include_router(security.router) 40 | app.include_router(premium_access.router) 41 | app.include_router(rbac.router) 42 | app.include_router(github_login.router) 43 | app.include_router(mfa.router) 44 | app.include_router(user_session.router) 45 | app.include_router(api_key.router) 46 | 47 | 48 | @app.post( 49 | "/register/user", 50 | status_code=status.HTTP_201_CREATED, 51 | response_model=ResponseCreateUser, 52 | responses={ 53 | status.HTTP_409_CONFLICT: { 54 | "description": "The user already exists" 55 | } 56 | }, 57 | ) 58 | def register( 59 | user: UserCreateBody, 60 | session: Session = Depends(get_session), 61 | ) -> dict[str, UserCreateResponse]: 62 | user = add_user( 63 | session=session, **user.model_dump() 64 | ) 65 | if not user: 66 | raise HTTPException( 67 | status.HTTP_409_CONFLICT, 68 | "username or email already exists", 69 | ) 70 | user_response = UserCreateResponse( 71 | username=user.username, email=user.email 72 | ) 73 | return { 74 | "message": "user created", 75 | "user": user_response, 76 | } 77 | 78 | 79 | @app.get( 80 | "/home", 81 | responses={ 82 | status.HTTP_403_FORBIDDEN: { 83 | "description": "token not valid" 84 | } 85 | }, 86 | ) 87 | def homepage( 88 | user: UserCreateResponse = Depends( 89 | resolve_github_token 90 | ), 91 | ): 92 | return {"message": f"logged in {user.username} !"} 93 | -------------------------------------------------------------------------------- /Chapter01/bookstore/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from fastapi import ( 4 | FastAPI, 5 | HTTPException, 6 | Request, 7 | status, 8 | ) 9 | from fastapi.exceptions import RequestValidationError 10 | from fastapi.responses import PlainTextResponse 11 | from pydantic import BaseModel 12 | from starlette.responses import JSONResponse 13 | 14 | from models import Book 15 | 16 | app = FastAPI() 17 | 18 | 19 | @app.get("/books/{book_id}") 20 | async def read_book(book_id: int): 21 | return { 22 | "book_id": book_id, 23 | "title": "The Great Gatsby", 24 | "author": "F. Scott Fitzgerald", 25 | } 26 | 27 | 28 | @app.get("/authors/{author_id}") 29 | async def read_author(author_id: int): 30 | return { 31 | "author_id": author_id, 32 | "name": "Ernest Hemingway", 33 | } 34 | 35 | 36 | @app.get("/books") 37 | async def read_books(year: int = None): 38 | if year: 39 | return { 40 | "year": year, 41 | "books": ["Book 1", "Book 2"], 42 | } 43 | return {"books": ["All Books"]} 44 | 45 | 46 | @app.post("/books") 47 | async def create_book(book: Book): 48 | return book 49 | 50 | 51 | class BookResponse(BaseModel): 52 | title: str 53 | author: str 54 | 55 | 56 | @app.get("/allbooks") 57 | async def read_all_books() -> list[BookResponse]: 58 | return [ 59 | { 60 | "id": 1, 61 | "title": "1984", 62 | "author": "George Orwell", 63 | }, 64 | { 65 | "id": 2, 66 | "title": "The Great Gatsby", 67 | "author": "F. Scott Fitzgerald", 68 | }, 69 | ] 70 | 71 | 72 | @app.exception_handler(HTTPException) 73 | async def http_exception_handler(request, exc): 74 | return JSONResponse( 75 | status_code=exc.status_code, 76 | content={ 77 | "message": "Oops! Something went wrong" 78 | }, 79 | ) 80 | 81 | 82 | @app.get("/error_endpoint") 83 | async def raise_exception(): 84 | raise HTTPException(status_code=400) 85 | 86 | 87 | @app.exception_handler(RequestValidationError) 88 | async def validation_exception_handler( 89 | request: Request, exc: RequestValidationError 90 | ): 91 | return PlainTextResponse( 92 | "This is a plain text response:" 93 | f" \n{json.dumps(exc.errors(), indent=2)}", 94 | status_code=status.HTTP_400_BAD_REQUEST, 95 | ) 96 | -------------------------------------------------------------------------------- /Chapter08/trip_platform/app/internationalization.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from babel import Locale, negotiate_locale 4 | from babel.numbers import get_currency_name 5 | from fastapi import APIRouter, Depends, Header, Request 6 | 7 | from app.rate_limiter import limiter 8 | 9 | router = APIRouter(tags=["Localizad Content Endpoints"]) 10 | 11 | SUPPORTED_LOCALES = [ 12 | "en_US", 13 | "fr_FR", 14 | ] 15 | 16 | 17 | def resolve_accept_language( 18 | accept_language: str = Header("en-US"), 19 | ) -> Locale: 20 | client_locales = [] 21 | for language_q in accept_language.split(","): 22 | if ";q=" in language_q: 23 | language, q = language_q.split(";q=") 24 | else: 25 | language, q = (language_q, float("inf")) 26 | try: 27 | Locale.parse(language, sep="-") 28 | client_locales.append((language, float(q))) 29 | except ValueError: 30 | continue 31 | 32 | client_locales.sort( 33 | key=lambda x: x[1], reverse=True 34 | ) 35 | 36 | locales = [locale for locale, _ in client_locales] 37 | 38 | locale = negotiate_locale( 39 | [str(locale) for locale in locales], 40 | SUPPORTED_LOCALES, 41 | ) 42 | if locale is None: 43 | locale = "en_US" 44 | 45 | return locale 46 | 47 | 48 | home_page_content = { 49 | "en_US": "Welcome to Trip Platform", 50 | "fr_FR": "Bienvenue sur Trip Platform", 51 | } 52 | 53 | 54 | @router.get("/homepage") 55 | @limiter.limit("2/minute") 56 | async def home( 57 | request: Request, 58 | language: Annotated[ 59 | resolve_accept_language, Depends() 60 | ], 61 | ): 62 | return {"message": home_page_content[language]} 63 | 64 | 65 | async def get_currency( 66 | language: Annotated[ 67 | resolve_accept_language, Depends() 68 | ], 69 | ): 70 | currencies = { 71 | "en_US": "USD", 72 | "fr_FR": "EUR", 73 | } 74 | 75 | return currencies[language] 76 | 77 | 78 | @router.get("/show/currency") 79 | async def show_currency( 80 | currency: Annotated[get_currency, Depends()], 81 | language: Annotated[ 82 | resolve_accept_language, Depends(use_cache=True) 83 | ], 84 | ): 85 | currency_name = get_currency_name( 86 | currency, locale=language 87 | ) 88 | return { 89 | "currency": currency, 90 | "currency_name": currency_name, 91 | } 92 | -------------------------------------------------------------------------------- /Chapter11/middleware_project/middleware/webhook.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from asyncio import create_task 3 | from datetime import datetime 4 | 5 | from fastapi import Request 6 | from httpx import AsyncClient 7 | from pydantic import BaseModel 8 | from starlette.types import ( 9 | ASGIApp, 10 | Receive, 11 | Scope, 12 | Send, 13 | ) 14 | 15 | 16 | class Event(BaseModel): 17 | host: str 18 | path: str 19 | time: str 20 | body: str | None = None 21 | 22 | model_config = { 23 | "json_schema_extra": { 24 | "examples": [ 25 | { 26 | "host": "127.0.0.1", 27 | "path": "/send", 28 | "time": "2024-05-22T14:24:28.847663", 29 | "body": '"body content"', 30 | } 31 | ] 32 | } 33 | } 34 | 35 | 36 | client = AsyncClient() 37 | 38 | logger = logging.getLogger("uvicorn") 39 | 40 | 41 | async def send_event_to_url(url: str, event: Event): 42 | logger.info(f"Sending event to {url}") 43 | try: 44 | await client.post( 45 | f"{url}/fastapi-webhook", 46 | json=event.model_dump(), 47 | ) 48 | except Exception as e: 49 | logger.error( 50 | f"Error sending webhook event to {url}: {e}" 51 | ) 52 | 53 | 54 | class WebhookSenderMiddleWare: 55 | def __init__(self, app: ASGIApp): 56 | self.app = app 57 | 58 | async def __call__( 59 | self, 60 | scope: Scope, 61 | receive: Receive, 62 | send: Send, 63 | ): 64 | if scope["type"] == "http": 65 | message = await receive() 66 | body = message.get("body", b"") 67 | request = Request(scope=scope) 68 | 69 | event = Event( 70 | host=request.client.host, 71 | path=request.url.path, 72 | time=datetime.now().isoformat(), 73 | body=body, 74 | ) 75 | urls = request.state.webhook_urls 76 | for url in urls: 77 | await create_task( 78 | send_event_to_url(url, event) 79 | ) 80 | 81 | async def continue_receive(): 82 | return message 83 | 84 | await self.app( 85 | scope, continue_receive, send 86 | ) 87 | return 88 | 89 | await self.app(scope, receive, send) 90 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/tests/test_main.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | 3 | 4 | def test_add_song(test_client, mongo_db_mock): 5 | song = { 6 | "title": "Test Song", 7 | "artist": "Test Artist", 8 | } 9 | 10 | response = test_client.post("/song", json=song) 11 | 12 | assert response.status_code == 200 13 | assert ( 14 | response.json()["message"] 15 | == "Song added successfully" 16 | ) 17 | mongo_db_mock.songs.insert_one.assert_called_once_with( 18 | song 19 | ) 20 | 21 | 22 | def test_get_song(test_client, mongo_db_mock): 23 | response = test_client.get( 24 | "/song/0123456789abcdef01234567" 25 | ) 26 | 27 | assert response.status_code == 200 28 | assert response.json() == { 29 | "_id": "0123456789abcdef01234567", 30 | "title": "Test Song", 31 | "artist": "Test Artist", 32 | } 33 | mongo_db_mock.songs.find_one.assert_called_once_with( 34 | {"_id": ObjectId("0123456789abcdef01234567")} 35 | ) 36 | 37 | 38 | def test_get_songs(test_client, mongo_db_mock): 39 | response = test_client.get("/songs") 40 | 41 | assert response.status_code == 200 42 | assert response.json() == [ 43 | { 44 | "_id": "89abcdef0123456789abcdef", 45 | "title": "Song 1", 46 | "artist": "Artist 1", 47 | }, 48 | { 49 | "_id": "23456789abcdef0123456789", 50 | "title": "Song 2", 51 | "artist": "Artist 2", 52 | }, 53 | ] 54 | mongo_db_mock.songs.find.assert_called_once() 55 | 56 | 57 | def test_update_song(test_client, mongo_db_mock): 58 | song_id = "0123456789abcdef01234567" 59 | updated_song = { 60 | "title": "Updated Song", 61 | "artist": "Updated Artist", 62 | } 63 | 64 | response = test_client.put( 65 | f"/song/{song_id}", json=updated_song 66 | ) 67 | 68 | assert response.status_code == 200 69 | assert response.json() == { 70 | "message": "Song updated successfully" 71 | } 72 | 73 | mongo_db_mock.songs.update_one.assert_called_once_with( 74 | {"_id": ObjectId(song_id)}, 75 | {"$set": updated_song}, 76 | ) 77 | 78 | 79 | def test_delete_song(test_client, mongo_db_mock): 80 | response = test_client.delete( 81 | "/song/0123456789abcdef01234567" 82 | ) 83 | 84 | assert response.status_code == 200 85 | assert response.json() == { 86 | "message": "Song deleted successfully" 87 | } 88 | mongo_db_mock.songs.delete_one.assert_called_once_with( 89 | {"_id": ObjectId("0123456789abcdef01234567")} 90 | ) 91 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/app/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import ( 3 | DeclarativeBase, 4 | Mapped, 5 | mapped_column, 6 | relationship, 7 | ) 8 | 9 | 10 | class Base(DeclarativeBase): 11 | pass 12 | 13 | 14 | class Ticket(Base): 15 | __tablename__ = "tickets" 16 | 17 | id: Mapped[int] = mapped_column(primary_key=True) 18 | price: Mapped[float] = mapped_column(nullable=True) 19 | show: Mapped[str | None] 20 | user: Mapped[str | None] 21 | sold: Mapped[bool] = mapped_column(default=False) 22 | details: Mapped["TicketDetails"] = relationship( 23 | back_populates="ticket" 24 | ) 25 | event_id: Mapped[int | None] = mapped_column( 26 | ForeignKey("events.id") 27 | ) 28 | event: Mapped["Event | None"] = relationship( 29 | back_populates="tickets" 30 | ) 31 | 32 | 33 | class TicketDetails(Base): 34 | __tablename__ = "ticket_details" 35 | 36 | id: Mapped[int] = mapped_column(primary_key=True) 37 | ticket_id: Mapped[int] = mapped_column( 38 | ForeignKey("tickets.id") 39 | ) 40 | ticket: Mapped["Ticket"] = relationship( 41 | back_populates="details" 42 | ) 43 | seat: Mapped[str | None] 44 | ticket_type: Mapped[str | None] 45 | 46 | 47 | class Event(Base): 48 | __tablename__ = "events" 49 | 50 | id: Mapped[int] = mapped_column(primary_key=True) 51 | name: Mapped[str] 52 | tickets: Mapped[list["Ticket"]] = relationship( 53 | back_populates="event" 54 | ) 55 | sponsors: Mapped[list["Sponsor"]] = relationship( 56 | secondary="sponsorships", 57 | back_populates="events", 58 | ) 59 | 60 | 61 | class Sponsor(Base): 62 | __tablename__ = "sponsors" 63 | 64 | id: Mapped[int] = mapped_column(primary_key=True) 65 | name: Mapped[str] = mapped_column(unique=True) 66 | events: Mapped[list["Event"]] = relationship( 67 | secondary="sponsorships", 68 | back_populates="sponsors", 69 | ) 70 | 71 | 72 | class Sponsorship(Base): 73 | __tablename__ = "sponsorships" 74 | 75 | event_id: Mapped[int] = mapped_column( 76 | ForeignKey("events.id"), primary_key=True 77 | ) 78 | sponsor_id: Mapped[int] = mapped_column( 79 | ForeignKey("sponsors.id"), primary_key=True 80 | ) 81 | amount: Mapped[float] = mapped_column( 82 | nullable=False, default=10 83 | ) 84 | 85 | 86 | class CreditCard(Base): 87 | __tablename__ = "credit_cards" 88 | 89 | id: Mapped[int] = mapped_column(primary_key=True) 90 | number: Mapped[str] 91 | expiration_date: Mapped[str] 92 | cvv: Mapped[str] 93 | card_holder_name: Mapped[str] 94 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/tests/test_client.py: -------------------------------------------------------------------------------- 1 | def test_client_does_not_found_specific_ticket( 2 | test_client, 3 | ): 4 | request = test_client.get("/ticket/1") 5 | assert request.status_code == 404 6 | assert request.json() == { 7 | "detail": "Ticket not found" 8 | } 9 | 10 | 11 | def test_client_create_ticket( 12 | test_client, 13 | ): 14 | request = test_client.post( 15 | "/ticket", 16 | json={ 17 | "price": 100.0, 18 | "show": "The Rolling Stones Concert", 19 | "user": "John Doe", 20 | }, 21 | ) 22 | assert request.status_code == 200 23 | assert request.json() == {"ticket_id": 1} 24 | 25 | 26 | def test_client_update_ticket_price( 27 | test_client, add_special_ticket 28 | ): 29 | request = test_client.put( 30 | "/ticket/1234/price/250.0" 31 | ) 32 | assert request.status_code == 200 33 | assert request.json() == {"detail": "Price updated"} 34 | 35 | 36 | def test_client_update_ticket( 37 | test_client, add_special_ticket 38 | ): 39 | request = test_client.put( 40 | "/ticket/1234", json={"price": 250.0} 41 | ) 42 | assert request.status_code == 200 43 | assert request.json() == {"detail": "Price updated"} 44 | 45 | 46 | def test_client_delete_ticket( 47 | test_client, add_special_ticket 48 | ): 49 | request = test_client.delete("/ticket/1234") 50 | assert request.status_code == 200 51 | assert request.json() == { 52 | "detail": "Ticket removed" 53 | } 54 | request = test_client.get("/ticket/1234") 55 | assert request.status_code == 404 56 | assert request.json() == { 57 | "detail": "Ticket not found" 58 | } 59 | 60 | 61 | def test_client_get_all_tickets_for_concert( 62 | test_client, fill_database_with_tickets 63 | ): 64 | request = test_client.get( 65 | "/tickets/The Rolling Stones Concert" 66 | ) 67 | assert request.status_code == 200 68 | assert request.json() == [ 69 | { 70 | "id": id_, 71 | "price": None, 72 | "show": "The Rolling Stones Concert", 73 | "user": None, 74 | } 75 | for id_ in range(1, 11) 76 | ] 77 | 78 | 79 | def test_client_create_event(test_client): 80 | request = test_client.post( 81 | "/event", 82 | params={ 83 | "event_name": "The Rolling Stones Concert", 84 | "nb_tickets": 10, 85 | }, 86 | ) 87 | assert request.status_code == 200 88 | assert request.json() == {"event_id": 1} 89 | 90 | request = test_client.get( 91 | "/tickets/The Rolling Stones Concert" 92 | ) 93 | 94 | assert len(request.json()) == 10 95 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Login Page 5 | 6 | 7 | 8 |

      Login

      9 |
      10 | 11 |

      17 | 18 |

      24 | 25 |
      26 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Chapter11/middleware_project/http_server.py: -------------------------------------------------------------------------------- 1 | import socketserver 2 | import json 3 | import http.server 4 | from colorama import Fore, Back 5 | 6 | 7 | class RequestHandler( 8 | http.server.SimpleHTTPRequestHandler 9 | ): 10 | def not_allow_method(self): 11 | self.send_response(403) 12 | self.send_header("Content-type", "text/html") 13 | self.end_headers() 14 | self.wfile.write(b"405 Method Not Allowed") 15 | 16 | def do_POST(self): 17 | if self.path == "/fastapi-webhook": 18 | content_length = int( 19 | self.headers.get("Content-Length", 0) 20 | ) 21 | event = json.loads( 22 | self.rfile.read( 23 | content_length 24 | ).decode("utf-8") 25 | ) 26 | 27 | host = event.get("host") 28 | path = event.get("path") 29 | timestamp = event.get("time") 30 | body = event.get("body") 31 | 32 | print( 33 | f"{Back.CYAN}time{Back.RESET}: " 34 | f"{Fore.CYAN}{timestamp}{Fore.CYAN}" 35 | ) 36 | print( 37 | f"{Back.RED}host{Back.RESET}: " 38 | f"{Fore.RED}{host}{Fore.RESET}" 39 | ) 40 | print( 41 | f"{Back.BLUE}path{Back.RESET}: " 42 | f"{Fore.BLUE}{path}{Fore.RESET}" 43 | ) 44 | print( 45 | f"{Back.LIGHTBLACK_EX}body{Back.RESET}: " 46 | f"{Fore.LIGHTBLACK_EX}{body}{Fore.RESET}\n" 47 | ) 48 | 49 | self.send_response(200) 50 | self.send_header( 51 | "Content-type", "text/html" 52 | ) 53 | self.end_headers() 54 | else: 55 | self.send_response(404) 56 | self.send_header( 57 | "Content-type", "text/html" 58 | ) 59 | self.end_headers() 60 | self.wfile.write(b"404 Not Found") 61 | 62 | def do_GET(self): 63 | self.not_allow_method() 64 | 65 | def do_PUT(self): 66 | self.not_allow_method() 67 | 68 | def do_DELETE(self): 69 | self.not_allow_method() 70 | 71 | def do_PATCH(self): 72 | self.not_allow_method() 73 | 74 | def do_HEAD(self): 75 | self.not_allow_method() 76 | 77 | def do_OPTIONS(self): 78 | self.not_allow_method() 79 | 80 | 81 | def run_server(): 82 | PORT = 8080 83 | Handler = RequestHandler 84 | 85 | with socketserver.TCPServer( 86 | ("", PORT), Handler 87 | ) as httpd: 88 | print(f"Server running on port {PORT}") 89 | httpd.serve_forever() 90 | 91 | 92 | if __name__ == "__main__": 93 | run_server() 94 | -------------------------------------------------------------------------------- /Chapter08/trip_platform/app/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Annotated 3 | 4 | from fastapi import BackgroundTasks, Depends, FastAPI 5 | from slowapi import _rate_limit_exceeded_handler 6 | from slowapi.errors import RateLimitExceeded 7 | from slowapi.middleware import SlowAPIMiddleware 8 | 9 | from app import internationalization 10 | from app.background_task import ( 11 | store_query_to_external_db, 12 | ) 13 | from app.dependencies import ( 14 | CommonQueryParams, 15 | check_coupon_validity, 16 | select_category, 17 | time_range, 18 | ) 19 | from app.middleware import ClientInfoMiddleware 20 | from app.profiler import router as profiler_router 21 | from app.rate_limiter import limiter 22 | 23 | logger = logging.getLogger("uvicorn") 24 | 25 | app = FastAPI() 26 | 27 | 28 | app.add_middleware(ClientInfoMiddleware) 29 | 30 | app.include_router(internationalization.router) 31 | app.include_router(profiler_router) 32 | 33 | app.state.limiter = limiter 34 | app.add_exception_handler( 35 | RateLimitExceeded, _rate_limit_exceeded_handler 36 | ) 37 | app.add_middleware(SlowAPIMiddleware) 38 | 39 | 40 | @app.get("/v1/trips") 41 | def get_trips( 42 | time_range: Annotated[time_range, Depends()], 43 | ): 44 | start, end = time_range 45 | message = f"Request trips from {start}" 46 | if end: 47 | return f"{message} to {end}" 48 | return message 49 | 50 | 51 | @app.get("/v2/trips/{category}") 52 | def get_trips_by_category( 53 | background_tasks: BackgroundTasks, 54 | category: Annotated[select_category, Depends()], 55 | discount_applicable: Annotated[ 56 | bool, Depends(check_coupon_validity) 57 | ], 58 | ): 59 | category = category.replace("-", " ").title() 60 | message = f"You requested {category} trips." 61 | 62 | if discount_applicable: 63 | message += ( 64 | "\n. The coupon code is valid! " 65 | "You will get a discount!" 66 | ) 67 | 68 | background_tasks.add_task( 69 | store_query_to_external_db, message 70 | ) 71 | logger.info( 72 | "Query sent to background task, end of request." 73 | ) 74 | return message 75 | 76 | 77 | @app.get("/v3/trips/{category}") 78 | def get_trips_by_category_v3( 79 | common_params: Annotated[ 80 | CommonQueryParams, Depends() 81 | ], 82 | ): 83 | start = common_params.start 84 | end = common_params.end 85 | category = common_params.category.replace( 86 | "-", " " 87 | ).title() 88 | message = f"You requested {category} trips within" 89 | message += f" from {start}" 90 | if end: 91 | message += f" to {end}" 92 | if common_params.applicable_discount: 93 | message += ( 94 | "\n. The coupon code is valid! " 95 | "You will get a discount!" 96 | ) 97 | 98 | return message 99 | -------------------------------------------------------------------------------- /Chapter11/middleware_project/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import asynccontextmanager 3 | 4 | from fastapi import Body, FastAPI, Request 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from fastapi.middleware.trustedhost import ( 7 | TrustedHostMiddleware, 8 | ) 9 | from starlette.middleware import Middleware 10 | 11 | from middleware.asgi_middleware import ( 12 | ASGIMiddleware, 13 | asgi_middleware, 14 | ) 15 | from middleware.request_middleware import ( 16 | HashBodyContentMiddleWare, 17 | ) 18 | from middleware.response_middlaware import ( 19 | ExtraHeadersResponseMiddleware, 20 | ) 21 | from middleware.webhook import ( 22 | Event, 23 | WebhookSenderMiddleWare, 24 | ) 25 | 26 | logger = logging.getLogger("uvicorn") 27 | 28 | 29 | @asynccontextmanager 30 | async def lifespan(app: FastAPI): 31 | yield {"webhook_urls": set()} 32 | 33 | 34 | app = FastAPI( 35 | title="Middleware Application", 36 | lifespan=lifespan, 37 | middleware=[ 38 | Middleware( 39 | ASGIMiddleware, 40 | parameter="example_parameter", 41 | ), 42 | Middleware( 43 | asgi_middleware, 44 | parameter="example_parameter", 45 | ), 46 | ], 47 | ) 48 | 49 | 50 | app.add_middleware( 51 | HashBodyContentMiddleWare, 52 | allowed_paths=["/send"], 53 | ) 54 | 55 | app.add_middleware( 56 | ExtraHeadersResponseMiddleware, 57 | headers=( 58 | ("new-header", "fastapi-cookbook"), 59 | ( 60 | "another-header", 61 | "fastapi-cookbook", 62 | ), 63 | ), 64 | ) 65 | 66 | 67 | @app.get("/") 68 | async def read_root(): 69 | return {"Hello": "Middleware World"} 70 | 71 | 72 | @app.post("/send") 73 | async def send(message: str = Body()): 74 | logger.info(f"Message: {message}") 75 | return message 76 | 77 | 78 | app.add_middleware( 79 | CORSMiddleware, 80 | allow_origins=["*"], 81 | allow_methods=["*"], 82 | allow_headers=["*"], 83 | ) 84 | 85 | 86 | app.add_middleware( 87 | TrustedHostMiddleware, 88 | allowed_hosts=["localhost"], 89 | ) 90 | 91 | 92 | @app.post("/register-webhook-url") 93 | async def add_webhook_url( 94 | request: Request, url: str = Body() 95 | ): 96 | if not url.startswith("http"): 97 | url = f"http://{url}" 98 | request.state.webhook_urls.add(url) 99 | return {"url added": url} 100 | 101 | 102 | app.add_middleware( 103 | WebhookSenderMiddleWare, 104 | ) 105 | 106 | 107 | @app.webhooks.post("/fastapi-webhook") 108 | def fastapi_webhook(event: Event): 109 | """_summary_ 110 | 111 | Args: 112 | event (Event): Received event from webhook 113 | It contains information about the 114 | host, path, timestamp and body of the request 115 | """ 116 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/app/main_search.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from elasticsearch import BadRequestError 5 | from fastapi import APIRouter, Depends, HTTPException 6 | from fastapi_cache.decorator import cache 7 | 8 | from app.db_connection import es_client, redis_client 9 | from app.es_queries import ( 10 | top_ten_artists_query, 11 | top_ten_songs_query, 12 | ) 13 | 14 | logger = logging.getLogger("uvicorn") 15 | 16 | 17 | router = APIRouter( 18 | prefix="/search", 19 | tags=["search"], 20 | ) 21 | 22 | 23 | def get_elasticsearch_client(): 24 | return es_client 25 | 26 | 27 | def get_redis_client(): 28 | return redis_client 29 | 30 | 31 | @router.get("/top/ten/artists/{country}") 32 | async def top_ten_artist_by_country( 33 | country: str, 34 | es_client=Depends(get_elasticsearch_client), 35 | redis_client=Depends(get_redis_client), 36 | ): 37 | cache_key = f"top_ten_artists_{country}" 38 | 39 | cached_data = await redis_client.get(cache_key) 40 | if cached_data: 41 | logger.info( 42 | f"Returning cached data for {country}" 43 | ) 44 | return json.loads(cached_data) 45 | 46 | logger.info( 47 | f"Getting top ten artists for {country}" 48 | ) 49 | try: 50 | response = await es_client.search( 51 | **top_ten_artists_query(country) 52 | ) 53 | except BadRequestError as e: 54 | logger.error(e) 55 | 56 | raise HTTPException( 57 | status_code=400, 58 | detail="Invalid country", 59 | ) 60 | 61 | artists = [ 62 | { 63 | "artist": record.get("key"), 64 | "views": record.get("views", {}).get( 65 | "value" 66 | ), 67 | } 68 | for record in response["aggregations"][ 69 | "top_ten_artists" 70 | ]["buckets"] 71 | ] 72 | 73 | await redis_client.set( 74 | cache_key, json.dumps(artists), ex=3600 75 | ) 76 | 77 | return artists 78 | 79 | 80 | @router.get("/top/ten/songs/{country}") 81 | @cache(expire=60) 82 | async def get_top_ten_by_country( 83 | country: str, 84 | es_client=Depends(get_elasticsearch_client), 85 | ): 86 | try: 87 | response = await es_client.search( 88 | **top_ten_songs_query(country) 89 | ) 90 | except BadRequestError as e: 91 | logger.error(e) 92 | 93 | raise HTTPException( 94 | status_code=400, 95 | detail="Invalid country", 96 | ) 97 | 98 | songs = [] 99 | for record in response["hits"]["hits"]: 100 | song = { 101 | "title": record["_source"]["title"], 102 | "artist": record["_source"]["artist"], 103 | "album": record["_source"]["album"][ 104 | "title" 105 | ], 106 | "views": record["_source"] 107 | .get("views_per_country", {}) 108 | .get(country), 109 | } 110 | songs.append(song) 111 | 112 | return songs 113 | -------------------------------------------------------------------------------- /Chapter10/graphql/database.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | id: int 6 | username: str 7 | phone_number: str 8 | country: str 9 | 10 | 11 | users_db: list[User] = [ 12 | User( 13 | id=1, 14 | username="user1", 15 | phone_number="1234567890", 16 | country="USA", 17 | ), 18 | User( 19 | id=2, 20 | username="user2", 21 | phone_number="0987654321", 22 | country="Canada", 23 | ), 24 | User( 25 | id=3, 26 | username="user3", 27 | phone_number="9876543210", 28 | country="UK", 29 | ), 30 | User( 31 | id=4, 32 | username="user4", 33 | phone_number="5555555555", 34 | country="Mexico", 35 | ), 36 | User( 37 | id=5, 38 | username="user5", 39 | phone_number="9999999999", 40 | country="Brazil", 41 | ), 42 | User( 43 | id=6, 44 | username="user6", 45 | phone_number="1111111111", 46 | country="Germany", 47 | ), 48 | User( 49 | id=7, 50 | username="user7", 51 | phone_number="2222222222", 52 | country="France", 53 | ), 54 | User( 55 | id=8, 56 | username="user8", 57 | phone_number="3333333333", 58 | country="Italy", 59 | ), 60 | User( 61 | id=9, 62 | username="user9", 63 | phone_number="4444444444", 64 | country="Spain", 65 | ), 66 | User( 67 | id=10, 68 | username="user10", 69 | phone_number="5555555555", 70 | country="Portugal", 71 | ), 72 | User( 73 | id=11, 74 | username="user11", 75 | phone_number="6666666666", 76 | country="Russia", 77 | ), 78 | User( 79 | id=12, 80 | username="user12", 81 | phone_number="7777777777", 82 | country="China", 83 | ), 84 | User( 85 | id=13, 86 | username="user13", 87 | phone_number="8888888888", 88 | country="Japan", 89 | ), 90 | User( 91 | id=14, 92 | username="user14", 93 | phone_number="9999999999", 94 | country="India", 95 | ), 96 | User( 97 | id=15, 98 | username="user15", 99 | phone_number="0000000000", 100 | country="South Africa", 101 | ), 102 | User( 103 | id=16, 104 | username="user16", 105 | phone_number="1111111111", 106 | country="Egypt", 107 | ), 108 | User( 109 | id=17, 110 | username="user17", 111 | phone_number="2222222222", 112 | country="Nigeria", 113 | ), 114 | User( 115 | id=18, 116 | username="user18", 117 | phone_number="3333333333", 118 | country="Kenya", 119 | ), 120 | User( 121 | id=19, 122 | username="user19", 123 | phone_number="4444444444", 124 | country="New Zealand", 125 | ), 126 | User( 127 | id=20, 128 | username="user20", 129 | phone_number="0123456789", 130 | country="Australia", 131 | ), 132 | ] 133 | -------------------------------------------------------------------------------- /Chapter08/trip_platform/app/dependencies.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | from typing import Annotated, Tuple 3 | 4 | from fastapi import Depends, HTTPException, Path, Query 5 | 6 | 7 | def check_start_end_condition(start: date, end: date): 8 | if end and end < start: 9 | raise HTTPException( 10 | status_code=400, 11 | detail=( 12 | "End date must be " 13 | "greater than start date" 14 | ), 15 | ) 16 | 17 | 18 | def time_range( 19 | start: date | None = Query( 20 | default=date.today(), 21 | description=( 22 | "If not provided the current date is used" 23 | ), 24 | examples=[date.today().isoformat()], 25 | ), 26 | end: date | None = Query( 27 | None, 28 | examples=[date.today() + timedelta(days=7)], 29 | ), 30 | ) -> Tuple[date, date | None]: 31 | check_start_end_condition(start, end) 32 | return start, end 33 | 34 | 35 | def select_category( 36 | category: Annotated[ 37 | str, 38 | Path( 39 | description=( 40 | "Kind of travel " 41 | "you are interested in" 42 | ), 43 | enum=[ 44 | "cruises", 45 | "city-breaks", 46 | "resort-stays", 47 | ], 48 | ), 49 | ], 50 | ) -> str: 51 | return category 52 | 53 | 54 | def check_coupon_validity( 55 | category: Annotated[select_category, Depends()], 56 | code: str | None = Query( 57 | None, description="Coupon code" 58 | ), 59 | ) -> bool: 60 | coupon_dict = { 61 | "cruises": "CRUISE10", 62 | "city-breaks": "CITYBREAK15", 63 | "resort-stays": "RESORT20", 64 | } 65 | if ( 66 | code is not None 67 | and coupon_dict.get(category, ...) == code 68 | ): 69 | return True 70 | return False 71 | 72 | 73 | class CommonQueryParams: 74 | def __init__( 75 | self, 76 | category: Annotated[ 77 | str | None, 78 | Path( 79 | description=( 80 | "Kind of travel " 81 | "you are interested in" 82 | ), 83 | enum=[ 84 | "cruises", 85 | "city-breaks", 86 | "resort-stays", 87 | ], 88 | ), 89 | ], 90 | start: Annotated[ 91 | date | None, 92 | Query( 93 | description="required if end date is not provided", 94 | examples=["2023-01-01"], 95 | ), 96 | ] = None, 97 | end: Annotated[ 98 | date | None, 99 | Query( 100 | description="required if start date is not provided", 101 | examples=["2023-01-01"], 102 | ), 103 | ] = None, 104 | code: str | None = Query( 105 | None, description="Coupon code" 106 | ), 107 | ): 108 | check_start_end_condition(start, end) 109 | self.start = start 110 | self.end = end 111 | self.category = category 112 | self.applicable_discount = ( 113 | check_coupon_validity(category, code) 114 | ) 115 | -------------------------------------------------------------------------------- /Chapter09/chat_platform/app/security.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import ( 4 | APIRouter, 5 | Depends, 6 | HTTPException, 7 | Request, 8 | WebSocketException, 9 | status, 10 | ) 11 | from fastapi.responses import HTMLResponse 12 | from fastapi.security import ( 13 | OAuth2PasswordRequestForm, 14 | ) 15 | from fastapi.templating import Jinja2Templates 16 | from pydantic import BaseModel 17 | 18 | from app.ws_password_bearer import ( 19 | OAuth2WebSocketPasswordBearer, 20 | ) 21 | 22 | fake_users_db = { 23 | "johndoe": { 24 | "username": "johndoe", 25 | "hashed_password": "hashedsecret", 26 | }, 27 | "janedoe": { 28 | "username": "janedoe", 29 | "hashed_password": "hashedsecret2", 30 | }, 31 | } 32 | 33 | 34 | def fakely_hash_password(password: str): 35 | return f"hashed{password}" 36 | 37 | 38 | class User(BaseModel): 39 | username: str 40 | 41 | 42 | def get_user( 43 | db: dict[str, str], username: str 44 | ) -> User | None: 45 | if username in db: 46 | user_dict = db[username] 47 | return User(**user_dict) 48 | 49 | 50 | def fake_token_generator(username: str) -> str: 51 | # This doesn't provide any security at all 52 | return f"tokenized{username}" 53 | 54 | 55 | def fake_token_resolver( 56 | token: str, 57 | ) -> User | None: 58 | if token.startswith("tokenized"): 59 | user_id = token.removeprefix("tokenized") 60 | user = get_user(fake_users_db, user_id) 61 | return user 62 | 63 | 64 | oauth2_scheme_for_ws = OAuth2WebSocketPasswordBearer( 65 | tokenUrl="/token" 66 | ) 67 | 68 | 69 | def get_username_from_token( 70 | token: str = Depends(oauth2_scheme_for_ws), 71 | ) -> str: 72 | user = fake_token_resolver(token) 73 | if not user: 74 | raise WebSocketException( 75 | code=status.HTTP_401_UNAUTHORIZED, 76 | reason="Invalid authentication credentials", 77 | ) 78 | return user.username 79 | 80 | 81 | router = APIRouter() 82 | 83 | 84 | @router.post("/token") 85 | async def login( 86 | form_data: OAuth2PasswordRequestForm = Depends(), 87 | ): 88 | user_dict = fake_users_db.get(form_data.username) 89 | if not user_dict: 90 | raise HTTPException( 91 | status_code=400, 92 | detail="Incorrect username or password", 93 | ) 94 | hashed_password = fakely_hash_password( 95 | form_data.password 96 | ) 97 | if not hashed_password == user_dict.get( 98 | "hashed_password" 99 | ): 100 | raise HTTPException( 101 | status_code=400, 102 | detail="Incorrect username or password", 103 | ) 104 | 105 | token = fake_token_generator(form_data.username) 106 | 107 | return { 108 | "access_token": token, 109 | "token_type": "bearer", 110 | } 111 | 112 | 113 | @router.get("/login") 114 | async def login_form( 115 | request: Request, 116 | redirecturl: Optional[str] = None, 117 | ) -> HTMLResponse: 118 | templates = Jinja2Templates( 119 | directory="templates" 120 | ) 121 | if redirecturl: 122 | context = {"redirection_url": redirecturl} 123 | else: 124 | context = {} 125 | return templates.TemplateResponse( 126 | request=request, 127 | name="login.html", 128 | context=context, 129 | ) 130 | -------------------------------------------------------------------------------- /Chapter03/task_manager_app/operations.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from typing import Optional 3 | 4 | from models import Task, TaskV2WithID, TaskWithID 5 | 6 | DATABASE_FILENAME = "tasks.csv" 7 | 8 | column_fields = ["id", "title", "description", "status"] 9 | 10 | 11 | def read_all_tasks() -> list[TaskWithID]: 12 | with open(DATABASE_FILENAME) as csvfile: 13 | reader = csv.DictReader( 14 | csvfile, 15 | ) 16 | return [TaskWithID(**row) for row in reader] 17 | 18 | 19 | def read_task(task_id) -> Optional[TaskWithID]: 20 | with open(DATABASE_FILENAME) as csvfile: 21 | reader = csv.DictReader( 22 | csvfile, 23 | ) 24 | for row in reader: 25 | if int(row["id"]) == task_id: 26 | return TaskWithID(**row) 27 | 28 | 29 | def get_next_id(): 30 | try: 31 | with open(DATABASE_FILENAME, "r") as csvfile: 32 | reader = csv.DictReader(csvfile) 33 | max_id = max( 34 | int(row["id"]) for row in reader 35 | ) 36 | return max_id + 1 37 | except (FileNotFoundError, ValueError): 38 | return 1 39 | 40 | 41 | def write_task_into_csv(task: TaskWithID): 42 | with open( 43 | DATABASE_FILENAME, mode="a", newline="" 44 | ) as file: 45 | writer = csv.DictWriter( 46 | file, 47 | fieldnames=column_fields, 48 | ) 49 | writer.writerow(task.model_dump()) 50 | 51 | 52 | def create_task(task: Task) -> TaskWithID: 53 | id = get_next_id() 54 | task_with_id = TaskWithID( 55 | id=id, **task.model_dump() 56 | ) 57 | write_task_into_csv(task_with_id) 58 | return task_with_id 59 | 60 | 61 | def modify_task( 62 | id: int, task: dict 63 | ) -> Optional[TaskWithID]: 64 | updated_task: Optional[TaskWithID] = None 65 | tasks = read_all_tasks() 66 | for number, task_ in enumerate(tasks): 67 | if task_.id == id: 68 | tasks[number] = updated_task = ( 69 | task_.model_copy(update=task) 70 | ) 71 | with open( 72 | DATABASE_FILENAME, mode="w", newline="" 73 | ) as csvfile: # rewrite the file 74 | writer = csv.DictWriter( 75 | csvfile, 76 | fieldnames=column_fields, 77 | ) 78 | writer.writeheader() 79 | for task in tasks: 80 | writer.writerow(task.model_dump()) 81 | if updated_task: 82 | return updated_task 83 | 84 | 85 | def remove_task(id: int) -> bool: 86 | deleted_task: Optional[Task] = None 87 | tasks = read_all_tasks() 88 | with open( 89 | DATABASE_FILENAME, mode="w", newline="" 90 | ) as csvfile: # rewrite the file 91 | writer = csv.DictWriter( 92 | csvfile, 93 | fieldnames=column_fields, 94 | ) 95 | writer.writeheader() 96 | for task in tasks: 97 | if task.id == id: 98 | deleted_task = task 99 | continue 100 | writer.writerow(task.model_dump()) 101 | if deleted_task: 102 | dict_task_without_id = deleted_task.model_dump() 103 | del dict_task_without_id["id"] 104 | return Task(**dict_task_without_id) 105 | 106 | 107 | def read_all_tasks_v2() -> list[TaskV2WithID]: 108 | with open(DATABASE_FILENAME) as csvfile: 109 | reader = csv.DictReader( 110 | csvfile, 111 | ) 112 | return [TaskV2WithID(**row) for row in reader] 113 | -------------------------------------------------------------------------------- /Chapter04/saas_app/security.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from fastapi import ( 4 | APIRouter, 5 | Depends, 6 | HTTPException, 7 | status, 8 | ) 9 | from fastapi.security import ( 10 | OAuth2PasswordBearer, 11 | OAuth2PasswordRequestForm, 12 | ) 13 | from jose import JWTError, jwt 14 | from pydantic import BaseModel 15 | from sqlalchemy.orm import Session 16 | 17 | from db_connection import get_session 18 | from models import User 19 | from operations import get_user, pwd_context 20 | 21 | 22 | def authenticate_user( 23 | session: Session, 24 | username_or_email: str, 25 | password: str, 26 | ) -> User | None: 27 | user = get_user(session, username_or_email) 28 | 29 | if not user or not pwd_context.verify( 30 | password, user.hashed_password 31 | ): 32 | return 33 | return user 34 | 35 | 36 | SECRET_KEY = "a_very_secret_key" 37 | ALGORITHM = "HS256" 38 | ACCESS_TOKEN_EXPIRE_MINUTES = 30 39 | 40 | 41 | def create_access_token(data: dict) -> str: 42 | to_encode = data.copy() 43 | expire = datetime.utcnow() + timedelta( 44 | minutes=ACCESS_TOKEN_EXPIRE_MINUTES 45 | ) 46 | to_encode.update({"exp": expire}) 47 | encoded_jwt = jwt.encode( 48 | to_encode, SECRET_KEY, algorithm=ALGORITHM 49 | ) 50 | return encoded_jwt 51 | 52 | 53 | def decode_access_token( 54 | token: str, db_session: Session 55 | ) -> User | None: 56 | try: 57 | payload = jwt.decode( 58 | token, SECRET_KEY, algorithms=[ALGORITHM] 59 | ) 60 | username: str = payload.get("sub") 61 | except JWTError: 62 | return 63 | if not username: 64 | return 65 | user = get_user(db_session, username) 66 | return user 67 | 68 | 69 | router = APIRouter() 70 | 71 | 72 | class Token(BaseModel): 73 | access_token: str 74 | token_type: str 75 | 76 | 77 | @router.post( 78 | "/token", 79 | response_model=Token, 80 | responses={ 81 | status.HTTP_401_UNAUTHORIZED: { 82 | "description": "Incorrect username or password" 83 | } 84 | }, 85 | ) 86 | def get_user_access_token( 87 | form_data: OAuth2PasswordRequestForm = Depends(), 88 | session: Session = Depends(get_session), 89 | ): 90 | user = authenticate_user( 91 | session, form_data.username, form_data.password 92 | ) 93 | if not user: 94 | raise HTTPException( 95 | status_code=status.HTTP_401_UNAUTHORIZED, 96 | detail="Incorrect username or password", 97 | ) 98 | access_token = create_access_token( 99 | data={"sub": user.username} 100 | ) 101 | return { 102 | "access_token": access_token, 103 | "token_type": "bearer", 104 | } 105 | 106 | 107 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") 108 | 109 | 110 | @router.get( 111 | "/users/me", 112 | responses={ 113 | status.HTTP_401_UNAUTHORIZED: { 114 | "description": "User not authorized" 115 | }, 116 | status.HTTP_200_OK: { 117 | "description": "username authorized" 118 | }, 119 | }, 120 | ) 121 | def read_user_me( 122 | token: str = Depends(oauth2_scheme), 123 | session: Session = Depends(get_session), 124 | ): 125 | user = decode_access_token(token, session) 126 | if not user: 127 | raise HTTPException( 128 | status_code=status.HTTP_401_UNAUTHORIZED, 129 | detail="User not authorized", 130 | ) 131 | return { 132 | "description": f"{user.username} authorized", 133 | } 134 | -------------------------------------------------------------------------------- /Chapter10/ai_doctor/app/utils.py: -------------------------------------------------------------------------------- 1 | symptoms_list = [ 2 | "itching", 3 | "skin_rash", 4 | "nodal_skin_eruptions", 5 | "continuous_sneezing", 6 | "shivering", 7 | "chills", 8 | "joint_pain", 9 | "stomach_pain", 10 | "acidity", 11 | "ulcers_on_tongue", 12 | "muscle_wasting", 13 | "vomiting", 14 | "burning_micturition", 15 | "spotting_ urination", 16 | "fatigue", 17 | "weight_gain", 18 | "anxiety", 19 | "cold_hands_and_feets", 20 | "mood_swings", 21 | "weight_loss", 22 | "restlessness", 23 | "lethargy", 24 | "patches_in_throat", 25 | "irregular_sugar_level", 26 | "cough", 27 | "high_fever", 28 | "sunken_eyes", 29 | "breathlessness", 30 | "sweating", 31 | "dehydration", 32 | "indigestion", 33 | "headache", 34 | "yellowish_skin", 35 | "dark_urine", 36 | "nausea", 37 | "loss_of_appetite", 38 | "pain_behind_the_eyes", 39 | "back_pain", 40 | "constipation", 41 | "abdominal_pain", 42 | "diarrhoea", 43 | "mild_fever", 44 | "yellow_urine", 45 | "yellowing_of_eyes", 46 | "acute_liver_failure", 47 | "fluid_overload", 48 | "swelling_of_stomach", 49 | "swelled_lymph_nodes", 50 | "malaise", 51 | "blurred_and_distorted_vision", 52 | "phlegm", 53 | "throat_irritation", 54 | "redness_of_eyes", 55 | "sinus_pressure", 56 | "runny_nose", 57 | "congestion", 58 | "chest_pain", 59 | "weakness_in_limbs", 60 | "fast_heart_rate", 61 | "pain_during_bowel_movements", 62 | "pain_in_anal_region", 63 | "bloody_stool", 64 | "irritation_in_anus", 65 | "neck_pain", 66 | "dizziness", 67 | "cramps", 68 | "bruising", 69 | "obesity", 70 | "swollen_legs", 71 | "swollen_blood_vessels", 72 | "puffy_face_and_eyes", 73 | "enlarged_thyroid", 74 | "brittle_nails", 75 | "swollen_extremeties", 76 | "excessive_hunger", 77 | "extra_marital_contacts", 78 | "drying_and_tingling_lips", 79 | "slurred_speech", 80 | "knee_pain", 81 | "hip_joint_pain", 82 | "muscle_weakness", 83 | "stiff_neck", 84 | "swelling_joints", 85 | "movement_stiffness", 86 | "spinning_movements", 87 | "loss_of_balance", 88 | "unsteadiness", 89 | "weakness_of_one_body_side", 90 | "loss_of_smell", 91 | "bladder_discomfort", 92 | "foul_smell_of urine", 93 | "continuous_feel_of_urine", 94 | "passage_of_gases", 95 | "internal_itching", 96 | "toxic_look_(typhos)", 97 | "depression", 98 | "irritability", 99 | "muscle_pain", 100 | "altered_sensorium", 101 | "red_spots_over_body", 102 | "belly_pain", 103 | "abnormal_menstruation", 104 | "dischromic _patches", 105 | "watering_from_eyes", 106 | "increased_appetite", 107 | "polyuria", 108 | "family_history", 109 | "mucoid_sputum", 110 | "rusty_sputum", 111 | "lack_of_concentration", 112 | "visual_disturbances", 113 | "receiving_blood_transfusion", 114 | "receiving_unsterile_injections", 115 | "coma", 116 | "stomach_bleeding", 117 | "distention_of_abdomen", 118 | "history_of_alcohol_consumption", 119 | "fluid_overload.1", 120 | "blood_in_sputum", 121 | "prominent_veins_on_calf", 122 | "palpitations", 123 | "painful_walking", 124 | "pus_filled_pimples", 125 | "blackheads", 126 | "scurring", 127 | "skin_peeling", 128 | "silver_like_dusting", 129 | "small_dents_in_nails", 130 | "inflammatory_nails", 131 | "blister", 132 | "red_sore_around_nose", 133 | "yellow_crust_ooze", 134 | ] 135 | -------------------------------------------------------------------------------- /Chapter10/grpc_gateway/grpcserver_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | import warnings 5 | 6 | import grpcserver_pb2 as grpcserver__pb2 7 | 8 | GRPC_GENERATED_VERSION = '1.63.0' 9 | GRPC_VERSION = grpc.__version__ 10 | EXPECTED_ERROR_RELEASE = '1.65.0' 11 | SCHEDULED_RELEASE_DATE = 'June 25, 2024' 12 | _version_not_supported = False 13 | 14 | try: 15 | from grpc._utilities import first_version_is_lower 16 | _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) 17 | except ImportError: 18 | _version_not_supported = True 19 | 20 | if _version_not_supported: 21 | warnings.warn( 22 | f'The grpc package installed is at version {GRPC_VERSION},' 23 | + f' but the generated code in grpcserver_pb2_grpc.py depends on' 24 | + f' grpcio>={GRPC_GENERATED_VERSION}.' 25 | + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' 26 | + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' 27 | + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' 28 | + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', 29 | RuntimeWarning 30 | ) 31 | 32 | 33 | class GrpcServerStub(object): 34 | """Missing associated documentation comment in .proto file.""" 35 | 36 | def __init__(self, channel): 37 | """Constructor. 38 | 39 | Args: 40 | channel: A grpc.Channel. 41 | """ 42 | self.GetServerResponse = channel.unary_unary( 43 | '/GrpcServer/GetServerResponse', 44 | request_serializer=grpcserver__pb2.Message.SerializeToString, 45 | response_deserializer=grpcserver__pb2.MessageResponse.FromString, 46 | _registered_method=True) 47 | 48 | 49 | class GrpcServerServicer(object): 50 | """Missing associated documentation comment in .proto file.""" 51 | 52 | def GetServerResponse(self, request, context): 53 | """Missing associated documentation comment in .proto file.""" 54 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 55 | context.set_details('Method not implemented!') 56 | raise NotImplementedError('Method not implemented!') 57 | 58 | 59 | def add_GrpcServerServicer_to_server(servicer, server): 60 | rpc_method_handlers = { 61 | 'GetServerResponse': grpc.unary_unary_rpc_method_handler( 62 | servicer.GetServerResponse, 63 | request_deserializer=grpcserver__pb2.Message.FromString, 64 | response_serializer=grpcserver__pb2.MessageResponse.SerializeToString, 65 | ), 66 | } 67 | generic_handler = grpc.method_handlers_generic_handler( 68 | 'GrpcServer', rpc_method_handlers) 69 | server.add_generic_rpc_handlers((generic_handler,)) 70 | 71 | 72 | # This class is part of an EXPERIMENTAL API. 73 | class GrpcServer(object): 74 | """Missing associated documentation comment in .proto file.""" 75 | 76 | @staticmethod 77 | def GetServerResponse(request, 78 | target, 79 | options=(), 80 | channel_credentials=None, 81 | call_credentials=None, 82 | insecure=False, 83 | compression=None, 84 | wait_for_ready=None, 85 | timeout=None, 86 | metadata=None): 87 | return grpc.experimental.unary_unary( 88 | request, 89 | target, 90 | '/GrpcServer/GetServerResponse', 91 | grpcserver__pb2.Message.SerializeToString, 92 | grpcserver__pb2.MessageResponse.FromString, 93 | options, 94 | channel_credentials, 95 | insecure, 96 | call_credentials, 97 | compression, 98 | wait_for_ready, 99 | timeout, 100 | metadata, 101 | _registered_method=True) 102 | -------------------------------------------------------------------------------- /Chapter10/ecotech_RAG/docs/faq_ecotech.txt: -------------------------------------------------------------------------------- 1 | Ecotech FAQs 2 | 3 | What various types of electrical goods does EcoTech Electronics offer? 4 | * EcoTech Electronics offers a wide range of electrical goods, including smart home devices, kitchen appliances, home entertainment systems, personal gadgets, and more. 5 | 6 | What is EcoTech Electronics' return policy for electrical goods? 7 | * We accept returns within 30 days of purchase, provided the product is in its original condition and packaging. Please refer to our Returns Policy page for detailed information. 8 | 9 | How can I submit a claim or contact the customer service team at EcoTech Electronics? 10 | * You can reach out to our customer service team by email at support@ecotechelectronics.com or by calling our toll-free number 1-800-ECO-HELP. Our dedicated team is available to assist you with any inquiries or concerns. 11 | 12 | How much does international shipping of electrical goods cost from EcoTech Electronics? 13 | * International shipping costs vary depending on the destination and the size of the order. Shipping rates start at $20 and will be calculated at checkout. 14 | 15 | What are EcoTech Electronics' business hours for brick and mortar stores and online support?** 16 | * Our online store is open 24/7 for your convenience. For our brick and mortar stores, we are open from 9 AM to 8 PM, Monday through Saturday. We are closed on Sundays. 17 | 18 | Are your electrical goods energy-efficient or environmentally friendly? 19 | * Yes, at EcoTech Electronics, we prioritize sustainability and offer a selection of energy-efficient and environmentally friendly products. Look for our eco-friendly label on eligible items. 20 | 21 | Does EcoTech Electronics have a loyalty program for customers? 22 | * Yes! We value our customers' loyalty. Our EcoRewards program allows you to earn points with every purchase, which can be redeemed for discounts on future orders. Sign up today to start earning rewards. 23 | 24 | Do you offer installation services for larger electrical goods like home appliances or home theater systems?** 25 | * Yes, EcoTech Electronics provides professional installation services for a variety of products. Contact our customer service team to schedule an installation appointment and receive a quote. 26 | 27 | What payment methods do you accept for online purchases? 28 | * We accept major credit cards (Visa, Mastercard, American Express), PayPal, and other secure payment methods through our online checkout process. 29 | 30 | Can I track the status of my order? 31 | * Yes, you can track your order status online by logging into your EcoTech Electronics account. Once your order has been shipped, you will receive a tracking number via email to monitor its delivery progress. 32 | 33 | Do you offer extended warranties for electrical goods? 34 | * Yes, EcoTech Electronics offers extended warranty options for many of our products to provide additional peace of mind. Please inquire about warranty extensions at the time of purchase. 35 | 36 | Are there any special promotions or discounts available for first-time customers?** 37 | * Yes, we often run special promotions and discounts for new customers. Be sure to sign up for our newsletter or follow us on social media to stay updated on the latest offers. 38 | 39 | Do you offer repair services for electrical goods that are out of warranty?** 40 | * Yes, EcoTech Electronics provides repair services for a wide range of electrical goods, even if they are no longer under warranty. Contact our customer service team for more information on repair options and pricing. 41 | 42 | Are your electrical goods compatible with smart home systems like Alexa or Google Home?** 43 | * Many of our products are compatible with popular smart home systems. Look for compatibility information in the product descriptions or contact our customer service team for assistance. 44 | 45 | Can I return or exchange an electrical good purchased online in one of your brick and mortar stores?** 46 | * Yes, you can return or exchange online purchases at any of our brick and mortar store locations within the specified return period, as long as the product meets our return policy criteria. 47 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 10 | # for all available tokens 11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 12 | 13 | # sys.path path, will be prepended to sys.path if present. 14 | # defaults to the current working directory. 15 | prepend_sys_path = . 16 | 17 | # timezone to use when rendering the date within the migration file 18 | # as well as the filename. 19 | # If specified, requires the python>=3.9 or backports.zoneinfo library. 20 | # Any required deps can installed by adding `alembic[tz]` to the pip requirements 21 | # string value is passed to ZoneInfo() 22 | # leave blank for localtime 23 | # timezone = 24 | 25 | # max length of characters to apply to the 26 | # "slug" field 27 | # truncate_slug_length = 40 28 | 29 | # set to 'true' to run the environment during 30 | # the 'revision' command, regardless of autogenerate 31 | # revision_environment = false 32 | 33 | # set to 'true' to allow .pyc and .pyo files without 34 | # a source .py file to be detected as revisions in the 35 | # versions/ directory 36 | # sourceless = false 37 | 38 | # version location specification; This defaults 39 | # to alembic/versions. When using multiple version 40 | # directories, initial revisions must be specified with --version-path. 41 | # The path separator used here should be the separator specified by "version_path_separator" below. 42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 43 | 44 | # version path separator; As mentioned above, this is the character used to split 45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 47 | # Valid values for version_path_separator are: 48 | # 49 | # version_path_separator = : 50 | # version_path_separator = ; 51 | # version_path_separator = space 52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 53 | 54 | # set to 'true' to search source files recursively 55 | # in each "version_locations" directory 56 | # new in Alembic version 1.10 57 | # recursive_version_locations = false 58 | 59 | # the output encoding used when revision files 60 | # are written from script.py.mako 61 | # output_encoding = utf-8 62 | 63 | sqlalchemy.url = sqlite:///.database.db 64 | 65 | 66 | [post_write_hooks] 67 | # post_write_hooks defines scripts or Python functions that are run 68 | # on newly generated revision scripts. See the documentation for further 69 | # detail and examples 70 | 71 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 72 | # hooks = black 73 | # black.type = console_scripts 74 | # black.entrypoint = black 75 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 76 | 77 | # lint with attempts to fix using "ruff" - use the exec runner, execute a binary 78 | # hooks = ruff 79 | # ruff.type = exec 80 | # ruff.executable = %(here)s/.venv/bin/ruff 81 | # ruff.options = --fix REVISION_SCRIPT_FILENAME 82 | 83 | # Logging configuration 84 | [loggers] 85 | keys = root,sqlalchemy,alembic 86 | 87 | [handlers] 88 | keys = console 89 | 90 | [formatters] 91 | keys = generic 92 | 93 | [logger_root] 94 | level = WARN 95 | handlers = console 96 | qualname = 97 | 98 | [logger_sqlalchemy] 99 | level = WARN 100 | handlers = 101 | qualname = sqlalchemy.engine 102 | 103 | [logger_alembic] 104 | level = INFO 105 | handlers = 106 | qualname = alembic 107 | 108 | [handler_console] 109 | class = StreamHandler 110 | args = (sys.stderr,) 111 | level = NOTSET 112 | formatter = generic 113 | 114 | [formatter_generic] 115 | format = %(levelname)-5.5s [%(name)s] %(message)s 116 | datefmt = %H:%M:%S 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI Cookbook 2 | 3 | Book Name 4 | 5 | This is the code repository for [FastAPI Cookbook](https://www.packtpub.com/en-us/product/fastapi-cookbook-9781805127857), published by Packt. 6 | 7 | **Develop high-performance APIs and web applications with Python** 8 | 9 | ## What is this book about? 10 | This book helps you unlock the power of FastAPI to build high-performing web apps and APIs by taking you through the basics like routing and data validation through to advanced topics, such as custom middleware and WebSockets. 11 | 12 | This book covers the following exciting features: 13 | * Explore advanced FastAPI functionalities such as dependency injection, custom middleware, and WebSockets 14 | * Discover various types of data storage for powerful app functionality with SQL and NoSQL 15 | * Implement testing and debugging practices for clean, robust code 16 | * Integrate authentication and authorization mechanisms to secure web apps 17 | * Acquire skills to seamlessly migrate existing applications to FastAPI 18 | * Write unit and integration tests, ensuring reliability and security for your apps 19 | * Deploy your FastAPI apps to production environments for real-world use 20 | 21 | If you feel this book is for you, get your [copy](https://www.amazon.com/FastAPI-Cookbook-Develop-high-performance-applications/dp/1805127853) today! 22 | 23 | https://www.packtpub.com/ 25 | 26 | 27 | ## Instructions and Navigations 28 | All of the code is organized into folders. For example, Chapter02. 29 | 30 | The code will look like the following: 31 | ``` 32 | from locust import HttpUser, task 33 | 34 | 35 | class ProtoappUser(HttpUser): 36 | host = "http://localhost:8000" 37 | 38 | @task 39 | def hello_world(self): 40 | self.client.get("/home") 41 | ``` 42 | 43 | **Following is what you need for this book:** 44 | This book is for Python developers looking to enhance their skills to build scalable, high-performance web apps using FastAPI. Professionals seeking practical guidance to create APIs and web apps that can handle significant traffic and scale as needed will also find this book helpful by learning from both foundational insights and advanced techniques. The book is also designed for anyone familiar with RESTful APIs, HTTP protocols, and database systems, as well as developers looking to migrate existing applications to FastAPI or explore its advanced features. 45 | 46 | With the following software and hardware list you can run all code files present in the book (Chapter 1-12). 47 | 48 | ### Software and Hardware List 49 | 50 | | Chapter | Software required | OS required | 51 | | -------- | ------------------------------------| -----------------------------------| 52 | | 1-12 | Python 3.9 or higher | Windows, Mac OS X, and Linux (Any) | 53 | 54 | 55 | ### Related products 56 | * Hands-On Microservices with Django [[Packt]](https://www.packtpub.com/en-in/product/hands-on-microservices-with-django-9781835468524) [[Amazon]](https://www.amazon.com/Hands-Microservices-Django-cloud-native-applications/dp/1835468527) 57 | 58 | * Node.js for Beginners [[Packt]](https://www.packtpub.com/en-us/product/nodejs-for-beginners-9781803245171) [[Amazon]](https://www.amazon.com/Node-js-Beginners-comprehensive-full-featured-applications/dp/1803245174) 59 | 60 | ## Get to Know the Author 61 | **Giunio De Luca** is a software engineer with over 10 years of experience in fields such as physics, sports, and administration. He graduated in industrial engineering from the University of Basilicata and holds a PhD in numerical simulations from Paris-Saclay University. His work spans developing advanced algorithms, creating sports analytics applications, and improving administrative processes. As an independent consultant, he collaborates with research labs, government agencies, and start-ups across Europe. He also supports coding education in schools and universities through workshops, lectures, and mentorship programs, inspiring the next generation of software engineers with his expertise and dedication. 62 | -------------------------------------------------------------------------------- /Chapter03/task_manager_app/main.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import Depends, FastAPI, HTTPException 4 | from fastapi.openapi.utils import get_openapi 5 | from fastapi.security import OAuth2PasswordRequestForm 6 | from pydantic import BaseModel 7 | 8 | from models import Task, TaskV2WithID, TaskWithID 9 | from operations import ( 10 | create_task, 11 | modify_task, 12 | read_all_tasks, 13 | read_all_tasks_v2, 14 | read_task, 15 | remove_task, 16 | ) 17 | from security import ( 18 | User, 19 | UserInDB, 20 | fake_token_generator, 21 | fake_users_db, 22 | fakely_hash_password, 23 | get_user_from_token, 24 | ) 25 | 26 | 27 | def custom_openapi(): 28 | if app.openapi_schema: 29 | return app.openapi_schema 30 | openapi_schema = get_openapi( 31 | title="Customized Title", 32 | version="2.0.0", 33 | description="This is a custom OpenAPI schema", 34 | routes=app.routes, 35 | ) 36 | del openapi_schema["paths"]["/token"] 37 | app.openapi_schema = openapi_schema 38 | return app.openapi_schema 39 | 40 | 41 | app = FastAPI( 42 | title="Task Manager API", 43 | description="This is a task management API", 44 | version="0.1.0", 45 | ) 46 | 47 | app.openapi = custom_openapi 48 | 49 | 50 | @app.get("/tasks", response_model=list[TaskWithID]) 51 | def get_tasks( 52 | status: Optional[str] = None, 53 | title: Optional[str] = None, 54 | ): 55 | tasks = read_all_tasks() 56 | if status: 57 | tasks = [ 58 | task 59 | for task in tasks 60 | if task.status == status 61 | ] 62 | if title: 63 | tasks = [ 64 | task 65 | for task in tasks 66 | if task.title == title 67 | ] 68 | return tasks 69 | 70 | 71 | @app.get("/task/{task_id}") 72 | def get_task(task_id: int): 73 | task = read_task(task_id) 74 | if not task: 75 | raise HTTPException( 76 | status_code=404, detail="task not found" 77 | ) 78 | return task 79 | 80 | 81 | @app.post("/task", response_model=TaskWithID) 82 | def add_task(task: Task): 83 | return create_task(task) 84 | 85 | 86 | class UpdateTask(BaseModel): 87 | title: str | None = None 88 | description: str | None = None 89 | status: str | None = None 90 | 91 | 92 | @app.put("/task/{task_id}", response_model=TaskWithID) 93 | def update_task(task_id: int, task_update: UpdateTask): 94 | modified = modify_task( 95 | task_id, 96 | task_update.model_dump(exclude_unset=True), 97 | ) 98 | if not modified: 99 | raise HTTPException( 100 | status_code=404, detail="task not found" 101 | ) 102 | 103 | return modified 104 | 105 | 106 | @app.delete("/task/{task_id}", response_model=Task) 107 | def delete_task(task_id: int): 108 | removed_task = remove_task(task_id) 109 | if not removed_task: 110 | raise HTTPException( 111 | status_code=404, detail="task not found" 112 | ) 113 | return removed_task 114 | 115 | 116 | @app.get( 117 | "/tasks/search", response_model=list[TaskWithID] 118 | ) 119 | def search_tasks(keyword: str): 120 | tasks = read_all_tasks() 121 | filtered_tasks = [ 122 | task 123 | for task in tasks 124 | if keyword.lower() 125 | in (task.title + task.description).lower() 126 | ] 127 | return filtered_tasks 128 | 129 | 130 | @app.get("/v2/tasks", response_model=list[TaskV2WithID]) 131 | def get_tasks_v2(): 132 | tasks = read_all_tasks_v2() 133 | return tasks 134 | 135 | 136 | @app.post("/token") 137 | async def login( 138 | form_data: OAuth2PasswordRequestForm = Depends(), 139 | ): 140 | user_dict = fake_users_db.get(form_data.username) 141 | if not user_dict: 142 | raise HTTPException( 143 | status_code=400, 144 | detail="Incorrect username or password", 145 | ) 146 | user = UserInDB(**user_dict) 147 | hashed_password = fakely_hash_password( 148 | form_data.password 149 | ) 150 | if not hashed_password == user.hashed_password: 151 | raise HTTPException( 152 | status_code=400, 153 | detail="Incorrect username or password", 154 | ) 155 | 156 | token = fake_token_generator(user) 157 | 158 | return { 159 | "access_token": token, 160 | "token_type": "bearer", 161 | } 162 | 163 | 164 | @app.get("/users/me", response_model=User) 165 | def read_users_me( 166 | current_user: User = Depends(get_user_from_token), 167 | ): 168 | return current_user 169 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | from sqlalchemy.ext.asyncio import ( 4 | AsyncSession, 5 | create_async_engine, 6 | ) 7 | from sqlalchemy.orm import sessionmaker 8 | 9 | from app.database import ( 10 | Base, 11 | Event, 12 | Sponsor, 13 | Sponsorship, 14 | Ticket, 15 | TicketDetails, 16 | ) 17 | from app.db_connection import get_db_session 18 | from app.main import app 19 | 20 | 21 | @pytest.fixture 22 | def db_engine_test(): 23 | engine = create_async_engine( 24 | "sqlite+aiosqlite:///:memory:" 25 | ) 26 | return engine 27 | 28 | 29 | @pytest.fixture 30 | async def db_session_test( 31 | db_engine_test, 32 | ): 33 | TestingAsynSessionLocal = sessionmaker( 34 | bind=db_engine_test, class_=AsyncSession 35 | ) 36 | async with db_engine_test.begin() as conn: 37 | await conn.run_sync(Base.metadata.drop_all) 38 | await conn.run_sync(Base.metadata.create_all) 39 | 40 | async with TestingAsynSessionLocal() as session: 41 | yield session 42 | 43 | await conn.run_sync(Base.metadata.drop_all) 44 | await db_engine_test.dispose() 45 | 46 | 47 | @pytest.fixture 48 | async def second_session_test( 49 | db_engine_test, 50 | ): 51 | TestingAsynSessionLocal = sessionmaker( 52 | bind=db_engine_test, class_=AsyncSession 53 | ) 54 | async with TestingAsynSessionLocal() as session: 55 | yield session 56 | 57 | 58 | @pytest.fixture 59 | async def third_session_test( 60 | db_engine_test, 61 | ): 62 | TestingAsynSessionLocal = sessionmaker( 63 | bind=db_engine_test, class_=AsyncSession 64 | ) 65 | async with TestingAsynSessionLocal() as session: 66 | yield session 67 | 68 | 69 | @pytest.fixture 70 | async def fourth_session_test( 71 | db_engine_test, 72 | ): 73 | TestingAsynSessionLocal = sessionmaker( 74 | bind=db_engine_test, class_=AsyncSession 75 | ) 76 | async with TestingAsynSessionLocal() as session: 77 | yield session 78 | 79 | 80 | @pytest.fixture 81 | async def fill_database_with_tickets(db_session_test): 82 | tickets = [ 83 | Ticket(show="The Rolling Stones Concert") 84 | for _ in range(10) 85 | ] 86 | async with db_session_test.begin(): 87 | db_session_test.add_all(tickets) 88 | await db_session_test.commit() 89 | 90 | 91 | @pytest.fixture 92 | async def add_special_ticket(db_session_test): 93 | ticket = Ticket( 94 | id=1234, 95 | show="Special Show", 96 | details=TicketDetails(), 97 | ) 98 | async with db_session_test.begin(): 99 | db_session_test.add(ticket) 100 | await db_session_test.commit() 101 | 102 | 103 | @pytest.fixture 104 | async def add_special_sold_ticket(db_session_test): 105 | ticket = Ticket( 106 | id=1234, 107 | show="Special Show", 108 | details=TicketDetails(), 109 | sold=True, 110 | user="John Doe", 111 | ) 112 | async with db_session_test.begin(): 113 | db_session_test.add(ticket) 114 | await db_session_test.commit() 115 | 116 | 117 | @pytest.fixture 118 | def test_client(db_session_test): 119 | client = TestClient(app=app) 120 | app.dependency_overrides[get_db_session] = ( 121 | lambda: db_session_test 122 | ) 123 | return client 124 | 125 | 126 | @pytest.fixture 127 | async def add_event_and_sponsor(db_session_test): 128 | event = Event(name="The Rolling Stones Concert") 129 | sponsor = Sponsor(name="Live Nation") 130 | async with db_session_test.begin(): 131 | db_session_test.add(event) 132 | db_session_test.add(sponsor) 133 | await db_session_test.commit() 134 | 135 | 136 | @pytest.fixture 137 | async def add_event_with_tickets(db_session_test): 138 | event = Event(name="The Rolling Stones Concert") 139 | users = [ 140 | "John Doe", 141 | "Jane Doe", 142 | "Foo Bar", 143 | "Bar Foo", 144 | ] 145 | tickets = [ 146 | Ticket( 147 | show="The Rolling Stones Concert", 148 | event_id=1, 149 | price=100, 150 | user=users[j] if j < 3 else None, 151 | sold=True if j < 3 else False, 152 | ) 153 | for j in range(10) 154 | ] 155 | async with db_session_test.begin(): 156 | db_session_test.add(event) 157 | await db_session_test.flush() 158 | db_session_test.add_all(tickets) 159 | await db_session_test.commit() 160 | 161 | 162 | @pytest.fixture 163 | async def add_event_and_sponsor_and_sponsorship( 164 | db_session_test, add_event_and_sponsor 165 | ): 166 | sponsorship = Sponsorship( 167 | event_id=1, sponsor_id=1, amount=10 168 | ) 169 | async with db_session_test.begin(): 170 | db_session_test.add(sponsorship) 171 | await db_session_test.commit() 172 | 173 | 174 | @pytest.fixture 175 | async def add_sponsors_for_event( 176 | db_session_test, 177 | ): 178 | event = Event(name="The Rolling Stones Concert") 179 | sponsors = [ 180 | Sponsor(name="Live Nation"), 181 | Sponsor(name="Ticketmaster"), 182 | Sponsor(name="Spotify"), 183 | ] 184 | 185 | async with db_session_test.begin(): 186 | db_session_test.add(event) 187 | db_session_test.add_all(sponsors) 188 | await db_session_test.flush() 189 | for k, sponsor in enumerate(sponsors): 190 | db_session_test.add( 191 | Sponsorship( 192 | event_id=event.id, 193 | sponsor_id=sponsor.id, 194 | amount=10 + k * 20, 195 | ) 196 | ) 197 | 198 | await db_session_test.commit() 199 | -------------------------------------------------------------------------------- /Chapter06/ticketing_system/app/main.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import Annotated 3 | 4 | from fastapi import Depends, FastAPI, HTTPException 5 | from pydantic import BaseModel, Field 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | 8 | 9 | from app.database import Base 10 | from app.db_connection import get_db_session, get_engine 11 | from app.operations import ( 12 | add_sponsor_to_event, 13 | create_event, 14 | create_sponsor, 15 | create_ticket, 16 | delete_ticket, 17 | get_all_tickets_for_show, 18 | get_ticket, 19 | update_ticket, 20 | update_ticket_price, 21 | ) 22 | 23 | 24 | @asynccontextmanager 25 | async def lifespan(app: FastAPI): 26 | engine = get_engine() 27 | async with engine.begin() as conn: 28 | await conn.run_sync(Base.metadata.create_all) 29 | yield 30 | await engine.dispose() 31 | 32 | 33 | app = FastAPI(lifespan=lifespan) 34 | 35 | 36 | @app.get("/ticket/{ticket_id}") 37 | async def read_ticket( 38 | db_session: Annotated[ 39 | AsyncSession, Depends(get_db_session) 40 | ], 41 | ticket_id: int, 42 | ): 43 | ticket = await get_ticket(db_session, ticket_id) 44 | if ticket is None: 45 | raise HTTPException( 46 | status_code=404, detail="Ticket not found" 47 | ) 48 | return ticket 49 | 50 | 51 | class TicketRequest(BaseModel): 52 | price: float | None 53 | show: str | None 54 | user: str | None = None 55 | 56 | 57 | @app.post("/ticket", response_model=dict[str, int]) 58 | async def create_ticket_route( 59 | db_session: Annotated[ 60 | AsyncSession, Depends(get_db_session) 61 | ], 62 | ticket: TicketRequest, 63 | ): 64 | ticket_id = await create_ticket( 65 | db_session, 66 | ticket.show, 67 | ticket.user, 68 | ticket.price, 69 | ) 70 | return {"ticket_id": ticket_id} 71 | 72 | 73 | class TicketDetailsUpateRequest(BaseModel): 74 | seat: str | None = None 75 | ticket_type: str | None = None 76 | 77 | 78 | class TicketUpdateRequest(BaseModel): 79 | price: float | None = Field(None, ge=0) 80 | 81 | 82 | @app.put("/ticket/{ticket_id}") 83 | async def update_ticket_route( 84 | ticket_id: int, 85 | ticket_update: TicketUpdateRequest, 86 | db_session: Annotated[ 87 | AsyncSession, Depends(get_db_session) 88 | ], 89 | ): 90 | update_dict_args = ticket_update.model_dump( 91 | exclude_unset=True 92 | ) 93 | 94 | updated = await update_ticket( 95 | db_session, ticket_id, update_dict_args 96 | ) 97 | if not updated: 98 | raise HTTPException( 99 | status_code=404, detail="Ticket not found" 100 | ) 101 | return {"detail": "Price updated"} 102 | 103 | 104 | @app.put("/ticket/{ticket_id}/price/{new_price}") 105 | async def update_ticket_price_route( 106 | db_session: Annotated[ 107 | AsyncSession, Depends(get_db_session) 108 | ], 109 | ticket_id: int, 110 | new_price: float, 111 | ): 112 | updated = await update_ticket_price( 113 | db_session, ticket_id, new_price 114 | ) 115 | if not updated: 116 | raise HTTPException( 117 | status_code=404, detail="Ticket not found" 118 | ) 119 | return {"detail": "Price updated"} 120 | 121 | 122 | @app.delete("/ticket/{ticket_id}") 123 | async def delete_ticket_route( 124 | db_session: Annotated[ 125 | AsyncSession, Depends(get_db_session) 126 | ], 127 | ticket_id: int, 128 | ): 129 | ticket = await delete_ticket(db_session, ticket_id) 130 | if not ticket: 131 | raise HTTPException( 132 | status_code=404, detail="Ticket not found" 133 | ) 134 | return {"detail": "Ticket removed"} 135 | 136 | 137 | class TicketResponse(TicketRequest): 138 | id: int 139 | 140 | 141 | @app.get( 142 | "/tickets/{show}", 143 | response_model=list[TicketResponse], 144 | ) 145 | async def get_tickets_for_show( 146 | db_session: Annotated[ 147 | AsyncSession, Depends(get_db_session) 148 | ], 149 | show: str, 150 | ): 151 | tickets = await get_all_tickets_for_show( 152 | db_session, show 153 | ) 154 | return tickets 155 | 156 | 157 | @app.post("/event", response_model=dict[str, int]) 158 | async def create_event_route( 159 | db_session: Annotated[ 160 | AsyncSession, Depends(get_db_session) 161 | ], 162 | event_name: str, 163 | nb_tickets: int | None = 0, 164 | ): 165 | event_id = await create_event( 166 | db_session, event_name, nb_tickets 167 | ) 168 | return {"event_id": event_id} 169 | 170 | 171 | @app.post( 172 | "/sponsor/{sponsor_name}", 173 | response_model=dict[str, int], 174 | responses={ 175 | 200: { 176 | "description": "Successful Response", 177 | "content": { 178 | "application/json": { 179 | "example": {"sponsor_id": 12345} 180 | } 181 | }, 182 | } 183 | }, 184 | ) 185 | async def register_sponsor( 186 | db_session: Annotated[ 187 | AsyncSession, Depends(get_db_session) 188 | ], 189 | sponsor_name: str, 190 | ): 191 | sponsor_id = await create_sponsor( 192 | db_session, sponsor_name 193 | ) 194 | if not sponsor_id: 195 | raise HTTPException( 196 | status_code=400, 197 | detail="Sponsor not created", 198 | ) 199 | 200 | return {"sponsor_id": sponsor_id} 201 | 202 | 203 | @app.post("/event/{event_id}/sponsor/{sponsor_id}") 204 | async def register_sponsor_amount_contribution( 205 | db_session: Annotated[ 206 | AsyncSession, Depends(get_db_session) 207 | ], 208 | sponsor_id: int, 209 | event_id: int, 210 | amount: float | None = 0, 211 | ): 212 | registered = await add_sponsor_to_event( 213 | db_session, event_id, sponsor_id, amount 214 | ) 215 | if not registered: 216 | raise HTTPException( 217 | status_code=400, 218 | detail="Contribution not registered", 219 | ) 220 | 221 | return {"detail": "Contribution registered"} 222 | -------------------------------------------------------------------------------- /Chapter07/streaming_platform/app/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from asyncio import gather 3 | from contextlib import asynccontextmanager 4 | 5 | from bson import ObjectId 6 | from fastapi import ( 7 | Body, 8 | Depends, 9 | FastAPI, 10 | HTTPException, 11 | ) 12 | from fastapi.encoders import ENCODERS_BY_TYPE 13 | from fastapi_cache import FastAPICache 14 | from fastapi_cache.backends.redis import RedisBackend 15 | from pydantic import BaseModel 16 | 17 | from app import main_search, third_party_endpoint 18 | from app.database import mongo_database 19 | from app.db_connection import ( 20 | ping_elasticsearch_server, 21 | ping_mongo_db_server, 22 | ping_redis_server, 23 | redis_client, 24 | ) 25 | 26 | logger = logging.getLogger("uvicorn") 27 | 28 | ENCODERS_BY_TYPE[ObjectId] = str 29 | 30 | 31 | @asynccontextmanager 32 | async def lifespan(app: FastAPI): 33 | await gather( 34 | ping_mongo_db_server(), 35 | ping_elasticsearch_server(), 36 | ping_redis_server(), 37 | ) 38 | 39 | db = mongo_database() 40 | await db.songs.drop_indexes() 41 | await db.songs.create_index( 42 | {"album.release_year": -1} 43 | ) 44 | await db.songs.create_index({"artist": "text"}) 45 | 46 | FastAPICache.init( 47 | RedisBackend(redis_client), 48 | prefix="fastapi-cache", 49 | ) 50 | 51 | yield 52 | 53 | 54 | app = FastAPI(lifespan=lifespan) 55 | app.include_router(third_party_endpoint.router) 56 | 57 | 58 | try: 59 | app.include_router(main_search.router) 60 | except Exception: 61 | pass 62 | 63 | 64 | @app.post("/song") 65 | async def add_song( 66 | song: dict = Body( 67 | example={ 68 | "title": "My Song", 69 | "artist": "My Artist", 70 | "genre": "My Genre", 71 | }, 72 | ), 73 | mongo_db=Depends(mongo_database), 74 | ): 75 | await mongo_db.songs.insert_one(song) 76 | 77 | return { 78 | "message": "Song added successfully", 79 | "id": song.get("_id"), 80 | } 81 | 82 | 83 | @app.get("/song/{song_id}") 84 | async def get_song( 85 | song_id: str, 86 | db=Depends(mongo_database), 87 | ): 88 | song = await db.songs.find_one( 89 | { 90 | "_id": ObjectId(song_id) 91 | if ObjectId.is_valid(song_id) 92 | else None 93 | } 94 | ) 95 | if not song: 96 | raise HTTPException( 97 | status_code=404, detail="Song not found" 98 | ) 99 | song.pop("album", None) 100 | return song 101 | 102 | 103 | @app.get("/songs") 104 | async def get_songs( 105 | db=Depends(mongo_database), 106 | ): 107 | songs = await db.songs.find().to_list(None) 108 | return songs 109 | 110 | 111 | @app.put("/song/{song_id}") 112 | async def update_song( 113 | song_id: str, 114 | updated_song: dict, 115 | db=Depends(mongo_database), 116 | ): 117 | result = await db.songs.update_one( 118 | { 119 | "_id": ObjectId(song_id) 120 | if ObjectId.is_valid(song_id) 121 | else None 122 | }, 123 | {"$set": updated_song}, 124 | ) 125 | if result.modified_count == 1: 126 | return {"message": "Song updated successfully"} 127 | 128 | raise HTTPException( 129 | status_code=404, detail="Song not found" 130 | ) 131 | 132 | 133 | @app.delete("/song/{song_id}") 134 | async def delete_song( 135 | song_id: str, 136 | db=Depends(mongo_database), 137 | ): 138 | result = await db.songs.delete_one( 139 | { 140 | "_id": ObjectId(song_id) 141 | if ObjectId.is_valid(song_id) 142 | else None 143 | } 144 | ) 145 | if result.deleted_count == 1: 146 | return {"message": "Song deleted successfully"} 147 | 148 | raise HTTPException( 149 | status_code=404, detail="Song not found" 150 | ) 151 | 152 | 153 | class Playlist(BaseModel): 154 | name: str 155 | songs: list[str] = [] 156 | 157 | 158 | @app.post("/playlist") 159 | async def create_playlist( 160 | playlist: Playlist = Body( 161 | example={ 162 | "name": "My Playlist", 163 | "songs": ["song_id"], 164 | } 165 | ), 166 | db=Depends(mongo_database), 167 | ): 168 | result = await db.playlists.insert_one( 169 | playlist.model_dump() 170 | ) 171 | return { 172 | "message": "Playlist created successfully", 173 | "id": str(result.inserted_id), 174 | } 175 | 176 | 177 | @app.get("/playlist/{playlist_id}") 178 | async def get_playlist( 179 | playlist_id: str, 180 | db=Depends(mongo_database), 181 | ): 182 | playlist = await db.playlists.find_one( 183 | { 184 | "_id": ObjectId(playlist_id) 185 | if ObjectId.is_valid(playlist_id) 186 | else None 187 | } 188 | ) 189 | if not playlist: 190 | raise HTTPException( 191 | status_code=404, detail="Playlist not found" 192 | ) 193 | 194 | songs = await db.songs.find( 195 | { 196 | "_id": { 197 | "$in": [ 198 | ObjectId(song_id) 199 | for song_id in playlist["songs"] 200 | ] 201 | } 202 | } 203 | ).to_list(None) 204 | 205 | return {"name": playlist["name"], "songs": songs} 206 | 207 | 208 | @app.get("/songs/year") 209 | async def get_songs_by_released_year( 210 | year: int, 211 | db=Depends(mongo_database), 212 | ): 213 | query = db.songs.find({"album.release_year": year}) 214 | explained_query = await query.explain() 215 | logger.info( 216 | "Index used: %s", 217 | explained_query.get("queryPlanner", {}) 218 | .get("winningPlan", {}) 219 | .get("inputStage", {}) 220 | .get("indexName", "No index used"), 221 | ) 222 | 223 | songs = await query.to_list(None) 224 | return songs 225 | 226 | 227 | @app.get("/songs/artist") 228 | async def get_songs_by_artist( 229 | artist: str, db=Depends(mongo_database) 230 | ): 231 | query = db.songs.find( 232 | {"$text": {"$search": artist}} 233 | ) 234 | explained_query = await query.explain() 235 | logger.info( 236 | "Index used: %s", 237 | explained_query.get("queryPlanner", {}) 238 | .get("winningPlan", {}) 239 | .get("indexName", "No index used"), 240 | ) 241 | 242 | songs = await query.to_list(None) 243 | return songs 244 | --------------------------------------------------------------------------------