├── README.md ├── example ├── 10-16.py ├── 10-17.py ├── 10-18.py ├── 10-19.py ├── 10-2.py ├── 10-22.py ├── 10-3.py ├── 10-4.py ├── 10-5.py ├── 10-7.py ├── 11-1.py ├── 11-10.py ├── 11-11.py ├── 11-12.py ├── 11-13.py ├── 11-4.py ├── 11-7.py ├── 11-8.py ├── 11-9.py ├── 12-1.py ├── 12-11.py ├── 12-12.py ├── 12-13.py ├── 12-14.py ├── 12-15.py ├── 12-16.py ├── 12-2.py ├── 12-3.py ├── 12-5.py ├── 12-7.py ├── 12-8.py ├── 12-9.py ├── 14-1.py ├── 14-2.py ├── 14-3.py ├── 14-5.py ├── 15-1.py ├── 15-10.py ├── 15-12.py ├── 15-4.py ├── 15-7.py ├── 16-1.py ├── 16-4.py ├── 16-6.template ├── 16-7.py ├── 17-1.py ├── 17-10.py ├── 17-3.py ├── 17-5.py ├── 17-8.py ├── 17-9.py ├── 18-1.py ├── 18-2.py ├── 18-3.template ├── 18-4.py ├── 18-5.py ├── 18-6.py ├── 18-7.py ├── 2-1.py ├── 3-1.py ├── 3-11.py ├── 3-15.py ├── 3-21.py ├── 3-24.py ├── 3-26.py ├── 3-28.py ├── 3-3.py ├── 3-30.py ├── 3-32.py ├── 3-33.py ├── 3-34.py ├── 4-5.py ├── 4-6.py ├── 4-7.py ├── 5-10.py ├── 5-11.py ├── 5-14.py ├── 5-8.py ├── 6-1.py ├── 6-2.py ├── 6-3.py ├── 6-4.py ├── 6-5.py ├── 7-1.py ├── 7-2.py ├── 7-3.py ├── 7-4.py ├── 7-5.py ├── 7-6.py ├── 7-7.py ├── 7-8.py ├── 8-1.py ├── 8-10.py ├── 8-11.py ├── 8-12.py ├── 8-13.py ├── 8-14.py ├── 8-15.py ├── 8-15a.py ├── 8-4.py ├── 8-7.py ├── 8-8.py ├── 9-1.py ├── 9-2.py ├── 9-3.py └── README.md └── src ├── data ├── __init__.py ├── creature.py ├── explorer.py ├── game.py ├── init.py └── user.py ├── db ├── README.md ├── creature.psv ├── cryptid.db ├── explorer.psv └── load.sh ├── error.py ├── fake ├── __init__.py ├── creature.py ├── explorer.py └── user.py ├── main.py ├── model ├── __init__.py ├── creature.py ├── explorer.py └── user.py ├── service ├── __init__.py ├── creature.py ├── explorer.py ├── game.py └── user.py ├── static ├── abc.txt ├── form1.html ├── form2.html ├── index.html └── xyz │ └── xyz.txt ├── template ├── game.html └── list.html ├── test ├── __init__.py ├── full │ ├── test_creature.py │ ├── test_explorer.py │ └── test_user.py └── unit │ ├── __init__.py │ ├── data │ ├── __init__.py │ ├── test_creature.py │ ├── test_explorer.py │ └── test_user.py │ ├── service │ ├── __init__.py │ ├── test_creature.py │ ├── test_explorer.py │ ├── test_game.py │ └── test_user.py │ └── web │ ├── __init__.py │ ├── test_creature.py │ ├── test_explorer.py │ ├── test_json.py │ └── test_user.py └── web ├── __init__.py ├── creature.py ├── explorer.py ├── game.py └── user.py /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI: Modern Python Web Development 2 | 3 | This repo is a companion to the O'Reilly book, 4 | [FastAPI: Modern Python Web Development](https://learning.oreilly.com/library/view/fastapi/9781098135492/). 5 | It contains: 6 | 7 | * `README.md`: This file. 8 | * `example/`: The numbered Example code files from the book. 9 | Most are Python, but a few are Jinja templates. 10 | * `src/`: Source files for the website. 11 | * `data/`: Python modules for the bottom Data layer. 12 | * `db/`: Text and SQLite data sources for book examples. 13 | * `error.py`: A Python module of exception definitions. 14 | * `fake/`: Fake service and data source during development. 15 | * `main.py`: Sample top website file. 16 | * `model/`: Pydantic Python modules that define data aggregates. 17 | * `service/`: Python modules for the intermediate Service layer. 18 | * `static/`: Non-code files that are directly served by the web server. 19 | * `template/`: Jinja template files. 20 | * `test/`: Test scripts for the various layers. 21 | * `web/`: FastAPI Python modules for the site's top Web layer. 22 | -------------------------------------------------------------------------------- /example/10-16.py: -------------------------------------------------------------------------------- 1 | class Missing(Exception): 2 | def __init__(self, msg:str): 3 | self.msg = msg 4 | 5 | class Duplicate(Exception): 6 | def __init__(self, msg:str): 7 | self.msg = msg 8 | -------------------------------------------------------------------------------- /example/10-17.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import connect, IntegrityError 2 | -------------------------------------------------------------------------------- /example/10-18.py: -------------------------------------------------------------------------------- 1 | from .init import (curs, IntegrityError) 2 | from model.explorer import Explorer 3 | from errors import Missing, Duplicate 4 | 5 | curs.execute("""create table if not exists explorer( 6 | name text primary key, 7 | country text, 8 | description text)""") 9 | 10 | def row_to_model(row: tuple) -> Explorer: 11 | name, country, description = row 12 | return Explorer(name=name, 13 | country=country, description=description) 14 | 15 | def model_to_dict(explorer: Explorer) -> dict: 16 | return explorer.dict() 17 | 18 | def get_one(name: str) -> Explorer: 19 | qry = "select * from explorer where name=:name" 20 | params = {"name": name} 21 | curs.execute(qry, params) 22 | row = curs.fetchone() 23 | if row: 24 | return row_to_model(row) 25 | else: 26 | raise Missing(msg=f"Explorer {name} not found") 27 | 28 | def get_all() -> list[Explorer]: 29 | qry = "select * from explorer" 30 | curs.execute(qry) 31 | return [row_to_model(row) for row in curs.fetchall()] 32 | 33 | def create(explorer: Explorer) -> Explorer: 34 | if not explorer: return None 35 | qry = """insert into explorer (name, country, description) values 36 | (:name, :country, :description)""" 37 | params = model_to_dict(explorer) 38 | try: 39 | curs.execute(qry, params) 40 | except IntegrityError: 41 | raise Duplicate(msg= 42 | f"Explorer {explorer.name} already exists") 43 | return get_one(explorer.name) 44 | 45 | def modify(name: str, explorer: Explorer) -> Explorer: 46 | if not (name and explorer): return None 47 | qry = """update explorer 48 | set name=:name, 49 | country=:country, 50 | description=:description 51 | where name=:name_orig""" 52 | params = model_to_dict(explorer) 53 | params["name_orig"] = explorer.name 54 | curs.execute(qry, params) 55 | if curs.rowcount == 1: 56 | return get_one(explorer.name) 57 | else: 58 | raise Missing(msg=f"Explorer {name} not found") 59 | 60 | def delete(name: str): 61 | if not name: return False 62 | qry = "delete from explorer where name = :name" 63 | params = {"name": name} 64 | curs.execute(qry, params) 65 | if curs.rowcount != 1: 66 | raise Missing(msg=f"Explorer {name} not found") 67 | -------------------------------------------------------------------------------- /example/10-19.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | from model.explorer import Explorer 3 | from service import explorer as service 4 | from errors import Duplicate, Missing 5 | 6 | router = APIRouter(prefix = "/explorer") 7 | 8 | @router.get("") 9 | @router.get("/") 10 | def get_all() -> list[Explorer]: 11 | return service.get_all() 12 | 13 | @router.get("/{name}") 14 | def get_one(name) -> Explorer: 15 | try: 16 | return service.get_one(name) 17 | except Missing as exc: 18 | raise HTTPException(status_code=404, detail=exc.msg) 19 | 20 | @router.post("", status_code=201) 21 | @router.post("/", status_code=201) 22 | def create(explorer: Explorer) -> Explorer: 23 | try: 24 | return service.create(explorer) 25 | except Duplicate as exc: 26 | raise HTTPException(status_code=404, detail=exc.msg) 27 | 28 | @router.patch("/") 29 | def modify(name: str, explorer: Explorer) -> Explorer: 30 | try: 31 | return service.modify(name, explorer) 32 | except Missing as exc: 33 | raise HTTPException(status_code=404, detail=exc.msg) 34 | 35 | @router.delete("/{name}", status_code=204) 36 | def delete(name: str): 37 | try: 38 | return service.delete(name) 39 | except Missing as exc: 40 | raise HTTPException(status_code=404, detail=exc.msg) 41 | -------------------------------------------------------------------------------- /example/10-2.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from model.creature import Creature 3 | 4 | DB_NAME = "cryptid.db" 5 | conn = sqlite3.connect(DB_NAME) 6 | curs = conn.cursor() 7 | 8 | def init(): 9 | curs.execute("create table creature(name, description, country, area, aka)") 10 | 11 | def row_to_model(row: tuple) -> Creature: 12 | name, description, country, area, aka = row 13 | return Creature(name, description, country, area, aka) 14 | 15 | def model_to_dict(creature: Creature) -> dict: 16 | return creature.dict() 17 | 18 | def get_one(name: str) -> Creature: 19 | qry = "select * from creature where name=:name" 20 | params = {"name": name} 21 | curs.execute(qry, params) 22 | row = curs.fetchone() 23 | return row_to_model(row) 24 | 25 | def get_all(name: str) -> list[Creature]: 26 | qry = "select * from creature" 27 | curs.execute(qry) 28 | rows = list(curs.fetchall()) 29 | return [row_to_model(row) for row in rows] 30 | 31 | def create(creature: Creature): 32 | qry = """insert into creature values 33 | (:name, :description, :country, :area, :aka)""" 34 | params = model_to_dict(creature) 35 | curs.execute(qry, params) 36 | 37 | def modify(creature: Creature): 38 | return creature 39 | 40 | def replace(creature: Creature): 41 | return creature 42 | 43 | def delete(creature: Creature): 44 | qry = "delete from creature where name = :name" 45 | params = {"name": creature.name} 46 | curs.execute(qry, params) 47 | -------------------------------------------------------------------------------- /example/10-22.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from model.creature import Creature 4 | from error import Missing, Duplicate 5 | 6 | # Set this before data import below 7 | os.environ["CRYPTID_SQLITE_DB"] = ":memory:" 8 | from data import creature 9 | 10 | @pytest.fixture 11 | def sample() -> Creature: 12 | return Creature(name="yeti", 13 | description="Hirsute Himalayan", 14 | aka="Abominable Snowman", 15 | country="CN", 16 | area="Himalayas") 17 | 18 | def test_create(sample): 19 | resp = creature.create(sample) 20 | assert resp == sample 21 | 22 | def test_create_duplicate(sample): 23 | with pytest.raises(Duplicate): 24 | _ = creature.create(sample) 25 | 26 | def test_get_exists(sample): 27 | resp = creature.get_one(sample.name) 28 | assert resp == sample 29 | 30 | def test_get_missing(): 31 | with pytest.raises(Missing): 32 | _ = creature.get_one("boxturtle") 33 | 34 | def test_modify(sample): 35 | creature.country = "GL" # Greenland! 36 | resp = creature.modify(sample.name, sample) 37 | assert resp == sample 38 | 39 | def test_modify_missing(): 40 | bob: Creature = Creature(name="bob", 41 | description="some guy", country="ZZ") 42 | with pytest.raises(Missing): 43 | _ = creature.modify(bob.name, bob) 44 | 45 | def test_delete(sample): 46 | resp = creature.delete(sample.name) 47 | assert resp is None 48 | 49 | def test_delete_missing(sample): 50 | with pytest.raises(Missing): 51 | _ = creature.delete(sample.name) 52 | -------------------------------------------------------------------------------- /example/10-3.py: -------------------------------------------------------------------------------- 1 | """Initialize SQLite database""" 2 | 3 | import os 4 | from pathlib import Path 5 | from sqlite3 import connect, Connection, Cursor, IntegrityError 6 | 7 | conn: Connection | None = None 8 | curs: Cursor | None = None 9 | 10 | def get_db(name: str|None = None, reset: bool = False): 11 | """Connect to SQLite database file""" 12 | global conn, curs 13 | if conn: 14 | if not reset: 15 | return 16 | conn = None 17 | if not name: 18 | name = os.getenv("CRYPTID_SQLITE_DB") 19 | top_dir = Path(__file__).resolve().parents[1] # repo top 20 | db_dir = top_dir / "db" 21 | db_name = "cryptid.db" 22 | db_path = str(db_dir / db_name) 23 | name = os.getenv("CRYPTID_SQLITE_DB", db_path) 24 | conn = connect(name, check_same_thread=False) 25 | curs = conn.cursor() 26 | 27 | get_db() 28 | -------------------------------------------------------------------------------- /example/10-4.py: -------------------------------------------------------------------------------- 1 | from .init import curs 2 | from model.creature import Creature 3 | 4 | curs.execute("""create table if not exists creature( 5 | name text primary key, 6 | description text, 7 | country text, 8 | area text, 9 | aka text)""") 10 | 11 | def row_to_model(row: tuple) -> Creature: 12 | (name, description, country, area, aka) = row 13 | return Creature(name, description, country, area, aka) 14 | 15 | def model_to_dict(creature: Creature) -> dict: 16 | return creature.dict() 17 | 18 | def get_one(name: str) -> Creature: 19 | qry = "select * from creature where name=:name" 20 | params = {"name": name} 21 | curs.execute(qry, params) 22 | return row_to_model(curs.fetchone()) 23 | 24 | def get_all() -> list[Creature]: 25 | qry = "select * from creature" 26 | curs.execute(qry) 27 | return [row_to_model(row) for row in curs.fetchall()] 28 | 29 | def create(creature: Creature) -> Creature: 30 | qry = """insert into creature values 31 | (:name, :description, :country, :area, :aka)""" 32 | params = model_to_dict(creature) 33 | curs.execute(qry, params) 34 | return get_one(creature.name) 35 | 36 | def modify(creature: Creature) -> Creature: 37 | qry = """update creature 38 | set country=:country, 39 | name=:name, 40 | description=:description, 41 | area=:area, 42 | aka=:aka 43 | where name=:name_orig""" 44 | params = model_to_dict(creature) 45 | params["name_orig"] = creature.name 46 | _ = curs.execute(qry, params) 47 | return get_one(creature.name) 48 | 49 | def delete(creature: Creature) -> bool: 50 | qry = "delete from creature where name = :name" 51 | params = {"name": creature.name} 52 | res = curs.execute(qry, params) 53 | return bool(res) 54 | -------------------------------------------------------------------------------- /example/10-5.py: -------------------------------------------------------------------------------- 1 | from .init import curs 2 | from model.explorer import Explorer 3 | 4 | curs.execute("""create table if not exists explorer( 5 | name text primary key, 6 | country text, 7 | description text)""") 8 | 9 | def row_to_model(row: tuple) -> Explorer: 10 | return Explorer(name=row[0], country=row[1], description=row[2]) 11 | 12 | def model_to_dict(explorer: Explorer) -> dict: 13 | return explorer.dict() if explorer else None 14 | 15 | def get_one(name: str) -> Explorer: 16 | qry = "select * from explorer where name=:name" 17 | params = {"name": name} 18 | curs.execute(qry, params) 19 | return row_to_model(curs.fetchone()) 20 | 21 | def get_all() -> list[Explorer]: 22 | qry = "select * from explorer" 23 | curs.execute(qry) 24 | return [row_to_model(row) for row in curs.fetchall()] 25 | 26 | def create(explorer: Explorer) -> Explorer: 27 | qry = """insert into explorer (name, country, description) 28 | values (:name, :country, :description)""" 29 | params = model_to_dict(explorer) 30 | _ = curs.execute(qry, params) 31 | return get_one(explorer.name) 32 | 33 | def modify(name: str, explorer: Explorer) -> Explorer: 34 | qry = """update explorer 35 | set country=:country, 36 | name=:name, 37 | description=:description 38 | where name=:name_orig""" 39 | params = model_to_dict(explorer) 40 | params["name_orig"] = explorer.name 41 | _ = curs.execute(qry, params) 42 | explorer2 = get_one(explorer.name) 43 | return explorer2 44 | 45 | def delete(explorer: Explorer) -> bool: 46 | qry = "delete from explorer where name = :name" 47 | params = {"name": explorer.name} 48 | res = curs.execute(qry, params) 49 | return bool(res) 50 | -------------------------------------------------------------------------------- /example/10-7.py: -------------------------------------------------------------------------------- 1 | @router.get("") 2 | @router.get("/") 3 | def get_all() -> list[Explorer]: 4 | return service.get_all() 5 | -------------------------------------------------------------------------------- /example/11-1.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import Depends, FastAPI 3 | from fastapi.security import HTTPBasic, HTTPBasicCredentials 4 | 5 | app = FastAPI() 6 | 7 | basic = HTTPBasic() 8 | 9 | @app.get("/who") 10 | def get_user( 11 | creds: HTTPBasicCredentials = Depends(basic)): 12 | return {"username": creds.username, "password": creds.password} 13 | 14 | if __name__ == "__main__": 15 | uvicorn.run("auth:app", reload=True) 16 | -------------------------------------------------------------------------------- /example/11-10.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | import os 3 | from jose import jwt 4 | from model.user import User 5 | 6 | if os.getenv("CRYPTID_UNIT_TEST"): 7 | from fake import user as data 8 | else: 9 | from data import user as data 10 | 11 | # --- New auth stuff 12 | 13 | from passlib.context import CryptContext 14 | 15 | # Change SECRET_KEY for production! 16 | SECRET_KEY = "keep-it-secret-keep-it-safe" 17 | ALGORITHM = "HS256" 18 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 19 | 20 | def verify_password(plain: str, hash: str) -> bool: 21 | """Hash and compare with from the database""" 22 | return pwd_context.verify(plain, hash) 23 | 24 | def get_hash(plain: str) -> str: 25 | """Return the hash of a string""" 26 | return pwd_context.hash(plain) 27 | 28 | def get_jwt_username(token:str) -> str | None: 29 | """Return username from JWT access """ 30 | try: 31 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 32 | if not (username := payload.get("sub")): 33 | return None 34 | except jwt.JWTError: 35 | return None 36 | return username 37 | 38 | def get_current_user(token: str) -> User | None: 39 | """Decode an OAuth access and return the User""" 40 | if not (username := get_jwt_username(token)): 41 | return None 42 | if (user := lookup_user(username)): 43 | return user 44 | return None 45 | 46 | def lookup_user(username: str) -> User | None: 47 | """Return a matching User from the database for """ 48 | if (user := data.get(username)): 49 | return user 50 | return None 51 | 52 | def auth_user(name: str, plain: str) -> User | None: 53 | """Authenticate user and password""" 54 | if not (user := lookup_user(name)): 55 | return None 56 | if not verify_password(plain, user.hash): 57 | return None 58 | return user 59 | 60 | def create_access_token(data: dict, 61 | expires: timedelta | None = None 62 | ): 63 | """Return a JWT access token""" 64 | src = data.copy() 65 | now = datetime.utcnow() 66 | if not expires: 67 | expires = timedelta(minutes=15) 68 | src.update({"exp": now + expires}) 69 | encoded_jwt = jwt.encode(src, SECRET_KEY, algorithm=ALGORITHM) 70 | return encoded_jwt 71 | 72 | # --- CRUD passthrough stuff 73 | 74 | def get_all() -> list[User]: 75 | return data.get_all() 76 | 77 | def get_one(name) -> User: 78 | return data.get_one(name) 79 | 80 | def create(user: User) -> User: 81 | return data.create(user) 82 | 83 | def modify(name: str, user: User) -> User: 84 | return data.modify(name, user) 85 | 86 | def delete(name: str) -> None: 87 | return data.delete(name) 88 | -------------------------------------------------------------------------------- /example/11-11.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import os 3 | from fastapi import APIRouter, HTTPException, Depends 4 | from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm 5 | from model.user import User 6 | if os.getenv("CRYPTID_UNIT_TEST"): 7 | from fake import user as service 8 | else: 9 | from service import user as service 10 | from error import Missing, Duplicate 11 | 12 | ACCESS_TOKEN_EXPIRE_MINUTES = 30 13 | 14 | router = APIRouter(prefix = "/user") 15 | 16 | # --- new auth stuff 17 | 18 | # This dependency makes a post to "/user/token" 19 | # (from a form containing a username and password) 20 | # and returns an access token. 21 | oauth2_dep = OAuth2PasswordBearer(tokenUrl="token") 22 | 23 | def unauthed(): 24 | raise HTTPException( 25 | status_code=401, 26 | detail="Incorrect username or password", 27 | headers={"WWW-Authenticate": "Bearer"}, 28 | ) 29 | 30 | # This endpoint is directed to by any call that has the 31 | # oauth2_dep() dependency: 32 | @router.post("/token") 33 | async def create_access_token( 34 | form_data: OAuth2PasswordRequestForm = Depends() 35 | ): 36 | """Get username and password from OAuth form, 37 | return access token""" 38 | user = service.auth_user(form_data.username, form_data.password) 39 | if not user: 40 | unauthed() 41 | expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 42 | access_token = service.create_access_token( 43 | data={"sub": user.username}, expires=expires 44 | ) 45 | return {"access_token": access_token, "token_type": "bearer"} 46 | 47 | @router.get("/token") 48 | def get_access_token(token: str = Depends(oauth2_dep)) -> dict: 49 | """Return the current access token""" 50 | return {"token": token} 51 | 52 | # --- previous CRUD stuff 53 | 54 | @router.get("/") 55 | def get_all() -> list[User]: 56 | return service.get_all() 57 | 58 | @router.get("/{name}") 59 | def get_one(name) -> User: 60 | try: 61 | return service.get_one(name) 62 | except Missing as exc: 63 | raise HTTPException(status_code=404, detail=exc.msg) 64 | 65 | @router.post("/", status_code=201) 66 | def create(user: User) -> User: 67 | try: 68 | return service.create(user) 69 | except Duplicate as exc: 70 | raise HTTPException(status_code=409, detail=exc.msg) 71 | 72 | @router.patch("/") 73 | def modify(name: str, user: User) -> User: 74 | try: 75 | return service.modify(name, user) 76 | except Missing as exc: 77 | raise HTTPException(status_code=404, detail=exc.msg) 78 | 79 | @router.delete("/{name}") 80 | def delete(name: str) -> None: 81 | try: 82 | return service.delete(name) 83 | except Missing as exc: 84 | raise HTTPException(status_code=404, detail=exc.msg) 85 | -------------------------------------------------------------------------------- /example/11-12.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from web import explorer, creature, user 3 | 4 | app = FastAPI() 5 | app.include_router(explorer.router) 6 | app.include_router(creature.router) 7 | app.include_router(user.router) 8 | -------------------------------------------------------------------------------- /example/11-13.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | app = FastAPI() 5 | 6 | app.add_middleware( 7 | CORSMiddleware, 8 | allow_origins=["https://ui.cryptids.com",], 9 | allow_credentials=True, 10 | allow_methods=["*"], 11 | allow_headers=["*"], 12 | ) 13 | 14 | @app.get("/test_cors") 15 | def test_cors(request: Request): 16 | print(request) 17 | -------------------------------------------------------------------------------- /example/11-4.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import Depends, FastAPI, HTTPException 3 | from fastapi.security import HTTPBasic, HTTPBasicCredentials 4 | 5 | app = FastAPI() 6 | 7 | secret_user: str = "newphone" 8 | secret_password: str = "whodis?" 9 | 10 | basic: HTTPBasicCredentials = HTTPBasic() 11 | 12 | @app.get("/who") 13 | def get_user( 14 | creds: HTTPBasicCredentials = Depends(basic)) -> dict: 15 | if (creds.username == secret_user and 16 | creds.password == secret_password): 17 | return {"username": creds.username, 18 | "password": creds.password} 19 | raise HTTPException(status_code=401, detail="Hey!") 20 | 21 | if __name__ == "__main__": 22 | uvicorn.run("auth:app", reload=True) 23 | -------------------------------------------------------------------------------- /example/11-7.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class User(BaseModel): 4 | name: str 5 | hash: str 6 | -------------------------------------------------------------------------------- /example/11-8.py: -------------------------------------------------------------------------------- 1 | from model.user import User 2 | from .init import (curs, IntegrityError) 3 | from error import Missing, Duplicate 4 | 5 | curs.execute("""create table if not exists 6 | user( 7 | name text primary key, 8 | hash text)""") 9 | curs.execute("""create table if not exists 10 | xuser( 11 | name text primary key, 12 | hash text)""") 13 | 14 | def row_to_model(row: tuple) -> User: 15 | name, hash = row 16 | return User(name=name, hash=hash) 17 | 18 | def model_to_dict(user: User) -> dict: 19 | return user.dict() 20 | 21 | def get_one(name: str) -> User: 22 | qry = "select * from user where name=:name" 23 | params = {"name": name} 24 | curs.execute(qry, params) 25 | row = curs.fetchone() 26 | if row: 27 | return row_to_model(row) 28 | else: 29 | raise Missing(msg=f"User {name} not found") 30 | 31 | def get_all() -> list[User]: 32 | qry = "select * from user" 33 | curs.execute(qry) 34 | return [row_to_model(row) for row in curs.fetchall()] 35 | 36 | def create(user: User, table:str = "user"): 37 | """Add to user or xuser table""" 38 | qry = f"""insert into {table} 39 | (name, hash) 40 | values 41 | (:name, :hash)""" 42 | params = model_to_dict(user) 43 | try: 44 | curs.execute(qry, params) 45 | except IntegrityError: 46 | raise Duplicate(msg= 47 | f"{table}: user {user.name} already exists") 48 | 49 | def modify(name: str, user: User) -> User: 50 | qry = """update user set 51 | name=:name, hash=:hash 52 | where name=:name0""" 53 | params = { 54 | "name": user.name, 55 | "hash": user.hash, 56 | "name0": name} 57 | curs.execute(qry, params) 58 | if curs.rowcount == 1: 59 | return get_one(user.name) 60 | else: 61 | raise Missing(msg=f"User {name} not found") 62 | 63 | def delete(name: str) -> None: 64 | """Drop user with from user table, add to xuser table""" 65 | user = get_one(name) 66 | qry = "delete from user where name = :name" 67 | params = {"name": name} 68 | curs.execute(qry, params) 69 | if curs.rowcount != 1: 70 | raise Missing(msg=f"User {name} not found") 71 | create(user, table="xuser") 72 | -------------------------------------------------------------------------------- /example/11-9.py: -------------------------------------------------------------------------------- 1 | from model.user import User 2 | from error import Missing, Duplicate 3 | 4 | # (no hashed password checking in this module) 5 | fakes = [ 6 | User(name="kwijobo", 7 | hash="abc"), 8 | User(name="ermagerd", 9 | hash="xyz"), 10 | ] 11 | 12 | def find(name: str) -> User | None: 13 | for e in fakes: 14 | if e.name == name: 15 | return e 16 | return None 17 | 18 | def check_missing(name: str): 19 | if not find(name): 20 | raise Missing(msg=f"Missing user {name}") 21 | 22 | def check_duplicate(name: str): 23 | if find(name): 24 | raise Duplicate(msg=f"Duplicate user {name}") 25 | 26 | def get_all() -> list[User]: 27 | """Return all users""" 28 | return fakes 29 | 30 | def get_one(name: str) -> User: 31 | """Return one user""" 32 | check_missing(name) 33 | return find(name) 34 | 35 | def create(user: User) -> User: 36 | """Add a user""" 37 | check_duplicate(user.name) 38 | return user 39 | 40 | def modify(name: str, user: User) -> User: 41 | """Partially modify a user""" 42 | check_missing(name) 43 | return user 44 | 45 | def delete(name: str) -> None: 46 | """Delete a user""" 47 | check_missing(name) 48 | return None 49 | -------------------------------------------------------------------------------- /example/12-1.py: -------------------------------------------------------------------------------- 1 | def preamble() -> str: 2 | return "The sum is " 3 | -------------------------------------------------------------------------------- /example/12-11.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fastapi import APIRouter, HTTPException 3 | from model.creature import Creature 4 | if os.getenv("CRYPTID_UNIT_TEST"): 5 | from fake import creature as service 6 | else: 7 | from service import creature as service 8 | from error import Missing, Duplicate 9 | 10 | router = APIRouter(prefix = "/creature") 11 | 12 | @router.get("/") 13 | def get_all() -> list[Creature]: 14 | return service.get_all() 15 | 16 | @router.get("/{name}") 17 | def get_one(name) -> Creature: 18 | try: 19 | return service.get_one(name) 20 | except Missing as exc: 21 | raise HTTPException(status_code=404, detail=exc.msg) 22 | 23 | @router.post("/", status_code=201) 24 | def create(creature: Creature) -> Creature: 25 | try: 26 | return service.create(creature) 27 | except Duplicate as exc: 28 | raise HTTPException(status_code=409, detail=exc.msg) 29 | 30 | @router.patch("/") 31 | def modify(name: str, creature: Creature) -> Creature: 32 | try: 33 | return service.modify(name, creature) 34 | except Missing as exc: 35 | raise HTTPException(status_code=404, detail=exc.msg) 36 | 37 | 38 | @router.delete("/{name}") 39 | def delete(name: str) -> None: 40 | try: 41 | return service.delete(name) 42 | except Missing as exc: 43 | raise HTTPException(status_code=404, detail=exc.msg) 44 | -------------------------------------------------------------------------------- /example/12-12.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import pytest 3 | import os 4 | os.environ["CRYPTID_UNIT_TEST"] = "true" 5 | from model.creature import Creature 6 | from web import creature 7 | 8 | @pytest.fixture 9 | def sample() -> Creature: 10 | return Creature(name="dragon", 11 | description="Wings! Fire! Aieee!", 12 | country="*") 13 | 14 | @pytest.fixture 15 | def fakes() -> list[Creature]: 16 | return creature.get_all() 17 | 18 | def assert_duplicate(exc): 19 | assert exc.value.status_code == 404 20 | assert "Duplicate" in exc.value.msg 21 | 22 | def assert_missing(exc): 23 | assert exc.value.status_code == 404 24 | assert "Missing" in exc.value.msg 25 | 26 | def test_create(sample): 27 | assert creature.create(sample) == sample 28 | 29 | def test_create_duplicate(fakes): 30 | with pytest.raises(HTTPException) as exc: 31 | _ = creature.create(fakes[0]) 32 | assert_duplicate(exc) 33 | 34 | def test_get_one(fakes): 35 | assert creature.get_one(fakes[0].name) == fakes[0] 36 | 37 | def test_get_one_missing(): 38 | with pytest.raises(HTTPException) as exc: 39 | _ = creature.get_one("bobcat") 40 | assert_missing(exc) 41 | 42 | def test_modify(fakes): 43 | assert creature.modify(fakes[0].name, fakes[0]) == fakes[0] 44 | 45 | def test_modify_missing(sample): 46 | with pytest.raises(HTTPException) as exc: 47 | _ = creature.modify(sample.name, sample) 48 | assert_missing(exc) 49 | 50 | def test_delete(fakes): 51 | assert creature.delete(fakes[0].name) is None 52 | 53 | def test_delete_missing(sample): 54 | with pytest.raises(HTTPException) as exc: 55 | _ = creature.delete("emu") 56 | assert_missing(exc) 57 | -------------------------------------------------------------------------------- /example/12-13.py: -------------------------------------------------------------------------------- 1 | import os 2 | from model.creature import Creature 3 | if os.getenv("CRYPTID_UNIT_TEST"): 4 | from fake import creature as data 5 | else: 6 | from data import creature as data 7 | 8 | def get_all() -> list[Creature]: 9 | return data.get_all() 10 | 11 | def get_one(name) -> Creature: 12 | return data.get_one(name) 13 | 14 | def create(creature: Creature) -> Creature: 15 | return data.create(creature) 16 | 17 | def modify(name: str, creature: Creature) -> Creature: 18 | return data.modify(name, creature) 19 | 20 | def delete(name: str) -> None: 21 | return data.delete(name) 22 | -------------------------------------------------------------------------------- /example/12-14.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ["CRYPTID_UNIT_TEST"]= "true" 3 | import pytest 4 | 5 | from model.creature import Creature 6 | from data import creature as data 7 | from error import Missing, Duplicate 8 | 9 | @pytest.fixture 10 | def sample() -> Creature: 11 | return Creature(name="yeti", 12 | aka="Abominable Snowman", 13 | country="CN", 14 | area="Himalayas", 15 | description="Handsome Himalayan") 16 | 17 | def test_create(sample): 18 | resp = data.create(sample) 19 | assert resp == sample 20 | 21 | def test_create_duplicate(sample): 22 | resp = data.create(sample) 23 | assert resp == sample 24 | with pytest.raises(Duplicate): 25 | resp = data.create(sample) 26 | 27 | def test_get_exists(sample): 28 | resp = data.create(sample) 29 | assert resp == sample 30 | resp = data.get_one(sample.name) 31 | assert resp == sample 32 | 33 | def test_get_missing(): 34 | with pytest.raises(Missing): 35 | _ = data.get_one("boxturtle") 36 | 37 | def test_modify(sample): 38 | resp = data.create(sample) 39 | assert resp == sample 40 | sample.country = "CA" # Canada! 41 | print("sample", sample) 42 | resp = data.modify(sample.name, sample) 43 | assert resp == sample 44 | 45 | def test_modify_missing(): 46 | bob: Creature = Creature(name="bob", country="US", area="*", 47 | description="some guy", aka="??") 48 | with pytest.raises(Missing): 49 | _ = data.modify(bob.name, bob) 50 | -------------------------------------------------------------------------------- /example/12-15.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from model.creature import Creature 4 | from error import Missing, Duplicate 5 | 6 | # set this before data import below 7 | os.environ["CRYPTID_SQLITE_DB"] = ":memory:" 8 | from data import creature 9 | 10 | @pytest.fixture 11 | def sample() -> Creature: 12 | return Creature(name="yeti", 13 | aka="Abominable Snowman", 14 | country="CN", 15 | area="Himalayas", 16 | description="Hapless Himalayan") 17 | 18 | def test_create(sample): 19 | resp = creature.create(sample) 20 | assert resp == sample 21 | 22 | def test_create_duplicate(sample): 23 | with pytest.raises(Duplicate): 24 | _ = creature.create(sample) 25 | 26 | def test_get_one(sample): 27 | resp = creature.get_one(sample.name) 28 | assert resp == sample 29 | 30 | def test_get_one_missing(): 31 | with pytest.raises(Missing): 32 | _ = creature.get_one("boxturtle") 33 | 34 | def test_modify(sample): 35 | creature.country = "JP" # Japan! 36 | resp = creature.modify(sample.name, sample) 37 | assert resp == sample 38 | 39 | def test_modify_missing(): 40 | thing: Creature = Creature(name="snurfle", 41 | description="some thing", country="somewhere") 42 | with pytest.raises(Missing): 43 | _ = creature.modify(thing.name, thing) 44 | 45 | def test_delete(sample): 46 | resp = creature.delete(sample.name) 47 | assert resp is None 48 | 49 | def test_delete_missing(sample): 50 | with pytest.raises(Missing): 51 | _ = creature.delete(sample.name) 52 | -------------------------------------------------------------------------------- /example/12-16.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from web import explorer, creature 3 | 4 | app = FastAPI() 5 | app.include_router(explorer.router) 6 | app.include_router(creature.router) 7 | -------------------------------------------------------------------------------- /example/12-2.py: -------------------------------------------------------------------------------- 1 | import mod1 2 | 3 | def summer(x: int, y:int) -> str: 4 | return mod1.preamble() + f"{x+y}" 5 | -------------------------------------------------------------------------------- /example/12-3.py: -------------------------------------------------------------------------------- 1 | import mod2 2 | 3 | def test_summer(): 4 | assert "The sum is 11" == mod2.summer(5,6) 5 | -------------------------------------------------------------------------------- /example/12-5.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | import mod1 3 | import mod2 4 | 5 | def test_summer_a(): 6 | with mock.patch("mod1.preamble", return_value=""): 7 | assert "11" == mod2.summer(5,6) 8 | 9 | def test_summer_b(): 10 | with mock.patch("mod1.preamble") as mock_preamble: 11 | mock_preamble.return_value="" 12 | assert "11" == mod2.summer(5,6) 13 | 14 | @mock.patch("mod1.preamble", return_value="") 15 | def test_summer_c(mock_preamble): 16 | assert "11" == mod2.summer(5,6) 17 | 18 | @mock.patch("mod1.preamble") 19 | def test_caller_d(mock_preamble): 20 | mock_preamble.return_value = "" 21 | assert "11" == mod2.summer(5,6) 22 | -------------------------------------------------------------------------------- /example/12-7.py: -------------------------------------------------------------------------------- 1 | import os 2 | if os.get_env("UNIT_TEST"): 3 | import fake_mod1 as mod1 4 | else: 5 | import mod1 6 | 7 | def summer(x: int, y:int) -> str: 8 | return mod1.preamble() + f"{x+y}" 9 | -------------------------------------------------------------------------------- /example/12-8.py: -------------------------------------------------------------------------------- 1 | import os 2 | if os.get_env("UNIT_TEST"): 3 | import fake_mod1 as mod1 4 | else: 5 | import mod1 6 | 7 | def summer(x: int, y:int) -> str: 8 | return mod1.preamble() + f"{x+y}" 9 | -------------------------------------------------------------------------------- /example/12-9.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ["UNIT_TEST"] = "true" 3 | import mod2 4 | 5 | def test_summer_fake(): 6 | assert "11" == mod2.summer(5,6) 7 | -------------------------------------------------------------------------------- /example/14-1.py: -------------------------------------------------------------------------------- 1 | def get_one(name: str) -> Explorer: 2 | qry = "select * from explorer where name=:name" 3 | params = {"name": name} 4 | curs.execute(qry, params) 5 | return row_to_model(curs.fetchone()) 6 | -------------------------------------------------------------------------------- /example/14-2.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Metadata, Table, Column, Text 2 | from sqlalchemy import connect, insert 3 | 4 | conn = connect("sqlite:///cryptid.db") 5 | meta = Metadata() 6 | explorer_table = Table( 7 | "explorer", 8 | meta, 9 | Column("name", Text, primary_key=True), 10 | Column("country", Text), 11 | Column("description", Text), 12 | ) 13 | insert(explorer_table).values( 14 | name="Beau Buffette", 15 | country="US", 16 | description="...") 17 | -------------------------------------------------------------------------------- /example/14-3.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | from time import perf_counter 3 | 4 | def load(): 5 | from error import Duplicate 6 | from data.explorer import create 7 | from model.explorer import Explorer 8 | 9 | f = Faker() 10 | NUM = 100_000 11 | t1 = perf_counter() 12 | for row in range(NUM): 13 | try: 14 | create(Explorer(name=f.name(), 15 | country=f.country(), 16 | description=f.description)) 17 | except Duplicate: 18 | pass 19 | t2 = perf_counter() 20 | print(NUM, "rows") 21 | print("write time:", t2-t1) 22 | 23 | def read_db(): 24 | from data.explorer import get_all 25 | 26 | t1 = perf_counter() 27 | _ = get_all() 28 | t2 = perf_counter() 29 | print("db read time:", t2-t1) 30 | 31 | def read_api(): 32 | from fastapi.testclient import TestClient 33 | from main import app 34 | 35 | t1 = perf_counter() 36 | client = TestClient(app) 37 | _ = client.get("/explorer/") 38 | t2 = perf_counter() 39 | print("api read time:", t2-t1) 40 | 41 | load() 42 | read_db() 43 | read_db() 44 | read_api() 45 | -------------------------------------------------------------------------------- /example/14-5.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | from transformers import (AutoTokenizer, 6 | AutoModelForSeq2SeqLM, GenerationConfig) 7 | model_name = "google/flan-t5-base" 8 | tokenizer = AutoTokenizer.from_pretrained(model_name) 9 | model = AutoModelForSeq2SeqLM.from_pretrained(model_name) 10 | config = GenerationConfig(max_new_tokens=200) 11 | 12 | @app.get("/ai") 13 | def prompt(line: str) -> str: 14 | tokens = tokenizer(line, return_tensors="pt") 15 | outputs = model.generate(**tokens, 16 | generator_config=config) 17 | result = tokenizer.batch_decode(outputs, 18 | skip_special_tokens=True) 19 | return result[0] 20 | -------------------------------------------------------------------------------- /example/15-1.py: -------------------------------------------------------------------------------- 1 | from fastapi import File 2 | 3 | @app.post("/small") 4 | async def upload_small_file(small_file: bytes = File()) -> str: 5 | return f"file size: {len(small_file)}" 6 | -------------------------------------------------------------------------------- /example/15-10.py: -------------------------------------------------------------------------------- 1 | from pathlib import path 2 | from typing import Generator 3 | from fastapi import FastAPI 4 | from fastapi.responses import StreamingResponse 5 | 6 | app = FastAPI() 7 | 8 | def gen_file(path: str) -> Generator: 9 | with open(file=path, mode="rb") as file: 10 | yield file.read() 11 | 12 | @app.get("/download_big/{name}") 13 | async def download_big_file(name:str): 14 | gen_expr = gen_file(file_path=path) 15 | response = StreamingResponse( 16 | content=gen_expr, 17 | status_code=200, 18 | ) 19 | return response 20 | -------------------------------------------------------------------------------- /example/15-12.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from fastapi import FastAPI 3 | from fastapi.staticfiles import StaticFiles 4 | 5 | app = FastAPI() 6 | 7 | # Directory containing main.py: 8 | top = Path(__file__).resolve.parent 9 | 10 | app.mount("/static", 11 | StaticFiles(directory=f"{top}/static", html=True), 12 | name="free") 13 | -------------------------------------------------------------------------------- /example/15-4.py: -------------------------------------------------------------------------------- 1 | from fastapi import UploadFile 2 | 3 | @app.post("/big") 4 | async def upload_big_file(big_file: UploadFile) -> str: 5 | return f"file size: {big_file.size}, name: {big_file.filename}" 6 | -------------------------------------------------------------------------------- /example/15-7.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.responses import FileResponse 3 | 4 | app = FastAPI() 5 | 6 | @app.get("/small/{name}") 7 | async def download_small_file(name): 8 | return FileResponse(name) 9 | -------------------------------------------------------------------------------- /example/16-1.py: -------------------------------------------------------------------------------- 1 | from fastapi import Form, FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/who2") 6 | def greet2(name: str = Form()): 7 | return f"Hello, {name}?" 8 | -------------------------------------------------------------------------------- /example/16-4.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Form 2 | 3 | app = FastAPI() 4 | 5 | @app.post("/who2") 6 | def greet3(name: str = Form()): 7 | return f"Hello, {name}?" 8 | -------------------------------------------------------------------------------- /example/16-6.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for creature in creatures: %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 |
Creatures
NameDescriptionCountryAreaAKA
{{ creature.name }}{{ creature.description }}{{ creature.country }}{{ creature.area }}{{ creature.aka }}
23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% for explorer in explorers: %} 36 | 37 | 38 | 39 | 40 | 41 | {% endfor %} 42 |
Explorers
NameCountryDescription
{{ explorer.name }}{{ explorer.country }}{{ explorer.description }}
43 | 44 | -------------------------------------------------------------------------------- /example/16-7.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from fastapi import FastAPI, Request 3 | from fastapi.templating import Jinja2Templates 4 | 5 | app = FastAPI() 6 | 7 | top = Path(__file__).resolve().parent 8 | 9 | template_obj = Jinja2Templates(directory=f"{top}/template") 10 | 11 | # Get some small predefined lists of our buddies: 12 | from fake.creature import fakes as fake_creatures 13 | from fake.explorer import fakes as fake_explorers 14 | 15 | @app.get("/list") 16 | def explorer_list(request: Request): 17 | return template_obj.TemplateResponse("list.html", 18 | {"request": request, 19 | "explorers": fake_explorers, 20 | "creatures": fake_creatures}) 21 | -------------------------------------------------------------------------------- /example/17-1.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import sys 3 | 4 | def read_csv(fname: str) -> list[tuple]: 5 | with open(fname) as file: 6 | data = [row for row in csv.reader(file, delimiter="|")] 7 | return data 8 | 9 | if __name__ == "__main__": 10 | data = read_csv(sys.argv[1]) 11 | for row in data[0:5]: 12 | print(row) 13 | -------------------------------------------------------------------------------- /example/17-10.py: -------------------------------------------------------------------------------- 1 | # (insert these lines in web/creature.py) 2 | 3 | from fastapi import Response 4 | import plotly.express as px 5 | import country_converter as coco 6 | 7 | @router.get("/map") 8 | def map(): 9 | creatures = service.get_all() 10 | iso2_codes = set(creature.country for creature in creatures) 11 | iso3_codes = coco.convert(names=iso2_codes, to="ISO3") 12 | fig = px.choropleth( 13 | locationmode="ISO-3", 14 | locations=iso3_codes) 15 | fig_bytes = fig.to_image(format="png") 16 | return Response(content=fig_bytes, media_type="image/png") 17 | -------------------------------------------------------------------------------- /example/17-3.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from tabulate import tabulate 3 | import sys 4 | 5 | def read_csv(fname: str) -> list[tuple]: 6 | with open(fname) as file: 7 | data = [row for row in csv.reader(file, delimiter="|")] 8 | return data 9 | 10 | if __name__ == "__main__": 11 | data = read_csv(sys.argv[1]) 12 | print(tabulate(data[0:5])) 13 | -------------------------------------------------------------------------------- /example/17-5.py: -------------------------------------------------------------------------------- 1 | import pandas 2 | import sys 3 | 4 | def read_pandas(fname: str) -> pandas.DataFrame: 5 | data = pandas.read_csv(fname, sep="|") 6 | return data 7 | 8 | if __name__ == "__main__": 9 | data = read_pandas(sys.argv[1]) 10 | print(data.head(5)) 11 | -------------------------------------------------------------------------------- /example/17-8.py: -------------------------------------------------------------------------------- 1 | # (insert these lines in web/creature.py) 2 | 3 | from fastapi import Response 4 | import plotly.express as px 5 | 6 | @router.get("/test") 7 | def test(): 8 | df = px.data.iris() 9 | fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species") 10 | fig_bytes = fig.to_image(format="png") 11 | return Response(content=fig_bytes, media_type="image/png") 12 | -------------------------------------------------------------------------------- /example/17-9.py: -------------------------------------------------------------------------------- 1 | # (insert these lines in web/creature.py) 2 | 3 | from collections import Counter 4 | from fastapi import Response 5 | import plotly.express as px 6 | from service.creature import get_all 7 | 8 | @router.get("/plot") 9 | def plot(): 10 | creatures = get_all() 11 | letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 12 | counts = Counter(creature.name[0] for creature in creatures) 13 | y = { letter: counts.get(letter, 0) for letter in letters } 14 | fig = px.histogram(x=list(letters), y=y, title="Creature Names", 15 | labels={"x": "Initial", "y": "Initial"}) 16 | fig_bytes = fig.to_image(format="png") 17 | return Response(content=fig_bytes, media_type="image/png") 18 | -------------------------------------------------------------------------------- /example/18-1.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from fastapi import APIRouter, Body, Request 4 | from fastapi.templating import Jinja2Templates 5 | 6 | from service import game as service 7 | 8 | router = APIRouter(prefix = "/game") 9 | 10 | # Initial game request 11 | @router.get("") 12 | def game_start(request: Request): 13 | name = service.get_word() 14 | top = Path(__file__).resolve().parents[1] # grandparent 15 | print(f"{top=}") 16 | templates = Jinja2Templates(directory=f"{top}/template") 17 | return templates.TemplateResponse("game.html", 18 | {"request": request, "word": name}) 19 | 20 | 21 | # Subsequent game requests 22 | @router.post("") 23 | async def game_step(word: str = Body(), guess: str = Body()): 24 | score = service.get_score(word, guess) 25 | return score 26 | -------------------------------------------------------------------------------- /example/18-2.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from web import creature, explorer, game 3 | 4 | app = FastAPI() 5 | 6 | app.include_router(explorer.router) 7 | app.include_router(creature.router) 8 | app.include_router(game.router) 9 | 10 | if __name__ == "__main__": 11 | import uvicorn 12 | uvicorn.run("main:app", 13 | host="localhost", port=8000, reload=True) 14 | -------------------------------------------------------------------------------- /example/18-3.template: -------------------------------------------------------------------------------- 1 | 2 | 26 | 27 | 28 | 72 |

Cryptonamicon

73 | 74 | 75 |
76 | 77 | 78 | 79 |
80 | 81 |
82 | {% for letter in word %}{% endfor %} 83 | 84 |

85 | 86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /example/18-4.py: -------------------------------------------------------------------------------- 1 | import data.game as data 2 | 3 | def get_word() -> str: 4 | return data.get_word() 5 | -------------------------------------------------------------------------------- /example/18-5.py: -------------------------------------------------------------------------------- 1 | from collections import Counter, defaultdict 2 | 3 | HIT = "H" 4 | MISS = "M" 5 | CLOSE = "C" # (letter is in the word, but at another position) 6 | ERROR = "" 7 | def get_score(actual: str, guess: str) -> str: 8 | length: int = len(actual) 9 | if len(guess) != length: 10 | return ERROR 11 | actual_counter = Counter(actual) # {letter: count, ...} 12 | guess_counter = defaultdict(int) 13 | result = [MISS] * length 14 | for pos, letter in enumerate(guess): 15 | if letter == actual[pos]: 16 | result[pos] = HIT 17 | guess_counter[letter] += 1 18 | for pos, letter in enumerate(guess): 19 | if result[pos] == HIT: 20 | continue 21 | guess_counter[letter] += 1 22 | if (letter in actual and 23 | guess_counter[letter] <= actual_counter[letter]): 24 | result[pos] = CLOSE 25 | result = ''.join(result) 26 | return result 27 | -------------------------------------------------------------------------------- /example/18-6.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from service import game 3 | 4 | word = "bigfoot" 5 | guesses = [ 6 | ("bigfoot", "HHHHHHH"), 7 | ("abcdefg", "MCMMMCC"), 8 | ("toofgib", "CCCHCCC"), 9 | ("wronglength", ""), 10 | ("", ""), 11 | ] 12 | 13 | @pytest.mark.parametrize("guess,score", guesses) 14 | def test_match(guess, score): 15 | assert game.get_score(word, guess) == score 16 | -------------------------------------------------------------------------------- /example/18-7.py: -------------------------------------------------------------------------------- 1 | from .init import curs 2 | 3 | def get_word() -> str: 4 | qry = "select name from creature order by random() limit 1" 5 | curs.execute(qry) 6 | row = curs.fetchone() 7 | if row: 8 | print(f"{row=}") 9 | name = row[0] 10 | else: 11 | name = "bigfoot" 12 | return name 13 | -------------------------------------------------------------------------------- /example/2-1.py: -------------------------------------------------------------------------------- 1 | def paid_promotion(): 2 | print("(that calls this function!)") 3 | 4 | print("This is the program") 5 | paid_promotion() 6 | print("that goes like this.") 7 | -------------------------------------------------------------------------------- /example/3-1.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/hi") 6 | def greet(): 7 | return "Hello? World?" 8 | -------------------------------------------------------------------------------- /example/3-11.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/hi/{who}") 6 | def greet(who): 7 | return f"Hello? {who}?" 8 | -------------------------------------------------------------------------------- /example/3-15.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/hi") 6 | def greet(who): 7 | return f"Hello? {who}?" 8 | -------------------------------------------------------------------------------- /example/3-21.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Body 2 | 3 | app = FastAPI() 4 | 5 | @app.post("/hi") 6 | def greet(who:str = Body(embed=True)): 7 | return f"Hello? {who}?" 8 | -------------------------------------------------------------------------------- /example/3-24.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Header 2 | 3 | app = FastAPI() 4 | 5 | @app.post("/hi") 6 | def greet(who:str = Header()): 7 | return f"Hello? {who}?" 8 | -------------------------------------------------------------------------------- /example/3-26.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Header 2 | 3 | app = FastAPI() 4 | 5 | @app.post("/agent") 6 | def get_agent(user_agent:str = Header()): 7 | return user_agent 8 | -------------------------------------------------------------------------------- /example/3-28.py: -------------------------------------------------------------------------------- 1 | @app.get("/happy") 2 | def happy(status_code=200): 3 | return ":)" 4 | -------------------------------------------------------------------------------- /example/3-3.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/hi") 6 | def greet(): 7 | return "Hello? World?" 8 | 9 | if __name__ == "__main__": 10 | import uvicorn 11 | uvicorn.run("hello:app", reload=True) 12 | -------------------------------------------------------------------------------- /example/3-30.py: -------------------------------------------------------------------------------- 1 | from fastapi import Response 2 | 3 | @app.get("/header/{name}/{value}") 4 | def header(name: str, value: str, response:Response): 5 | response.headers[name] = value 6 | return "normal body" 7 | -------------------------------------------------------------------------------- /example/3-32.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import pytest 4 | from fastapi.encoders import jsonable_encoder 5 | 6 | @pytest.fixture 7 | def data(): 8 | return datetime.datetime.now() 9 | 10 | def test_json_dump(data): 11 | with pytest.raises(Exception): 12 | _ = json.dumps(data) 13 | 14 | def test_encoder(data): 15 | out = jsonable_encoder(data) 16 | assert out 17 | json_out = json.dumps(out) 18 | assert json_out 19 | -------------------------------------------------------------------------------- /example/3-33.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pydantic import BaseClass 3 | 4 | class TagIn(BaseClass): 5 | tag: str 6 | 7 | class Tag(BaseClass): 8 | tag: str 9 | created: datetime 10 | secret: str 11 | 12 | class TagOut(BaseClass): 13 | tag: str 14 | created: datetime 15 | -------------------------------------------------------------------------------- /example/3-34.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from fastapi import FastAPI 3 | from model.tag import TagIn, Tag, TagOut 4 | import service.tag as service 5 | 6 | app = FastAPI() 7 | 8 | @app.post('/') 9 | def create(tag_in: TagIn) -> TagIn: 10 | tag: Tag = Tag(tag=tag_in.tag, created=datetime.utcnow(), 11 | secret="shhhh") 12 | service.create(tag) 13 | return tag_in 14 | 15 | @app.get('/{tag_str}', response_model=TagOut) 16 | def get_one(tag_str: str) -> TagOut: 17 | tag: Tag = service.get(tag_str) 18 | return tag 19 | -------------------------------------------------------------------------------- /example/4-5.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | import asyncio 3 | 4 | app = FastAPI() 5 | 6 | @app.get("/hi") 7 | async def greet(): 8 | await asyncio.sleep(1) 9 | return "Hello? World?" 10 | -------------------------------------------------------------------------------- /example/4-6.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | import asyncio 3 | import uvicorn 4 | 5 | app = FastAPI() 6 | 7 | @app.get("/hi") 8 | async def greet(): 9 | await asyncio.sleep(1) 10 | return "Hello? World?" 11 | 12 | if __name__ == "__main__": 13 | uvicorn.run("greet_async_uvicorn:app") 14 | -------------------------------------------------------------------------------- /example/4-7.py: -------------------------------------------------------------------------------- 1 | from starlette.applications import Starlette 2 | from starlette.responses import JSONResponse 3 | from starlette.routing import Route 4 | 5 | async def greeting(request): 6 | return JSONResponse('Hello? World?') 7 | 8 | app = Starlette(debug=True, routes=[ 9 | Route('/hi', greeting), 10 | ]) 11 | -------------------------------------------------------------------------------- /example/5-10.py: -------------------------------------------------------------------------------- 1 | from model import Creature 2 | 3 | _creatures: list[Creature] = [ 4 | Creature(name="yeti", 5 | country="CN", 6 | area="Himalayas", 7 | description="Hirsute Himalayan", 8 | aka="Abominable Snowman" 9 | ), 10 | Creature(name="sasquatch", 11 | country="US", 12 | area="*", 13 | description="Yeti's Cousin Eddie", 14 | aka="Bigfoot") 15 | ] 16 | 17 | def get_creatures() -> list[Creature]: 18 | return _creatures 19 | -------------------------------------------------------------------------------- /example/5-11.py: -------------------------------------------------------------------------------- 1 | from model import Creature 2 | from fastapi import FastAPI 3 | 4 | app = FastAPI() 5 | 6 | @app.get("/creature") 7 | def get_all() -> list[Creature]: 8 | from data import get_creatures 9 | return get_creatures() 10 | -------------------------------------------------------------------------------- /example/5-14.py: -------------------------------------------------------------------------------- 1 | from model import Creature 2 | 3 | dragon = Creature( 4 | name="dragon", 5 | description=["incorrect", "string", "list"], 6 | country="*" 7 | ) 8 | -------------------------------------------------------------------------------- /example/5-8.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class Creature(BaseModel): 4 | name: str 5 | country: str 6 | area: str 7 | description: str 8 | aka: str 9 | 10 | thing = Creature( 11 | name="yeti", 12 | country="CN", 13 | area="Himalayas", 14 | description="Hirsute Himalayan", 15 | aka="Abominable Snowman") 16 | 17 | print("Name is", thing.name) 18 | -------------------------------------------------------------------------------- /example/6-1.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Depends, Params 2 | 3 | app = FastAPI() 4 | 5 | # the dependency function: 6 | def user_dep(name: str = Params, password: str = Params): 7 | return {"name": name, "valid": True} 8 | 9 | # the path function / web endpoint: 10 | @app.get("/user") 11 | def get_user(user: dict = Depends(user_dep)) -> dict: 12 | return user 13 | -------------------------------------------------------------------------------- /example/6-2.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Depends, Params 2 | 3 | app = FastAPI() 4 | 5 | # the dependency function: 6 | def user_dep(name: str = Params, password: str = Params): 7 | return {"name": name, "valid": True} 8 | 9 | # the path function / web endpoint: 10 | @app.get("/user") 11 | def get_user(user: dict = Depends(user_dep)) -> dict: 12 | return user 13 | -------------------------------------------------------------------------------- /example/6-3.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Depends, Params 2 | 3 | app = FastAPI() 4 | 5 | # the dependency function: 6 | def check_dep(name: str = Params, password: str = Params): 7 | if not name: 8 | raise 9 | 10 | # the path function / web endpoint: 11 | @app.get("/check_user", dependencies=[Depends(check_dep)]) 12 | def check_user() -> bool: 13 | return True 14 | -------------------------------------------------------------------------------- /example/6-4.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, APIRouter 2 | 3 | def depfunc(): 4 | pass 5 | 6 | router = APIRouter(..., dependencies=[Depends(depfunc)]) 7 | -------------------------------------------------------------------------------- /example/6-5.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Depends 2 | 3 | def depfunc1(): 4 | pass 5 | 6 | def depfunc2(): 7 | pass 8 | 9 | app = FastAPI(dependencies=[Depends(depfunc1), Depends(depfunc2)]) 10 | 11 | @app.get("/main") 12 | def get_main(): 13 | pass 14 | -------------------------------------------------------------------------------- /example/7-1.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/hi/{who}") 6 | def greet(who: str): 7 | return f"Hello? {who}?" 8 | -------------------------------------------------------------------------------- /example/7-2.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/hi/", methods=["GET"]) 6 | def greet(who: str): 7 | return jsonify(f"Hello? {who}?") 8 | -------------------------------------------------------------------------------- /example/7-3.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/hi") 6 | def greet(who): 7 | return f"Hello? {who}?" 8 | -------------------------------------------------------------------------------- /example/7-4.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/hi", methods=["GET"]) 6 | def greet(): 7 | who: str = request.args.get("who") 8 | return jsonify(f"Hello? {who}?") 9 | -------------------------------------------------------------------------------- /example/7-5.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/hi") 6 | def greet(who): 7 | return f"Hello? {who}?" 8 | -------------------------------------------------------------------------------- /example/7-6.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/hi", methods=["GET"]) 6 | def greet(): 7 | who: str = request.json["who"] 8 | return jsonify(f"Hello? {who}?") 9 | -------------------------------------------------------------------------------- /example/7-7.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Header 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/hi") 6 | def greet(who:str = Header()): 7 | return f"Hello? {who}?" 8 | -------------------------------------------------------------------------------- /example/7-8.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/hi", methods=["GET"]) 6 | def greet(): 7 | who: str = request.headers.get("who") 8 | return jsonify(f"Hello? {who}?") 9 | -------------------------------------------------------------------------------- /example/8-1.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/") 6 | def top(): 7 | return "top here" 8 | 9 | if __name__ == "__main__": 10 | import uvicorn 11 | uvicorn.run("main:app", reload=True) 12 | -------------------------------------------------------------------------------- /example/8-10.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class Explorer(BaseModel): 4 | name: str 5 | country: str 6 | description: str 7 | -------------------------------------------------------------------------------- /example/8-11.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class Creature(BaseModel): 4 | name: str 5 | country: str 6 | area: str 7 | description: str 8 | aka: str 9 | -------------------------------------------------------------------------------- /example/8-12.py: -------------------------------------------------------------------------------- 1 | from model.explorer import Explorer 2 | 3 | # fake data, replaced in Chapter 10 by a real database and SQL 4 | _explorers = [ 5 | Explorer(name="Claude Hande", 6 | country="FR", 7 | description="Scarce during full moons"), 8 | Explorer(name="Noah Weiser", 9 | country="DE", 10 | description="Myopic machete man"), 11 | ] 12 | 13 | def get_all() -> list[Explorer]: 14 | """Return all explorers""" 15 | return _explorers 16 | 17 | def get_one(name: str) -> Explorer | None: 18 | for _explorer in _explorers: 19 | if _explorer.name == name: 20 | return _explorer 21 | return None 22 | 23 | # The following are nonfunctional for now, 24 | # so they just act like they work, without modifying 25 | # the actual fake _explorers list: 26 | def create(explorer: Explorer) -> Explorer: 27 | """Add an explorer""" 28 | return explorer 29 | 30 | def modify(explorer: Explorer) -> Explorer: 31 | """Partially modify an explorer""" 32 | return explorer 33 | 34 | def replace(explorer: Explorer) -> Explorer: 35 | """Completely replace an explorer""" 36 | return explorer 37 | 38 | def delete(name: str) -> bool: 39 | """Delete an explorer; return None if it existed""" 40 | return None 41 | -------------------------------------------------------------------------------- /example/8-13.py: -------------------------------------------------------------------------------- 1 | from model.creature import Creature 2 | 3 | # fake data, until we use a real database and SQL 4 | _creatures = [ 5 | Creature(name="Yeti", 6 | aka="Abominable Snowman", 7 | country="CN", 8 | area="Himalayas", 9 | description="Hirsute Himalayan"), 10 | Creature(name="Bigfoot", 11 | description="Yeti's Cousin Eddie", 12 | country="US", 13 | area="*", 14 | aka="Sasquatch"), 15 | ] 16 | 17 | def get_all() -> list[Creature]: 18 | """Return all creatures""" 19 | return _creatures 20 | 21 | def get_one(name: str) -> Creature | None: 22 | """Return one creature""" 23 | for _creature in _creatures: 24 | if _creature.name == name: 25 | return _creature 26 | return None 27 | 28 | # The following are nonfunctional for now, 29 | # so they just act like they work, without modifying 30 | # the actual fake _creatures list: 31 | def create(creature: Creature) -> Creature: 32 | """Add a creature""" 33 | return creature 34 | 35 | def modify(creature: Creature) -> Creature: 36 | """Partially modify a creature""" 37 | return creature 38 | 39 | def replace(creature: Creature) -> Creature: 40 | """Completely replace a creature""" 41 | return creature 42 | 43 | def delete(name: str): 44 | """Delete a creature; return None if it existed""" 45 | return None 46 | -------------------------------------------------------------------------------- /example/8-14.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from model.explorer import Explorer 3 | import fake.explorer as service 4 | 5 | router = APIRouter(prefix = "/explorer") 6 | 7 | @router.get("/") 8 | def get_all() -> list[Explorer]: 9 | return service.get_all() 10 | 11 | @router.get("/{name}") 12 | def get_one(name) -> Explorer | None: 13 | return service.get_one(name) 14 | 15 | # all the remaining endpoints do nothing yet: 16 | @router.post("/") 17 | def create(explorer: Explorer) -> Explorer: 18 | return service.create(explorer) 19 | 20 | @router.patch("/") 21 | def modify(explorer: Explorer) -> Explorer: 22 | return service.modify(explorer) 23 | 24 | @router.put("/") 25 | def replace(explorer: Explorer) -> Explorer: 26 | return service.replace(explorer) 27 | 28 | @router.delete("/{name}") 29 | def delete(name: str): 30 | return None 31 | -------------------------------------------------------------------------------- /example/8-15.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from web import explorer, creature 4 | 5 | app = FastAPI() 6 | 7 | app.include_router(explorer.router) 8 | app.include_router(creature.router) 9 | 10 | if __name__ == "__main__": 11 | uvicorn.run("main:app", reload=True) 12 | -------------------------------------------------------------------------------- /example/8-15a.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from model.creature import Creature 3 | import fake.creature as service 4 | 5 | router = APIRouter(prefix = "/creature") 6 | 7 | @router.get("/") 8 | def get_all() -> list[Creature]: 9 | return service.get_all() 10 | 11 | @router.get("/{name}") 12 | def get_one(name) -> Creature: 13 | return service.get_one(name) 14 | 15 | # all the remaining endpoints do nothing yet: 16 | @router.post("/") 17 | def create(creature: Creature) -> Creature: 18 | return service.create(creature) 19 | 20 | @router.patch("/") 21 | def modify(creature: Creature) -> Creature: 22 | return service.modify(creature) 23 | 24 | @router.put("/") 25 | def replace(creature: Creature) -> Creature: 26 | return service.replace(creature) 27 | 28 | @router.delete("/{name}") 29 | def delete(name: str): 30 | return service.delete(name) 31 | -------------------------------------------------------------------------------- /example/8-4.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | 4 | app = FastAPI() 5 | 6 | @app.get("/") 7 | def top(): 8 | return "top here" 9 | 10 | @app.get("/echo/{thing}") 11 | def echo(thing): 12 | return f"echoing {thing}" 13 | 14 | if __name__ == "__main__": 15 | uvicorn.run("main:app", reload=True) 16 | -------------------------------------------------------------------------------- /example/8-7.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter(prefix = "/explorer") 4 | 5 | @router.get("/") 6 | def top(): 7 | return "top explorer endpoint" 8 | -------------------------------------------------------------------------------- /example/8-8.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from .web import explorer 3 | 4 | app = FastAPI() 5 | 6 | app.include_router(explorer.router) 7 | -------------------------------------------------------------------------------- /example/9-1.py: -------------------------------------------------------------------------------- 1 | from models.creature import Creature 2 | import fake.creature as data 3 | 4 | def get_all() -> list[Creature]: 5 | return data.get_all() 6 | 7 | def get_one(name: str) -> Creature | None: 8 | return data.get(id) 9 | 10 | def create(creature: Creature) -> Creature: 11 | return data.create(creature) 12 | 13 | def replace(id, creature: Creature) -> Creature: 14 | return data.replace(id, creature) 15 | 16 | def modify(id, creature: Creature) -> Creature: 17 | return data.modify(id, creature) 18 | 19 | def delete(id, creature: Creature) -> bool: 20 | return data.delete(id) 21 | -------------------------------------------------------------------------------- /example/9-2.py: -------------------------------------------------------------------------------- 1 | from models.explorer import Explorer 2 | import fake.explorer as data 3 | 4 | def get_all() -> list[Explorer]: 5 | return data.get_all() 6 | 7 | def get_one(name: str) -> Explorer | None: 8 | return data.get(name) 9 | 10 | def create(explorer: Explorer) -> Explorer: 11 | return data.create(explorer) 12 | 13 | def replace(id, explorer: Explorer) -> Explorer: 14 | return data.replace(id, explorer) 15 | 16 | def modify(id, explorer: Explorer) -> Explorer: 17 | return data.modify(id, explorer) 18 | 19 | def delete(id, explorer: Explorer) -> bool: 20 | return data.delete(id) 21 | -------------------------------------------------------------------------------- /example/9-3.py: -------------------------------------------------------------------------------- 1 | from model.creature import Creature 2 | from service import creature as code 3 | 4 | sample = Creature(name="yeti", 5 | country="CN", 6 | area="Himalayas", 7 | description="Hirsute Himalayan", 8 | aka="Abominable Snowman", 9 | ) 10 | 11 | def test_create(): 12 | resp = code.create(sample) 13 | assert resp == sample 14 | 15 | def test_get_exists(): 16 | resp = code.get_one("yeti") 17 | assert resp == sample 18 | 19 | def test_get_missing(): 20 | resp = code.get_one("boxturtle") 21 | assert resp is None 22 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # FastAPI: Modern Python Web Development 2 | 3 | This directory contains Python and Jinja template files matching the examples in the book. 4 | -------------------------------------------------------------------------------- /src/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/data/__init__.py -------------------------------------------------------------------------------- /src/data/creature.py: -------------------------------------------------------------------------------- 1 | from .init import (curs, IntegrityError) 2 | from model.creature import Creature 3 | from error import Missing, Duplicate 4 | 5 | curs.execute("""create table if not exists creature( 6 | name text primary key, 7 | country text, 8 | area text, 9 | description text, 10 | aka text)""") 11 | 12 | def row_to_model(row: tuple) -> Creature: 13 | name, country, area, description, aka = row 14 | return Creature(name=name, 15 | country=country, 16 | area=area, 17 | description=description, 18 | aka=aka) 19 | 20 | def model_to_dict(creature: Creature) -> dict: 21 | return creature.dict() 22 | 23 | def get_one(name: str) -> Creature: 24 | qry = "select * from creature where name=:name" 25 | params = {"name": name} 26 | curs.execute(qry, params) 27 | row = curs.fetchone() 28 | if row: 29 | return row_to_model(row) 30 | else: 31 | raise Missing(msg=f"Creature {name} not found") 32 | 33 | def get_all() -> list[Creature]: 34 | qry = "select * from creature" 35 | curs.execute(qry) 36 | return [row_to_model(row) for row in curs.fetchall()] 37 | 38 | def get_random_name() -> str: 39 | qry = "select name from creature order by random() limit 1" 40 | curs.execute(qry) 41 | row = curs.fetchone() 42 | name = row[0] 43 | return name 44 | 45 | def create(creature: Creature) -> Creature: 46 | qry = """insert into creature 47 | (name, country, area, description, aka) 48 | values 49 | (:name, :country, :area, :description, :aka)""" 50 | params = model_to_dict(creature) 51 | try: 52 | curs.execute(qry, params) 53 | return get_one(creature.name) 54 | except IntegrityError: 55 | raise Duplicate(msg= 56 | f"Creature {creature.name} already exists") 57 | 58 | def modify(name: str, creature: Creature) -> Creature: 59 | qry = """update creature set 60 | name=:name, 61 | country=:country, 62 | area=:area, 63 | description=:description, 64 | aka=:aka 65 | where name=:orig_name""" 66 | params = model_to_dict(creature) 67 | params["orig_name"] = name 68 | curs.execute(qry, params) 69 | if curs.rowcount == 1: 70 | return get_one(creature.name) 71 | else: 72 | raise Missing(msg=f"Creature {name} not found") 73 | 74 | def delete(name: str): 75 | qry = "delete from creature where name = :name" 76 | params = {"name": name} 77 | curs.execute(qry, params) 78 | if curs.rowcount != 1: 79 | raise Missing(msg=f"Creature {name} not found") 80 | -------------------------------------------------------------------------------- /src/data/explorer.py: -------------------------------------------------------------------------------- 1 | from .init import (curs, IntegrityError) 2 | from model.explorer import Explorer 3 | from error import Missing, Duplicate 4 | 5 | curs.execute("""create table if not exists explorer( 6 | name primary key, 7 | country text, 8 | description text)""") 9 | 10 | def row_to_model(row: tuple) -> Explorer: 11 | name, country, description = row 12 | return Explorer(name=name, country=country, 13 | description=description) 14 | 15 | def model_to_dict(explorer: Explorer) -> dict: 16 | return explorer.dict() 17 | 18 | def get_one(name: str) -> Explorer: 19 | qry = "select * from explorer where name=:name" 20 | params = {"name": name} 21 | curs.execute(qry, params) 22 | row = curs.fetchone() 23 | if row: 24 | return row_to_model(row) 25 | else: 26 | raise Missing(msg=f"Explorer {name} not found") 27 | 28 | def get_all() -> list[Explorer]: 29 | qry = "select * from explorer" 30 | curs.execute(qry) 31 | return [row_to_model(row) for row in curs.fetchall()] 32 | 33 | def create(explorer: Explorer) -> Explorer: 34 | if not explorer: return None 35 | qry = """insert into explorer 36 | (name, country, description) values 37 | (:name, :country, :description)""" 38 | params = model_to_dict(explorer) 39 | try: 40 | curs.execute(qry, params) 41 | except IntegrityError: 42 | raise Duplicate(msg= 43 | f"Explorer {explorer.name} already exists") 44 | return get_one(explorer.name) 45 | 46 | def modify(name: str, explorer: Explorer) -> Explorer: 47 | if not (name and explorer): return None 48 | qry = """update explorer 49 | set name=:name, country=:country, 50 | description=:description 51 | where name=:orig_name""" 52 | params = model_to_dict(explorer) 53 | params["orig_name"] = name 54 | curs.execute(qry, params) 55 | if curs.rowcount == 1: 56 | return get_one(explorer.name) 57 | else: 58 | raise Missing(msg=f"Explorer {name} not found") 59 | 60 | def delete(name: str): 61 | if not name: return False 62 | qry = "delete from explorer where name = :name" 63 | params = {"name": name} 64 | curs.execute(qry, params) 65 | if curs.rowcount != 1: 66 | raise Missing(msg=f"Explorer {name} not found") 67 | -------------------------------------------------------------------------------- /src/data/game.py: -------------------------------------------------------------------------------- 1 | from .init import curs 2 | 3 | def get_word() -> str: 4 | qry = "select name from creature order by random() limit 1" 5 | curs.execute(qry) 6 | row = curs.fetchone() 7 | if row: 8 | name = row[0] 9 | else: 10 | name = "bigfoot" 11 | return name 12 | -------------------------------------------------------------------------------- /src/data/init.py: -------------------------------------------------------------------------------- 1 | """Initialize SQLite database""" 2 | 3 | import os 4 | from pathlib import Path 5 | from sqlite3 import connect, Connection, Cursor, IntegrityError 6 | 7 | conn: Connection | None = None 8 | curs: Cursor | None = None 9 | 10 | def get_db(name: str|None = None, reset: bool = False): 11 | """Connect to SQLite database file""" 12 | global conn, curs 13 | if conn: 14 | if not reset: 15 | return 16 | conn = None 17 | if not name: 18 | name = os.getenv("CRYPTID_SQLITE_DB") 19 | top_dir = Path(__file__).resolve().parents[1] # repo top 20 | db_dir = top_dir / "db" 21 | db_name = "cryptid.db" 22 | db_path = str(db_dir / db_name) 23 | name = os.getenv("CRYPTID_SQLITE_DB", db_path) 24 | conn = connect(name, check_same_thread=False) 25 | curs = conn.cursor() 26 | 27 | get_db() 28 | -------------------------------------------------------------------------------- /src/data/user.py: -------------------------------------------------------------------------------- 1 | from model.user import User 2 | from .init import (curs, IntegrityError) 3 | from error import Missing, Duplicate 4 | 5 | curs.execute("""create table if not exists 6 | user( 7 | name text primary key, 8 | hash text)""") 9 | curs.execute("""create table if not exists 10 | xuser( 11 | name text primary key, 12 | hash text)""") 13 | 14 | def row_to_model(row: tuple) -> User: 15 | name, hash = row 16 | return User(name=name, hash=hash) 17 | 18 | def model_to_dict(user: User) -> dict: 19 | return user.dict() 20 | 21 | def get_one(name: str) -> User: 22 | qry = "select * from user where name=:name" 23 | params = {"name": name} 24 | curs.execute(qry, params) 25 | row = curs.fetchone() 26 | if row: 27 | return row_to_model(row) 28 | else: 29 | raise Missing(msg=f"User {name} not found") 30 | 31 | def get_all() -> list[User]: 32 | qry = "select * from user" 33 | curs.execute(qry) 34 | return [row_to_model(row) for row in curs.fetchall()] 35 | 36 | def create(user: User, table:str = "user") -> User: 37 | """Add to user or xuser table""" 38 | if table not in ("user", "xuser"): 39 | raise Exception(f"Invalid table name {table}") 40 | qry = f"""insert into {table} 41 | (name, hash) 42 | values 43 | (:name, :hash)""" 44 | params = model_to_dict(user) 45 | try: 46 | curs.execute(qry, params) 47 | except IntegrityError: 48 | raise Duplicate(msg= 49 | f"{table}: user {user.name} already exists") 50 | return user 51 | 52 | def modify(name: str, user: User) -> User: 53 | qry = """update user set 54 | name=:name, hash=:hash 55 | where name=:name0""" 56 | params = { 57 | "name": user.name, 58 | "hash": user.hash, 59 | "name0": name} 60 | curs.execute(qry, params) 61 | if curs.rowcount == 1: 62 | return get_one(user.name) 63 | else: 64 | raise Missing(msg=f"User {name} not found") 65 | 66 | def delete(name: str) -> None: 67 | """Drop user with from user table, add to xuser table""" 68 | user = get_one(name) 69 | qry = "delete from user where name = :name" 70 | params = {"name": name} 71 | curs.execute(qry, params) 72 | if curs.rowcount != 1: 73 | raise Missing(msg=f"User {name} not found") 74 | create(user, table="xuser") 75 | -------------------------------------------------------------------------------- /src/db/README.md: -------------------------------------------------------------------------------- 1 | # FastAPI: Modern Python Web Development 2 | This directory cointains data used by teh site: 3 | 4 | * `cryptid.db`: The SQLite database 5 | * `creature.psv`: The pipe (|)-separated creature text file 6 | * `explorer.psv`: The pipe-separated explorer text file 7 | * `load.sh`: Loads the psv files into the database 8 | -------------------------------------------------------------------------------- /src/db/creature.psv: -------------------------------------------------------------------------------- 1 | name|country|area|description|aka 2 | Abaia|FJ| |Lake eel| 3 | Afanc|UK|CYM|Welsh lake monster| 4 | Agropelter|US|ME|Forest twig flinger| 5 | Akkorokamui|JP| |Giant Ainu octopus| 6 | Albatwitch|US|PA|Apple stealing mini Bigfoot| 7 | Alicanto|CL| |Gold eating bird| 8 | Altamata-ha|US|GA|Swamp creature|Altie 9 | Amarok|CA| |Inuit wolf spirit| 10 | Auli|CY| |Ayia Napa Sea Monster|The Friendly Monster 11 | Azeban|CA| |Trickster spirit|The Raccoon 12 | Batsquatch|US|WA|Flying sasquatch| 13 | Beast of Bladenboro|US|NC|Dog bloodsucker| 14 | Beast of Bray Road|US|WI|Wisconsin werewolf| 15 | Beast of Busco|US|IN|Giant turtle| 16 | Beast of Gevaudan|FR| |French werewolf| 17 | Beaver Eater|CA| |Lodge flipper|Saytoechin 18 | Bigfoot|US| |Yeti's Cousin Eddie|Sasquatch 19 | Bukavac|HR| |Lake strangler| 20 | Bunyip|AU| |Aquatic Aussie| 21 | Cadborosaurus|CA|BC|Sea serpent|Caddie 22 | Champ|US|VT|Lake Champlain lurker|Champy 23 | Chupacabra|MX| |Goat bloodsucker| 24 | Dahu|FR| |French cousin of Wampahoofus| 25 | Doyarchu|IE| |Dog-otter|Irish crocodile 26 | Dragon|*| |Wings! Fire!| 27 | Drop bear|AU| |Carnivorous koala| 28 | Dungavenhooter|US| |Pounds prey to vapor, then inhales| 29 | Encantado|BR| |Frisky river dolphin| 30 | Fouke Monster|US|AR|Stinky bigfoot|Boggy Creek Monster 31 | Glocester Ghoul|US|RI|Rhode Island dragon| 32 | Gloucester Sea Serpent|US|MA|American Nessie| 33 | Igopogo|CA|ON|Canadian Nessie| 34 | Isshii|JP| |Lake monster|Issie 35 | Jackalope|US|*|Antlered rabbit| 36 | Jersey Devil|US|NJ|Snowy roof leaper| 37 | Kodiak Dinosaur|US|AK|Giant ocean saurian| 38 | Kraken|*| |Megasquid| 39 | Lizard Man|US|SC|Swamp creature| 40 | Llllaammaa|CL| |Head of a llama, body of a llama. But not the same llama.| 41 | Loch Ness Monster|UK|SC|Famed loch beastie|Nessie 42 | Lusca|BS| |Giant octopus| 43 | Maero|NZ| |Giants| 44 | Menehune|US|HI|Hawaiian elves| 45 | Mokele-mbembe|CG| |Swamp monster| 46 | Mongolian Death Worm|MN| |Arrakis visitor| 47 | Mothman|US|WV|Only cryptid with a Richard Gere movie| 48 | Nain Rouge|WS|MI|Red dwarf| 49 | Snarly Yow|US|MD|Hellhound| 50 | Vampire|*| |Bloodsucker| 51 | Vlad the Impala|KE| |Savannah vampire| 52 | Wendigo|CA| |Cannibal bigfoot| 53 | Werewolf|*| |Shapeshifter|Loup-garou, Rougarou 54 | Wyvern|UK| |Hind-legless dragon| 55 | Wampahoofus|US|VT|Asymmetric mountain dweller|Sidehill Gouger 56 | Yeti|CN| |Hirsute Himalayan|Abominable Snowman 57 | -------------------------------------------------------------------------------- /src/db/cryptid.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/db/cryptid.db -------------------------------------------------------------------------------- /src/db/explorer.psv: -------------------------------------------------------------------------------- 1 | name|country|description 2 | Claude Hande|UK|Scarce during full moons 3 | Helena Hande-Basquette|UK|Dame with a claim to fame 4 | Beau Buffette|US|Never removes his pith helmet 5 | O. B. Juan Cannoli|MX|Wise in the ways of the forest 6 | Simon N. Glorfindel|FR|Curly haired, keen-eared woodsman 7 | “Pa” Tuohy|IE|Explorer/expectorator 8 | Radha Tuohy|IN|Mystic earth mother 9 | Noah Weiser|DE|Myopic machete man 10 | -------------------------------------------------------------------------------- /src/db/load.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Load creature and explorer tables 4 | # from their psv text files. 5 | # Destroys any existing creature or explorer tables. 6 | 7 | sqlite3 cryptid.db < Creature | None: 18 | for c in fakes: 19 | if c.name == name: 20 | return c 21 | return None 22 | 23 | def check_missing(name: str): 24 | if not find(name): 25 | raise Missing(msg=f"Missing creature {name}") 26 | 27 | def check_duplicate(name: str): 28 | if find(name): 29 | raise Duplicate(msg=f"Duplicate creature {name}") 30 | 31 | def get_all() -> list[Creature]: 32 | """Return all creatures""" 33 | return fakes 34 | 35 | def get_one(name: str) -> Creature: 36 | """Return one creature""" 37 | check_missing(name) 38 | return find(name) 39 | 40 | # The following are non-functional for now, 41 | # so they just act like they work, without modifying 42 | # the actual fakes list: 43 | def create(creature: Creature) -> Creature: 44 | """Add a creature""" 45 | check_duplicate(creature.name) 46 | return creature 47 | 48 | def modify(name: str, creature: Creature) -> Creature: 49 | """Partially modify a creature""" 50 | check_missing(creature.name) 51 | return creature 52 | 53 | def delete(name: str) -> None: 54 | """Delete a creature""" 55 | check_missing(name) 56 | return None 57 | -------------------------------------------------------------------------------- /src/fake/explorer.py: -------------------------------------------------------------------------------- 1 | from model.explorer import Explorer 2 | from error import Missing, Duplicate 3 | 4 | fakes = [ 5 | Explorer(name="Claude Hande", 6 | country="FR", 7 | description="Scarce during full moons"), 8 | Explorer(name="Noah Weiser", 9 | country="DE", 10 | description="Myopic machete man"), 11 | ] 12 | 13 | def find(name: str) -> Explorer | None: 14 | for e in fakes: 15 | if e.name == name: 16 | return e 17 | return None 18 | 19 | def check_missing(name: str): 20 | if not find(name): 21 | raise Missing(msg=f"Missing explorer {name}") 22 | 23 | def check_duplicate(name: str): 24 | if find(name): 25 | raise Duplicate(msg=f"Duplicate explorer {name}") 26 | 27 | def get_all() -> list[Explorer]: 28 | """Return all explorers""" 29 | return fakes 30 | 31 | def get_one(name: str) -> Explorer: 32 | """Return one explorer""" 33 | check_missing(name) 34 | return find(name) 35 | 36 | def create(explorer: Explorer) -> Explorer: 37 | """Add a explorer""" 38 | check_duplicate(explorer.name) 39 | return explorer 40 | 41 | def modify(name: str, explorer: Explorer) -> Explorer: 42 | """Partially modify a explorer""" 43 | check_missing(name) 44 | return explorer 45 | 46 | def delete(name: str) -> None: 47 | """Delete a explorer""" 48 | check_missing(name) 49 | return None 50 | -------------------------------------------------------------------------------- /src/fake/user.py: -------------------------------------------------------------------------------- 1 | from model.user import User 2 | from error import Missing, Duplicate 3 | 4 | # (no hashed password checking in this module) 5 | fakes = [ 6 | User(name="kwijobo", 7 | hash="abc"), 8 | User(name="ermagerd", 9 | hash="xyz"), 10 | ] 11 | 12 | def find(name: str) -> User | None: 13 | for e in fakes: 14 | if e.name == name: 15 | return e 16 | return None 17 | 18 | def check_missing(name: str): 19 | if not find(name): 20 | raise Missing(msg=f"Missing user {name}") 21 | 22 | def check_duplicate(name: str): 23 | if find(name): 24 | raise Duplicate(msg=f"Duplicate user {name}") 25 | 26 | def get_all() -> list[User]: 27 | """Return all users""" 28 | return fakes 29 | 30 | def get_one(name: str) -> User: 31 | """Return one user""" 32 | check_missing(name) 33 | return find(name) 34 | 35 | def create(user: User) -> User: 36 | """Add a user""" 37 | check_duplicate(user.name) 38 | return user 39 | 40 | def modify(name: str, user: User) -> User: 41 | """Partially modify a user""" 42 | check_missing(name) 43 | return user 44 | 45 | def delete(name: str) -> None: 46 | """Delete a user""" 47 | check_missing(name) 48 | return None 49 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from web import creature, explorer, game, user 3 | 4 | app = FastAPI() 5 | 6 | app.include_router(explorer.router) 7 | app.include_router(creature.router) 8 | app.include_router(game.router) 9 | app.include_router(user.router) 10 | 11 | if __name__ == "__main__": 12 | import uvicorn 13 | uvicorn.run("main:app", 14 | host="localhost", port=8000, reload=True) 15 | -------------------------------------------------------------------------------- /src/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/model/__init__.py -------------------------------------------------------------------------------- /src/model/creature.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class Creature(BaseModel): 4 | name: str 5 | country: str 6 | area: str 7 | description: str 8 | aka: str 9 | -------------------------------------------------------------------------------- /src/model/explorer.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class Explorer(BaseModel): 4 | name: str 5 | country: str 6 | description: str 7 | -------------------------------------------------------------------------------- /src/model/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class User(BaseModel): 4 | name: str 5 | hash: str 6 | -------------------------------------------------------------------------------- /src/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/service/__init__.py -------------------------------------------------------------------------------- /src/service/creature.py: -------------------------------------------------------------------------------- 1 | import os 2 | from model.creature import Creature 3 | if os.getenv("CRYPTID_UNIT_TEST"): 4 | from fake import creature as data 5 | else: 6 | from data import creature as data 7 | 8 | def get_all() -> list[Creature]: 9 | return data.get_all() 10 | 11 | def get_one(name) -> Creature: 12 | return data.get_one(name) 13 | 14 | def create(creature: Creature) -> Creature: 15 | return data.create(creature) 16 | 17 | def modify(name: str, creature: Creature) -> Creature: 18 | return data.modify(name, creature) 19 | 20 | def delete(name: str) -> None: 21 | return data.delete(name) 22 | -------------------------------------------------------------------------------- /src/service/explorer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from model.explorer import Explorer 3 | if os.getenv("CRYPTID_UNIT_TEST"): 4 | from fake import explorer as data 5 | else: 6 | from data import explorer as data 7 | 8 | def get_all() -> list[Explorer]: 9 | return data.get_all() 10 | 11 | def get_one(name: str) -> Explorer: 12 | return data.get_one(name) 13 | 14 | def create(explorer: Explorer) -> Explorer: 15 | return data.create(explorer) 16 | 17 | def modify(name: str, explorer: Explorer) -> Explorer: 18 | return data.modify(name, explorer) 19 | 20 | def delete(name: str): 21 | return data.delete(name) 22 | -------------------------------------------------------------------------------- /src/service/game.py: -------------------------------------------------------------------------------- 1 | from collections import Counter, defaultdict 2 | import data.game as data 3 | 4 | HIT = "H" # right letter and position 5 | MISS = "M" # letter not in word 6 | CLOSE = "C" # letter in word but wrong position 7 | ERROR = "" 8 | 9 | def get_word() -> str: 10 | return data.get_word() 11 | 12 | def get_score(actual: str, guess: str) -> str: 13 | length: int = len(actual) 14 | if len(guess) != length: 15 | return ERROR 16 | actual_counter = Counter(actual) # {letter: count, ...} 17 | guess_counter = defaultdict(int) 18 | result = [MISS] * length 19 | for pos, letter in enumerate(guess): 20 | if letter == actual[pos]: 21 | result[pos] = HIT 22 | guess_counter[letter] += 1 23 | for pos, letter in enumerate(guess): 24 | if result[pos] == HIT: 25 | continue 26 | guess_counter[letter] += 1 27 | if (letter in actual and 28 | guess_counter[letter] <= actual_counter[letter]): 29 | result[pos] = CLOSE 30 | result = ''.join(result) 31 | return result 32 | -------------------------------------------------------------------------------- /src/service/user.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | from jose import jwt 4 | from model.user import User 5 | if os.getenv("CRYPTID_UNIT_TEST"): 6 | from fake import user as data 7 | else: 8 | from data import user as data 9 | 10 | from passlib.context import CryptContext 11 | 12 | TOKEN_EXPIRES = 15 # minutes 13 | 14 | # --- New auth stuff 15 | 16 | # Change SECRET_KEY for production! 17 | SECRET_KEY = "keep-it-secret-keep-it-safe" 18 | ALGORITHM = "HS256" 19 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 20 | 21 | def verify_password(plain: str, hash: str) -> bool: 22 | """Hash and compare with from the database""" 23 | return pwd_context.verify(plain, hash) 24 | 25 | def get_hash(plain: str) -> str: 26 | """Return the hash of a string""" 27 | return pwd_context.hash(plain) 28 | 29 | def get_jwt_username(token:str) -> str | None: 30 | """Return username from JWT access """ 31 | try: 32 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 33 | if not (username := payload.get("sub")): 34 | return None 35 | except jwt.JWTError: 36 | return None 37 | return username 38 | 39 | def get_current_user(token: str) -> User | None: 40 | """Decode an OAuth access and return the User""" 41 | if not (username := get_jwt_username(token)): 42 | return None 43 | if (user := lookup_user(username)): 44 | return user 45 | return None 46 | 47 | def lookup_user(name: str) -> User | None: 48 | """Return a matching User fron the database for """ 49 | if (user := data.get(name)): 50 | return user 51 | return None 52 | 53 | def auth_user(name: str, plain: str) -> User | None: 54 | """Authenticate user and password""" 55 | if not (user := lookup_user(name)): 56 | return None 57 | if not verify_password(plain, user.hash): 58 | return None 59 | return user 60 | 61 | def create_access_token(data: dict, 62 | expires: datetime.timedelta | None = None 63 | ): 64 | """Return a JWT access token""" 65 | src = data.copy() 66 | now = datetime.utcnow() 67 | expires = expires or datetime.timedelta(minutes=TOKEN_EXPIRES) 68 | src.update({"exp": now + expires}) 69 | encoded_jwt = jwt.encode(src, SECRET_KEY, algorithm=ALGORITHM) 70 | return encoded_jwt 71 | 72 | # --- CRUD passthrough stuff 73 | 74 | def get_all() -> list[User]: 75 | return data.get_all() 76 | 77 | def get_one(name) -> User: 78 | return data.get_one(name) 79 | 80 | def create(user: User) -> User: 81 | return data.create(user) 82 | 83 | def modify(name: str, user: User) -> User: 84 | return data.modify(name, user) 85 | 86 | def delete(name: str) -> None: 87 | return data.delete(name) 88 | -------------------------------------------------------------------------------- /src/static/abc.txt: -------------------------------------------------------------------------------- 1 | abc! 2 | -------------------------------------------------------------------------------- /src/static/form1.html: -------------------------------------------------------------------------------- 1 |
2 | Say hello to my little friend: 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/static/form2.html: -------------------------------------------------------------------------------- 1 |
2 | Say hello to my little friend: 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/static/index.html: -------------------------------------------------------------------------------- 1 | Oh. Hi! 2 | -------------------------------------------------------------------------------- /src/static/xyz/xyz.txt: -------------------------------------------------------------------------------- 1 | xyz 2 | -------------------------------------------------------------------------------- /src/template/game.html: -------------------------------------------------------------------------------- 1 | 2 | 26 | 27 | 28 | 66 |

Cryptonamicon

67 | 68 | 69 |
70 | 71 | 72 | 73 |
74 | 75 |
76 | {% for letter in word %}{% endfor %} 77 | 78 |

79 | 80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /src/template/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for creature in creatures: %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 |
Creatures
NameDescriptionCountryAreaAKA
{{ creature.name }}{{ creature.description }}{{ creature.country }}{{ creature.area }}{{ creature.aka }}
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% for explorer in explorers: %} 34 | 35 | 36 | 37 | 38 | 39 | {% endfor %} 40 |
Explorers
NameCountryDescription
{{ explorer.name }}{{ explorer.country }}{{ explorer.description }}
41 | 42 | -------------------------------------------------------------------------------- /src/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/test/__init__.py -------------------------------------------------------------------------------- /src/test/full/test_creature.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | from model.creature import Creature 4 | from main import app 5 | 6 | client = TestClient(app) 7 | 8 | @pytest.fixture(scope="session") 9 | def sample() -> Creature: 10 | return Creature(name="Cthulhu", 11 | description="ichorous", 12 | country="*", area="*", aka="Ancient One") 13 | 14 | def test_create(sample): 15 | resp = client.post("/creature", json=sample.dict()) 16 | assert resp.status_code == 201 17 | 18 | def test_create_duplicate(sample): 19 | resp = client.post("/creature", json=sample.dict()) 20 | assert resp.status_code == 409 21 | 22 | def test_get_one(sample): 23 | resp = client.get(f"/creature/{sample.name}") 24 | assert resp.json() == sample.dict() 25 | 26 | def test_get_one_missing(): 27 | resp = client.get("/creature/bobcat") 28 | assert resp.status_code == 404 29 | 30 | def test_modify(sample): 31 | resp = client.patch(f"/creature/{sample.name}", json=sample.dict()) 32 | assert resp.json() == sample.dict() 33 | 34 | def test_modify_missing(sample): 35 | resp = client.patch("/creature/rougarou", json=sample.dict()) 36 | assert resp.status_code == 404 37 | 38 | def test_delete(sample): 39 | resp = client.delete(f"/creature/{sample.name}") 40 | assert resp.status_code == 200 41 | assert resp.json() is None 42 | 43 | def test_delete_missing(sample): 44 | resp = client.delete(f"/creature/{sample.name}") 45 | assert resp.status_code == 404 46 | -------------------------------------------------------------------------------- /src/test/full/test_explorer.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import pytest 3 | import os 4 | os.environ["CRYPTID_UNIT_TEST"] = "true" 5 | from model.explorer import Explorer 6 | from web import explorer 7 | 8 | @pytest.fixture 9 | def sample() -> Explorer: 10 | return Explorer(name="Pa Tuohy", country="IE", 11 | description="Expectorating explorer") 12 | 13 | @pytest.fixture 14 | def fakes() -> list[Explorer]: 15 | return explorer.get_all() 16 | 17 | def assert_duplicate(exc): 18 | assert exc.value.status_code == 404 19 | assert "Duplicate" in exc.value.detail 20 | 21 | def assert_missing(exc): 22 | assert exc.value.status_code == 404 23 | assert "not found" in exc.value.detail 24 | 25 | def test_create(sample): 26 | assert explorer.create(sample) == sample 27 | 28 | def test_create_duplicate(fakes): 29 | with pytest.raises(HTTPException) as exc: 30 | _ = explorer.create(fakes[0]) 31 | assert_duplicate(exc) 32 | 33 | def test_get_one(fakes): 34 | assert explorer.get_one(fakes[0].name) == fakes[0] 35 | 36 | def test_get_one_missing(): 37 | with pytest.raises(HTTPException) as exc: 38 | _ = explorer.get_one("Buffy") 39 | assert_missing(exc) 40 | 41 | def test_modify(fakes): 42 | assert explorer.modify(fakes[0].name, fakes[0]) == fakes[0] 43 | 44 | def test_modify_missing(): 45 | who = Explorer(name="Tonks", description="Dog, not explorer", country="US") 46 | with pytest.raises(HTTPException) as exc: 47 | _ = explorer.modify(who.name, who) 48 | assert_missing(exc) 49 | 50 | def test_delete(fakes): 51 | assert explorer.delete(fakes[0].name) is None 52 | 53 | def test_delete_missing(sample): 54 | with pytest.raises(HTTPException) as exc: 55 | _ = explorer.delete("Wally") 56 | assert_missing(exc) 57 | -------------------------------------------------------------------------------- /src/test/full/test_user.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | from model.user import User 4 | from main import app 5 | 6 | client = TestClient(app) 7 | 8 | @pytest.fixture 9 | def sample() -> User: 10 | return User(name="elsa", hash="123") 11 | 12 | def test_create(sample): 13 | resp = client.post("/user", json=sample.dict()) 14 | assert resp.status_code == 201 15 | 16 | def test_create_duplicate(sample): 17 | resp = client.post("/user", json=sample.dict()) 18 | assert resp.status_code == 409 19 | 20 | def test_get_one(sample): 21 | resp = client.get(f"/user/{sample.name}") 22 | assert resp.json() == sample.dict() 23 | 24 | def test_get_one_missing(): 25 | resp = client.get("/user/bobcat") 26 | assert resp.status_code == 404 27 | 28 | def test_modify(sample): 29 | resp = client.patch(f"/user/{sample.name}", json=sample.dict()) 30 | assert resp.json() == sample.dict() 31 | 32 | def test_modify_missing(sample): 33 | resp = client.patch("/user/dumbledore", json=sample.dict()) 34 | assert resp.status_code == 404 35 | 36 | def test_delete(sample): 37 | resp = client.delete(f"/user/{sample.name}") 38 | assert resp.json() is None 39 | assert resp.status_code == 200 40 | 41 | def test_delete_missing(sample): 42 | resp = client.delete(f"/user/{sample.name}") 43 | assert resp.status_code == 404 44 | -------------------------------------------------------------------------------- /src/test/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/test/unit/__init__.py -------------------------------------------------------------------------------- /src/test/unit/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/test/unit/data/__init__.py -------------------------------------------------------------------------------- /src/test/unit/data/test_creature.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from model.creature import Creature 4 | from error import Missing, Duplicate 5 | 6 | # set this before data imports below for data.init 7 | os.environ["CRYPTID_SQLITE_DB"] = ":memory:" 8 | from data import creature 9 | 10 | @pytest.fixture 11 | def sample() -> Creature: 12 | return Creature(name="yeti", country="CN", area="Himalayas", 13 | description="Harmless Himalayan", 14 | aka="Abominable Snowman") 15 | 16 | def test_create(sample): 17 | resp = creature.create(sample) 18 | assert resp == sample 19 | 20 | def test_create_duplicate(sample): 21 | with pytest.raises(Duplicate): 22 | _ = creature.create(sample) 23 | 24 | def test_get_one(sample): 25 | resp = creature.get_one(sample.name) 26 | assert resp == sample 27 | 28 | def test_get_one_missing(): 29 | with pytest.raises(Missing): 30 | _ = creature.get_one("boxturtle") 31 | 32 | def test_modify(sample): 33 | creature.area = "Sesame Street" 34 | resp = creature.modify(sample.name, sample) 35 | assert resp == sample 36 | 37 | def test_modify_missing(): 38 | thing: Creature = Creature(name="snurfle", country="RU", area="", 39 | description="some thing", aka="") 40 | with pytest.raises(Missing): 41 | _ = creature.modify(thing.name, thing) 42 | 43 | def test_delete(sample): 44 | resp = creature.delete(sample.name) 45 | assert resp is None 46 | 47 | def test_delete_missing(sample): 48 | with pytest.raises(Missing): 49 | _ = creature.delete(sample.name) 50 | -------------------------------------------------------------------------------- /src/test/unit/data/test_explorer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from model.explorer import Explorer 4 | from error import Missing, Duplicate 5 | 6 | # -set this before data imports below call data.init 7 | os.environ["CRYPTID_SQLITE_DB"] = ":memory:" 8 | from data import explorer 9 | 10 | @pytest.fixture 11 | def sample() -> Explorer: 12 | return Explorer(name="Pa Tuohy", 13 | description="Expectorating explorer", 14 | country="IE") 15 | 16 | def test_create(sample): 17 | resp = explorer.create(sample) 18 | assert resp == sample 19 | 20 | def test_create_duplicate(sample): 21 | with pytest.raises(Duplicate): 22 | _ = explorer.create(sample) 23 | 24 | def test_get_exists(sample): 25 | resp = explorer.get_one(sample.name) 26 | assert resp == sample 27 | 28 | def test_get_missing(): 29 | with pytest.raises(Missing): 30 | _ = explorer.get_one("Sam Gamgee") 31 | 32 | def test_modify(sample): 33 | sample.country = "CA" 34 | resp = explorer.modify(sample.name, sample) 35 | assert resp == sample 36 | 37 | def test_modify_missing(): 38 | bob: Explorer = Explorer(name="Bob", description="Bob who?", 39 | country="BE") 40 | with pytest.raises(Missing): 41 | _ = explorer.modify(bob.name, bob) 42 | 43 | def test_delete(sample): 44 | resp = explorer.delete(sample.name) 45 | assert resp is None 46 | 47 | def test_delete_missing(sample): 48 | with pytest.raises(Missing): 49 | _ = explorer.delete(sample.name) 50 | -------------------------------------------------------------------------------- /src/test/unit/data/test_user.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from model.user import User 4 | from error import Missing, Duplicate 5 | 6 | # set this before data.init import below 7 | os.environ["CRYPTID_SQLITE_DB"] = ":memory:" 8 | from data import user 9 | 10 | @pytest.fixture 11 | def sample() -> User: 12 | return User(name="renfield", hash="abc") 13 | 14 | def test_create(sample): 15 | resp = user.create(sample) 16 | assert resp == sample 17 | 18 | def test_create_duplicate(sample): 19 | with pytest.raises(Duplicate): 20 | _ = user.create(sample) 21 | 22 | def test_get_one(sample): 23 | resp = user.get_one(sample.name) 24 | assert resp == sample 25 | 26 | def test_get_one_missing(): 27 | with pytest.raises(Missing): 28 | _ = user.get_one("boxturtle") 29 | 30 | def test_modify(sample): 31 | user.location = "Sesame Street" 32 | resp = user.modify(sample.name, sample) 33 | assert resp == sample 34 | 35 | def test_modify_missing(): 36 | thing: User = User(name="snurfle", hash="124") 37 | with pytest.raises(Missing): 38 | _ = user.modify(thing.name, thing) 39 | 40 | def test_delete(sample): 41 | resp = user.delete(sample.name) 42 | assert resp is None 43 | 44 | def test_delete_missing(sample): 45 | with pytest.raises(Missing): 46 | _ = user.delete(sample.name) 47 | -------------------------------------------------------------------------------- /src/test/unit/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/test/unit/service/__init__.py -------------------------------------------------------------------------------- /src/test/unit/service/test_creature.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import pytest 3 | import os 4 | os.environ["CRYPTID_UNIT_TEST"] = "true" 5 | from model.creature import Creature 6 | from web import creature 7 | 8 | @pytest.fixture 9 | def sample() -> Creature: 10 | return Creature(name="dragon", 11 | description="Wings! Fire!", 12 | country="*", 13 | area="*", 14 | aka="firedrake") 15 | 16 | @pytest.fixture 17 | def fakes() -> list[Creature]: 18 | return creature.get_all() 19 | 20 | def test_create(sample): 21 | assert creature.create(sample) == sample 22 | 23 | def test_create_duplicate(fakes): 24 | with pytest.raises(HTTPException) as exc: 25 | _ = creature.create(fakes[0]) 26 | assert exc.value.status_code == 404 27 | 28 | def test_get_one(fakes): 29 | assert creature.get_one(fakes[0].name) == fakes[0] 30 | 31 | def test_get_one_missing(): 32 | with pytest.raises(HTTPException) as exc: 33 | _ = creature.get_one("bobcat") 34 | assert exc.value.status_code == 404 35 | -------------------------------------------------------------------------------- /src/test/unit/service/test_explorer.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import pytest 3 | import os 4 | os.environ["CRYPTID_UNIT_TEST"] = "true" 5 | from model.explorer import Explorer 6 | from web import explorer 7 | 8 | @pytest.fixture 9 | def sample() -> Explorer: 10 | return Explorer(name="Pa Tuohy", 11 | description="The old sod", 12 | country="IE") 13 | 14 | @pytest.fixture 15 | def fakes() -> list[Explorer]: 16 | return explorer.get_all() 17 | 18 | def assert_duplicate(exc): 19 | assert exc.value.status_code == 404 20 | assert "Duplicate" in exc.value.msg 21 | 22 | def assert_missing(exc): 23 | assert exc.value.status_code == 404 24 | assert "Missing" in exc.value.msg 25 | 26 | def test_create(sample): 27 | assert explorer.create(sample) == sample 28 | 29 | def test_create_duplicate(fakes): 30 | with pytest.raises(HTTPException) as exc: 31 | _ = explorer.create(fakes[0]) 32 | assert_duplicate(exc) 33 | 34 | def test_get_one(fakes): 35 | assert explorer.get_one(fakes[0].name) == fakes[0] 36 | 37 | def test_get_one_missing(): 38 | with pytest.raises(HTTPException) as exc: 39 | _ = explorer.get_one("bobcat") 40 | assert_missing(exc) 41 | 42 | def test_modify(fakes): 43 | assert explorer.modify(fakes[0].name, fakes[0]) == fakes[0] 44 | 45 | def test_modify_missing(sample): 46 | with pytest.raises(HTTPException) as exc: 47 | _ = explorer.modify(sample.name, sample) 48 | assert_missing(exc) 49 | 50 | def test_delete(fakes): 51 | assert explorer.delete(fakes[0].name) is None 52 | 53 | def test_delete_missing(sample): 54 | with pytest.raises(HTTPException) as exc: 55 | _ = explorer.delete("emu") 56 | assert_missing(exc) 57 | -------------------------------------------------------------------------------- /src/test/unit/service/test_game.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from service import game 3 | 4 | word = "bigfoot" 5 | guesses = [ 6 | ("bigfoot", "HHHHHHH"), 7 | ("abcdefg", "MCMMMCC"), 8 | ("toofgib", "CCCHCCC"), 9 | ("wronglength", ""), 10 | ("", ""), 11 | ] 12 | 13 | @pytest.mark.parametrize("guess,score", guesses) 14 | def test_match(guess, score): 15 | print(guess, score) 16 | assert game.get_score(word, guess) == score 17 | -------------------------------------------------------------------------------- /src/test/unit/service/test_user.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import pytest 3 | import os 4 | os.environ["CRYPTID_UNIT_TEST"] = "true" 5 | from model.user import User 6 | from web import user 7 | 8 | @pytest.fixture 9 | def sample() -> User: 10 | return User(name="faxfayfaz", hash="ghi") 11 | 12 | @pytest.fixture 13 | def fakes() -> list[User]: 14 | return user.get_all() 15 | 16 | def test_create(sample): 17 | assert user.create(sample) == sample 18 | 19 | def test_create_duplicate(fakes): 20 | with pytest.raises(HTTPException) as exc: 21 | _ = user.create(fakes[0]) 22 | assert exc.value.status_code == 404 23 | 24 | def test_get_one(fakes): 25 | assert user.get_one(fakes[0].name) == fakes[0] 26 | 27 | def test_get_one_missing(): 28 | with pytest.raises(HTTPException) as exc: 29 | _ = user.get_one("bobcat") 30 | assert exc.value.status_code == 404 31 | -------------------------------------------------------------------------------- /src/test/unit/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/test/unit/web/__init__.py -------------------------------------------------------------------------------- /src/test/unit/web/test_creature.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import pytest 3 | import os 4 | os.environ["CRYPTID_UNIT_TEST"] = "true" 5 | from model.creature import Creature 6 | from web import creature 7 | from error import Missing, Duplicate 8 | 9 | @pytest.fixture 10 | def sample() -> Creature: 11 | return Creature(name="dragon", 12 | description="Wings! Fire!", 13 | country="*", area="*", aka="firedrake") 14 | 15 | @pytest.fixture 16 | def fakes() -> list[Creature]: 17 | return creature.get_all() 18 | 19 | def assert_duplicate(exc): 20 | assert exc.value.status_code == 404 21 | assert "Duplicate" in exc.value.msg 22 | 23 | def assert_missing(exc): 24 | assert exc.value.status_code == 404 25 | assert "Missing" in exc.value.msg 26 | 27 | def test_create(sample): 28 | assert creature.create(sample) == sample 29 | 30 | def test_create_duplicate(fakes): 31 | with pytest.raises(HTTPException) as exc: 32 | resp = creature.create(fakes[0]) 33 | assert_duplicate(exc) 34 | 35 | def test_get_one(fakes): 36 | assert creature.get_one(fakes[0].name) == fakes[0] 37 | 38 | def test_get_one_missing(): 39 | with pytest.raises(HTTPException) as exc: 40 | resp = creature.get_one("bobcat") 41 | assert_missing(exc) 42 | 43 | def test_modify(fakes): 44 | assert creature.modify(fakes[0].name, fakes[0]) == fakes[0] 45 | 46 | def test_modify_missing(sample): 47 | with pytest.raises(HTTPException) as exc: 48 | resp = creature.modify(sample.name, sample) 49 | assert_missing(exc) 50 | 51 | def test_delete(fakes): 52 | assert creature.delete(fakes[0].name) is None 53 | 54 | def test_delete_missing(sample): 55 | with pytest.raises(HTTPException) as exc: 56 | resp = creature.delete("emu") 57 | assert_missing(exc) 58 | -------------------------------------------------------------------------------- /src/test/unit/web/test_explorer.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import pytest 3 | import os 4 | os.environ["CRYPTID_UNIT_TEST"] = "true" 5 | from model.explorer import Explorer 6 | from web import explorer 7 | from error import Missing, Duplicate 8 | 9 | @pytest.fixture 10 | def sample() -> Explorer: 11 | return Explorer(name="Pa Tuohy", description="Gaelic gaffer", country="IE") 12 | 13 | @pytest.fixture 14 | def fakes() -> list[Explorer]: 15 | return explorer.get_all() 16 | 17 | def assert_duplicate(exc): 18 | assert exc.value.status_code == 404 19 | assert "Duplicate" in exc.value.msg 20 | 21 | def assert_missing(exc): 22 | assert exc.value.status_code == 404 23 | assert "Missing" in exc.value.msg 24 | 25 | def test_create(sample): 26 | assert explorer.create(sample) == sample 27 | 28 | def test_create_duplicate(fakes): 29 | with pytest.raises(HTTPException) as exc: 30 | resp = explorer.create(fakes[0]) 31 | assert_duplicate(exc) 32 | 33 | def test_get_one(fakes): 34 | assert explorer.get_one(fakes[0].name) == fakes[0] 35 | 36 | def test_get_one_missing(): 37 | with pytest.raises(HTTPException) as exc: 38 | resp = explorer.get_one("Buffy") 39 | assert_missing(exc) 40 | 41 | def test_modify(fakes): 42 | assert explorer.modify(fakes[0].name, fakes[0]) == fakes[0] 43 | 44 | def test_modify_missing(sample): 45 | with pytest.raises(HTTPException) as exc: 46 | resp = explorer.modify(sample.name, sample) 47 | assert_missing(exc) 48 | 49 | def test_delete(fakes): 50 | assert explorer.delete(fakes[0].name) is None 51 | 52 | def test_delete_missing(sample): 53 | with pytest.raises(HTTPException) as exc: 54 | resp = explorer.delete("Wally") 55 | assert_missing(exc) 56 | -------------------------------------------------------------------------------- /src/test/unit/web/test_json.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pytest 3 | from fastapi.encoders import jsonable_encoder 4 | import json 5 | 6 | @pytest.fixture 7 | def data(): 8 | return datetime.datetime.now() 9 | 10 | def test_json_dump(data): 11 | with pytest.raises(Exception): 12 | json_out = json.dumps(data) 13 | 14 | def test_encoder(data): 15 | out = jsonable_encoder(data) 16 | assert out 17 | print(out, type(out)) 18 | json_out = json.dumps(out) 19 | assert json_out 20 | print(json_out) 21 | -------------------------------------------------------------------------------- /src/test/unit/web/test_user.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import pytest 3 | import os 4 | os.environ["CRYPTID_UNIT_TEST"] = "true" 5 | from model.user import User 6 | from web import user 7 | from error import Missing, Duplicate 8 | 9 | @pytest.fixture 10 | def sample() -> User: 11 | return User(name="Pa Tuohy", hash="...") 12 | 13 | @pytest.fixture 14 | def fakes() -> list[User]: 15 | return user.get_all() 16 | 17 | def assert_duplicate(exc): 18 | assert exc.value.status_code == 404 19 | assert "Duplicate" in exc.value.msg 20 | 21 | def assert_missing(exc): 22 | assert exc.value.status_code == 404 23 | assert "Missing" in exc.value.msg 24 | 25 | def test_create(sample): 26 | assert user.create(sample) == sample 27 | 28 | def test_create_duplicate(fakes): 29 | with pytest.raises(HTTPException) as exc: 30 | resp = user.create(fakes[0]) 31 | assert_duplicate(exc) 32 | 33 | def test_get_one(fakes): 34 | assert user.get_one(fakes[0].name) == fakes[0] 35 | 36 | def test_get_one_missing(): 37 | with pytest.raises(HTTPException) as exc: 38 | resp = user.get_one("Buffy") 39 | assert_missing(exc) 40 | 41 | def test_modify(fakes): 42 | assert user.modify(fakes[0].name, fakes[0]) == fakes[0] 43 | 44 | def test_modify_missing(sample): 45 | with pytest.raises(HTTPException) as exc: 46 | resp = user.modify(sample.name, sample) 47 | assert_missing(exc) 48 | 49 | def test_delete(fakes): 50 | assert user.delete(fakes[0].name) is None 51 | 52 | def test_delete_missing(sample): 53 | with pytest.raises(HTTPException) as exc: 54 | resp = user.delete("Wally") 55 | assert_missing(exc) 56 | -------------------------------------------------------------------------------- /src/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madscheme/fastapi/693f48b693e08908c86353abd8c448b7d119a99e/src/web/__init__.py -------------------------------------------------------------------------------- /src/web/creature.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from fastapi import APIRouter, HTTPException 5 | from fastapi.templating import Jinja2Templates 6 | 7 | from model.creature import Creature 8 | if os.getenv("CRYPTID_UNIT_TEST"): 9 | from fake import creature as service 10 | else: 11 | from service import creature as service 12 | from error import Missing, Duplicate 13 | 14 | router = APIRouter(prefix = "/creature") 15 | top = Path(__file__).resolve().parent 16 | templates = Jinja2Templates(directory=f"{top}/template") 17 | 18 | @router.get("/") 19 | def get_all() -> list[Creature]: 20 | return service.get_all() 21 | 22 | @router.get("/{name}") 23 | def get_one(name: str) -> Creature: 24 | try: 25 | return service.get_one(name) 26 | except Missing as exc: 27 | raise HTTPException(status_code=404, detail=exc.msg) 28 | 29 | @router.post("/", status_code=201) 30 | def create(creature: Creature) -> Creature: 31 | try: 32 | return service.create(creature) 33 | except Duplicate as exc: 34 | raise HTTPException(status_code=409, detail=exc.msg) 35 | 36 | @router.patch("/{name}") 37 | def modify(name: str, creature: Creature) -> Creature: 38 | try: 39 | return service.modify(name, creature) 40 | except Missing as exc: 41 | raise HTTPException(status_code=404, detail=exc.msg) 42 | 43 | @router.delete("/{name}") 44 | def delete(name: str) -> None: 45 | try: 46 | return service.delete(name) 47 | except Missing as exc: 48 | raise HTTPException(status_code=404, detail=exc.msg) 49 | -------------------------------------------------------------------------------- /src/web/explorer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fastapi import APIRouter, HTTPException 3 | from model.explorer import Explorer 4 | if os.getenv("CRYPTID_UNIT_TEST"): 5 | from fake import explorer as service 6 | else: 7 | from service import explorer as service 8 | from error import Duplicate, Missing 9 | 10 | router = APIRouter(prefix = "/explorer") 11 | 12 | @router.get("") 13 | @router.get("/") 14 | def get_all() -> list[Explorer]: 15 | return service.get_all() 16 | 17 | @router.get("/{name}") 18 | def get_one(name) -> Explorer: 19 | try: 20 | return service.get_one(name) 21 | except Missing as exc: 22 | raise HTTPException(status_code=404, detail=exc.msg) 23 | 24 | @router.post("", status_code=201) 25 | @router.post("/", status_code=201) 26 | def create(explorer: Explorer) -> Explorer: 27 | try: 28 | return service.create(explorer) 29 | except Duplicate as exc: 30 | raise HTTPException(status_code=404, detail=exc.msg) 31 | 32 | @router.patch("/") 33 | def modify(name: str, explorer: Explorer) -> Explorer: 34 | try: 35 | return service.modify(name, explorer) 36 | except Missing as exc: 37 | raise HTTPException(status_code=404, detail=exc.msg) 38 | 39 | @router.delete("/{name}", status_code=204) 40 | def delete(name: str): 41 | try: 42 | return service.delete(name) 43 | except Missing as exc: 44 | raise HTTPException(status_code=404, detail=exc.msg) 45 | -------------------------------------------------------------------------------- /src/web/game.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from fastapi import APIRouter, Body, Request 4 | from fastapi.templating import Jinja2Templates 5 | 6 | from service import game as service 7 | 8 | router = APIRouter(prefix = "/game") 9 | 10 | # Initial game request 11 | @router.get("") 12 | def game_start(request: Request): 13 | name = service.get_word() 14 | top = Path(__file__).resolve().parents[1] # grandparent 15 | templates = Jinja2Templates(directory=f"{top}/template") 16 | return templates.TemplateResponse("game.html", 17 | {"request": request, "word": name}) 18 | 19 | 20 | # Subsequent game requests 21 | @router.post("") 22 | async def game_step(word: str = Body(), guess: str = Body()): 23 | score = service.get_score(word, guess) 24 | return score 25 | -------------------------------------------------------------------------------- /src/web/user.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | from fastapi import APIRouter, HTTPException, Depends 4 | from fastapi.security import ( 5 | OAuth2PasswordBearer, OAuth2PasswordRequestForm) 6 | from model.user import User 7 | if os.getenv("CRYPTID_UNIT_TEST"): 8 | from fake import user as service 9 | else: 10 | from service import user as service 11 | from error import Missing, Duplicate 12 | 13 | ACCESS_TOKEN_EXPIRE_MINUTES = 15 14 | 15 | router = APIRouter(prefix = "/user") 16 | 17 | # --- new auth stuff 18 | 19 | # This dependency makes a post to "/user/token" 20 | # (from a form containing a username and password) 21 | # return an access token. 22 | oauth2_dep = OAuth2PasswordBearer(tokenUrl="token") 23 | 24 | def unauthed(): 25 | raise HTTPException( 26 | status_code=401, 27 | detail="Incorrect username or password", 28 | headers={"WWW-Authenticate": "Bearer"}, 29 | ) 30 | 31 | # This endpoint is directed to by any call that has the 32 | # oauth2_dep() dependency: 33 | @router.post("/token") 34 | async def create_access_token( 35 | form_data: OAuth2PasswordRequestForm = Depends() 36 | ): 37 | """Get username and password from OAuth form, 38 | return access token""" 39 | user = service.auth_user(form_data.username, form_data.password) 40 | if not user: 41 | unauthed() 42 | expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 43 | access_token = service.create_access_token( 44 | data={"sub": user.username}, expires=expires 45 | ) 46 | return {"access_token": access_token, "token_type": "bearer"} 47 | 48 | @router.get("/token") 49 | def get_access_token(token: str = Depends(oauth2_dep)) -> dict: 50 | """Return the current access token""" 51 | return {"token": token} 52 | 53 | # --- previous CRUD stuff 54 | 55 | @router.get("/") 56 | def get_all() -> list[User]: 57 | return service.get_all() 58 | 59 | @router.get("/{name}") 60 | def get_one(name) -> User: 61 | try: 62 | return service.get_one(name) 63 | except Missing as exc: 64 | raise HTTPException(status_code=404, detail=exc.msg) 65 | 66 | @router.post("/", status_code=201) 67 | def create(user: User) -> User: 68 | try: 69 | return service.create(user) 70 | except Duplicate as exc: 71 | raise HTTPException(status_code=409, detail=exc.msg) 72 | 73 | @router.patch("/{name}") 74 | def modify(name: str, user: User) -> User: 75 | try: 76 | return service.modify(name, user) 77 | except Missing as exc: 78 | raise HTTPException(status_code=404, detail=exc.msg) 79 | 80 | @router.delete("/{name}") 81 | def delete(name: str) -> None: 82 | try: 83 | return service.delete(name) 84 | except Missing as exc: 85 | raise HTTPException(status_code=404, detail=exc.msg) 86 | --------------------------------------------------------------------------------