├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt └── sql_app ├── __init__.py ├── crud.py ├── database.py ├── main.py ├── models.py └── schemas.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gord Thompson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastapi-tutorial-mssql 2 | The FastAPI SQL database tutorial modified to use `mssql+pyodbc`. 3 | 4 | Proof-of-concept that using FastAPI with mssql does *not* require 5 | aioodbc and `async def` for path operation functions. 6 | 7 | ### How to run: 8 | 9 | To launch uvicorn: 10 | 11 | ``` 12 | uvicorn sql_app.main:app --reload 13 | ``` 14 | 15 | Then load the fancy interactive docs page at 16 | 17 | http://localhost:8000/docs 18 | 19 | Details at 20 | 21 | https://fastapi.tiangolo.com/tutorial/ 22 | 23 | and 24 | 25 | https://fastapi.tiangolo.com/tutorial/sql-databases/ 26 | 27 | GitHub discussion: 28 | 29 | https://github.com/sqlalchemy/sqlalchemy/issues/6521 30 | 31 | Notes: 32 | 33 | - You may need to tweak `SQLALCHEMY_DATABASE_URL` in database.py to connect 34 | to your SQL Server instance. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | pyodbc 3 | sqlalchemy 4 | uvicorn 5 | -------------------------------------------------------------------------------- /sql_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gordthompson/fastapi-tutorial-mssql/912962f1762b93b1ac2c5f97ca6ed1cf3bc7e81d/sql_app/__init__.py -------------------------------------------------------------------------------- /sql_app/crud.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models, schemas 4 | 5 | 6 | def get_user(db: Session, user_id: int): 7 | return db.query(models.User).filter(models.User.id == user_id).first() 8 | 9 | 10 | def get_user_by_email(db: Session, email: str): 11 | return db.query(models.User).filter(models.User.email == email).first() 12 | 13 | 14 | def get_users(db: Session, skip: int = 0, limit: int = 100): 15 | return ( 16 | db.query(models.User) 17 | .order_by(models.User.id) 18 | .offset(skip) 19 | .limit(limit) 20 | .all() 21 | ) 22 | 23 | 24 | def create_user(db: Session, user: schemas.UserCreate): 25 | fake_hashed_password = user.password + "notreallyhashed" 26 | db_user = models.User( 27 | email=user.email, hashed_password=fake_hashed_password 28 | ) 29 | db.add(db_user) 30 | db.commit() 31 | db.refresh(db_user) 32 | return db_user 33 | 34 | 35 | def get_items(db: Session, skip: int = 0, limit: int = 100): 36 | return ( 37 | db.query(models.Item) 38 | .order_by(models.Item.id) 39 | .offset(skip) 40 | .limit(limit) 41 | .all() 42 | ) 43 | 44 | 45 | def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): 46 | db_item = models.Item(**item.dict(), owner_id=user_id) 47 | db.add(db_item) 48 | db.commit() 49 | db.refresh(db_item) 50 | return db_item 51 | -------------------------------------------------------------------------------- /sql_app/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import declarative_base, sessionmaker 3 | 4 | SQLALCHEMY_DATABASE_URL = ( 5 | "mssql+pyodbc://scott:tiger^5HHH@localhost/test" 6 | "?driver=ODBC+Driver+17+for+SQL+Server" 7 | ) 8 | 9 | engine = create_engine( 10 | SQLALCHEMY_DATABASE_URL, 11 | # connect_args={"check_same_thread": False}, # only needed for SQLite 12 | ) 13 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 14 | 15 | Base = declarative_base() 16 | -------------------------------------------------------------------------------- /sql_app/main.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import Depends, FastAPI, HTTPException 4 | from sqlalchemy.orm import Session 5 | 6 | from . import crud, models, schemas 7 | from .database import SessionLocal, engine 8 | 9 | models.Base.metadata.create_all(bind=engine) 10 | 11 | app = FastAPI() 12 | 13 | 14 | # Dependency 15 | def get_db(): 16 | db = SessionLocal() 17 | try: 18 | yield db 19 | finally: 20 | db.close() 21 | 22 | 23 | @app.post("/users/", response_model=schemas.User) 24 | def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): 25 | db_user = crud.get_user_by_email(db, email=user.email) 26 | if db_user: 27 | raise HTTPException(status_code=400, detail="Email already registered") 28 | return crud.create_user(db=db, user=user) 29 | 30 | 31 | @app.get("/users/", response_model=List[schemas.User]) 32 | def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 33 | users = crud.get_users(db, skip=skip, limit=limit) 34 | return users 35 | 36 | 37 | @app.get("/users/{user_id}", response_model=schemas.User) 38 | def read_user(user_id: int, db: Session = Depends(get_db)): 39 | db_user = crud.get_user(db, user_id=user_id) 40 | if db_user is None: 41 | raise HTTPException(status_code=404, detail="User not found") 42 | return db_user 43 | 44 | 45 | @app.post("/users/{user_id}/items/", response_model=schemas.Item) 46 | def create_item_for_user( 47 | user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) 48 | ): 49 | return crud.create_user_item(db=db, item=item, user_id=user_id) 50 | 51 | 52 | @app.get("/items/", response_model=List[schemas.Item]) 53 | def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 54 | items = crud.get_items(db, skip=skip, limit=limit) 55 | return items 56 | -------------------------------------------------------------------------------- /sql_app/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, ForeignKey, Integer, String 2 | from sqlalchemy.orm import relationship 3 | 4 | from .database import Base 5 | 6 | 7 | class User(Base): 8 | __tablename__ = "users" 9 | 10 | id = Column(Integer, primary_key=True, index=True) 11 | email = Column(String(255), unique=True, index=True) 12 | hashed_password = Column(String(255)) 13 | is_active = Column(Boolean, default=True) 14 | 15 | items = relationship("Item", back_populates="owner") 16 | 17 | 18 | class Item(Base): 19 | __tablename__ = "items" 20 | 21 | id = Column(Integer, primary_key=True, index=True) 22 | title = Column(String(255), index=True) 23 | description = Column(String(255), index=True) 24 | owner_id = Column(Integer, ForeignKey("users.id")) 25 | 26 | owner = relationship("User", back_populates="items") 27 | -------------------------------------------------------------------------------- /sql_app/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class ItemBase(BaseModel): 7 | title: str 8 | description: Optional[str] = None 9 | 10 | 11 | class ItemCreate(ItemBase): 12 | pass 13 | 14 | 15 | class Item(ItemBase): 16 | id: int 17 | owner_id: int 18 | 19 | class Config: 20 | orm_mode = True 21 | 22 | 23 | class UserBase(BaseModel): 24 | email: str 25 | 26 | 27 | class UserCreate(UserBase): 28 | password: str 29 | 30 | 31 | class User(UserBase): 32 | id: int 33 | is_active: bool 34 | items: List[Item] = [] 35 | 36 | class Config: 37 | orm_mode = True 38 | --------------------------------------------------------------------------------