Fast API を一からまとめる

Fast APIアプリを構築するのに
少し手間取ってしまているので一旦まとめてみる。

# ロードマップ
1. アプリイメージ図
2. 環境構築
3. GET/POSTを受け取る最小アプリ例
4. DBとの連携
5. スキーマ(responce_model)
6. Model定義例 
7. デコレーターの中のオプション
8. 関数の中のオプション  

# 0.ミニマムCRUDアプリリポジトリ

https://github.com/raisack8/minimum_fastappi

# 1. アプリイメージ図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/2e8271e4-039a-2c55-fdbf-e9f201c93149.png)
イメージは簡単。
ざっとこんな感じ。

ただ、よりオブジェクト指向を考慮して、
各機能毎にフォルダ分けをすることにする。

考えている機能としては、以下
### API受付(/apis)
APIの受付部分。
ここではクエリのパラメータや
オプションパラメータなど、
APIの入り口として必要な処理を行う。

呼び出した際にschemasを利用し、
主にservicesのファイルを呼ぶ
### メイン処理(/services)
apisから呼び出されたメインの処理。
ここではクエリ等を活用して実際に処理を行う。
必要に応じてcrudを呼び出す。
### スキーマ(/schemas)
response_modelに格納するクラスを定義。
response_modelはapisにて設定する。
### DB定義(/models)
テーブル定義を作成。
1ファイル1テーブルが好ましい。
### DB操作(/crud)
実際にCRUDを行う。
主にservicesから呼ばれる

これらを踏まえてアプリイメージ図を
細かく分けると以下のようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/18391b1e-f984-26d3-6bd1-75799242e513.png)

「router」はURLに応じて振り分ける箇所、
main.pyに記載しておく。
「database.py」はsqlalchemyの
Sessionを作成するファイル。
厳密には作成したSessionを処理内で使いまわすのだが、
いちいち気にしていないので一旦この箇所に記載。

# 2. 環境構築

最小のアプリとしては以下をpip installすれば
問題なさそう。
・fastapi
・sqlalchemy
　…DB操作のために必要。ORM
・uvicorn
　…サーバーを起動するために必要

 この3つをinstallして
 ```
 pip freeze > requirements.txt
 ```
 を行った結果がこちら
 ```
 annotated-types==0.6.0
anyio==3.7.1
click==8.1.7
colorama==0.4.6
exceptiongroup==1.2.0
fastapi==0.105.0
greenlet==3.0.3
h11==0.14.0
idna==3.6
pydantic==2.5.3
pydantic_core==2.14.6
sniffio==1.3.0
SQLAlchemy==2.0.23
starlette==0.27.0
typing_extensions==4.9.0
uvicorn==0.25.0
 ```

# 3. GET/POSTを受け取る最小アプリ例
手順
①仮想環境を作成
```
python -m venv venv
```
②アクティベート
```
venv/Scripts/activate
```
③requirements.txtを作成、
上記をコピペし
```
pip install -r requirements.txt
```

## GET
④main.pyを作成し、下記記載
```python:main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}
```
⑤サーバー起動
```
uvicorn main:app --reload
```
上記はほっとリロードが可能なコマンド

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/d839bb4e-0e69-fa75-a5de-c5b5f4891242.png)

無事起動できました。

https://fastapi.tiangolo.com/ja/tutorial/query-params/

公式ドキュメントのリクエスト例
```python
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}
```
:::note warn
警告
Union ： 「str or int」のように「いくつかのある型のうちいずれか」にマッチすればOKという型ヒント。
この場合、あったらstr、なかったらNoneという意味合い？
:::

## POST

```Python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    item: str

@app.post("/itemsa/")
async def create_item(item: Item):
    return item
```

Postの場合はクラスでパラメータを
受け取らねばいけないらしい。

ここではPydantec(ピダンテック)というモデルを使用する。

https://docs.pydantic.dev/latest/

ちなみにGETでもモデルを宣言する方法があるが
以下に示しておく。
例としてEnumとかを宣言しているが、
詳細は後述するとしよう。。

https://fastapi.tiangolo.com/ja/tutorial/path-params/

# 4.DBとの連携

DB接続の準備。
例ではsqliteで想定しているが、
Postgresを使う場合はコメントアウトを解除

```Python:database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()
Base.metadata.create_all(bind=engine)

# Dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
```
リクエストを受け取り箇所で**get_db**をimportする

```python
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session

from src.models import database

@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(database.get_db)):
    db_user = crud.get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(status_code=400, detail="Email already registered")
    return crud.create_user(db=db, user=user)
```
dbで接続するDBを決定している
```
db: Session = Depends(database.get_db)
```

user: schemas.UserCreateの箇所では
リクエストのボディの中身の定義を行う。

BaseModelを継承しているschemasを参照して
何を渡せばいいのかがわかる。
下記jsonをリクエストボディに含めると成功する。
```
{
  "email": "string",
  "password": "string"
}
```

高度なクエリについてはこちら

https://qiita.com/raisack8/items/c3c18f6a0f960378a08b

簡単なクエリについては冒頭のリポジトリに
記載しているのでそちらを参照

### メモ

DBのカラム名を修正

```sql
alter table t_saving_history rename column price to amount;
```

# 5.スキーマ(responce_model)

参考記事

https://zenn.dev/sh0nk/books/537bb028709ab9/viewer/2c02d7

スキーマクラスを作成

```python
class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []

@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Any:
    return item


@app.get("/items/", response_model=list[Item])
async def read_items() -> Any:
    return [
        {"name": "Portal Gun", "price": 42.0},
        {"name": "Plumbus", "price": 32.0},
    ]
```
<details><summary>(FastAPIのUnion型とOptional型についてはこちら)</summary>

FastAPIでは、Pythonの型ヒントを利用してAPIのリクエストとレスポンスを定義します。この中で、`Union`型と`Optional`型は特に重要な役割を果たします。

### Union 型
`Union`は、Pythonの標準ライブラリ `typing` の一部で、複数の型を許容する場合に使用されます。FastAPIにおいて`Union`を使用すると、指定されたいずれかの型に一致するデータを受け入れることができます。

例えば、文字列または整数を受け入れるAPIエンドポイントを定義する場合、以下のように`Union`を使用できます：

```python
from typing import Union
from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: Union[int, str]):
    return {"item_id": item_id}
```

この例では、エンドポイント`/items/{item_id}`は、`item_id`として整数または文字列のいずれかを受け入れます。

### Optional 型
`Optional`型も`typing`ライブラリの一部で、ある変数が特定の型、または`None`を取ることができることを示します。これは、パラメータが省略可能であるか、または値が存在しない場合があることを意味します。

例えば、あるパラメータが提供されない場合に`None`を許容するAPIエンドポイントを定義する場合、以下のように`Optional`を使用できます：

```python
from typing import Optional
from fastapi import FastAPI

app = FastAPI()

@app.get("/items/")
async def read_item(name: Optional[str] = None):
    return {"name": name}
```

この例では、`name`パラメータは省略可能で、提供されない場合は`None`になります。`Optional[str]`は`Union[str, None]`のショートカットとして機能します。

### FastAPIにおける型ヒントの利用
FastAPIでは、これらの型ヒントを利用してリクエストの検証やドキュメントの自動生成を行います。`Union`や`Optional`を使用すると、エンドポイントが複数の入力タイプを受け入れることや、一部のパラメータが省略可能であることを明示的に示すことができます。

また、これらの型ヒントは、エディタやIDEでの自動補完やエラーチェックにも役立ちます。これにより、開発者はAPIの仕様に合わせて正しい型のデータを提供しているかどうかを簡単に確認できます。
</details>


### レスポンス型定義

レスポンスの方の定義を行う。
上記例だと、`responce_model`で宣言した`Item`クラスの
型でAPIの応答が返ってくる。

### リクエスト型定義

create_item( **item: Item** )
リクエストパラメーターや、ボディなどの
型定義をクラスにて行う

# 6.Model定義例

参考

https://qiita.com/petitviolet/items/e03c67794c4e335b6706

https://qiita.com/arkuchy/items/8ae90e4a73ef30dc4749

- __tablename__ 
テーブル名を記載。
PgAdminのようなDB操作ツールで
どのようにテーブル名を表示するか

- __table_args__ 
文字コードや複数ユニークなどを指定できる

```python
__table_args__ = (
            (UniqueConstraint('personid', 'address', name='unique_idx_personid_address')),
            {"mysql_charset": "utf8mb4"})
```

DB定義例
```python
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship

from api.db import Base


class Task(Base):
    __tablename__ = "tasks"

    id = Column(Integer, primary_key=True)
    title = Column(String(1024))

    done = relationship("Done", back_populates="task", cascade="delete")


class Done(Base):
    __tablename__ = "dones"

    id = Column(Integer, ForeignKey("tasks.id"), primary_key=True)

    task = relationship("Task", back_populates="done")
```

## 主に使いそうなジェネリック型

| Object Name | Description |
|:-|:-|
|  Boolean |  A bool datatype. |
|  Date |  A type for datetime.date() objects. |
|  DateTime | 	A type for datetime.datetime() objects. |
| Enum | Generic Enum Type. |
| Float | Type representing floating point types, such as FLOAT or REAL. |
| Integer | A type for int integers. |
| String | The base for all string and character types. |
| Text | A variably sized string type.(文字数がかなり多いようだったらText推奨) |

※それぞれ使用する際はimportする

# 7.デコレーターの中のオプション

- responce_model
レスポンス型を定義する。

# 8.関数の中のオプション

- リクエスト型の定義

# 作者メモ：ツリー構造取得
```python
import os

IGNORE_DIR = [
    'venv', 
    '__pycache__',
    '.git'
    ]

IGNORE_FILE = [
    '__init__.py',
    '.gitignore',
    'get_folder_tree.py',
]

def print_directory_structure(startpath = os.getcwd()):
    for root, dirs, files in os.walk(startpath, topdown=True):
        dirs[:] = [d for d in dirs if d not in IGNORE_DIR]  # 不要なディレクトリを除外
        files = [f for f in files if f not in IGNORE_FILE]  # 不要なファイルを除外

        level = root.replace(startpath, '').count(os.sep)
        indent = ' ' * 4 * level
        print(f'{indent}└─ {os.path.basename(root)}/')
        subindent = ' ' * 4 * (level + 1)
        for f in files:
            print(f'{subindent}├─ {f}')


if __name__ == "__main__":
    print_directory_structure()
```

上記を実行するとツリー構造を出力できる

```
└─ FastApi_minimum/
    ├─ README.md
    ├─ requirements.txt
    ├─ test.py
    └─ src/
        ├─ database.py
        ├─ main.py
        └─ apis/
            ├─ test.py
        └─ models/
        └─ services/
```

...ちょっといまいち不十分な感じはあるが、、