12
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FastAPIのフォルダ構成をChatGPTに教えてもらった

Last updated at Posted at 2023-04-11

はじめに

FastAPIを使ってみて便利だったのですが、フォルダ・ファイルはどう構成したらいいの?と困りました。チュートリアルを熟読したらいいのですがページ数が多く億劫になり、まずはChatGPT(GPT-3.5)に教わりました。
コードや説明は、ほとんどそのまま転記しています。
コードの動作検証はしておりません。あくまでファイルの役割を把握するためのものです。

ファイル構成

(ChatGPTによると、)FastAPIのフォルダ構成には特定の規定はありませんが、一般的に推奨されるフォルダ構成は以下のようになります。

myproject/
├── app/
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   └── endpoints/
│   │       ├── __init__.py
│   │       └── some_endpoint.py
│   ├── models/
│   │   ├── __init__.py
│   │   └── some_model.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── some_service.py
│   ├── database/
│   │   ├── __init__.py
│   │   └── some_database.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   └── some_schema.py
│   ├── config.py
│   └── main.py
├── tests/
│   ├── __init__.py
│   └── test_some_endpoint.py
├── requirements.txt
└── README.md

ファイルのサンプル

ChatGPTに書いてもらったサンプルコードとその説明を貼り付けていきます。

requirements.txt

以下は、このファイル構成で必要となるパッケージの例です。ただし、使用するパッケージによって異なります。

requirements.txt
fastapi[all]
SQLAlchemy
psycopg2-binary
pytest

README.md

README.md
# My Project

This is a sample project created with FastAPI.

## Getting Started

### Prerequisites

- Python 3.8 or higher
- pip

### Installation

1. Clone the repository:

```
git clone https://github.com/username/my-project.git
```

2. Install the dependencies:

```
pip install -r requirements.txt
```

### Usage

1. Start the FastAPI application:

```
uvicorn app.main:app --reload
```

2. Open the API documentation in your browser:

```
http://localhost:8000/docs
```

## Running the tests

```
pytest
```

## Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin feature/my-new-feature`)
5. Create a new pull request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

app/

app/__init__.py
# app/__init__.py
# 空のファイル
app/config.py
from pydantic import BaseSettings


class Settings(BaseSettings):
    APP_NAME: str = "My FastAPI App"
    DATABASE_URL: str

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

settings = Settings()

このファイルは、FastAPIアプリケーションの設定を管理するためのクラスSettingsを定義しています。

Settingsクラスのプロパティには、アプリケーション名(APP_NAME)、、データベースのURL(DATABASE_URL)が含まれます。これらの値は、アプリケーションで使用されます。

Settingsクラスには、Configという内部クラスが含まれています。Configクラスには、.envという名前の環境変数ファイルを使用することを指示するenv_fileプロパティと、ファイルのエンコーディングをUTF-8に設定するenv_file_encodingプロパティが含まれています。.envファイルは、アプリケーションが実行される前に、環境変数として読み込まれます。これにより、アプリケーションが使用する設定を変更することができます。

最後の行では、Settingsクラスをインスタンス化し、settings変数に代入しています。アプリケーションの異なる部分でこの変数を使用することができます。

.env
DB_USER=myuser
DB_PASSWORD=mypassword
DB_HOST=localhost
DB_PORT=3306
DB_NAME=mydatabase
DATABASE_URL=mysql+pymysql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME

注意点:

  • .env ファイルは、アプリケーションのルートディレクトリに配置することが多いです。
  • = の前後にスペースを含めないようにしてください。
  • DATABASE_URL には、使用するデータベースの種類や接続情報が含まれます。この例では MySQL を使用しています。ユーザー名とパスワード、ホスト名、データベース名を指定してください。
app/main.py
from fastapi import FastAPI
from .api import router as api_router
from .database import database, engine
from .config import settings

app = FastAPI(title=settings.APP_NAME)

@app.on_event("startup")
async def startup():
    await database.connect()

@app.on_event("shutdown")
async def shutdown():
    await database.disconnect()

app.include_router(api_router)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)

上記のコードでは、FastAPIのインスタンスを作成し、myproject/app/api/endpoints/some_endpoint.pyで定義されたエンドポイントのルーターをインクルードしています。エンドポイントにアクセスするには、FastAPIを起動して、エンドポイントが公開されているURLにアクセスする必要があります。

app/api/

app/api/__init__.pyapp/api モジュールの初期化ファイルであり、FastAPIアプリケーションのAPIルートエンドポイントのURLパスを定義します。通常、このファイルは空のままで、このディレクトリの他のファイルからインポートされるモジュールを提供する役割があります。

例えば、app/api/endpoints/some_endpoint.py ファイルを作成した場合、そのエンドポイントを app/api/__init__.py からインポートし、APIルーターに追加します。

以下は app/api/__init__.py の例です:

app/api/__init__.py
from fastapi import APIRouter
from .endpoints import some_endpoint

router = APIRouter()
router.include_router(some_endpoint.router, prefix="/somepath", tags=["some"])

この例では、FastAPIの APIRouter をインスタンス化し、some_endpoint モジュールからインポートした router を含め、URLのプレフィックスとタグを定義し、ルーターに追加しています。

APIRouter は、FastAPIアプリケーションに対して、複数のAPIエンドポイントを管理し、それらを一元的に扱うことができるようにします。また、prefixパラメータを指定することで、APIエンドポイントのURLを特定のパス以下にグループ化することもできます。tagsパラメータは、SwaggerUIやReDocなどのAPIドキュメント生成ツールで使用される、APIエンドポイントのグループ化に役立ちます。

app/api/endpoints/

app/api/endpoints/__init__.py
# app/api/endpoints/__init__.py
# 空のファイル
app/api/endpoints/some_endpoint.py
from fastapi import APIRouter, Depends, HTTPException
from typing import List
from sqlalchemy.orm import Session

from app.database.some_database import SessionLocal
from app.schemas.some_schema import UserCreate, UserUpdate, User
from app.services.some_service import UserService


router = APIRouter()


@router.post("/users/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(SessionLocal)):
    db_user = UserService(db).create_user(user)
    return db_user


@router.get("/users/", response_model=List[User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal)):
    users = UserService(db).get_users(skip=skip, limit=limit)
    return users


@router.get("/users/{user_id}", response_model=User)
def read_user(user_id: int, db: Session = Depends(SessionLocal)):
    db_user = UserService(db).get_user_by_id(user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user


@router.put("/users/{user_id}", response_model=User)
def update_user(user_id: int, user: UserUpdate, db: Session = Depends(SessionLocal)):
    db_user = UserService(db).get_user_by_id(user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    db_user = UserService(db).update_user(db_user, user.dict(exclude_unset=True))
    return db_user


@router.delete("/users/{user_id}", response_model=User)
def delete_user(user_id: int, db: Session = Depends(SessionLocal)):
    db_user = UserService(db).get_user_by_id(user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    UserService(db).delete_user(db_user)
    return db_user


このエンドポイントは、POSTメソッドを使って、/users/エンドポイントにユーザーを作成するAPIを提供します。UserCreateクラスは、ユーザーのデータを取得するために使用され、Userクラスは、作成されたユーザーのデータを返すために使用されます。get_db関数はデータベースセッションを作成し、Dependsを使って関数内で使用できます。ユーザーが作成されたら、POST要求によって作成されたユーザー情報が返されます。

app/models/

app/models/__init__.py
# app/models/__init__.py
# 空のファイル
app/models/some_model.py
from sqlalchemy import Column, Integer, String
from app.database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(50), index=True)
    email = Column(String(50), unique=True, index=True)
    password = Column(String(50))
    is_active = Column(Boolean(), default=True)

このコードでは、SQLAlchemyのColumnクラスを使って、データベースに保存するUserモデルを定義しています。Baseクラスは、SQLAlchemyのdeclarative_base関数で生成されたベースクラスで、すべてのモデルクラスはこのベースクラスを継承します。

Userモデルには、idnameemailpasswordis_activeの5つの属性があります。id属性は主キーであり、自動的にインクリメントされます。nameemailpassword属性はそれぞれ文字列で、email属性はユニークである必要があります。is_active属性はブール値で、デフォルト値はTrueです。

What is SQLAlchemy?

SQLAlchemy(エスキューエル・オーエル・ケミストリー)は、Pythonで書かれたオープンソースのSQLツールキットで、リレーショナルデータベースとPythonアプリケーションの相互作用を容易にします。 SQLAlchemyを使用することで、Python開発者はPythonコードでSQLクエリを書いたり、Pythonオブジェクトをデータベースに保存したりできます。 SQLAlchemyにはORM(Object-Relational Mapping)と呼ばれる機能もあり、Pythonオブジェクトとデータベーステーブルをマッピングすることで、Pythonアプリケーションのデータベースへのアクセスをより簡単にすることができます。 SQLAlchemyは、Pythonの様々なORMライブラリの中でも最も人気のあるものの1つで、多数のアプリケーションで使用されています。

What is Pydantic?

Pydanticは、Pythonの型注釈(Type hints)を使用して、データ検証とシリアル化/逆シリアル化の機能を提供するライブラリです。 Pydanticは、FastAPIなどのフレームワークで使用されることが多く、APIのパラメータやレスポンス、データモデルの検証に役立ちます。また、PythonオブジェクトをJSONやYAMLなどのシリアル化可能な形式に変換する機能を提供し、APIなどでのデータの受け渡しに役立ちます。Pydanticは、簡単に使えるという特徴があります。また、非常に高速であり、ユーザーフレンドリーなエラーメッセージを提供することで開発者の生産性を向上させることができます。

app/services/

app/services/__init__.py
# app/services/__init__.py
# 空のファイル

app/services/some_service.pyでは、サービスのロジックを実装します。以下は、単純なユーザーサービスの例です。ユーザーモデルがあり、ユーザー情報を検証、作成、更新する関数を提供します。

app/services/some_service.py
from typing import List, Optional
from sqlalchemy.orm import Session
from app.models.some_model import User
from app.database.some_database import SessionLocal

class UserService:
    def __init__(self):
        self.db = SessionLocal()

    def get_user_by_id(self, user_id: int) -> Optional[User]:
        return self.db.query(User).filter(User.id == user_id).first()

    def get_users(self, skip: int = 0, limit: int = 100) -> List[User]:
        return self.db.query(User).offset(skip).limit(limit).all()

    def create_user(self, user: User) -> User:
        self.db.add(user)
        self.db.commit()
        self.db.refresh(user)
        return user

    def update_user(self, user: User, update_data: dict) -> User:
        for field, value in update_data.items():
            setattr(user, field, value)
        self.db.commit()
        self.db.refresh(user)
        return user

ここでは、データベースのセッションがサービスに渡され、データベースへのクエリと操作が行われます。

app/database/

app/database/__init__.py
# app/database/__init__.py
# 空のファイル
app/database/some_database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.config import settings

engine = create_engine(settings.DATABASE_URL, echo=True)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

app/schemas/

app/schemas/__init__.py
# app/schemas/__init__.py
# 空のファイル

someschema.pyでは、Pydanticを使用して、APIエンドポイントから受け取るデータを検証するためのスキーマを定義しています。

app/schemas/some_schema.py
from pydantic import BaseModel

class UserBase(BaseModel):
    email: str
    username: str

class UserCreate(UserBase):
    password: str

class UserUpdate(UserBase):
    password: str = None

class User(UserBase):
    id: int

    class Config:
        orm_mode = True

someschema.pyファイルで定義されている各クラスは、PydanticのBaseModelクラスを継承しています。各クラスは、APIエンドポイントで必要なデータを検証するために使用されます。

UserBaseemailusernameを必須のフィールドとして持ち、他のクラスで継承されます。
UserCreateUserBaseを継承し、passwordを必須のフィールドとして追加します。これは、新しいユーザーを作成するときに使用されます。
UserUpdateUserBaseを継承し、passwordをオプションのフィールドとして追加します。これは、ユーザー情報を更新するときに使用されます。
UserUserBaseを継承し、idを必須のフィールドとして追加します。orm_modeを有効にすることで、SQLAlchemyからのモデルインスタンスを直接Pydanticモデルに変換することができます。これは、データベースから取得されたユーザー情報をAPIエンドポイントのレスポンスに使用する場合に使用されます。

app/tests/

app/tests/__init__.py
# app/tests/__init__.py
app/tests/test_some_endpoint.py
# app/tests/test_some_endpoint.py
from fastapi.testclient import TestClient

from app.main import app


client = TestClient(app)


def test_create_user():
    data = {"email": "test@example.com", "username": "testuser", "password": "password"}
    response = client.post("/users/", json=data)
    assert response.status_code == 200
    assert response.json()["email"] == data["email"]
    assert response.json()["username"] == data["username"]
    assert "id" in response.json()

def test_read_user():
    data = {"email": "test@example.com", "username": "testuser", "password": "password"}
    response = client.post("/users/", json=data)
    user_id = response.json()["id"]
    response = client.get(f"/users/{user_id}")
    assert response.status_code == 200
    assert response.json()["email"] == data["email"]
    assert response.json()["username"] == data["username"]
    assert response.json()["id"] == user_id

def test_update_user():
    data = {"email": "test@example.com", "username": "testuser", "password": "password"}
    response = client.post("/users/", json=data)
    user_id = response.json()["id"]
    data["username"] = "newusername"
    response = client.put(f"/users/{user_id}", json=data)
    assert response.status_code == 200
    assert response.json()["email"] == data["email"]
    assert response.json()["username"] == data["username"]
    assert response.json()["id"] == user_id

def test_delete_user():
    data = {"email": "test@example.com", "username": "testuser", "password": "password"}
    response = client.post("/users/", json=data)
    user_id = response.json()["id"]
    response = client.delete(f"/users/{user_id}")
    assert response.status_code == 200
    assert response.json() == {"id": user_id} 

この例では、pytestfastapi.testclient.TestClientを使用しています。pytest.fixtureTestClientを作成し、そのfixturetest_some_endpoint関数に注入しています。test_some_endpoint関数では、client.getメソッドを使用して、エンドポイントへのGETリクエストを行い、レスポンスの状態コードとボディをテストしています。

まとめ

ファイル名 説明
app/__init__.py appパッケージの初期化
app/api/__init__.py apiパッケージの初期化
app/api/endpoints/__init__.py endpointsパッケージの初期化
app/api/endpoints/some_endpoint.py FastAPIエンドポイント定義ファイル
app/models/__init__.py モデルパッケージの初期化
app/models/some_model.py SQLAlchemyモデル定義ファイル
app/services/__init__.py サービスパッケージの初期化
app/services/some_service.py サービス定義ファイル(主にDB処理を記述)
app/database/__init__.py データベースパッケージの初期化
app/database/some_database.py SQLAlchemyデータベース接続処理定義ファイル
app/schemas/__init__.py スキーマパッケージの初期化
app/schemas/some_schema.py Pydanticモデル定義ファイル
app/config.py アプリケーションの設定を管理するファイル
app/main.py アプリケーションのエントリーポイント
tests/__init__.py テストパッケージの初期化
tests/test_some_endpoint.py some_endpoint.pyのテストコード
requirements.txt アプリケーションが依存するPythonパッケージの一覧を記述するファイル
README.md プロジェクトのドキュメンテーションファイル

さいごに

今回のように情報量が多い場合、ChatGPTと複数回やりとりする必要がある。
すると、この程度でもChatGPTが前後で矛盾した回答をすることがままある。
ChatGPT3.5を利用すればプログラマの仕事がなくなる!とはとても言える状況にないし
ChatGPT4はどうかな?将来のバージョンでの精度向上を期待したい。

12
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?