FastAPI Adminでデータ管理ツールを爆速で立ち上げてみた

Fast APIにもDjango Adminのような管理画面フレームワークないかなと思って探していたら、ちょうど同じような名前で実装されていたので、使ってみました。

# tl;dr
FastAPI Adminを使えば、ログイン画面つきのデータ管理画面を爆速で立ち上げられます。
機能的には現段階ではシンプルなCRUD操作が可能で、独自のカスタマイズも可能。
今回のサンプルコードは[こちら](https://github.com/soymsk/fastapi-admin-tutorial)

![スクリーンショット 2022-07-20 0.06.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/fe8f1072-8fd4-74a9-f094-ea576d1ccd38.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/2af33689-692c-68f2-2107-90ce486dd9a5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/a1f9b942-15a9-28bf-d3e0-8c189870af11.png)


# FastAPI Adminとは
FastAPI Adminとは、

https://github.com/fastapi-admin/
```
A fast admin dashboard based on FastAPI and TortoiseORM with tabler ui,
inspired by Django admin
```

と公式サイトにあるように、Django adminに影響を受けてFast APIに実装されたDBデータ管理画面用のフレームワークです。

できることを簡単に列挙すると、
- 定義されたORMモデルに対して、
    - 一覧表示
    - 検索
    - 作成
    - 編集
    - 削除
- ほか、画面やレコードへのアクションの独自定義
等々、一般的なCRUD操作ができる管理画面を爆速で立ち上げることができます。

# 実際の構築手順
今回は、
- FastAPI Admin立ち上げ
- ID/Passwordでログインでき、
- １つモデルを定義してみて、追加/編集/削除機能ができるようにする。

までをやっていきます。

なお、本記事は


```
- M1 Mac 2021
- macOS 12.4
- Homebrew
- Python 3.10
```

の環境で実験しています。


## Step.0 事前準備
公式サイトにはあまり明確には書かれていないところですが、
まず、FastAPI Adminではログイン管理にRedisを使っているらしく、事前にRedisサーバーを立ち上げておく必要があります。

```
$ brew install redis
$ redis-server --version
Redis server v=7.0.0 sha=00000000:0 malloc=libc bits=64 build=fa9ffba7836907da

$ redis-server # Redisの起動
```

また、今回立ち上げる管理画面が管理する対象のデータベース、MySQLを使いますので、これもローカルで立ち上げておきます。MySQLサーバーのインストールは適宜ネットの記事を参考にしてください。
また、個々の部分はTortoiseで抽象化されているので、Tortoiseが対応しているDBならMySQLでなくても問題ない、はずです。

```
# mysql server はインストール済みとする
$ mysql.server start
```

また、利用するデータベースはあらかじめ作成しておきます。ここでは、`fastapi_admin`で作りました。

それでは今回のプロジェクト用にPython環境を構築し、事前に以下のパッケージをインストールしておきます。（バージョンは執筆当時のもの）
```
fastapi-admin = "^1.0.4"
fastapi = "^0.79.0"
aioredis = "^2.0.1"
aiomysql = "^0.1.1"
```

## Step.1 ログイン画面を立ち上げる
ログイン画面の立ち上げまでは、５０行程度で実現できます。

まずは、ログインユーザー自体もDBで管理するため、モデルを作成します。
```python: src/models.py
import datetime

from fastapi_admin.models import AbstractAdmin
from tortoise import fields


class Admin(AbstractAdmin):
    last_login = fields.DatetimeField(
        description="Last Login", default=datetime.datetime.now
    )
    email = fields.CharField(max_length=200, default="")
    avatar = fields.CharField(max_length=200, default="")
    intro = fields.TextField(default="")
    created_at = fields.DatetimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.pk}#{self.username}"
```

username/password意外にカスタムフィールドを設定する場合には、追加でフィールドを設定できます（必須ではありません）

続いて、サーバーを立ち上げるために、`FastAPI`インスタンスを作成します。サーバーの立ち上げ自体はただのFastAPIアプリケーションを立ち上げるプロセスと全く同じですが、１つ違うのが、`fastapi_admin.app` をメインのアプリケーションに[マウント](https://fastapi.tiangolo.com/advanced/sub-applications/)します。（これでアプリケーションの実装とFastAPI Adminの実装を分離できるわけですね）

```python: src/constants.py
import os

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
```

```python: src/main.py
import os

import aioredis
from fastapi import FastAPI
from fastapi_admin.app import app as admin_app
from fastapi_admin.providers.login import UsernamePasswordProvider
from starlette.staticfiles import StaticFiles
from tortoise.contrib.fastapi import register_tortoise

from .constants import BASE_DIR
from .models import Admin


def _create_app():
    app = FastAPI()
    # /admin以下に、FastAPI Adminを構築する。以下、/admin以下が管理画面。
    app.mount("/admin", admin_app)

    register_tortoise(
        app,
        config={
            # 接続する先のDBを指定
            "connections": {"default": "mysql://root@127.0.0.1:3306/fastapi_admin"},
            "apps": {
                "models": {
                    # モデル定義をパッケージレベルで指定
                    "models": ["src.models"],
                    "default_connection": "default",
                }
            },
        },
        # 起動時にスキーマも流し込む
        generate_schemas=True,
    )

    # ファイルアップロード機能を利用する場合、保存したファイルを表示するためのエンドポイントもここで作っておく。
    # アップロード画像の表示に使用
    app.mount(
        "/static",
        StaticFiles(directory=os.path.join(BASE_DIR, "static")),
        name="static",
    )

    @app.on_event("startup")
    async def startup():
        # 起動しておいたRedisへのコネクションを作成
        redis = await aioredis.from_url("redis://localhost", encoding="utf8")
        await admin_app.configure(
            logo_url="https://preview.tabler.io/static/logo-white.svg",
            # カスタムテンプレートディレクトリを指定
            template_folders=[os.path.join(BASE_DIR, "templates")],
            providers=[
                # ログイン機能の指定
                UsernamePasswordProvider(
                    # ここは任意
                    login_logo_url="https://preview.tabler.io/static/logo.svg",
                    # ログインユーザー用のモデルを指定
                    admin_model=Admin,
                )
            ],
            redis=redis,
        )

    return app


app = _create_app()


```

**register_tortoise**

```python
    register_tortoise(
         xxxxx
    )
```
ここで、接続先（データ格納先）のDBを設定します。`generate_schema` オプションは起動時にスキーマを流し込むかどうかの設定なので、本番環境では不要でしょう。

```python
    async def startup()(
         xxxxx,
         template_folders=[],
         providers=[],
         redis=xxx,
    )
```
FastAPI Adminの基本設定です。ここで接続先Redisのインスタンスを渡します。また`ｐroviders`で様々なオプション機能をプラグインとして追加できる作りなっており、標準ではログイン機能などが標準のproviderとして用意されています。
ここでは、単純なUsername/Passwordログイン機能をproviderに指定しています。

また、表示される画面のHTMLテンプレートを独自にオーバーライドできるのですが、その場合のカスタムテンプレートを置く場所も指定できる（`template_folders`)ので、指定しておきます。（ダッシュボード立ち上げに必要なため）

いったん、FastAPIサーバーを立ち上げてみます。
```server.py
import uvicorn

if __name__ == "__main__":
    uvicorn.run("src.main:app", debug=True, reload=True)
```

と、ここまででログイン画面だけは表示できます。http://127.0.0.1/8000/admin/init にアクセスすると最初の管理者を作成でき、/admin/loginでログインできます。**ただし、ログイン後`/admin`にリダイレクトしても404エラーになります(仕様）**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/6a68d14b-38b3-f891-1576-e66f1a11145b.png)


## Step.2 ダッシュボード画面を立ち上げる
続いて、先ほど４０４エラーになってしまった`/admin`のリクエスト先であるダッシュボードページを作ります。
これが**なぜか標準でテンプレートが用意されておらず**、自分でテンプレート作成する必要があります。が、そんなに難しくはありません。
以下のファイルを作成します。
```python:src/routes.py
import os

from fastapi import Depends
from fastapi_admin.app import app
from fastapi_admin.depends import get_resources
from fastapi_admin.template import templates
from starlette.requests import Request

BASE_DIR = os.path.dirname(os.path.abspath(__file__))


@app.get("/")
async def home(
    request: Request,
    resources=Depends(get_resources),
):
    return templates.TemplateResponse(
        # /templates以下のファイルを指定(相対パス)
        "dashboard.html",
        context={
            "request": request,
            "resources": resources,
            "resource_label": "Dashboard",
            "page_pre_title": "overview",
            "page_title": "Dashboard",
        },
    )

```
テンプレートファイル
```html:src/templates/dashboard.html
{% extends 'layout.html' %} {% block page_body %}
This is dashboard
{% endblock %}
```

この状態で`/admin`にアクセスするとうまくダッシュボード画面が表示されました！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/8e144e95-c667-66b0-11a6-4f724ee75fdd.png)

## Step.3 データ管理ができるように実装を追加する

ようやく画面表示までこぎ着けました。
ここからがFastAPI Adminの本当のスタートです。

それではここから本来やりたかった、「**データ管理機能の実装**」を追加していきます。FastAPI Adminでは、管理画面で表示する各種ツール（リンク、アクションボタン）や管理データを`Resources` という仕組みで定義します。このResourcesは現時点でいくつか実装されており、UI上の役割が異なるので少し紹介します。

### Link Resource
一番簡単なResouceです。ダッシュボード上のメニューバーにリンクを追加するだけです。
```python:src/resources.py
#### フル実装は後述

@app.register
class Dashboard(Link):
    label = "Dashboard"
    icon = "fas fa-home"
    url = "/admin"


@app.register
class GitHub(Link):
    label = "GitHub"
    icon = "fab fa-github"
    url = "https://github.com/soymsk/"
    target = "_blank"
```
これだけで、UI上には以下のリンクメニューが２つ追加されました。内部リンク、外部リンク、またリンクの開き方など細かく指定することができ、ちゃんと機能することが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/2da0844e-3426-b345-d838-7aca908c8e83.png)

### Model Resource
Model Resourceが管理対象のモデルをラップして定義するためのラッパークラスになっています。管理するデータのDB定義は`model` に渡すTortoiseオブジェクトが管理しますので、ここで指定するのは**あくまでFastAPI Admin上のViewやフィールドを定義しているだけ**です。
```python:src/resources.py

upload = FileUpload(uploads_dir=os.path.join(BASE_DIR, "static", "uploads"))

@app.register
class AdminResource(Model):
    label = "Admin"
    # 管理するTortoise ORMモデルを指定
    model = Admin
    icon = "fas fa-user"
    fields = [
        "id",
        "username",
        Field(
            name="password",
            label="Password",
            display=displays.InputOnly(),  # リストに表示しない
            input_=inputs.Password(),  # HTML form input typeをpasswordに指定
        ),
        Field(
            name="email",
            label="Email",
            input_=inputs.Email(),  # HTML form input typeをemailに指定
        ),
        Field(
            name="avatar",
            label="Avatar",
            display=displays.Image(width="40"),
            input_=inputs.Image(null=True, upload=upload),
        ),
        "created_at",
    ]

```
特筆すべきは、様々なデータ型に対応するために、displayやinputsが定義されていることです。（定義されているDisplay一覧は[こちら](https://fastapi-admin-docs.long2ice.io/reference/widget/display/#image))

とくに、avatarフィールドには画像が格納・表示できるよう、displayにはImage Displayウィジェットを、inputにはUplaod File ウィジェットを指定しています（FastAPI AdminではこのようなUIパーツを`Widget`とよんでいます)

また、今回は使っていないですが、検索するための条件を指定することもできるようです。

ここまでで、メニューには`Admin` パネルが追加され、Adminパネル内には初回作成したAdminユーザーのデータが表示されるようになりました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/b93c39ac-0ec7-0541-f5f4-f36178e9a41a.png)

この画面からはCreate/Edit/Delete（+ List)が可能で、試しにEditしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/13783e69-2e38-fa6b-f959-a00322eb90ed.png)

saveすると、きちんと更新されていることがリストで確認できました！（地味に、画像フィールドがちゃんと表示されているのがうれしいですね）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67687/eb3d75e8-9fd6-a43e-a438-da24784e4f5e.png)

```python:src/resource.py(全体）
import os

from fastapi_admin.app import app
from fastapi_admin.file_upload import FileUpload
from fastapi_admin.resources import (
    Action,
    Dropdown,
    Field,
    Link,
    Model,
    ToolbarAction,
    displays,
    inputs,
)

from .constants import BASE_DIR
from .models import Admin

upload = FileUpload(uploads_dir=os.path.join(BASE_DIR, "static", "uploads"))


@app.register
class Dashboard(Link):
    label = "Dashboard"
    icon = "fas fa-home"
    url = "/admin"


@app.register
class GitHub(Link):
    label = "GitHub"
    icon = "fab fa-github"
    url = "https://github.com/soymsk/"
    target = "_blank"


@app.register
class AdminResource(Model):
    label = "Admin"
    # 管理するTortoise ORMモデルを指定
    model = Admin
    icon = "fas fa-user"
    fields = [
        "id",
        "username",
        Field(
            name="password",
            label="Password",
            display=displays.InputOnly(),  # リストに表示しない
            input_=inputs.Password(),  # HTML form input typeをpasswordに指定
        ),
        Field(
            name="email",
            label="Email",
            input_=inputs.Email(),  # HTML form input typeをemailに指定
        ),
        Field(
            name="avatar",
            label="Avatar",
            display=displays.Image(width="40"),
            input_=inputs.Image(null=True, upload=upload),
        ),
        "created_at",
    ]

```

あまり数は多くないですが、このほかレコードごとのアクションをカスタマイズできるResourceも用意されており、汎用性はありそうです。([公式ドキュメント](https://fastapi-admin-docs.long2ice.io/reference/resource/#toolbaraction))

# Pro Version
本家サイトにあるように、作者さんにDonateすることで、追加の機能が実装されたコードも入手することができるようです。
アンロックされる機能としては、

- ログイン
    - Google OAuthでのログイン
    - ログイン時Recaptchaを追加
    - ログイン試行回数制限
- 操作ログの取得
- サイト全体サーチ
- ファイルアップロード先にAWS S3等、ローカルストレージ以外が選択可
- ユーザー権限管理機能

などがあるようです。どちらかというと、大きな組織でプロダクション環境で運用するための機能が使えるようになるみたいです。
今後さらに機能が追加されるような気配もあるので、興味があればDonateしてみてはいかがでしょうか？

# まとめ
FastAPI Adminを触ってみて、個人的によかった点と気になる点を書きます。

**良かった点**
- 既存のモデル定義をそのままに、簡単な実装でCRUD処理のダッシュボードが構築できる
- 既存のORMモデル定義をラップするかたちなので、既存の実装を変更する必要がない
- 宣言的にUIが記述でき、記述量少なく実装できる
- tablerのUIが綺麗
- CRUD操作周りについては、フィールドの細かな制御やカスタムアクションの定義まで実装できるので、かなりの用途をカバーできそう。
  - 画像にも対応していて◎

**気になる点**
- チュートリアルがあまり親切ではないので、初期構築は試行錯誤の必要あり
- Tortoiseが必須なので、SQLAlchemy等のORMマッパーを利用している場合は移植が必要
    - 公式にもサポートの予定はない、とのこと
- Redisが必須
- 日本語対応はなし（英語または中国語のみ）

現時点ではプロダクションで使うには検討が必要そうですが、GitHub Startも1400ほどついており、今後の開発も期待できそうに思っています！

(以下、ぼやき)
*完全に余談ですが、気になる点でも書いている通り、本家のドキュメントの「Getting Started」を見ても何もわかりません。セットアップコードもエラーですし、単体で立ち上がりません。どの設定をすれば良いのかも記述がないので、初回の起動までに小１時間がかかりました。Dockerでとりあえずデモを起動するだけならすぐにできますが、0からセットアップするのはあまり想定されていないのか？今回は結局[本家のサンプルコードと](https://github.com/fastapi-admin/fastapi-admin)実装を見ながら、解読していくことになりました :sweat:*