├── ch05 └── planner │ ├── database │ └── __init__.py │ ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── events.cpython-310.pyc │ │ ├── users.cpython-310.pyc │ │ └── __init__.cpython-310.pyc │ ├── events.py │ └── users.py │ ├── routes │ ├── __init__.py │ ├── __pycache__ │ │ ├── events.cpython-310.pyc │ │ ├── users.cpython-310.pyc │ │ └── __init__.cpython-310.pyc │ ├── users.py │ └── events.py │ ├── __pycache__ │ └── main.cpython-310.pyc │ ├── main.py │ └── requirements.txt ├── ch06 └── planner │ ├── database │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-310.pyc │ │ └── connection.cpython-310.pyc │ └── connection.py │ ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── events.cpython-310.pyc │ │ ├── users.cpython-310.pyc │ │ └── __init__.cpython-310.pyc │ ├── users.py │ └── events.py │ ├── routes │ ├── __init__.py │ ├── __pycache__ │ │ ├── events.cpython-310.pyc │ │ ├── users.cpython-310.pyc │ │ └── __init__.cpython-310.pyc │ ├── users.py │ └── events.py │ ├── __pycache__ │ └── main.cpython-310.pyc │ ├── requirements.txt │ └── main.py ├── ch07 └── planner │ ├── auth │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-310.pyc │ │ ├── jwt_handler.cpython-310.pyc │ │ ├── authenticate.cpython-310.pyc │ │ └── hash_password.cpython-310.pyc │ ├── hash_password.py │ ├── authenticate.py │ └── jwt_handler.py │ ├── database │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-310.pyc │ │ └── connection.cpython-310.pyc │ └── connection.py │ ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── events.cpython-310.pyc │ │ ├── users.cpython-310.pyc │ │ └── __init__.cpython-310.pyc │ ├── users.py │ └── events.py │ ├── routes │ ├── __init__.py │ ├── __pycache__ │ │ ├── events.cpython-310.pyc │ │ ├── users.cpython-310.pyc │ │ └── __init__.cpython-310.pyc │ ├── users.py │ └── events.py │ ├── __pycache__ │ └── main.cpython-310.pyc │ ├── requirements.txt │ └── main.py ├── ch08 └── planner │ ├── auth │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-310.pyc │ │ ├── jwt_handler.cpython-310.pyc │ │ ├── authenticate.cpython-310.pyc │ │ └── hash_password.cpython-310.pyc │ ├── hash_password.py │ ├── authenticate.py │ └── jwt_handler.py │ ├── database │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-310.pyc │ │ └── connection.cpython-310.pyc │ └── connection.py │ ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── events.cpython-310.pyc │ │ ├── users.cpython-310.pyc │ │ └── __init__.cpython-310.pyc │ ├── users.py │ └── events.py │ ├── routes │ ├── __init__.py │ ├── __pycache__ │ │ ├── events.cpython-310.pyc │ │ ├── users.cpython-310.pyc │ │ └── __init__.cpython-310.pyc │ ├── users.py │ └── events.py │ ├── pytest.ini │ ├── htmlcov │ ├── favicon_32.png │ ├── keybd_closed.png │ ├── keybd_open.png │ ├── status.json │ ├── d_e634d7a1dd90e049___init___py.html │ ├── d_60afb0a4f41d540c___init___py.html │ ├── d_f244bf8a352cf537___init___py.html │ ├── d_c44ac6e9bb193d64___init___py.html │ ├── d_60afb0a4f41d540c_hash_password_py.html │ ├── d_60afb0a4f41d540c_authenticate_py.html │ ├── d_a44f0ac069e85531_test_fixture_py.html │ ├── d_e634d7a1dd90e049_users_py.html │ ├── index.html │ ├── d_a44f0ac069e85531_test_arthmetic_operations_py.html │ ├── d_a44f0ac069e85531_conftest_py.html │ └── style.css │ ├── __pycache__ │ └── main.cpython-310.pyc │ ├── tests │ ├── __pycache__ │ │ ├── conftest.cpython-310-pytest-7.2.1.pyc │ │ ├── test_fixture.cpython-310-pytest-7.2.1.pyc │ │ ├── test_login.cpython-310-pytest-7.2.1.pyc │ │ ├── test_routes.cpython-310-pytest-7.2.1.pyc │ │ └── test_arthmetic_operations.cpython-310-pytest-7.2.1.pyc │ ├── test_arthmetic_operations.py │ ├── test_fixture.py │ ├── conftest.py │ ├── test_login.py │ └── test_routes.py │ ├── main.py │ └── requirements.txt ├── ch09 └── planner │ ├── auth │ ├── __init__.py │ ├── hash_password.py │ ├── authenticate.py │ └── jwt_handler.py │ ├── database │ ├── __init__.py │ └── connection.py │ ├── models │ ├── __init__.py │ ├── users.py │ └── events.py │ ├── routes │ ├── __init__.py │ ├── users.py │ └── events.py │ ├── pytest.ini │ ├── Dockerfile │ ├── requirements.txt │ ├── docker-compose.yml │ ├── tests │ ├── test_arthmetic_operations.py │ ├── test_fixture.py │ ├── conftest.py │ ├── test_login.py │ └── test_routes.py │ └── main.py ├── ch01 ├── hello.py ├── todos │ ├── requirements.txt │ ├── __pycache__ │ │ └── api.cpython-310.pyc │ └── api.py └── Dockerfile ├── ch02 └── todos │ ├── requirements.txt │ ├── __pycache__ │ ├── api.cpython-310.pyc │ ├── model.cpython-310.pyc │ └── todo.cpython-310.pyc │ ├── api.py │ ├── model.py │ └── todo.py ├── ch03 └── todos │ ├── requirements.txt │ ├── __pycache__ │ ├── api.cpython-310.pyc │ ├── model.cpython-310.pyc │ └── todo.cpython-310.pyc │ ├── api.py │ ├── model.py │ └── todo.py ├── ch04 └── todos │ ├── requirements.txt │ ├── __pycache__ │ ├── api.cpython-310.pyc │ ├── model.cpython-310.pyc │ └── todo.cpython-310.pyc │ ├── api.py │ ├── templates │ ├── home.html │ └── todo.html │ ├── model.py │ └── todo.py └── README.md /ch05/planner/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch05/planner/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch05/planner/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/planner/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/planner/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/planner/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/planner/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/planner/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/planner/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/planner/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch08/planner/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch08/planner/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch08/planner/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch08/planner/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/planner/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/planner/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/planner/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/planner/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch01/hello.py: -------------------------------------------------------------------------------- 1 | print("Hello!") 2 | -------------------------------------------------------------------------------- /ch01/todos/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | -------------------------------------------------------------------------------- /ch08/planner/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode=auto 3 | -------------------------------------------------------------------------------- /ch09/planner/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode=auto 3 | -------------------------------------------------------------------------------- /ch02/todos/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.70.0 2 | uvicorn==0.15.0 3 | -------------------------------------------------------------------------------- /ch03/todos/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.70.0 2 | uvicorn==0.15.0 3 | -------------------------------------------------------------------------------- /ch04/todos/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.70.0 2 | uvicorn==0.15.0 3 | jinja2 == 3.1.2 4 | python-multipart 5 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/favicon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/htmlcov/favicon_32.png -------------------------------------------------------------------------------- /ch08/planner/htmlcov/keybd_closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/htmlcov/keybd_closed.png -------------------------------------------------------------------------------- /ch08/planner/htmlcov/keybd_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/htmlcov/keybd_open.png -------------------------------------------------------------------------------- /ch01/todos/__pycache__/api.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch01/todos/__pycache__/api.cpython-310.pyc -------------------------------------------------------------------------------- /ch02/todos/__pycache__/api.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch02/todos/__pycache__/api.cpython-310.pyc -------------------------------------------------------------------------------- /ch03/todos/__pycache__/api.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch03/todos/__pycache__/api.cpython-310.pyc -------------------------------------------------------------------------------- /ch04/todos/__pycache__/api.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch04/todos/__pycache__/api.cpython-310.pyc -------------------------------------------------------------------------------- /ch02/todos/__pycache__/model.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch02/todos/__pycache__/model.cpython-310.pyc -------------------------------------------------------------------------------- /ch02/todos/__pycache__/todo.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch02/todos/__pycache__/todo.cpython-310.pyc -------------------------------------------------------------------------------- /ch03/todos/__pycache__/model.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch03/todos/__pycache__/model.cpython-310.pyc -------------------------------------------------------------------------------- /ch03/todos/__pycache__/todo.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch03/todos/__pycache__/todo.cpython-310.pyc -------------------------------------------------------------------------------- /ch04/todos/__pycache__/model.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch04/todos/__pycache__/model.cpython-310.pyc -------------------------------------------------------------------------------- /ch04/todos/__pycache__/todo.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch04/todos/__pycache__/todo.cpython-310.pyc -------------------------------------------------------------------------------- /ch05/planner/__pycache__/main.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch05/planner/__pycache__/main.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/__pycache__/main.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/__pycache__/main.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/__pycache__/main.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/__pycache__/main.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/__pycache__/main.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/__pycache__/main.cpython-310.pyc -------------------------------------------------------------------------------- /ch05/planner/models/__pycache__/events.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch05/planner/models/__pycache__/events.cpython-310.pyc -------------------------------------------------------------------------------- /ch05/planner/models/__pycache__/users.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch05/planner/models/__pycache__/users.cpython-310.pyc -------------------------------------------------------------------------------- /ch05/planner/routes/__pycache__/events.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch05/planner/routes/__pycache__/events.cpython-310.pyc -------------------------------------------------------------------------------- /ch05/planner/routes/__pycache__/users.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch05/planner/routes/__pycache__/users.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/models/__pycache__/events.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/models/__pycache__/events.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/models/__pycache__/users.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/models/__pycache__/users.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/routes/__pycache__/events.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/routes/__pycache__/events.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/routes/__pycache__/users.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/routes/__pycache__/users.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/auth/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/auth/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/models/__pycache__/events.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/models/__pycache__/events.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/models/__pycache__/users.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/models/__pycache__/users.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/routes/__pycache__/events.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/routes/__pycache__/events.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/routes/__pycache__/users.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/routes/__pycache__/users.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/auth/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/auth/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/models/__pycache__/events.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/models/__pycache__/events.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/models/__pycache__/users.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/models/__pycache__/users.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/routes/__pycache__/events.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/routes/__pycache__/events.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/routes/__pycache__/users.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/routes/__pycache__/users.cpython-310.pyc -------------------------------------------------------------------------------- /ch05/planner/models/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch05/planner/models/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch05/planner/routes/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch05/planner/routes/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/models/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/models/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/routes/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/routes/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/auth/__pycache__/jwt_handler.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/auth/__pycache__/jwt_handler.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/models/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/models/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/routes/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/routes/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/auth/__pycache__/jwt_handler.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/auth/__pycache__/jwt_handler.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/models/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/models/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/routes/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/routes/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/database/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/database/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/auth/__pycache__/authenticate.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/auth/__pycache__/authenticate.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/auth/__pycache__/hash_password.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/auth/__pycache__/hash_password.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/database/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/database/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/auth/__pycache__/authenticate.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/auth/__pycache__/authenticate.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/auth/__pycache__/hash_password.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/auth/__pycache__/hash_password.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/database/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/database/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /ch06/planner/database/__pycache__/connection.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch06/planner/database/__pycache__/connection.cpython-310.pyc -------------------------------------------------------------------------------- /ch07/planner/database/__pycache__/connection.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch07/planner/database/__pycache__/connection.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/database/__pycache__/connection.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/database/__pycache__/connection.cpython-310.pyc -------------------------------------------------------------------------------- /ch08/planner/tests/__pycache__/conftest.cpython-310-pytest-7.2.1.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/tests/__pycache__/conftest.cpython-310-pytest-7.2.1.pyc -------------------------------------------------------------------------------- /ch08/planner/tests/__pycache__/test_fixture.cpython-310-pytest-7.2.1.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/tests/__pycache__/test_fixture.cpython-310-pytest-7.2.1.pyc -------------------------------------------------------------------------------- /ch08/planner/tests/__pycache__/test_login.cpython-310-pytest-7.2.1.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/tests/__pycache__/test_login.cpython-310-pytest-7.2.1.pyc -------------------------------------------------------------------------------- /ch08/planner/tests/__pycache__/test_routes.cpython-310-pytest-7.2.1.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/tests/__pycache__/test_routes.cpython-310-pytest-7.2.1.pyc -------------------------------------------------------------------------------- /ch01/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | # 작업 디렉터리를 /usr/src/app으로 설정한다. 3 | WORKDIR /usr/src/app 4 | # 현재 로컬 디렉터리의 파일을 컨테이너의 작업 디렉터리로 복사한다. 5 | ADD . /usr/src/app 6 | # 명령을 실행한다. 7 | CMD ["python", "hello.py"] 8 | 9 | -------------------------------------------------------------------------------- /ch01/todos/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/") 7 | async def welcome() -> dict: 8 | return { 9 | "message": "Hello World" 10 | } 11 | -------------------------------------------------------------------------------- /ch08/planner/tests/__pycache__/test_arthmetic_operations.cpython-310-pytest-7.2.1.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanbit/web-with-fastapi/HEAD/ch08/planner/tests/__pycache__/test_arthmetic_operations.cpython-310-pytest-7.2.1.pyc -------------------------------------------------------------------------------- /ch02/todos/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from todo import todo_router 3 | 4 | app = FastAPI() 5 | 6 | @app.get("/") 7 | async def welcome() -> dict: 8 | return { 9 | "message": "Hello World" 10 | } 11 | 12 | app.include_router(todo_router) -------------------------------------------------------------------------------- /ch09/planner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt /app/requirements.txt 6 | 7 | RUN pip install --upgrade pip && pip install -r /app/requirements.txt 8 | 9 | EXPOSE 8000 10 | 11 | COPY ./ /app 12 | 13 | CMD ["python", "main.py"] 14 | -------------------------------------------------------------------------------- /ch03/todos/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from todo import todo_router 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.get("/") 9 | async def welcome() -> dict: 10 | return { 11 | "message": "Hello World" 12 | } 13 | 14 | 15 | app.include_router(todo_router) 16 | -------------------------------------------------------------------------------- /ch04/todos/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from todo import todo_router 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.get("/") 9 | async def welcome() -> dict: 10 | return { 11 | "message": "Hello World" 12 | } 13 | 14 | 15 | app.include_router(todo_router) 16 | -------------------------------------------------------------------------------- /ch09/planner/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.78.0 2 | bcrypt==3.2.2 3 | beanie==1.11.1 4 | email-validator==1.2.1 5 | httpx==0.22.0 6 | Jinja2==3.0.3 7 | motor==2.5.1 8 | passlib==1.7.4 9 | pytest==7.1.2 10 | python-multipart==.0.0.5 11 | python-dotenv==0.20.0 12 | python-jose==3.3.0 13 | sqlmodel==0.0.6 14 | uvicorn==0.17.6 15 | 16 | -------------------------------------------------------------------------------- /ch09/planner/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | build: . 6 | image: event-planner-api:latest 7 | ports: 8 | - "8000:8000" 9 | env_file: 10 | - .env.prod 11 | 12 | database: 13 | image: mongo 14 | ports: 15 | - "27017" 16 | volumes: 17 | - data:/data/db 18 | 19 | volumes: 20 | data: 21 | -------------------------------------------------------------------------------- /ch07/planner/auth/hash_password.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 4 | 5 | 6 | class HashPassword: 7 | def create_hash(self, password: str): 8 | return pwd_context.hash(password) 9 | 10 | def verify_hash(self, plain_password: str, hashed_password: str): 11 | return pwd_context.verify(plain_password, hashed_password) 12 | -------------------------------------------------------------------------------- /ch08/planner/auth/hash_password.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 4 | 5 | 6 | class HashPassword: 7 | def create_hash(self, password: str) -> str: 8 | return pwd_context.hash(password) 9 | 10 | def verify_hash(self, plain_password: str, hashed_password: str) -> bool: 11 | return pwd_context.verify(plain_password, hashed_password) 12 | -------------------------------------------------------------------------------- /ch09/planner/auth/hash_password.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 4 | 5 | 6 | class HashPassword: 7 | def create_hash(self, password: str) -> str: 8 | return pwd_context.hash(password) 9 | 10 | def verify_hash(self, plain_password: str, hashed_password: str) -> bool: 11 | return pwd_context.verify(plain_password, hashed_password) 12 | -------------------------------------------------------------------------------- /ch07/planner/models/users.py: -------------------------------------------------------------------------------- 1 | from beanie import Document 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class User(Document): 7 | email: EmailStr 8 | password: str 9 | 10 | class Collection: 11 | name = "users" 12 | 13 | class Config: 14 | schema_extra = { 15 | "example": { 16 | "email": "fastapi@packt.com", 17 | "password": "strong!!!" 18 | } 19 | } 20 | 21 | 22 | class TokenResponse(BaseModel): 23 | access_token: str 24 | token_type: str 25 | -------------------------------------------------------------------------------- /ch02/todos/model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Todo(BaseModel): 5 | id: int 6 | item: str 7 | 8 | class Config: 9 | schema_extra = { 10 | "example": { 11 | "id": 1, 12 | "item": "Example Schema!" 13 | } 14 | } 15 | 16 | 17 | class TodoItem(BaseModel): 18 | item: str 19 | 20 | class Config: 21 | schema_extra = { 22 | "example": { 23 | "item": "Read the next chapter of the book" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ch08/planner/models/users.py: -------------------------------------------------------------------------------- 1 | from beanie import Document 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class User(Document): 7 | email: EmailStr 8 | password: str 9 | 10 | class Settings: 11 | name = "users" 12 | 13 | class Config: 14 | schema_extra = { 15 | "example": { 16 | "email": "fastapi@packt.com", 17 | "password": "strong!!!" 18 | } 19 | } 20 | 21 | 22 | class TokenResponse(BaseModel): 23 | access_token: str 24 | token_type: str 25 | -------------------------------------------------------------------------------- /ch09/planner/models/users.py: -------------------------------------------------------------------------------- 1 | from beanie import Document 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | 6 | class User(Document): 7 | email: EmailStr 8 | password: str 9 | 10 | class Settings: 11 | name = "users" 12 | 13 | class Config: 14 | schema_extra = { 15 | "example": { 16 | "email": "fastapi@packt.com", 17 | "password": "strong!!!" 18 | } 19 | } 20 | 21 | 22 | class TokenResponse(BaseModel): 23 | access_token: str 24 | token_type: str 25 | -------------------------------------------------------------------------------- /ch05/planner/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.responses import RedirectResponse 3 | 4 | from routes.users import user_router 5 | from routes.events import event_router 6 | 7 | import uvicorn 8 | 9 | app = FastAPI() 10 | 11 | # 라우트 등록 12 | 13 | app.include_router(user_router, prefix="/user") 14 | app.include_router(event_router, prefix="/event") 15 | 16 | 17 | @app.get("/") 18 | async def home(): 19 | return RedirectResponse(url="/event/") 20 | 21 | if __name__ == '__main__': 22 | uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True) -------------------------------------------------------------------------------- /ch07/planner/auth/authenticate.py: -------------------------------------------------------------------------------- 1 | from auth.jwt_handler import verify_access_token 2 | from fastapi import Depends, HTTPException, status 3 | from fastapi.security import OAuth2PasswordBearer 4 | 5 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/user/signin") 6 | 7 | 8 | async def authenticate(token: str = Depends(oauth2_scheme)) -> str: 9 | if not token: 10 | raise HTTPException( 11 | status_code=status.HTTP_403_FORBIDDEN, 12 | detail="Sign in for access" 13 | ) 14 | 15 | decoded_token = verify_access_token(token) 16 | return decoded_token["user"] 17 | -------------------------------------------------------------------------------- /ch08/planner/auth/authenticate.py: -------------------------------------------------------------------------------- 1 | from auth.jwt_handler import verify_access_token 2 | from fastapi import Depends, HTTPException, status 3 | from fastapi.security import OAuth2PasswordBearer 4 | 5 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/user/signin") 6 | 7 | 8 | async def authenticate(token: str = Depends(oauth2_scheme)) -> str: 9 | if not token: 10 | raise HTTPException( 11 | status_code=status.HTTP_403_FORBIDDEN, 12 | detail="Sign in for access" 13 | ) 14 | 15 | decoded_token = await verify_access_token(token) 16 | return decoded_token["user"] 17 | -------------------------------------------------------------------------------- /ch09/planner/auth/authenticate.py: -------------------------------------------------------------------------------- 1 | from auth.jwt_handler import verify_access_token 2 | from fastapi import Depends, HTTPException, status 3 | from fastapi.security import OAuth2PasswordBearer 4 | 5 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/user/signin") 6 | 7 | 8 | async def authenticate(token: str = Depends(oauth2_scheme)) -> str: 9 | if not token: 10 | raise HTTPException( 11 | status_code=status.HTTP_403_FORBIDDEN, 12 | detail="Sign in for access" 13 | ) 14 | 15 | decoded_token = await verify_access_token(token) 16 | return decoded_token["user"] 17 | -------------------------------------------------------------------------------- /ch08/planner/tests/test_arthmetic_operations.py: -------------------------------------------------------------------------------- 1 | def add(a: int, b: int) -> int: 2 | return a + b 3 | 4 | 5 | def subtract(a: int, b: int) -> int: 6 | return b - a 7 | 8 | 9 | def multiply(a: int, b: int) -> int: 10 | return a * b 11 | 12 | 13 | def divide(a: int, b: int) -> int: 14 | return b // a 15 | 16 | 17 | def test_add() -> None: 18 | assert add(1, 1) == 2 19 | 20 | 21 | def test_subtract() -> None: 22 | assert subtract(2, 5) == 3 23 | 24 | 25 | def test_multiply() -> None: 26 | assert multiply(10, 10) == 100 27 | 28 | 29 | def test_divide() -> None: 30 | assert divide(25, 100) == 4 31 | -------------------------------------------------------------------------------- /ch09/planner/tests/test_arthmetic_operations.py: -------------------------------------------------------------------------------- 1 | def add(a: int, b: int) -> int: 2 | return a + b 3 | 4 | 5 | def subtract(a: int, b: int) -> int: 6 | return b - a 7 | 8 | 9 | def multiply(a: int, b: int) -> int: 10 | return a * b 11 | 12 | 13 | def divide(a: int, b: int) -> int: 14 | return b // a 15 | 16 | 17 | def test_add() -> None: 18 | assert add(1, 1) == 2 19 | 20 | 21 | def test_subtract() -> None: 22 | assert subtract(2, 5) == 3 23 | 24 | 25 | def test_multiply() -> None: 26 | assert multiply(10, 10) == 100 27 | 28 | 29 | def test_divide() -> None: 30 | assert divide(25, 100) == 4 31 | -------------------------------------------------------------------------------- /ch08/planner/tests/test_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | # Fixture is defined. 4 | from models.events import EventUpdate 5 | 6 | 7 | @pytest.fixture 8 | def event() -> EventUpdate: 9 | return EventUpdate( 10 | title="FastAPI Book Launch", 11 | image="https://packt.com/fastapi.png", 12 | description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 13 | tags=["python", "fastapi", "book", "launch"], 14 | location="Google Meet" 15 | ) 16 | 17 | 18 | def test_event_name(event: EventUpdate) -> None: 19 | assert event.title == "FastAPI Book Launch" 20 | -------------------------------------------------------------------------------- /ch09/planner/tests/test_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | # Fixture is defined. 4 | from models.events import EventUpdate 5 | 6 | 7 | @pytest.fixture 8 | def event() -> EventUpdate: 9 | return EventUpdate( 10 | title="FastAPI Book Launch", 11 | image="https://packt.com/fastapi.png", 12 | description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 13 | tags=["python", "fastapi", "book", "launch"], 14 | location="Google Meet" 15 | ) 16 | 17 | 18 | def test_event_name(event: EventUpdate) -> None: 19 | assert event.title == "FastAPI Book Launch" 20 | -------------------------------------------------------------------------------- /ch05/planner/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.5.0 2 | asgiref==3.5.0 3 | bcrypt==3.2.0 4 | beanie==1.10.4 5 | cffi==1.15.0 6 | click==8.0.4 7 | dnspython==2.2.0 8 | email-validator==1.1.3 9 | fastapi==0.74.1 10 | h11==0.13.0 11 | idna==3.3 12 | Jinja2==3.0.3 13 | MarkupSafe==2.1.0 14 | motor==2.5.1 15 | multidict==6.0.2 16 | passlib==1.7.4 17 | pycparser==2.21 18 | pydantic==1.9.0 19 | PyJWT==2.3.0 20 | pymongo==3.12.3 21 | python-dotenv==0.20.0 22 | python-multipart==0.0.5 23 | six==1.16.0 24 | sniffio==1.2.0 25 | SQLAlchemy==1.4.32 26 | sqlalchemy2-stubs==0.0.2a20 27 | sqlmodel==0.0.6 28 | starlette==0.17.1 29 | toml==0.10.2 30 | typing_extensions==4.1.1 31 | uvicorn==0.17.5 32 | yarl==1.7.2 33 | -------------------------------------------------------------------------------- /ch06/planner/models/users.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from beanie import Document, Link 3 | from pydantic import BaseModel, EmailStr 4 | from models.events import Event 5 | 6 | class User(Document): 7 | email: EmailStr 8 | password: str 9 | events: Optional[List[Event]] 10 | 11 | class Settings: 12 | name = "users" 13 | 14 | class Config: 15 | schema_extra = { 16 | "example": { 17 | "email": "fastapi@packt.com", 18 | "username": "strong!!!", 19 | "events": [], 20 | } 21 | } 22 | 23 | class UserSignIn(Document): 24 | email: EmailStr 25 | password: str 26 | -------------------------------------------------------------------------------- /ch06/planner/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.5.0 2 | asgiref==3.5.0 3 | bcrypt==3.2.0 4 | beanie==1.10.4 5 | cffi==1.15.0 6 | click==8.0.4 7 | dnspython==2.2.0 8 | email-validator==1.1.3 9 | fastapi==0.74.1 10 | h11==0.13.0 11 | idna==3.3 12 | Jinja2==3.0.3 13 | MarkupSafe==2.1.0 14 | motor==2.5.1 15 | multidict==6.0.2 16 | passlib==1.7.4 17 | pycparser==2.21 18 | pydantic==1.9.0 19 | PyJWT==2.3.0 20 | pymongo==3.12.3 21 | python-dotenv==0.20.0 22 | python-multipart==0.0.5 23 | six==1.16.0 24 | sniffio==1.2.0 25 | SQLAlchemy==1.4.32 26 | sqlalchemy2-stubs==0.0.2a20 27 | sqlmodel==0.0.6 28 | starlette==0.17.1 29 | toml==0.10.2 30 | typing_extensions==4.1.1 31 | uvicorn==0.17.5 32 | yarl==1.7.2 33 | -------------------------------------------------------------------------------- /ch07/planner/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.5.0 2 | asgiref==3.5.0 3 | bcrypt==3.2.0 4 | beanie==1.10.4 5 | cffi==1.15.0 6 | click==8.0.4 7 | cryptography==36.0.2 8 | dnspython==2.2.0 9 | ecdsa==0.17.0 10 | email-validator==1.1.3 11 | fastapi==0.74.1 12 | h11==0.13.0 13 | idna==3.3 14 | Jinja2==3.0.3 15 | MarkupSafe==2.1.0 16 | motor==2.5.1 17 | multidict==6.0.2 18 | passlib==1.7.4 19 | pyasn1==0.4.8 20 | pycparser==2.21 21 | pydantic==1.9.0 22 | pymongo==3.12.3 23 | python-dotenv==0.20.0 24 | python-jose==3.3.0 25 | python-multipart==0.0.5 26 | rsa==4.8 27 | six==1.16.0 28 | sniffio==1.2.0 29 | SQLAlchemy==1.4.32 30 | sqlalchemy2-stubs==0.0.2a20 31 | sqlmodel==0.0.6 32 | starlette==0.17.1 33 | toml==0.10.2 34 | typing_extensions==4.1.1 35 | uvicorn==0.17.5 36 | yarl==1.7.2 37 | -------------------------------------------------------------------------------- /ch05/planner/models/events.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Event(BaseModel): 7 | id: int 8 | title: str 9 | image: str 10 | description: str 11 | tags: List[str] 12 | location: str 13 | 14 | class Config: 15 | schema_extra = { 16 | "example": { 17 | "title": "FastAPI Book Launch", 18 | "image": "https://linktomyimage.com/image.png", 19 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 20 | "tags": ["python", "fastapi", "book", "launch"], 21 | "location": "Google Meet" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ch06/planner/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from fastapi.responses import RedirectResponse 4 | 5 | from database.connection import Settings 6 | from routes.events import event_router 7 | from routes.users import user_router 8 | 9 | app = FastAPI() 10 | 11 | settings = Settings() 12 | 13 | # 라우트 등록 14 | 15 | app.include_router(user_router, prefix="/user") 16 | app.include_router(event_router, prefix="/event") 17 | 18 | 19 | @app.on_event("startup") 20 | async def init_db(): 21 | await settings.initialize_database() 22 | 23 | 24 | @app.get("/") 25 | async def home(): 26 | return RedirectResponse(url="/event/") 27 | 28 | 29 | if __name__ == '__main__': 30 | uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True) 31 | -------------------------------------------------------------------------------- /ch05/planner/models/users.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | from typing import Optional, List 3 | from models.events import Event 4 | 5 | 6 | 7 | class User(BaseModel): 8 | email: EmailStr 9 | password: str 10 | events: Optional[List[Event]] 11 | 12 | 13 | class Config: 14 | schema_extra = { 15 | "example": { 16 | "email": "fastapi@packt.com", 17 | "username": "strong!!!", 18 | "events": [], 19 | } 20 | } 21 | 22 | 23 | 24 | 25 | class UserSignIn(BaseModel): 26 | email: EmailStr 27 | password: str 28 | 29 | class Config: 30 | schema_extra = { 31 | "example": { 32 | "email": "fastapi@packt.com", 33 | "password": "strong!!!", 34 | "events": [], 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ch08/planner/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import httpx 4 | import pytest 5 | 6 | from database.connection import Settings 7 | from main import app 8 | from models.events import Event 9 | from models.users import User 10 | 11 | 12 | @pytest.fixture(scope="session") 13 | def event_loop(): 14 | loop = asyncio.get_event_loop() 15 | yield loop 16 | loop.close() 17 | 18 | 19 | async def init_db(): 20 | test_settings = Settings() 21 | test_settings.DATABASE_URL = "mongodb://localhost:27017/testdb" 22 | 23 | await test_settings.initialize_database() 24 | 25 | 26 | @pytest.fixture(scope="session") 27 | async def default_client(): 28 | await init_db() 29 | async with httpx.AsyncClient(app=app, base_url="http://app") as client: 30 | yield client 31 | 32 | # 리소스 정리 33 | await Event.find_all().delete() 34 | await User.find_all().delete() 35 | -------------------------------------------------------------------------------- /ch09/planner/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import httpx 4 | import pytest 5 | from database.connection import Settings 6 | from main import app 7 | from models.events import Event 8 | from models.users import User 9 | 10 | 11 | @pytest.fixture(scope="session") 12 | def event_loop(): 13 | loop = asyncio.get_event_loop() 14 | yield loop 15 | loop.close() 16 | 17 | 18 | async def init_db(): 19 | test_settings = Settings() 20 | test_settings.DATABASE_URL = "mongodb://localhost:27017/testdb" 21 | 22 | await test_settings.initialize_database() 23 | 24 | 25 | @pytest.fixture(scope="session") 26 | async def default_client(): 27 | await init_db() 28 | async with httpx.AsyncClient(app=app, base_url="http://app") as client: 29 | yield client 30 | 31 | # Clean up resources 32 | await Event.find_all().delete() 33 | await User.find_all().delete() 34 | -------------------------------------------------------------------------------- /ch07/planner/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from fastapi.responses import RedirectResponse 4 | from fastapi.middleware.cors import CORSMiddleware 5 | 6 | from database.connection import Settings 7 | from routes.events import event_router 8 | from routes.users import user_router 9 | 10 | app = FastAPI() 11 | 12 | settings = Settings() 13 | 14 | # 출처 등록 15 | 16 | origins = ["*"] 17 | 18 | app.add_middleware( 19 | CORSMiddleware, 20 | allow_origins=origins, 21 | allow_credentials=True, 22 | allow_methods=["*"], 23 | allow_headers=["*"], 24 | ) 25 | 26 | # 라우트 등록 27 | 28 | app.include_router(user_router, prefix="/user") 29 | app.include_router(event_router, prefix="/event") 30 | 31 | 32 | @app.on_event("startup") 33 | async def init_db(): 34 | await settings.initialize_database() 35 | 36 | 37 | @app.get("/") 38 | async def home(): 39 | return RedirectResponse(url="/event/") 40 | 41 | 42 | if __name__ == '__main__': 43 | uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True) 44 | -------------------------------------------------------------------------------- /ch03/todos/model.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Todo(BaseModel): 7 | id: int 8 | item: str 9 | 10 | class Config: 11 | schema_extra = { 12 | "example": { 13 | "id": 1, 14 | "item": "Example schema!" 15 | } 16 | } 17 | 18 | class TodoItem(BaseModel): 19 | item: str 20 | 21 | class Config: 22 | schema_extra = { 23 | "example": { 24 | "item": "Read the next chapter of the book" 25 | } 26 | } 27 | 28 | 29 | class TodoItems(BaseModel): 30 | todos: List[TodoItem] 31 | 32 | class Config: 33 | schema_extra = { 34 | "example": { 35 | "todos": [ 36 | { 37 | "item": "Example schema 1!" 38 | }, 39 | { 40 | "item": "Example schema 2!" 41 | } 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ch08/planner/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from fastapi.responses import RedirectResponse 4 | from fastapi.middleware.cors import CORSMiddleware 5 | 6 | from database.connection import Settings 7 | from routes.events import event_router 8 | from routes.users import user_router 9 | 10 | app = FastAPI() 11 | 12 | settings = Settings() 13 | 14 | 15 | # 출처 등록 16 | 17 | origins = ["*"] 18 | 19 | app.add_middleware( 20 | CORSMiddleware, 21 | allow_origins=origins, 22 | allow_credentials=True, 23 | allow_methods=["*"], 24 | allow_headers=["*"], 25 | ) 26 | 27 | # 라우트 등록 28 | 29 | app.include_router(user_router, prefix="/user") 30 | app.include_router(event_router, prefix="/event") 31 | 32 | 33 | @app.on_event("startup") 34 | async def init_db(): 35 | await settings.initialize_database() 36 | 37 | 38 | @app.get("/") 39 | async def home(): 40 | return RedirectResponse(url="/event/") 41 | 42 | 43 | if __name__ == '__main__': 44 | uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True) 45 | -------------------------------------------------------------------------------- /ch09/planner/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from fastapi.responses import RedirectResponse 4 | from database.connection import Settings 5 | 6 | from routes.users import user_router 7 | from routes.events import event_router 8 | 9 | import uvicorn 10 | 11 | app = FastAPI() 12 | 13 | settings = Settings() 14 | 15 | 16 | # 출처 등록 17 | 18 | origins = ["*"] 19 | 20 | app.add_middleware( 21 | CORSMiddleware, 22 | allow_origins=origins, 23 | allow_credentials=True, 24 | allow_methods=["*"], 25 | allow_headers=["*"], 26 | ) 27 | 28 | 29 | # 라우트 등록 30 | 31 | app.include_router(user_router, prefix="/user") 32 | app.include_router(event_router, prefix="/event") 33 | 34 | 35 | @app.on_event("startup") 36 | async def init_db(): 37 | await settings.initialize_database() 38 | 39 | 40 | @app.get("/") 41 | async def home(): 42 | return RedirectResponse(url="/event/") 43 | 44 | if __name__ == '__main__': 45 | uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True) -------------------------------------------------------------------------------- /ch04/todos/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Packt Todo Application 8 | 10 | 12 | 13 | 14 |
15 | 22 |
23 |
24 | {% block todo_container %}{% endblock %} 25 |
26 | 27 | -------------------------------------------------------------------------------- /ch08/planner/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.5.0 2 | asgi-lifespan==1.0.1 3 | asgiref==3.5.0 4 | attrs==21.4.0 5 | bcrypt==3.2.2 6 | beanie==1.11.0 7 | certifi==2021.10.8 8 | cffi==1.15.0 9 | charset-normalizer==2.0.12 10 | click==8.0.4 11 | coverage==6.3.3 12 | cryptography==36.0.2 13 | dnspython==2.2.1 14 | ecdsa==0.17.0 15 | email-validator==1.1.3 16 | fastapi==0.77.1 17 | h11==0.12.0 18 | httpcore==0.14.7 19 | httpx==0.22.0 20 | idna==3.3 21 | iniconfig==1.1.1 22 | Jinja2==3.0.3 23 | MarkupSafe==2.1.0 24 | motor==3.0.0 25 | multidict==6.0.2 26 | packaging==21.3 27 | passlib==1.7.4 28 | pluggy==1.0.0 29 | py==1.11.0 30 | pyasn1==0.4.8 31 | pycparser==2.21 32 | pydantic==1.9.0 33 | pymongo==4.1.1 34 | pyparsing==3.0.9 35 | pytest==7.1.2 36 | pytest-asyncio==0.18.3 37 | python-dotenv==0.20.0 38 | python-jose==3.3.0 39 | python-multipart==0.0.5 40 | rfc3986==1.5.0 41 | rsa==4.8 42 | six==1.16.0 43 | sniffio==1.2.0 44 | SQLAlchemy==1.4.32 45 | sqlalchemy2-stubs==0.0.2a20 46 | sqlmodel==0.0.6 47 | starlette==0.19.1 48 | toml==0.10.2 49 | tomli==2.0.1 50 | typing_extensions==4.1.1 51 | uvicorn==0.17.6 52 | yarl==1.7.2 53 | -------------------------------------------------------------------------------- /ch05/planner/routes/users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException, status 2 | 3 | from models.users import User, UserSignIn 4 | 5 | user_router = APIRouter( 6 | tags=["User"], 7 | ) 8 | 9 | users = {} 10 | 11 | 12 | @user_router.post("/signup") 13 | async def sign_new_user(data: User) -> dict: 14 | if data.email in users: 15 | raise HTTPException( 16 | status_code=status.HTTP_409_CONFLICT, 17 | detail="User with supplied username exists" 18 | ) 19 | users[data.email] = data 20 | return { 21 | "message": "User successfully registered!" 22 | } 23 | 24 | 25 | 26 | @user_router.post("/signin") 27 | async def sign_user_in(user: UserSignIn) -> dict: 28 | if user.email not in users: 29 | raise HTTPException( 30 | status_code=status.HTTP_404_NOT_FOUND, 31 | detail="User does not exist" 32 | ) 33 | 34 | if users[user.email].password != user.password: 35 | raise HTTPException( 36 | status_code=status.HTTP_403_FORBIDDEN, 37 | detail="Wrong credential passed" 38 | ) 39 | return { 40 | "message": "User signed in successfully" 41 | } 42 | -------------------------------------------------------------------------------- /ch04/todos/model.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import Form 4 | from pydantic import BaseModel 5 | 6 | 7 | class Todo(BaseModel): 8 | id: Optional[int] 9 | item: str 10 | 11 | @classmethod 12 | def as_form( 13 | cls, 14 | item: str = Form(...) 15 | ): 16 | return cls(item=item) 17 | 18 | 19 | class Config: 20 | schema_extra = { 21 | "example": { 22 | "id": 1, 23 | "item": "Example schema!" 24 | } 25 | } 26 | 27 | 28 | class TodoItem(BaseModel): 29 | item: str 30 | 31 | class Config: 32 | schema_extra = { 33 | "example": { 34 | "item": "Read the next chapter of the book" 35 | } 36 | } 37 | 38 | 39 | class TodoItems(BaseModel): 40 | todos: List[TodoItem] 41 | 42 | class Config: 43 | schema_extra = { 44 | "example": { 45 | "todos": [ 46 | { 47 | "item": "Example schema 1!" 48 | }, 49 | { 50 | "item": "Example schema 2!" 51 | } 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ch08/planner/tests/test_login.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | 4 | 5 | @pytest.mark.asyncio 6 | async def test_sign_new_user(default_client: httpx.AsyncClient) -> None: 7 | payload = { 8 | "email": "testuser@packt.com", 9 | "password": "testpassword", 10 | } 11 | 12 | headers = { 13 | "accept": "application/json", 14 | "Content-Type": "application/json" 15 | } 16 | 17 | test_response = { 18 | "message": "User created successfully" 19 | } 20 | 21 | response = await default_client.post("/user/signup", json=payload, headers=headers) 22 | 23 | assert response.status_code == 200 24 | assert response.json() == test_response 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_sign_user_in(default_client: httpx.AsyncClient) -> None: 29 | payload = { 30 | "username": "testuser@packt.com", 31 | "password": "testpassword" 32 | } 33 | 34 | headers = { 35 | "accept": "application/json", 36 | "Content-Type": "application/x-www-form-urlencoded" 37 | } 38 | 39 | response = await default_client.post("/user/signin", data=payload, headers=headers) 40 | 41 | assert response.status_code == 200 42 | assert response.json()["token_type"] == "Bearer" 43 | -------------------------------------------------------------------------------- /ch09/planner/tests/test_login.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | 4 | 5 | @pytest.mark.asyncio 6 | async def test_sign_new_user(default_client: httpx.AsyncClient) -> None: 7 | payload = { 8 | "email": "testuser@packt.com", 9 | "password": "testpassword", 10 | } 11 | 12 | headers = { 13 | "accept": "application/json", 14 | "Content-Type": "application/json" 15 | } 16 | 17 | test_response = { 18 | "message": "User created successfully" 19 | } 20 | 21 | response = await default_client.post("/user/signup", json=payload, headers=headers) 22 | 23 | assert response.status_code == 200 24 | assert response.json() == test_response 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_sign_user_in(default_client: httpx.AsyncClient) -> None: 29 | payload = { 30 | "username": "testuser@packt.com", 31 | "password": "testpassword" 32 | } 33 | 34 | headers = { 35 | "accept": "application/json", 36 | "Content-Type": "application/x-www-form-urlencoded" 37 | } 38 | 39 | response = await default_client.post("/user/signin", data=payload, headers=headers) 40 | 41 | assert response.status_code == 200 42 | assert response.json()["token_type"] == "Bearer" 43 | -------------------------------------------------------------------------------- /ch07/planner/auth/jwt_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | 4 | from database.connection import Settings 5 | from fastapi import HTTPException, status 6 | from jose import jwt, JWTError 7 | 8 | settings = Settings() 9 | 10 | 11 | def create_access_token(user: str): 12 | payload = { 13 | "user": user, 14 | "expires": time.time() + 3600 15 | } 16 | 17 | token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") 18 | return token 19 | 20 | 21 | def verify_access_token(token: str): 22 | try: 23 | data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) 24 | 25 | expire = data.get("expires") 26 | 27 | if expire is None: 28 | raise HTTPException( 29 | status_code=status.HTTP_400_BAD_REQUEST, 30 | detail="No access token supplied" 31 | ) 32 | if datetime.utcnow() > datetime.utcfromtimestamp(expire): 33 | raise HTTPException( 34 | status_code=status.HTTP_403_FORBIDDEN, 35 | detail="Token expired!" 36 | ) 37 | return data 38 | 39 | except JWTError: 40 | raise HTTPException( 41 | status_code=status.HTTP_400_BAD_REQUEST, 42 | detail="Invalid token" 43 | ) 44 | -------------------------------------------------------------------------------- /ch06/planner/routes/users.py: -------------------------------------------------------------------------------- 1 | from database.connection import Database 2 | from fastapi import APIRouter, HTTPException, status 3 | from models.users import User, UserSignIn 4 | 5 | user_router = APIRouter( 6 | tags=["User"], 7 | ) 8 | 9 | user_database = Database(User) 10 | 11 | 12 | @user_router.post("/signup") 13 | async def sign_user_up(user: User) -> dict: 14 | user_exist = await User.find_one(User.email == user.email) 15 | 16 | if user_exist: 17 | raise HTTPException( 18 | status_code=status.HTTP_409_CONFLICT, 19 | detail="User with email provided exists already." 20 | ) 21 | await user_database.save(user) 22 | return { 23 | "message": "User created successfully" 24 | } 25 | 26 | 27 | @user_router.post("/signin") 28 | async def sign_user_in(user: UserSignIn) -> dict: 29 | user_exist = await User.find_one(User.email == user.email) 30 | if not user_exist: 31 | raise HTTPException( 32 | status_code=status.HTTP_404_NOT_FOUND, 33 | detail="User with email does not exist." 34 | ) 35 | if user_exist.password == user.password: 36 | return { 37 | "message": "User signed in successfully." 38 | } 39 | 40 | raise HTTPException( 41 | status_code=status.HTTP_401_UNAUTHORIZED, 42 | detail="Invalid details passed." 43 | ) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI를 사용한 파이썬 웹 개발 2 | 3 | 4 | 5 | 『[FastAPI를 사용한 파이썬 웹 개발](https://www.hanbit.co.kr/store/books/look.php?p_code=B9703548802)』(한빛미디어, 2023) 예제 코드 저장소입니다. 6 | 이 저장소의 코드는 원서의 저자가 제공하는 코드 중 실행 오류가 발생하거나 오탈자가 있는 부분을 수정한 것입니다. 7 | 8 | 원서의 저자가 제공하는 코드는 [여기](https://github.com/PacktPublishing/Building-Python-Web-APIs-with-FastAPI)에서 확인할 수 있습니다. 9 | 10 | 11 | ## 이 책에 대하여 12 | FastAPI는 파이썬으로 API를 구축할 수 있게 해주는 빠르고 효율적인 웹 프레임워크입니다. 이 책은 FastAPI 프레임워크를 사용한 애플리케이션 구축 방법을 안내합니다. 먼저 책에서 사용하는 기술의 기본 개념을 살펴보고 라우팅 시스템, 응답 모델링, 오류 처리, 템플릿 등 FastAPI 프레임워크의 주요 기능을 설명합니다. 13 | 14 | 여러분은 파이썬과 FastAPI를 사용해서 빠르고 효율적이며 확장 가능한 애플리케이션 구축 방법을 배우게 됩니다. 간단한 ‘Hello World’ 애플리케이션 개발부터 데이터베이스, 인증, 템플릿 등을 사용한 전체적인 API 구축 방법을 다루고 효율성, 가독성, 확장성을 개선하는 애플리케이션 설계 방법을 학습합니다. 또한 애플리케이션을 외부 라이브러리와 연동해서 SQL 또는 NoSQL 데이터베이스에 연결하고, 템플릿을 통합하고, 인증 시스템을 개발하는 방법도 배웁니다. 책의 후반부에서는 테스트 작성, 애플리케이션 컨테이너화 방법을 살펴보고 도커를 사용해 애플리케이션을 배포합니다. 이 모든 내용은 실습과 함께 설명합니다. 15 | 16 | 책을 다 읽고 나면 FastAPI 프레임워크를 사용해서 강력한 웹 API를 구축하고 배포할 수 있습니다. 17 | 18 | 19 | ## 목차 20 | **PART 1. FastAPI 학습하기** 21 | CHAPTER 1. FastAPI 소개 22 | CHAPTER 2. 라우팅 23 | CHAPTER 3. 응답 모델과 오류 처리 24 | CHAPTER 4. 템플릿팅 25 | 26 | **PART 2. FastAPI 애플리케이션 개발하기** 27 | CHAPTER 5. 구조화 28 | CHAPTER 6. 데이터베이스 연결 29 | CHAPTER 7. 보안 30 | 31 | **PART 3. FastAPI 애플리케이션 테스트 및 배포하기** 32 | CHAPTER 8. 테스트 33 | CHAPTER 9. 배포 34 | -------------------------------------------------------------------------------- /ch07/planner/models/events.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from beanie import Document 4 | from pydantic import BaseModel 5 | 6 | 7 | class Event(Document): 8 | creator: Optional[str] 9 | title: str 10 | image: str 11 | description: str 12 | tags: List[str] 13 | location: str 14 | 15 | class Config: 16 | schema_extra = { 17 | "example": { 18 | "title": "FastAPI BookLaunch", 19 | "image": "https://linktomyimage.com/image.png", 20 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 21 | "tags": ["python", "fastapi", "book", "launch"], 22 | "location": "Google Meet" 23 | } 24 | } 25 | 26 | class Collection: 27 | name = "events" 28 | 29 | 30 | class EventUpdate(BaseModel): 31 | title: Optional[str] 32 | image: Optional[str] 33 | description: Optional[str] 34 | tags: Optional[List[str]] 35 | location: Optional[str] 36 | 37 | class Config: 38 | schema_extra = { 39 | "example": { 40 | "title": "FastAPI BookLaunch", 41 | "image": "https://linktomyimage.com/image.png", 42 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 43 | "tags": ["python", "fastapi", "book", "launch"], 44 | "location": "Google Meet" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ch06/planner/models/events.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from beanie import Document 4 | from pydantic import BaseModel 5 | 6 | 7 | class Event(Document): 8 | title: str 9 | image: str 10 | description: str 11 | tags: List[str] 12 | location: str 13 | 14 | class Config: 15 | schema_extra = { 16 | "example": { 17 | "title": "FastAPI BookLaunch", 18 | "image": "https://linktomyimage.com/image.png", 19 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 20 | "tags": ["python", "fastapi", "book", "launch"], 21 | "location": "Google Meet" 22 | } 23 | } 24 | 25 | class Settings: 26 | name = "events" 27 | 28 | 29 | class EventUpdate(BaseModel): 30 | title: Optional[str] 31 | image: Optional[str] 32 | description: Optional[str] 33 | tags: Optional[List[str]] 34 | location: Optional[str] 35 | 36 | class Config: 37 | schema_extra = { 38 | "example": { 39 | "title": "FastAPI BookLaunch", 40 | "image": "https://linktomyimage.com/image.png", 41 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 42 | "tags": ["python", "fastapi", "book", "launch"], 43 | "location": "Google Meet" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ch05/planner/routes/events.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Body, HTTPException, status 4 | from models.events import Event 5 | 6 | event_router = APIRouter( 7 | tags=["Events"] 8 | ) 9 | 10 | events = [] 11 | 12 | 13 | @event_router.get("/", response_model=List[Event]) 14 | async def retrieve_all_events() -> List[Event]: 15 | return events 16 | 17 | @event_router.get("/{id}", response_model=Event) 18 | async def retrieve_event(id: int) -> Event: 19 | for event in events: 20 | if event.id == id: 21 | return event 22 | raise HTTPException( 23 | status_code=status.HTTP_404_NOT_FOUND, 24 | detail="Event with supplied ID does not exist" 25 | ) 26 | 27 | 28 | @event_router.post("/new") 29 | async def create_event(body: Event = Body(...)) -> dict: 30 | events.append(body) 31 | return { 32 | "message": "Event created successfully" 33 | } 34 | 35 | 36 | @event_router.delete("/{id}") 37 | async def delete_event(id: int) -> dict: 38 | for event in events: 39 | if event.id == id: 40 | events.remove(event) 41 | return { 42 | "message": "Event deleted successfully" 43 | } 44 | raise HTTPException( 45 | status_code=status.HTTP_404_NOT_FOUND, 46 | detail="Event with supplied ID does not exist" 47 | ) 48 | 49 | @event_router.delete("/") 50 | async def delete_all_events() -> dict: 51 | events.clear() 52 | return { 53 | "message": "Events deleted successfully" 54 | } 55 | -------------------------------------------------------------------------------- /ch08/planner/models/events.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from beanie import Document 4 | from pydantic import BaseModel 5 | 6 | 7 | class Event(Document): 8 | creator: Optional[str] 9 | title: str 10 | image: str 11 | description: str 12 | tags: List[str] 13 | location: str 14 | 15 | class Config: 16 | schema_extra = { 17 | "example": { 18 | "title": "FastAPI Book Launch", 19 | "image": "https://linktomyimage.com/image.png", 20 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 21 | "tags": ["python", "fastapi", "book", "launch"], 22 | "location": "Google Meet" 23 | } 24 | } 25 | 26 | class Settings: 27 | name = "events" 28 | 29 | 30 | class EventUpdate(BaseModel): 31 | title: Optional[str] 32 | image: Optional[str] 33 | description: Optional[str] 34 | tags: Optional[List[str]] 35 | location: Optional[str] 36 | 37 | class Config: 38 | schema_extra = { 39 | "example": { 40 | "title": "FastAPI BookLaunch", 41 | "image": "https://linktomyimage.com/image.png", 42 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 43 | "tags": ["python", "fastapi", "book", "launch"], 44 | "location": "Google Meet" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ch09/planner/models/events.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from beanie import Document 4 | from pydantic import BaseModel 5 | 6 | 7 | class Event(Document): 8 | creator: Optional[str] 9 | title: str 10 | image: str 11 | description: str 12 | tags: List[str] 13 | location: str 14 | 15 | class Config: 16 | schema_extra = { 17 | "example": { 18 | "title": "FastAPI Book Launch", 19 | "image": "https://linktomyimage.com/image.png", 20 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 21 | "tags": ["python", "fastapi", "book", "launch"], 22 | "location": "Google Meet" 23 | } 24 | } 25 | 26 | class Settings: 27 | name = "events" 28 | 29 | 30 | class EventUpdate(BaseModel): 31 | title: Optional[str] 32 | image: Optional[str] 33 | description: Optional[str] 34 | tags: Optional[List[str]] 35 | location: Optional[str] 36 | 37 | class Config: 38 | schema_extra = { 39 | "example": { 40 | "title": "FastAPI BookLaunch", 41 | "image": "https://linktomyimage.com/image.png", 42 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 43 | "tags": ["python", "fastapi", "book", "launch"], 44 | "location": "Google Meet" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ch08/planner/auth/jwt_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | 4 | from database.connection import Settings 5 | from fastapi import HTTPException, status 6 | from jose import jwt, JWTError 7 | from models.users import User 8 | 9 | settings = Settings() 10 | 11 | 12 | def create_access_token(user: str) -> str: 13 | payload = { 14 | "user": user, 15 | "expires": time.time() + 3600 16 | } 17 | 18 | token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") 19 | return token 20 | 21 | 22 | async def verify_access_token(token: str) -> dict: 23 | try: 24 | data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) 25 | 26 | expire = data.get("expires") 27 | 28 | if expire is None: 29 | raise HTTPException( 30 | status_code=status.HTTP_400_BAD_REQUEST, 31 | detail="No access token supplied" 32 | ) 33 | if datetime.utcnow() > datetime.utcfromtimestamp(expire): 34 | raise HTTPException( 35 | status_code=status.HTTP_403_FORBIDDEN, 36 | detail="Token expired!" 37 | ) 38 | user_exist = await User.find_one(User.email == data["user"]) 39 | if not user_exist: 40 | raise HTTPException( 41 | status_code=status.HTTP_400_BAD_REQUEST, 42 | detail="Invalid token" 43 | ) 44 | 45 | return data 46 | 47 | except JWTError: 48 | raise HTTPException( 49 | status_code=status.HTTP_400_BAD_REQUEST, 50 | detail="Invalid token" 51 | ) 52 | -------------------------------------------------------------------------------- /ch09/planner/auth/jwt_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | 4 | from database.connection import Settings 5 | from fastapi import HTTPException, status 6 | from jose import jwt, JWTError 7 | from models.users import User 8 | 9 | settings = Settings() 10 | 11 | 12 | def create_access_token(user: str) -> str: 13 | payload = { 14 | "user": user, 15 | "expires": time.time() + 3600 16 | } 17 | 18 | token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") 19 | return token 20 | 21 | 22 | async def verify_access_token(token: str) -> dict: 23 | try: 24 | data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) 25 | 26 | expire = data.get("expires") 27 | 28 | if expire is None: 29 | raise HTTPException( 30 | status_code=status.HTTP_400_BAD_REQUEST, 31 | detail="No access token supplied" 32 | ) 33 | if datetime.utcnow() > datetime.utcfromtimestamp(expire): 34 | raise HTTPException( 35 | status_code=status.HTTP_403_FORBIDDEN, 36 | detail="Token expired!" 37 | ) 38 | user_exist = await User.find_one(User.email == data["user"]) 39 | if not user_exist: 40 | raise HTTPException( 41 | status_code=status.HTTP_400_BAD_REQUEST, 42 | detail="Invalid token" 43 | ) 44 | 45 | return data 46 | 47 | except JWTError: 48 | raise HTTPException( 49 | status_code=status.HTTP_400_BAD_REQUEST, 50 | detail="Invalid token" 51 | ) 52 | -------------------------------------------------------------------------------- /ch04/todos/templates/todo.html: -------------------------------------------------------------------------------- 1 | {% extends "home.html" %} 2 | 3 | {% block todo_container %} 4 |
5 |
6 |
7 |
8 |
9 |
10 | 13 | 17 |
18 |
19 |
20 |
21 | {% if todo %} 22 |
23 |
24 |

Todo ID: {{ todo.id }}

25 |

26 | 27 | Item: {{ todo.item }} 28 | 29 |

30 |
31 | {% else %} 32 |
33 |

Todos

34 |
35 |
36 |
    37 | {% for todo in todos %} 38 |
  • 39 | {{ loop.index }}. {{ todo.item }} 40 |
  • 41 | {% endfor %} 42 |
43 |
44 | {% endif %} 45 |
46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /ch07/planner/routes/users.py: -------------------------------------------------------------------------------- 1 | from auth.hash_password import HashPassword 2 | from auth.jwt_handler import create_access_token 3 | from database.connection import Database 4 | from fastapi import APIRouter, Depends, HTTPException, status 5 | from fastapi.security import OAuth2PasswordRequestForm 6 | from models.users import User, TokenResponse 7 | 8 | user_router = APIRouter( 9 | tags=["User"], 10 | ) 11 | 12 | user_database = Database(User) 13 | hash_password = HashPassword() 14 | 15 | 16 | @user_router.post("/signup") 17 | async def sign_user_up(user: User) -> dict: 18 | user_exist = await User.find_one(User.email == user.email) 19 | 20 | if user_exist: 21 | raise HTTPException( 22 | status_code=status.HTTP_409_CONFLICT, 23 | detail="User with email provided exists already." 24 | ) 25 | hashed_password = hash_password.create_hash(user.password) 26 | user.password = hashed_password 27 | await user_database.save(user) 28 | return { 29 | "message": "User created successfully" 30 | } 31 | 32 | 33 | @user_router.post("/signin", response_model=TokenResponse) 34 | async def sign_user_in(user: OAuth2PasswordRequestForm = Depends()) -> dict: 35 | user_exist = await User.find_one(User.email == user.username) 36 | if not user_exist: 37 | raise HTTPException( 38 | status_code=status.HTTP_404_NOT_FOUND, 39 | detail="User with email does not exist." 40 | ) 41 | if hash_password.verify_hash(user.password, user_exist.password): 42 | access_token = create_access_token(user_exist.email) 43 | return { 44 | "access_token": access_token, 45 | "token_type": "Bearer" 46 | } 47 | 48 | raise HTTPException( 49 | status_code=status.HTTP_401_UNAUTHORIZED, 50 | detail="Invalid details passed." 51 | ) 52 | -------------------------------------------------------------------------------- /ch07/planner/database/connection.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional 2 | 3 | from beanie import init_beanie, PydanticObjectId 4 | from models.events import Event 5 | from models.users import User 6 | from motor.motor_asyncio import AsyncIOMotorClient 7 | from pydantic import BaseSettings, BaseModel 8 | 9 | 10 | class Settings(BaseSettings): 11 | DATABASE_URL: Optional[str] = None 12 | SECRET_KEY: Optional[str] = None 13 | 14 | async def initialize_database(self): 15 | client = AsyncIOMotorClient(self.DATABASE_URL) 16 | await init_beanie(database=client.get_default_database(), 17 | document_models=[Event, User]) 18 | 19 | class Config: 20 | env_file = ".env" 21 | 22 | 23 | class Database: 24 | def __init__(self, model): 25 | self.model = model 26 | 27 | async def save(self, document): 28 | await document.create() 29 | return 30 | 31 | async def get(self, id: PydanticObjectId): 32 | doc = await self.model.get(id) 33 | if doc: 34 | return doc 35 | return False 36 | 37 | async def get_all(self): 38 | docs = await self.model.find_all().to_list() 39 | return docs 40 | 41 | async def update(self, id: PydanticObjectId, body: BaseModel): 42 | doc_id = id 43 | des_body = body.dict() 44 | 45 | des_body = {k: v for k, v in des_body.items() if v is not None} 46 | update_query = {"$set": { 47 | field: value for field, value in des_body.items() 48 | }} 49 | 50 | doc = await self.get(doc_id) 51 | if not doc: 52 | return False 53 | await doc.update(update_query) 54 | return doc 55 | 56 | async def delete(self, id: PydanticObjectId): 57 | doc = await self.get(id) 58 | if not doc: 59 | return False 60 | await doc.delete() 61 | return True 62 | -------------------------------------------------------------------------------- /ch08/planner/routes/users.py: -------------------------------------------------------------------------------- 1 | from auth.hash_password import HashPassword 2 | from auth.jwt_handler import create_access_token 3 | from database.connection import Database 4 | from fastapi import APIRouter, Depends, HTTPException, status 5 | from fastapi.security import OAuth2PasswordRequestForm 6 | from models.users import User, TokenResponse 7 | 8 | user_router = APIRouter( 9 | tags=["User"], 10 | ) 11 | 12 | user_database = Database(User) 13 | hash_password = HashPassword() 14 | 15 | 16 | @user_router.post("/signup") 17 | async def sign_user_up(user: User) -> dict: 18 | user_exist = await User.find_one(User.email == user.email) 19 | 20 | if user_exist: 21 | raise HTTPException( 22 | status_code=status.HTTP_409_CONFLICT, 23 | detail="User with email provided exists already." 24 | ) 25 | hashed_password = hash_password.create_hash(user.password) 26 | user.password = hashed_password 27 | await user_database.save(user) 28 | return { 29 | "message": "User created successfully" 30 | } 31 | 32 | 33 | @user_router.post("/signin", response_model=TokenResponse) 34 | async def sign_user_in(user: OAuth2PasswordRequestForm = Depends()) -> dict: 35 | user_exist = await User.find_one(User.email == user.username) 36 | if not user_exist: 37 | raise HTTPException( 38 | status_code=status.HTTP_404_NOT_FOUND, 39 | detail="User with email does not exist." 40 | ) 41 | if hash_password.verify_hash(user.password, user_exist.password): 42 | access_token = create_access_token(user_exist.email) 43 | return { 44 | "access_token": access_token, 45 | "token_type": "Bearer" 46 | } 47 | 48 | raise HTTPException( 49 | status_code=status.HTTP_401_UNAUTHORIZED, 50 | detail="Invalid details passed." 51 | ) 52 | -------------------------------------------------------------------------------- /ch09/planner/routes/users.py: -------------------------------------------------------------------------------- 1 | from auth.hash_password import HashPassword 2 | from auth.jwt_handler import create_access_token 3 | from database.connection import Database 4 | from fastapi import APIRouter, Depends, HTTPException, status 5 | from fastapi.security import OAuth2PasswordRequestForm 6 | from models.users import User, TokenResponse 7 | 8 | user_router = APIRouter( 9 | tags=["User"], 10 | ) 11 | 12 | user_database = Database(User) 13 | hash_password = HashPassword() 14 | 15 | 16 | @user_router.post("/signup") 17 | async def sign_user_up(user: User) -> dict: 18 | user_exist = await User.find_one(User.email == user.email) 19 | 20 | if user_exist: 21 | raise HTTPException( 22 | status_code=status.HTTP_409_CONFLICT, 23 | detail="User with email provided exists already." 24 | ) 25 | hashed_password = hash_password.create_hash(user.password) 26 | user.password = hashed_password 27 | await user_database.save(user) 28 | return { 29 | "message": "User created successfully" 30 | } 31 | 32 | 33 | @user_router.post("/signin", response_model=TokenResponse) 34 | async def sign_user_in(user: OAuth2PasswordRequestForm = Depends()) -> dict: 35 | user_exist = await User.find_one(User.email == user.username) 36 | if not user_exist: 37 | raise HTTPException( 38 | status_code=status.HTTP_404_NOT_FOUND, 39 | detail="User with email does not exist." 40 | ) 41 | if hash_password.verify_hash(user.password, user_exist.password): 42 | access_token = create_access_token(user_exist.email) 43 | return { 44 | "access_token": access_token, 45 | "token_type": "Bearer" 46 | } 47 | 48 | raise HTTPException( 49 | status_code=status.HTTP_401_UNAUTHORIZED, 50 | detail="Invalid details passed." 51 | ) 52 | -------------------------------------------------------------------------------- /ch02/todos/todo.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Path 2 | from model import Todo, TodoItem 3 | 4 | todo_router = APIRouter() 5 | 6 | todo_list = [] 7 | 8 | @todo_router.post("/todo") 9 | async def add_todo(todo: Todo) -> dict: 10 | todo_list.append(todo) 11 | return { 12 | "message": "Todo added successfully." 13 | } 14 | 15 | @todo_router.get("/todo") 16 | async def retrieve_todos() -> dict: 17 | return { 18 | "todos": todo_list 19 | } 20 | 21 | @todo_router.get("/todo/{todo_id}") 22 | async def get_single_todo(todo_id: int) -> dict: 23 | for todo in todo_list: 24 | if todo.id == todo_id: 25 | return { 26 | "todo": todo 27 | } 28 | return { 29 | "message": "Todo with supplied ID doesn't exist." 30 | } 31 | 32 | @todo_router.put("/todo/{todo_id}") 33 | async def update_todo(todo_data: TodoItem, todo_id: int = Path(..., title="The ID of the todo to be updated.")) -> dict: 34 | for todo in todo_list: 35 | if todo.id == todo_id: 36 | todo.item = todo_data.item 37 | return { 38 | "message": "Todo updated successfully." 39 | } 40 | return { 41 | "message": "Todo with supplied ID doesn't exist." 42 | } 43 | 44 | 45 | @todo_router.delete("/todo/{todo_id}") 46 | async def delete_single_todo(todo_id: int) -> dict: 47 | for index in range(len(todo_list)): 48 | todo = todo_list[index] 49 | if todo.id == todo_id: 50 | todo_list.pop(index) 51 | return { 52 | "message": "Todo deleted successfully." 53 | } 54 | return { 55 | "message": "Todo with supplied ID doesn't exist." 56 | } 57 | 58 | @todo_router.delete("/todo") 59 | async def delete_all_todo() -> dict: 60 | todo_list.clear() 61 | return { 62 | "message": "Todos deleted successfully." 63 | } 64 | -------------------------------------------------------------------------------- /ch06/planner/database/connection.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional 2 | 3 | from beanie import init_beanie, PydanticObjectId 4 | from models.events import Event 5 | from models.users import User 6 | from motor.motor_asyncio import AsyncIOMotorClient 7 | from pydantic import BaseSettings, BaseModel 8 | 9 | 10 | class Settings(BaseSettings): 11 | DATABASE_URL: Optional[str] = None 12 | 13 | async def initialize_database(self): 14 | client = AsyncIOMotorClient(self.DATABASE_URL) 15 | await init_beanie(database=client.get_default_database(), 16 | document_models=[Event, User]) 17 | 18 | class Config: 19 | env_file = ".env" 20 | 21 | 22 | class Database: 23 | def __init__(self, model): 24 | self.model = model 25 | 26 | async def save(self, document) -> None: 27 | await document.create() 28 | return 29 | 30 | async def get(self, id: PydanticObjectId) -> Any: 31 | doc = await self.model.get(id) 32 | if doc: 33 | return doc 34 | return False 35 | 36 | async def get_all(self) -> List[Any]: 37 | docs = await self.model.find_all().to_list() 38 | return docs 39 | 40 | async def update(self, id: PydanticObjectId, body: BaseModel) -> Any: 41 | doc_id = id 42 | des_body = body.dict() 43 | 44 | des_body = {k: v for k, v in des_body.items() if v is not None} 45 | update_query = {"$set": { 46 | field: value for field, value in des_body.items() 47 | }} 48 | 49 | doc = await self.get(doc_id) 50 | if not doc: 51 | return False 52 | await doc.update(update_query) 53 | return doc 54 | 55 | async def delete(self, id: PydanticObjectId) -> bool: 56 | doc = await self.get(id) 57 | if not doc: 58 | return False 59 | await doc.delete() 60 | return True 61 | -------------------------------------------------------------------------------- /ch08/planner/database/connection.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any, List 2 | 3 | from beanie import init_beanie, PydanticObjectId 4 | from models.events import Event 5 | from models.users import User 6 | from motor.motor_asyncio import AsyncIOMotorClient 7 | from pydantic import BaseSettings, BaseModel 8 | 9 | 10 | class Settings(BaseSettings): 11 | DATABASE_URL: Optional[str] = None 12 | SECRET_KEY: Optional[str] = "default" 13 | 14 | async def initialize_database(self): 15 | client = AsyncIOMotorClient(self.DATABASE_URL) 16 | await init_beanie(database=client.get_default_database(), 17 | document_models=[Event, User]) 18 | 19 | class Config: 20 | env_file = ".env" 21 | 22 | 23 | class Database: 24 | def __init__(self, model): 25 | self.model = model 26 | 27 | async def save(self, document): 28 | await document.create() 29 | return 30 | 31 | async def get(self, id: PydanticObjectId) -> bool: 32 | doc = await self.model.get(id) 33 | if doc: 34 | return doc 35 | return False 36 | 37 | async def get_all(self) -> List[Any]: 38 | docs = await self.model.find_all().to_list() 39 | return docs 40 | 41 | async def update(self, id: PydanticObjectId, body: BaseModel) -> Any: 42 | doc_id = id 43 | des_body = body.dict() 44 | 45 | des_body = {k: v for k, v in des_body.items() if v is not None} 46 | update_query = {"$set": { 47 | field: value for field, value in des_body.items() 48 | }} 49 | 50 | doc = await self.get(doc_id) 51 | if not doc: 52 | return False 53 | await doc.update(update_query) 54 | return doc 55 | 56 | async def delete(self, id: PydanticObjectId) -> bool: 57 | doc = await self.get(id) 58 | if not doc: 59 | return False 60 | await doc.delete() 61 | return True 62 | -------------------------------------------------------------------------------- /ch09/planner/database/connection.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any, List 2 | 3 | from beanie import init_beanie, PydanticObjectId 4 | from models.events import Event 5 | from models.users import User 6 | from motor.motor_asyncio import AsyncIOMotorClient 7 | from pydantic import BaseSettings, BaseModel 8 | 9 | 10 | class Settings(BaseSettings): 11 | DATABASE_URL: Optional[str] = None 12 | SECRET_KEY: Optional[str] = "default" 13 | 14 | async def initialize_database(self): 15 | client = AsyncIOMotorClient(self.DATABASE_URL) 16 | await init_beanie(database=client.get_default_database(), 17 | document_models=[Event, User]) 18 | 19 | class Config: 20 | env_file = ".env" 21 | 22 | 23 | class Database: 24 | def __init__(self, model): 25 | self.model = model 26 | 27 | async def save(self, document): 28 | await document.create() 29 | return 30 | 31 | async def get(self, id: PydanticObjectId) -> bool: 32 | doc = await self.model.get(id) 33 | if doc: 34 | return doc 35 | return False 36 | 37 | async def get_all(self) -> List[Any]: 38 | docs = await self.model.find_all().to_list() 39 | return docs 40 | 41 | async def update(self, id: PydanticObjectId, body: BaseModel) -> Any: 42 | doc_id = id 43 | des_body = body.dict() 44 | 45 | des_body = {k: v for k, v in des_body.items() if v is not None} 46 | update_query = {"$set": { 47 | field: value for field, value in des_body.items() 48 | }} 49 | 50 | doc = await self.get(doc_id) 51 | if not doc: 52 | return False 53 | await doc.update(update_query) 54 | return doc 55 | 56 | async def delete(self, id: PydanticObjectId) -> bool: 57 | doc = await self.get(id) 58 | if not doc: 59 | return False 60 | await doc.delete() 61 | return True 62 | -------------------------------------------------------------------------------- /ch06/planner/routes/events.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from beanie import PydanticObjectId 4 | from database.connection import Database 5 | from fastapi import APIRouter, HTTPException, status 6 | from models.events import Event, EventUpdate 7 | 8 | event_router = APIRouter( 9 | tags=["Events"] 10 | ) 11 | 12 | event_database = Database(Event) 13 | 14 | 15 | @event_router.get("/", response_model=List[Event]) 16 | async def retrieve_all_events() -> List[Event]: 17 | events = await event_database.get_all() 18 | return events 19 | 20 | 21 | @event_router.get("/{id}", response_model=Event) 22 | async def retrieve_event(id: PydanticObjectId) -> Event: 23 | event = await event_database.get(id) 24 | if not event: 25 | raise HTTPException( 26 | status_code=status.HTTP_404_NOT_FOUND, 27 | detail="Event with supplied ID does not exist" 28 | ) 29 | return event 30 | 31 | 32 | @event_router.post("/new") 33 | async def create_event(body: Event) -> dict: 34 | await event_database.save(body) 35 | return { 36 | "message": "Event created successfully" 37 | } 38 | 39 | 40 | @event_router.put("/{id}", response_model=Event) 41 | async def update_event(id: PydanticObjectId, body: EventUpdate) -> Event: 42 | updated_event = await event_database.update(id, body) 43 | if not updated_event: 44 | raise HTTPException( 45 | status_code=status.HTTP_404_NOT_FOUND, 46 | detail="Event with supplied ID does not exist" 47 | ) 48 | return updated_event 49 | 50 | 51 | @event_router.delete("/{id}") 52 | async def delete_event(id: PydanticObjectId) -> dict: 53 | event = await event_database.delete(id) 54 | if not event: 55 | raise HTTPException( 56 | status_code=status.HTTP_404_NOT_FOUND, 57 | detail="Event with supplied ID does not exist" 58 | ) 59 | return { 60 | "message": "Event deleted successfully." 61 | } 62 | -------------------------------------------------------------------------------- /ch03/todos/todo.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Path, HTTPException, status 2 | from model import Todo, TodoItem, TodoItems 3 | 4 | todo_router = APIRouter() 5 | 6 | todo_list = [] 7 | 8 | 9 | @todo_router.post("/todo", status_code=201) 10 | async def add_todo(todo: Todo) -> dict: 11 | todo_list.append(todo) 12 | return { 13 | "message": "Todo added successfully." 14 | } 15 | 16 | 17 | @todo_router.get("/todo", response_model=TodoItems) 18 | async def retrieve_todo() -> dict: 19 | return { 20 | "todos": todo_list 21 | } 22 | 23 | 24 | @todo_router.get("/todo/{todo_id}") 25 | async def get_single_todo(todo_id: int = Path(..., title="The ID of the todo to retrieve.")) -> dict: 26 | for todo in todo_list: 27 | if todo.id == todo_id: 28 | return { 29 | "todo": todo 30 | } 31 | raise HTTPException( 32 | status_code=status.HTTP_404_NOT_FOUND, 33 | detail="Todo with supplied ID doesn't exist", 34 | ) 35 | 36 | 37 | @todo_router.put("/todo/{todo_id}") 38 | async def update_todo(todo_data: TodoItem, todo_id: int = Path(..., title="The ID of the todo to be updated.")) -> dict: 39 | for todo in todo_list: 40 | if todo.id == todo_id: 41 | todo.item = todo_data.item 42 | return { 43 | "message": "Todo updated successfully." 44 | } 45 | 46 | raise HTTPException( 47 | status_code=status.HTTP_404_NOT_FOUND, 48 | detail="Todo with supplied ID doesn't exist", 49 | ) 50 | 51 | 52 | @todo_router.delete("/todo/{todo_id}") 53 | async def delete_single_todo(todo_id: int) -> dict: 54 | for index in range(len(todo_list)): 55 | todo = todo_list[index] 56 | if todo.id == todo_id: 57 | todo_list.pop(index) 58 | return { 59 | "message": "Todo deleted successfully." 60 | } 61 | raise HTTPException( 62 | status_code=status.HTTP_404_NOT_FOUND, 63 | detail="Todo with supplied ID doesn't exist", 64 | ) 65 | 66 | 67 | @todo_router.delete("/todo") 68 | async def delete_all_todo() -> dict: 69 | todo_list.clear() 70 | return { 71 | "message": "Todos deleted successfully." 72 | } 73 | -------------------------------------------------------------------------------- /ch07/planner/routes/events.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from auth.authenticate import authenticate 4 | from beanie import PydanticObjectId 5 | from database.connection import Database 6 | from fastapi import APIRouter, Depends, HTTPException, status 7 | from models.events import Event, EventUpdate 8 | 9 | event_router = APIRouter( 10 | tags=["Events"] 11 | ) 12 | 13 | event_database = Database(Event) 14 | 15 | 16 | @event_router.get("/", response_model=List[Event]) 17 | async def retrieve_all_events() -> List[Event]: 18 | events = await event_database.get_all() 19 | return events 20 | 21 | 22 | @event_router.get("/{id}", response_model=Event) 23 | async def retrieve_event(id: PydanticObjectId) -> Event: 24 | event = await event_database.get(id) 25 | if not event: 26 | raise HTTPException( 27 | status_code=status.HTTP_404_NOT_FOUND, 28 | detail="Event with supplied ID does not exist" 29 | ) 30 | return event 31 | 32 | 33 | @event_router.post("/new") 34 | async def create_event(body: Event, user: str = Depends(authenticate)) -> dict: 35 | body.creator = user 36 | await event_database.save(body) 37 | return { 38 | "message": "Event created successfully" 39 | } 40 | 41 | 42 | @event_router.put("/{id}", response_model=Event) 43 | async def update_event(id: PydanticObjectId, body: EventUpdate, user: str = Depends(authenticate)) -> Event: 44 | event = await event_database.get(id) 45 | if event.creator != user: 46 | raise HTTPException( 47 | status_code=status.HTTP_400_BAD_REQUEST, 48 | detail="Operation not allowed" 49 | ) 50 | updated_event = await event_database.update(id, body) 51 | if not updated_event: 52 | raise HTTPException( 53 | status_code=status.HTTP_404_NOT_FOUND, 54 | detail="Event with supplied ID does not exist" 55 | ) 56 | return updated_event 57 | 58 | 59 | @event_router.delete("/{id}") 60 | async def delete_event(id: PydanticObjectId, user: str = Depends(authenticate)) -> dict: 61 | event = await event_database.get(id) 62 | if not event: 63 | raise HTTPException( 64 | status_code=status.HTTP_404_NOT_FOUND, 65 | detail="Event not found" 66 | ) 67 | if event.creator != user: 68 | raise HTTPException( 69 | status_code=status.HTTP_400_BAD_REQUEST, 70 | detail="Operation not allowed" 71 | ) 72 | event = await event_database.delete(id) 73 | 74 | return { 75 | "message": "Event deleted successfully." 76 | } 77 | -------------------------------------------------------------------------------- /ch08/planner/routes/events.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from auth.authenticate import authenticate 4 | from beanie import PydanticObjectId 5 | from database.connection import Database 6 | from fastapi import APIRouter, Depends, HTTPException, status 7 | from models.events import Event, EventUpdate 8 | 9 | event_router = APIRouter( 10 | tags=["Events"] 11 | ) 12 | 13 | event_database = Database(Event) 14 | 15 | 16 | @event_router.get("/", response_model=List[Event]) 17 | async def retrieve_all_events() -> List[Event]: 18 | events = await event_database.get_all() 19 | return events 20 | 21 | 22 | @event_router.get("/{id}", response_model=Event) 23 | async def retrieve_event(id: PydanticObjectId) -> Event: 24 | event = await event_database.get(id) 25 | if not event: 26 | raise HTTPException( 27 | status_code=status.HTTP_404_NOT_FOUND, 28 | detail="Event with supplied ID does not exist" 29 | ) 30 | return event 31 | 32 | 33 | @event_router.post("/new") 34 | async def create_event(body: Event, user: str = Depends(authenticate)) -> dict: 35 | body.creator = user 36 | await event_database.save(body) 37 | return { 38 | "message": "Event created successfully" 39 | } 40 | 41 | 42 | @event_router.put("/{id}", response_model=Event) 43 | async def update_event(id: PydanticObjectId, body: EventUpdate, user: str = Depends(authenticate)) -> Event: 44 | event = await event_database.get(id) 45 | if event.creator != user: 46 | raise HTTPException( 47 | status_code=status.HTTP_400_BAD_REQUEST, 48 | detail="Operation not allowed" 49 | ) 50 | updated_event = await event_database.update(id, body) 51 | if not updated_event: 52 | raise HTTPException( 53 | status_code=status.HTTP_404_NOT_FOUND, 54 | detail="Event with supplied ID does not exist" 55 | ) 56 | return updated_event 57 | 58 | 59 | @event_router.delete("/{id}") 60 | async def delete_event(id: PydanticObjectId, user: str = Depends(authenticate)) -> dict: 61 | event = await event_database.get(id) 62 | if event.creator != user: 63 | raise HTTPException( 64 | status_code=status.HTTP_404_NOT_FOUND, 65 | detail="Event not found" 66 | ) 67 | if not event: 68 | raise HTTPException( 69 | status_code=status.HTTP_404_NOT_FOUND, 70 | detail="Event with supplied ID does not exist" 71 | ) 72 | await event_database.delete(id) 73 | 74 | return { 75 | "message": "Event deleted successfully." 76 | } 77 | -------------------------------------------------------------------------------- /ch09/planner/routes/events.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from auth.authenticate import authenticate 4 | from beanie import PydanticObjectId 5 | from database.connection import Database 6 | from fastapi import APIRouter, Depends, HTTPException, status 7 | from models.events import Event, EventUpdate 8 | 9 | event_router = APIRouter( 10 | tags=["Events"] 11 | ) 12 | 13 | event_database = Database(Event) 14 | 15 | 16 | @event_router.get("/", response_model=List[Event]) 17 | async def retrieve_all_events() -> List[Event]: 18 | events = await event_database.get_all() 19 | return events 20 | 21 | 22 | @event_router.get("/{id}", response_model=Event) 23 | async def retrieve_event(id: PydanticObjectId) -> Event: 24 | event = await event_database.get(id) 25 | if not event: 26 | raise HTTPException( 27 | status_code=status.HTTP_404_NOT_FOUND, 28 | detail="Event with supplied ID does not exist" 29 | ) 30 | return event 31 | 32 | 33 | @event_router.post("/new") 34 | async def create_event(body: Event, user: str = Depends(authenticate)) -> dict: 35 | body.creator = user 36 | await event_database.save(body) 37 | return { 38 | "message": "Event created successfully" 39 | } 40 | 41 | 42 | @event_router.put("/{id}", response_model=Event) 43 | async def update_event(id: PydanticObjectId, body: EventUpdate, user: str = Depends(authenticate)) -> Event: 44 | event = await event_database.get(id) 45 | if event.creator != user: 46 | raise HTTPException( 47 | status_code=status.HTTP_400_BAD_REQUEST, 48 | detail="Operation not allowed" 49 | ) 50 | updated_event = await event_database.update(id, body) 51 | if not updated_event: 52 | raise HTTPException( 53 | status_code=status.HTTP_404_NOT_FOUND, 54 | detail="Event with supplied ID does not exist" 55 | ) 56 | return updated_event 57 | 58 | 59 | @event_router.delete("/{id}") 60 | async def delete_event(id: PydanticObjectId, user: str = Depends(authenticate)) -> dict: 61 | event = await event_database.get(id) 62 | if event.creator != user: 63 | raise HTTPException( 64 | status_code=status.HTTP_400_BAD_REQUEST, 65 | detail="Operation not allowed" 66 | ) 67 | if not event: 68 | raise HTTPException( 69 | status_code=status.HTTP_404_NOT_FOUND, 70 | detail="Event with supplied ID does not exist" 71 | ) 72 | await event_database.delete(id) 73 | 74 | return { 75 | "message": "Event deleted successfully." 76 | } 77 | -------------------------------------------------------------------------------- /ch04/todos/todo.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Path, HTTPException, status, Request, Depends 2 | from fastapi.templating import Jinja2Templates 3 | 4 | from model import Todo, TodoItem, TodoItems 5 | 6 | todo_router = APIRouter() 7 | 8 | todo_list = [] 9 | 10 | templates = Jinja2Templates(directory="templates/") 11 | 12 | 13 | @todo_router.post("/todo") 14 | async def add_todo(request: Request, todo: Todo = Depends(Todo.as_form)): 15 | todo.id = len(todo_list) + 1 16 | todo_list.append(todo) 17 | return templates.TemplateResponse("todo.html", 18 | { 19 | "request": request, 20 | "todos": todo_list 21 | }) 22 | 23 | 24 | @todo_router.get("/todo", response_model=TodoItems) 25 | async def retrieve_todo(request: Request): 26 | return templates.TemplateResponse("todo.html", { 27 | "request": request, 28 | "todos": todo_list 29 | }) 30 | 31 | 32 | 33 | @todo_router.get("/todo/{todo_id}") 34 | async def get_single_todo(request: Request, todo_id: int = Path(..., title="The ID of the todo to retrieve.")): 35 | for todo in todo_list: 36 | if todo.id == todo_id: 37 | return templates.TemplateResponse( 38 | "todo.html", { 39 | "request": request, 40 | "todo": todo 41 | }) 42 | raise HTTPException( 43 | status_code=status.HTTP_404_NOT_FOUND, 44 | detail="Todo with supplied ID doesn't exist", 45 | ) 46 | 47 | 48 | 49 | @todo_router.put("/todo/{todo_id}") 50 | async def update_todo(request: Request, todo_data: TodoItem, 51 | todo_id: int = Path(..., title="The ID of the todo to be updated.")) -> dict: 52 | for todo in todo_list: 53 | if todo.id == todo_id: 54 | todo.item = todo_data.item 55 | return { 56 | "message": "Todo updated successfully." 57 | } 58 | 59 | raise HTTPException( 60 | status_code=status.HTTP_404_NOT_FOUND, 61 | detail="Todo with supplied ID doesn't exist", 62 | ) 63 | 64 | 65 | @todo_router.delete("/todo/{todo_id}") 66 | async def delete_single_todo(request: Request, todo_id: int) -> dict: 67 | for index in range(len(todo_list)): 68 | todo = todo_list[index] 69 | if todo.id == todo_id: 70 | todo_list.pop(index) 71 | return { 72 | "message": "Todo deleted successfully." 73 | } 74 | raise HTTPException( 75 | status_code=status.HTTP_404_NOT_FOUND, 76 | detail="Todo with supplied ID doesn't exist", 77 | ) 78 | 79 | 80 | @todo_router.delete("/todo") 81 | async def delete_all_todo() -> dict: 82 | todo_list.clear() 83 | return { 84 | "message": "Todos deleted successfully." 85 | } 86 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/status.json: -------------------------------------------------------------------------------- 1 | {"format":2,"version":"7.1.0","globals":"14b6e0608c4fd2ba36b800f5d2f720a5","files":{"d_60afb0a4f41d540c___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"nums":[0,1,0,0,0,0,0,0],"html_filename":"d_60afb0a4f41d540c___init___py.html","relative_filename":"auth\\__init__.py"}},"d_60afb0a4f41d540c_authenticate_py":{"hash":"9c76cd9fb7ddd423906563443f6955c8","index":{"nums":[0,1,9,0,1,0,0,0],"html_filename":"d_60afb0a4f41d540c_authenticate_py.html","relative_filename":"auth\\authenticate.py"}},"d_60afb0a4f41d540c_hash_password_py":{"hash":"18e6320cb54afd2b1389630e6c5a33b7","index":{"nums":[0,1,7,0,0,0,0,0],"html_filename":"d_60afb0a4f41d540c_hash_password_py.html","relative_filename":"auth\\hash_password.py"}},"d_60afb0a4f41d540c_jwt_handler_py":{"hash":"a7f2ea4082c84cabdab080aa5d48b718","index":{"nums":[0,1,25,0,5,0,0,0],"html_filename":"d_60afb0a4f41d540c_jwt_handler_py.html","relative_filename":"auth\\jwt_handler.py"}},"d_c44ac6e9bb193d64___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"nums":[0,1,0,0,0,0,0,0],"html_filename":"d_c44ac6e9bb193d64___init___py.html","relative_filename":"database\\__init__.py"}},"d_c44ac6e9bb193d64_connection_py":{"hash":"367af5b11dcc7b41cf42bbc474b69a96","index":{"nums":[0,1,44,0,2,0,0,0],"html_filename":"d_c44ac6e9bb193d64_connection_py.html","relative_filename":"database\\connection.py"}},"main_py":{"hash":"7316d8dde3875c37b9bdaf3cf36fbbbf","index":{"nums":[0,1,21,0,3,0,0,0],"html_filename":"main_py.html","relative_filename":"main.py"}},"d_e634d7a1dd90e049___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"nums":[0,1,0,0,0,0,0,0],"html_filename":"d_e634d7a1dd90e049___init___py.html","relative_filename":"models\\__init__.py"}},"d_e634d7a1dd90e049_events_py":{"hash":"6c57da4c9612a4e488d18d6d136d306b","index":{"nums":[0,1,22,0,0,0,0,0],"html_filename":"d_e634d7a1dd90e049_events_py.html","relative_filename":"models\\events.py"}},"d_e634d7a1dd90e049_users_py":{"hash":"0958bce1157935bc9248d398a0cefb5a","index":{"nums":[0,1,12,0,0,0,0,0],"html_filename":"d_e634d7a1dd90e049_users_py.html","relative_filename":"models\\users.py"}},"d_f244bf8a352cf537___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"nums":[0,1,0,0,0,0,0,0],"html_filename":"d_f244bf8a352cf537___init___py.html","relative_filename":"routes\\__init__.py"}},"d_f244bf8a352cf537_events_py":{"hash":"3069f1c72ba2ac564790f6ea2169a664","index":{"nums":[0,1,41,0,4,0,0,0],"html_filename":"d_f244bf8a352cf537_events_py.html","relative_filename":"routes\\events.py"}},"d_f244bf8a352cf537_users_py":{"hash":"ac586e4b39e05755d201db64595fe8e4","index":{"nums":[0,1,27,0,3,0,0,0],"html_filename":"d_f244bf8a352cf537_users_py.html","relative_filename":"routes\\users.py"}},"d_a44f0ac069e85531_conftest_py":{"hash":"91639f3062d1484d80864c896184aec3","index":{"nums":[0,1,23,0,0,0,0,0],"html_filename":"d_a44f0ac069e85531_conftest_py.html","relative_filename":"tests\\conftest.py"}},"d_a44f0ac069e85531_test_arthmetic_operations_py":{"hash":"0e9d13b66946855cf7aa9e9d06bd7a48","index":{"nums":[0,1,16,0,0,0,0,0],"html_filename":"d_a44f0ac069e85531_test_arthmetic_operations_py.html","relative_filename":"tests\\test_arthmetic_operations.py"}},"d_a44f0ac069e85531_test_fixture_py":{"hash":"6594324c0bb9e114fb730b09053090e8","index":{"nums":[0,1,7,0,0,0,0,0],"html_filename":"d_a44f0ac069e85531_test_fixture_py.html","relative_filename":"tests\\test_fixture.py"}},"d_a44f0ac069e85531_test_login_py":{"hash":"b76ad9d5552869b70be14f6ce3b0fee4","index":{"nums":[0,1,17,0,0,0,0,0],"html_filename":"d_a44f0ac069e85531_test_login_py.html","relative_filename":"tests\\test_login.py"}},"d_a44f0ac069e85531_test_routes_py":{"hash":"8089ea6b1431a69c2a0ebc7cb655ea97","index":{"nums":[0,1,59,0,0,0,0,0],"html_filename":"d_a44f0ac069e85531_test_routes_py.html","relative_filename":"tests\\test_routes.py"}}}} -------------------------------------------------------------------------------- /ch08/planner/tests/test_routes.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | 4 | from auth.jwt_handler import create_access_token 5 | from models.events import Event 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | async def access_token() -> str: 10 | return create_access_token("testuser@packt.com") 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | async def mock_event() -> Event: 15 | new_event = Event( 16 | creator="testuser@packt.com", 17 | title="FastAPI Book Launch", 18 | image="https://linktomyimage.com/image.png", 19 | description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 20 | tags=["python", "fastapi", "book", "launch"], 21 | location="Google Meet" 22 | ) 23 | 24 | await Event.insert_one(new_event) 25 | 26 | yield new_event 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_get_events(default_client: httpx.AsyncClient, mock_event: Event) -> None: 31 | response = await default_client.get("/event/") 32 | 33 | assert response.status_code == 200 34 | assert response.json()[0]["_id"] == str(mock_event.id) 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_get_event(default_client: httpx.AsyncClient, mock_event: Event) -> None: 39 | url = f"/event/{str(mock_event.id)}" 40 | response = await default_client.get(url) 41 | 42 | assert response.status_code == 200 43 | assert response.json()["creator"] == mock_event.creator 44 | assert response.json()["_id"] == str(mock_event.id) 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_post_event(default_client: httpx.AsyncClient, access_token: str) -> None: 49 | payload = { 50 | "title": "FastAPI Book Launch", 51 | "image": "https://linktomyimage.com/image.png", 52 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 53 | "tags": ["python","fastapi","book","launch"], 54 | "location": "Google Meet", 55 | } 56 | 57 | headers = { 58 | "Content-Type": "application/json", 59 | "Authorization": f"Bearer {access_token}" 60 | } 61 | 62 | test_response = { 63 | "message": "Event created successfully" 64 | } 65 | 66 | response = await default_client.post("/event/new", json=payload, headers=headers) 67 | 68 | assert response.status_code == 200 69 | assert response.json() == test_response 70 | 71 | 72 | 73 | @pytest.mark.asyncio 74 | async def test_get_events_count(default_client: httpx.AsyncClient) -> None: 75 | response = await default_client.get("/event/") 76 | 77 | events = response.json() 78 | 79 | assert response.status_code == 200 80 | assert len(events) == 2 81 | 82 | 83 | @pytest.mark.asyncio 84 | async def test_update_event(default_client: httpx.AsyncClient, mock_event: Event, access_token: str) -> None: 85 | test_payload = { 86 | "title": "Updated FastAPI event" 87 | } 88 | 89 | headers = { 90 | "Content-Type": "application/json", 91 | "Authorization": f"Bearer {access_token}" 92 | } 93 | 94 | url = f"/event/{str(mock_event.id)}" 95 | 96 | response = await default_client.put(url, json=test_payload, headers=headers) 97 | 98 | assert response.status_code == 200 99 | assert response.json()["title"] == test_payload["title"] 100 | 101 | 102 | @pytest.mark.asyncio 103 | async def test_delete_event(default_client: httpx.AsyncClient, mock_event: Event, access_token: str) -> None: 104 | test_response = { 105 | "message": "Event deleted successfully." 106 | } 107 | 108 | headers = { 109 | "Content-Type": "application/json", 110 | "Authorization": f"Bearer {access_token}" 111 | } 112 | 113 | url = f"/event/{mock_event.id}" 114 | 115 | response = await default_client.delete(url, headers=headers) 116 | 117 | assert response.status_code == 200 118 | assert response.json() == test_response 119 | 120 | 121 | @pytest.mark.asyncio 122 | async def test_get_event_again(default_client: httpx.AsyncClient, mock_event: Event) -> None: 123 | url = f"/event/{str(mock_event.id)}" 124 | response = await default_client.get(url) 125 | 126 | assert response.status_code == 404 -------------------------------------------------------------------------------- /ch09/planner/tests/test_routes.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | 4 | from auth.jwt_handler import create_access_token 5 | from models.events import Event 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | async def access_token() -> str: 10 | return create_access_token("testuser@packt.com") 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | async def mock_event() -> Event: 15 | new_event = Event( 16 | creator="testuser@packt.com", 17 | title="FastAPI Book Launch", 18 | image="https://linktomyimage.com/image.png", 19 | description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 20 | tags=["python", "fastapi", "book", "launch"], 21 | location="Google Meet" 22 | ) 23 | 24 | await Event.insert_one(new_event) 25 | 26 | yield new_event 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_get_events(default_client: httpx.AsyncClient, mock_event: Event) -> None: 31 | response = await default_client.get("/event/") 32 | 33 | assert response.status_code == 200 34 | assert response.json()[0]["_id"] == str(mock_event.id) 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_get_event(default_client: httpx.AsyncClient, mock_event: Event) -> None: 39 | url = f"/event/{str(mock_event.id)}" 40 | response = await default_client.get(url) 41 | 42 | assert response.status_code == 200 43 | assert response.json()["creator"] == mock_event.creator 44 | assert response.json()["_id"] == str(mock_event.id) 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_post_event(default_client: httpx.AsyncClient, access_token: str) -> None: 49 | payload = { 50 | "title": "FastAPI Book Launch", 51 | "image": "https://linktomyimage.com/image.png", 52 | "description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 53 | "tags": [ 54 | "python", 55 | "fastapi", 56 | "book", 57 | "launch" 58 | ], 59 | "location": "Google Meet", 60 | } 61 | 62 | headers = { 63 | "Content-Type": "application/json", 64 | "Authorization": f"Bearer {access_token}" 65 | } 66 | 67 | test_response = { 68 | "message": "Event created successfully" 69 | } 70 | 71 | response = await default_client.post("/event/new", json=payload, headers=headers) 72 | 73 | assert response.status_code == 200 74 | assert response.json() == test_response 75 | 76 | 77 | @pytest.mark.asyncio 78 | async def test_get_events_count(default_client: httpx.AsyncClient) -> None: 79 | response = await default_client.get("/event/") 80 | 81 | events = response.json() 82 | 83 | assert response.status_code == 200 84 | assert len(events) == 2 85 | 86 | 87 | @pytest.mark.asyncio 88 | async def test_update_event(default_client: httpx.AsyncClient, mock_event: Event, access_token: str) -> None: 89 | test_payload = { 90 | "title": "Updated FastAPI event" 91 | } 92 | 93 | headers = { 94 | "Content-Type": "application/json", 95 | "Authorization": f"Bearer {access_token}" 96 | } 97 | 98 | url = f"/event/{str(mock_event.id)}" 99 | 100 | response = await default_client.put(url, json=test_payload, headers=headers) 101 | 102 | assert response.status_code == 200 103 | assert response.json()["title"] == test_payload["title"] 104 | 105 | 106 | @pytest.mark.asyncio 107 | async def test_delete_event(default_client: httpx.AsyncClient, mock_event: Event, access_token: str) -> None: 108 | test_response = { 109 | "message": "Event deleted successfully." 110 | } 111 | 112 | headers = { 113 | "Content-Type": "application/json", 114 | "Authorization": f"Bearer {access_token}" 115 | } 116 | 117 | url = f"/event/{mock_event.id}" 118 | 119 | response = await default_client.delete(url, headers=headers) 120 | 121 | assert response.status_code == 200 122 | assert response.json() == test_response 123 | 124 | 125 | @pytest.mark.asyncio 126 | async def test_get_event_again(default_client: httpx.AsyncClient, mock_event: Event) -> None: 127 | url = f"/event/{str(mock_event.id)}" 128 | response = await default_client.get(url) 129 | 130 | assert response.status_code == 404 131 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_e634d7a1dd90e049___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for models\__init__.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for models\__init__.py: 15 | 100% 16 |

17 | 56 |

57 | 0 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |
84 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_60afb0a4f41d540c___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for auth\__init__.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for auth\__init__.py: 15 | 100% 16 |

17 | 56 |

57 | 0 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |
84 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_f244bf8a352cf537___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for routes\__init__.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for routes\__init__.py: 15 | 100% 16 |

17 | 56 |

57 | 0 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |
84 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_c44ac6e9bb193d64___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for database\__init__.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for database\__init__.py: 15 | 100% 16 |

17 | 56 |

57 | 0 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |
84 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_60afb0a4f41d540c_hash_password_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for auth\hash_password.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for auth\hash_password.py: 15 | 100% 16 |

17 | 56 |

57 | 7 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |

1from passlib.context import CryptContext 

84 |

2 

85 |

3pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 

86 |

4 

87 |

5 

88 |

6class HashPassword: 

89 |

7 def create_hash(self, password: str) -> str: 

90 |

8 return pwd_context.hash(password) 

91 |

9 

92 |

10 def verify_hash(self, plain_password: str, hashed_password: str) -> bool: 

93 |

11 return pwd_context.verify(plain_password, hashed_password) 

94 |
95 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_60afb0a4f41d540c_authenticate_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for auth\authenticate.py: 89% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for auth\authenticate.py: 15 | 89% 16 |

17 | 56 |

57 | 9 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |

1from auth.jwt_handler import verify_access_token 

84 |

2from fastapi import Depends, HTTPException, status 

85 |

3from fastapi.security import OAuth2PasswordBearer 

86 |

4 

87 |

5oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/user/signin") 

88 |

6 

89 |

7 

90 |

8async def authenticate(token: str = Depends(oauth2_scheme)) -> str: 

91 |

9 if not token: 

92 |

10 raise HTTPException( 

93 |

11 status_code=status.HTTP_403_FORBIDDEN, 

94 |

12 detail="Sign in for access" 

95 |

13 ) 

96 |

14 

97 |

15 decoded_token = await verify_access_token(token) 

98 |

16 return decoded_token["user"] 

99 |
100 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_a44f0ac069e85531_test_fixture_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for tests\test_fixture.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for tests\test_fixture.py: 15 | 100% 16 |

17 | 56 |

57 | 7 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |

1import pytest 

84 |

2 

85 |

3# Fixture is defined. 

86 |

4from models.events import EventUpdate 

87 |

5 

88 |

6 

89 |

7@pytest.fixture 

90 |

8def event() -> EventUpdate: 

91 |

9 return EventUpdate( 

92 |

10 title="FastAPI Book Launch", 

93 |

11 image="https://packt.com/fastapi.png", 

94 |

12 description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!", 

95 |

13 tags=["python", "fastapi", "book", "launch"], 

96 |

14 location="Google Meet" 

97 |

15 ) 

98 |

16 

99 |

17 

100 |

18def test_event_name(event: EventUpdate) -> None: 

101 |

19 assert event.title == "FastAPI Book Launch" 

102 |
103 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_e634d7a1dd90e049_users_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for models\users.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for models\users.py: 15 | 100% 16 |

17 | 56 |

57 | 12 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |

1from beanie import Document 

84 |

2 

85 |

3from pydantic import BaseModel, EmailStr 

86 |

4 

87 |

5 

88 |

6class User(Document): 

89 |

7 email: EmailStr 

90 |

8 password: str 

91 |

9 

92 |

10 class Settings: 

93 |

11 name = "users" 

94 |

12 

95 |

13 class Config: 

96 |

14 schema_extra = { 

97 |

15 "example": { 

98 |

16 "email": "fastapi@packt.com", 

99 |

17 "password": "strong!!!" 

100 |

18 } 

101 |

19 } 

102 |

20 

103 |

21 

104 |

22class TokenResponse(BaseModel): 

105 |

23 access_token: str 

106 |

24 token_type: str 

107 |
108 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage report 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

Coverage report: 14 | 95% 15 |

16 | 43 |
44 | 45 |
46 |

47 | coverage.py v7.1.0, 48 | created at 2023-02-05 19:00 +0800 49 |

50 |
51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 |
Modulestatementsmissingexcludedcoverage
auth\__init__.py000100%
auth\authenticate.py91089%
auth\hash_password.py700100%
auth\jwt_handler.py255080%
database\__init__.py000100%
database\connection.py442095%
main.py213086%
models\__init__.py000100%
models\events.py2200100%
models\users.py1200100%
routes\__init__.py000100%
routes\events.py414090%
routes\users.py273089%
tests\conftest.py2300100%
tests\test_arthmetic_operations.py1600100%
tests\test_fixture.py700100%
tests\test_login.py1700100%
tests\test_routes.py5900100%
Total33018095%
201 |

202 | No items found using the specified filter. 203 |

204 |
205 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_a44f0ac069e85531_test_arthmetic_operations_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for tests\test_arthmetic_operations.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for tests\test_arthmetic_operations.py: 15 | 100% 16 |

17 | 56 |

57 | 16 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |

1def add(a: int, b: int) -> int: 

84 |

2 return a + b 

85 |

3 

86 |

4 

87 |

5def subtract(a: int, b: int) -> int: 

88 |

6 return b - a 

89 |

7 

90 |

8 

91 |

9def multiply(a: int, b: int) -> int: 

92 |

10 return a * b 

93 |

11 

94 |

12 

95 |

13def divide(a: int, b: int) -> int: 

96 |

14 return b // a 

97 |

15 

98 |

16 

99 |

17def test_add() -> None: 

100 |

18 assert add(1, 1) == 2 

101 |

19 

102 |

20 

103 |

21def test_subtract() -> None: 

104 |

22 assert subtract(2, 5) == 3 

105 |

23 

106 |

24 

107 |

25def test_multiply() -> None: 

108 |

26 assert multiply(10, 10) == 100 

109 |

27 

110 |

28 

111 |

29def test_divide() -> None: 

112 |

30 assert divide(25, 100) == 4 

113 |
114 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/d_a44f0ac069e85531_conftest_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coverage for tests\conftest.py: 100% 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Coverage for tests\conftest.py: 15 | 100% 16 |

17 | 56 |

57 | 23 statements   58 | 59 | 60 | 61 |

62 |

63 | « prev     64 | ^ index     65 | » next 66 |       67 | coverage.py v7.1.0, 68 | created at 2023-02-05 19:00 +0800 69 |

70 | 80 |
81 |
82 |
83 |

1import asyncio 

84 |

2 

85 |

3import httpx 

86 |

4import pytest 

87 |

5 

88 |

6from database.connection import Settings 

89 |

7from main import app 

90 |

8from models.events import Event 

91 |

9from models.users import User 

92 |

10 

93 |

11 

94 |

12@pytest.fixture(scope="session") 

95 |

13def event_loop(): 

96 |

14 loop = asyncio.get_event_loop() 

97 |

15 yield loop 

98 |

16 loop.close() 

99 |

17 

100 |

18 

101 |

19async def init_db(): 

102 |

20 test_settings = Settings() 

103 |

21 test_settings.DATABASE_URL = "mongodb://localhost:27017/testdb" 

104 |

22 

105 |

23 await test_settings.initialize_database() 

106 |

24 

107 |

25 

108 |

26@pytest.fixture(scope="session") 

109 |

27async def default_client(): 

110 |

28 await init_db() 

111 |

29 async with httpx.AsyncClient(app=app, base_url="http://app") as client: 

112 |

30 yield client 

113 |

31 

114 |

32 # 리소스 정리 

115 |

33 await Event.find_all().delete() 

116 |

34 await User.find_all().delete() 

117 |
118 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /ch08/planner/htmlcov/style.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ 3 | /* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ 4 | /* Don't edit this .css file. Edit the .scss file instead! */ 5 | html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } 6 | 7 | body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } 8 | 9 | @media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } 10 | 11 | @media (prefers-color-scheme: dark) { body { color: #eee; } } 12 | 13 | html > body { font-size: 16px; } 14 | 15 | a:active, a:focus { outline: 2px dashed #007acc; } 16 | 17 | p { font-size: .875em; line-height: 1.4em; } 18 | 19 | table { border-collapse: collapse; } 20 | 21 | td { vertical-align: top; } 22 | 23 | table tr.hidden { display: none !important; } 24 | 25 | p#no_rows { display: none; font-size: 1.2em; } 26 | 27 | a.nav { text-decoration: none; color: inherit; } 28 | 29 | a.nav:hover { text-decoration: underline; color: inherit; } 30 | 31 | .hidden { display: none; } 32 | 33 | header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } 34 | 35 | @media (prefers-color-scheme: dark) { header { background: black; } } 36 | 37 | @media (prefers-color-scheme: dark) { header { border-color: #333; } } 38 | 39 | header .content { padding: 1rem 3.5rem; } 40 | 41 | header h2 { margin-top: .5em; font-size: 1em; } 42 | 43 | header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } 44 | 45 | @media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } 46 | 47 | header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } 48 | 49 | header.sticky .text { display: none; } 50 | 51 | header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } 52 | 53 | header.sticky .content { padding: 0.5rem 3.5rem; } 54 | 55 | header.sticky .content p { font-size: 1em; } 56 | 57 | header.sticky ~ #source { padding-top: 6.5em; } 58 | 59 | main { position: relative; z-index: 1; } 60 | 61 | footer { margin: 1rem 3.5rem; } 62 | 63 | footer .content { padding: 0; color: #666; font-style: italic; } 64 | 65 | @media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } 66 | 67 | #index { margin: 1rem 0 0 3.5rem; } 68 | 69 | h1 { font-size: 1.25em; display: inline-block; } 70 | 71 | #filter_container { float: right; margin: 0 2em 0 0; } 72 | 73 | #filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } 74 | 75 | @media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } 76 | 77 | @media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } 78 | 79 | @media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } 80 | 81 | #filter_container input:focus { border-color: #007acc; } 82 | 83 | header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } 84 | 85 | @media (prefers-color-scheme: dark) { header button { border-color: #444; } } 86 | 87 | header button:active, header button:focus { outline: 2px dashed #007acc; } 88 | 89 | header button.run { background: #eeffee; } 90 | 91 | @media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } 92 | 93 | header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } 94 | 95 | @media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } 96 | 97 | header button.mis { background: #ffeeee; } 98 | 99 | @media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } 100 | 101 | header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } 102 | 103 | @media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } 104 | 105 | header button.exc { background: #f7f7f7; } 106 | 107 | @media (prefers-color-scheme: dark) { header button.exc { background: #333; } } 108 | 109 | header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } 110 | 111 | @media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } 112 | 113 | header button.par { background: #ffffd5; } 114 | 115 | @media (prefers-color-scheme: dark) { header button.par { background: #650; } } 116 | 117 | header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } 118 | 119 | @media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } 120 | 121 | #help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } 122 | 123 | #source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } 124 | 125 | #help_panel_wrapper { float: right; position: relative; } 126 | 127 | #keyboard_icon { margin: 5px; } 128 | 129 | #help_panel_state { display: none; } 130 | 131 | #help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } 132 | 133 | #help_panel .keyhelp p { margin-top: .75em; } 134 | 135 | #help_panel .legend { font-style: italic; margin-bottom: 1em; } 136 | 137 | .indexfile #help_panel { width: 25em; } 138 | 139 | .pyfile #help_panel { width: 18em; } 140 | 141 | #help_panel_state:checked ~ #help_panel { display: block; } 142 | 143 | kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } 144 | 145 | #source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } 146 | 147 | #source p { position: relative; white-space: pre; } 148 | 149 | #source p * { box-sizing: border-box; } 150 | 151 | #source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; } 152 | 153 | @media (prefers-color-scheme: dark) { #source p .n { color: #777; } } 154 | 155 | #source p .n.highlight { background: #ffdd00; } 156 | 157 | #source p .n a { margin-top: -4em; padding-top: 4em; text-decoration: none; color: #999; } 158 | 159 | @media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } 160 | 161 | #source p .n a:hover { text-decoration: underline; color: #999; } 162 | 163 | @media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } 164 | 165 | #source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } 166 | 167 | @media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } 168 | 169 | #source p .t:hover { background: #f2f2f2; } 170 | 171 | @media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } 172 | 173 | #source p .t:hover ~ .r .annotate.long { display: block; } 174 | 175 | #source p .t .com { color: #008000; font-style: italic; line-height: 1px; } 176 | 177 | @media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } 178 | 179 | #source p .t .key { font-weight: bold; line-height: 1px; } 180 | 181 | #source p .t .str { color: #0451a5; } 182 | 183 | @media (prefers-color-scheme: dark) { #source p .t .str { color: #9cdcfe; } } 184 | 185 | #source p.mis .t { border-left: 0.2em solid #ff0000; } 186 | 187 | #source p.mis.show_mis .t { background: #fdd; } 188 | 189 | @media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } 190 | 191 | #source p.mis.show_mis .t:hover { background: #f2d2d2; } 192 | 193 | @media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } 194 | 195 | #source p.run .t { border-left: 0.2em solid #00dd00; } 196 | 197 | #source p.run.show_run .t { background: #dfd; } 198 | 199 | @media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } 200 | 201 | #source p.run.show_run .t:hover { background: #d2f2d2; } 202 | 203 | @media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } 204 | 205 | #source p.exc .t { border-left: 0.2em solid #808080; } 206 | 207 | #source p.exc.show_exc .t { background: #eee; } 208 | 209 | @media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } 210 | 211 | #source p.exc.show_exc .t:hover { background: #e2e2e2; } 212 | 213 | @media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } 214 | 215 | #source p.par .t { border-left: 0.2em solid #bbbb00; } 216 | 217 | #source p.par.show_par .t { background: #ffa; } 218 | 219 | @media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } 220 | 221 | #source p.par.show_par .t:hover { background: #f2f2a2; } 222 | 223 | @media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } 224 | 225 | #source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } 226 | 227 | #source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } 228 | 229 | @media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } 230 | 231 | #source p .annotate.short:hover ~ .long { display: block; } 232 | 233 | #source p .annotate.long { width: 30em; right: 2.5em; } 234 | 235 | #source p input { display: none; } 236 | 237 | #source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } 238 | 239 | #source p input ~ .r label.ctx::before { content: "▶ "; } 240 | 241 | #source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } 242 | 243 | @media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } 244 | 245 | @media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } 246 | 247 | #source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } 248 | 249 | @media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } 250 | 251 | @media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } 252 | 253 | #source p input:checked ~ .r label.ctx::before { content: "▼ "; } 254 | 255 | #source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } 256 | 257 | #source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } 258 | 259 | @media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } 260 | 261 | #source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; } 262 | 263 | @media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } 264 | 265 | #source p .ctxs span { display: block; text-align: right; } 266 | 267 | #index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } 268 | 269 | #index table.index { margin-left: -.5em; } 270 | 271 | #index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } 272 | 273 | @media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } 274 | 275 | #index td.name, #index th.name { text-align: left; width: auto; } 276 | 277 | #index th { font-style: italic; color: #333; cursor: pointer; } 278 | 279 | @media (prefers-color-scheme: dark) { #index th { color: #ddd; } } 280 | 281 | #index th:hover { background: #eee; } 282 | 283 | @media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } 284 | 285 | #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } 286 | 287 | @media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } 288 | 289 | #index th[aria-sort="ascending"]::after { font-family: sans-serif; content: " ↑"; } 290 | 291 | #index th[aria-sort="descending"]::after { font-family: sans-serif; content: " ↓"; } 292 | 293 | #index td.name a { text-decoration: none; color: inherit; } 294 | 295 | #index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } 296 | 297 | #index tr.file:hover { background: #eee; } 298 | 299 | @media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } 300 | 301 | #index tr.file:hover td.name { text-decoration: underline; color: inherit; } 302 | 303 | #scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } 304 | 305 | @media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } 306 | 307 | @media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } 308 | 309 | #scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } 310 | 311 | @media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } 312 | --------------------------------------------------------------------------------