├── fastapi_project ├── __init__.py ├── .env.example ├── serializers │ └── user_serializer.py ├── api │ ├── health.py │ ├── root_index.py │ └── v1 │ │ └── user.py ├── schemas │ └── user_schema.py ├── models │ └── users.py ├── database │ └── database.py ├── services │ └── user_service.py ├── main.py └── utils │ └── base.py ├── run_fastapi_project.sh ├── requirements.txt ├── host.json ├── .funcignore ├── function_app.py ├── README.md └── .gitignore /fastapi_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /run_fastapi_project.sh: -------------------------------------------------------------------------------- 1 | uvicorn "fastapi_project.main:app" --reload --port=8000 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | azure-functions 2 | fastapi 3 | python-dotenv 4 | sqlalchemy 5 | psycopg2-binary -------------------------------------------------------------------------------- /fastapi_project/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL='postgresql://username:password@hostname.com/db_name' 2 | LOAD_SQL_PROJECT=yes -------------------------------------------------------------------------------- /host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "http": { 5 | "routePrefix": "" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | __azurite_db*__.json 4 | __blobstorage__ 5 | __queuestorage__ 6 | local.settings.json 7 | test 8 | .venv 9 | .azure_function_fastapi_venv -------------------------------------------------------------------------------- /function_app.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | from fastapi_project.main import app as fastapi_app 4 | 5 | app = func.AsgiFunctionApp(app=fastapi_app, http_auth_level=func.AuthLevel.ANONYMOUS) -------------------------------------------------------------------------------- /fastapi_project/serializers/user_serializer.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | 3 | class UserSerializer(BaseModel): 4 | id: int 5 | name: str 6 | email: EmailStr 7 | 8 | class Config: 9 | from_orm = True 10 | from_attributes=True -------------------------------------------------------------------------------- /fastapi_project/api/health.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, status 2 | from fastapi.responses import JSONResponse 3 | 4 | # Create a api router 5 | router = APIRouter() 6 | 7 | # Health check route 8 | @router.get("/") 9 | async def health_check(): 10 | data = {"status": "ok"} 11 | return JSONResponse(content=data, status_code=status.HTTP_200_OK) -------------------------------------------------------------------------------- /fastapi_project/api/root_index.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, status, Request 2 | from fastapi.responses import JSONResponse 3 | 4 | # Create a FastAPI app 5 | router = APIRouter() 6 | 7 | # root index 8 | @router.get("/") 9 | async def root_index(request: Request): 10 | data = { 11 | 'message': 'azure function project is running...' 12 | } 13 | return JSONResponse(content=data, status_code=status.HTTP_200_OK) 14 | -------------------------------------------------------------------------------- /fastapi_project/schemas/user_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | from typing import Optional 3 | 4 | # Pydantic model for creating a new user 5 | class UserCreateSchema(BaseModel): 6 | name: str 7 | email: EmailStr 8 | password: str 9 | 10 | 11 | # Pydantic model for updating user data 12 | class UserUpdateSchema(BaseModel): 13 | name: Optional[str] = None 14 | email: Optional[EmailStr] = None 15 | password: Optional[str] = None 16 | -------------------------------------------------------------------------------- /fastapi_project/models/users.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String 2 | from fastapi_project.database.database import Base 3 | 4 | class User(Base): 5 | __tablename__ = "users" 6 | id = Column(Integer, primary_key=True, index=True) 7 | name = Column(String, index=True) 8 | email = Column(String, unique=True, index=True, nullable=False) 9 | password = Column(String, nullable=False) 10 | 11 | def as_dict(self): 12 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} -------------------------------------------------------------------------------- /fastapi_project/database/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | from dotenv import load_dotenv 5 | import os 6 | 7 | load_dotenv() 8 | 9 | SQLALCHEMY_DATABASE_URL = os.getenv('DATABASE_URL') 10 | 11 | # Create the SQLAlchemy engine 12 | engine = create_engine(SQLALCHEMY_DATABASE_URL) 13 | 14 | # Create a session maker bound to the engine 15 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 16 | 17 | # Declarative base class for database models 18 | Base = declarative_base() -------------------------------------------------------------------------------- /fastapi_project/api/v1/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, status 2 | from fastapi.responses import JSONResponse 3 | from fastapi.encoders import jsonable_encoder 4 | from fastapi_project.utils.base import the_query 5 | from fastapi_project.services.user_service import UserService 6 | from fastapi_project.schemas.user_schema import UserCreateSchema 7 | 8 | router = APIRouter() 9 | 10 | user_service = UserService() 11 | 12 | @router.post("/users/create") 13 | async def create_order(request: Request, the_data: UserCreateSchema): 14 | # Retrieve data from the request 15 | request_data = await the_query(request) 16 | data = UserCreateSchema(**request_data) 17 | 18 | output = user_service.s_create_user(data) 19 | return JSONResponse(content=output, status_code=status.HTTP_200_OK) 20 | 21 | @router.get("/users/") 22 | async def get_users(request: Request): 23 | data = user_service.s_get_users(request) 24 | return JSONResponse(content=jsonable_encoder(data), status_code=status.HTTP_200_OK) 25 | 26 | @router.get("/users/{id}") 27 | async def get_user(request: Request, id: int): 28 | data = user_service.s_get_user_by_id(user_id=id) 29 | return JSONResponse(content=jsonable_encoder(data), status_code=status.HTTP_200_OK) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # azure-function-fastapi 2 | Deploy Python FastAPI serverless application on Azure Functions. 3 | 4 | * Good project structure 5 | 6 | # local development (fastapi) 7 | 8 | ## venv 9 | ```bash 10 | # create virtual environment 11 | python3 -m venv .azure_function_fastapi_venv 12 | 13 | # activate virtual environment 14 | source .azure_function_fastapi_venv/bin/activate 15 | ``` 16 | 17 | ## run fast api project 18 | 19 | ```bash 20 | # install packages 21 | pip3 install -r requirements.txt 22 | 23 | # run project with uvicorn 24 | uvicorn "fastapi_project.main:app" --reload --port=8000 25 | 26 | # or, run bash with shell 27 | ./run_fastapi_project.sh 28 | ``` 29 | 30 | # deploy on production 31 | 32 | ## vscode extension 33 | Screenshot 2024-05-19 at 5 50 24 PM 34 | 35 | * [Azure Function Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) 36 | 37 | ## sql project enable 38 | 39 | set the environment variable azure (Settings >> Environment variables (app settings)) 40 | * `DATABASE_URL` 41 | * `LOAD_SQL_PROJECT` value will be `yes` 42 | 43 | ## Project route 44 | 45 | ```bash 46 | http://localhost:8000/api/v1/users 47 | http://localhost:8000/health 48 | ``` -------------------------------------------------------------------------------- /fastapi_project/services/user_service.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | from fastapi_project.database.database import SessionLocal 3 | from fastapi_project.utils.base import the_sorting, paginate 4 | from fastapi_project.models.users import User 5 | from fastapi_project.schemas.user_schema import UserCreateSchema 6 | from fastapi_project.serializers.user_serializer import UserSerializer 7 | 8 | class UserService: 9 | def __init__(self): 10 | self.db = SessionLocal() 11 | 12 | def s_create_user(self, user: UserCreateSchema): 13 | db_user = self.db.query(User).filter(User.email == user.email).first() 14 | if db_user: 15 | raise HTTPException(status_code=400, detail="Email already registered") 16 | new_user = User(**user.model_dump()) 17 | self.db.add(new_user) 18 | self.db.commit() 19 | self.db.refresh(new_user) 20 | return new_user 21 | 22 | def s_get_users(self, request): 23 | users = self.db.query(User) 24 | users = the_sorting(request, users) 25 | return paginate(request, users, serilizer=UserSerializer, wrap='users') 26 | 27 | def s_get_user_by_id(self, user_id): 28 | user = self.db.query(User).filter(User.id == user_id).first() 29 | if user is None: 30 | raise HTTPException(status_code=404, detail="User not found") 31 | return user 32 | -------------------------------------------------------------------------------- /fastapi_project/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fastapi import FastAPI 3 | from dotenv import load_dotenv 4 | from fastapi.middleware.cors import CORSMiddleware 5 | from fastapi_project.api import health, root_index 6 | 7 | # Load .env file 8 | load_dotenv() 9 | 10 | # Get the value of LOAD_SQL_PROJECT environment variable 11 | load_sql_project = os.getenv('LOAD_SQL_PROJECT', 'false').lower() in ('true', '1', 't', 'y', 'yes') 12 | #load_sql_project = os.getenv('LOAD_SQL_PROJECT', 'true').lower() in ('true', '1', 't', 'y', 'yes') 13 | 14 | def create_application(): 15 | application = FastAPI() 16 | 17 | # Include the root index and health router 18 | application.include_router(root_index.router) 19 | application.include_router(health.router, prefix="/health") 20 | 21 | 22 | if load_sql_project == True: 23 | print("SQL_PROJECT is enabled") 24 | # Include additional routers if LOAD_SQL_PROJECT is enabled 25 | from fastapi_project.api.v1 import user 26 | application.include_router(user.router, prefix="/api/v1") 27 | 28 | # Add CORS middleware 29 | # In production, replace the "*" with the actual frontend URL 30 | application.add_middleware( 31 | CORSMiddleware, 32 | allow_origins=["*"], # Replace ["*"] with your frontend URL in production 33 | allow_credentials=True, 34 | allow_methods=["GET", "POST", "PUT", "DELETE"], 35 | allow_headers=["*"], 36 | ) 37 | 38 | return application 39 | 40 | # Create the FastAPI application 41 | app = create_application() 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | .azure_function_fastapi_venv 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | # Azure Functions artifacts 127 | bin 128 | obj 129 | appsettings.json 130 | local.settings.json 131 | 132 | # Azurite artifacts 133 | __blobstorage__ 134 | __queuestorage__ 135 | __azurite_db*__.json 136 | .python_packages -------------------------------------------------------------------------------- /fastapi_project/utils/base.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from typing import Any, Dict, Optional, Union 3 | from pydantic import BaseModel, ValidationError 4 | from sqlalchemy import desc 5 | from urllib.parse import urlparse 6 | 7 | async def the_query(request: Request, name = None) -> Dict[str, str]: 8 | data = {} 9 | 10 | if request.query_params: 11 | data = request.query_params 12 | elif request.headers.get("Content-Type") == "application/json": 13 | data = await request.json() 14 | else: 15 | data = await request.form() 16 | 17 | if name: 18 | return data.get(name) 19 | else: 20 | return data 21 | 22 | 23 | async def validate_data(data: Dict[str, Any], model: BaseModel) -> Dict[str, Union[str, Dict[str, Any]]]: 24 | output = {'status': 'valid'} 25 | 26 | try: 27 | instance = model(**data) 28 | output['data'] = instance.dict() 29 | except ValidationError as e: 30 | # If validation fails, return status as invalid and the validation errors 31 | output['status'] = 'invalid' 32 | output['errors'] = e.errors() 33 | 34 | return output 35 | 36 | 37 | def the_sorting(request, query): 38 | sort_params = request.query_params.get("sort") 39 | 40 | if sort_params: 41 | sort_fields = sort_params.split(",") 42 | ordering = [] 43 | for field in sort_fields: 44 | if field.startswith("-"): 45 | ordering.append(desc(field[1:])) 46 | else: 47 | ordering.append(field) 48 | query = query.order_by(*ordering) 49 | 50 | return query 51 | 52 | def app_path(path_name = None): 53 | from pathlib import Path 54 | the_path = str(Path(__file__).parent.parent) 55 | 56 | if path_name: 57 | the_path = f'{the_path}/{path_name}' 58 | 59 | return the_path 60 | 61 | 62 | def paginate(request: Request, query, serilizer, the_page: int = 1, the_per_page: int = 10, wrap='data'): 63 | """Paginate the query.""" 64 | 65 | page = int(request.query_params.get('page', the_page)) 66 | per_page = int(request.query_params.get('per_page', the_per_page)) 67 | 68 | total = query.count() 69 | last_page = (total + per_page - 1) // per_page 70 | offset = (page - 1) * per_page 71 | paginated_query = query.offset(offset).limit(per_page).all() 72 | 73 | data = [serilizer.from_orm(item) for item in paginated_query] 74 | 75 | base_url = str(request.base_url) 76 | 77 | full_path = str(request.url) 78 | parsed_url = urlparse(full_path) 79 | path_without_query = f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}" 80 | 81 | first_page_url = f"{path_without_query}?page=1&per_page={per_page}" 82 | last_page_url = f"{path_without_query}?page={last_page}&per_page={per_page}" 83 | next_page_url = f"{path_without_query}?page={page + 1}&per_page={per_page}" if page < last_page else None 84 | prev_page_url = f"{path_without_query}?page={page - 1}&per_page={per_page}" if page > 1 else None 85 | 86 | return { 87 | 'total': total, 88 | 'per_page': per_page, 89 | 'current_page': page, 90 | 'last_page': last_page, 91 | 'first_page_url': first_page_url, 92 | 'last_page_url': last_page_url, 93 | 'next_page_url': next_page_url, 94 | 'prev_page_url': prev_page_url, 95 | 'path': base_url, 96 | 'from': offset + 1 if data else None, 97 | 'to': offset + len(data) if data else None, 98 | wrap: data 99 | } --------------------------------------------------------------------------------