はじめに
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
以下は、このファイル構成で必要となるパッケージの例です。ただし、使用するパッケージによって異なります。
fastapi[all]
SQLAlchemy
psycopg2-binary
pytest
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
# 空のファイル
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
変数に代入しています。アプリケーションの異なる部分でこの変数を使用することができます。
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 を使用しています。ユーザー名とパスワード、ホスト名、データベース名を指定してください。
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__.py
は app/api
モジュールの初期化ファイルであり、FastAPIアプリケーションのAPIルートエンドポイントのURLパスを定義します。通常、このファイルは空のままで、このディレクトリの他のファイルからインポートされるモジュールを提供する役割があります。
例えば、app/api/endpoints/some_endpoint.py
ファイルを作成した場合、そのエンドポイントを app/api/__init__.py
からインポートし、APIルーターに追加します。
以下は 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
# 空のファイル
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
# 空のファイル
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
モデルには、id
、name
、email
、password
、is_active
の5つの属性があります。id
属性は主キーであり、自動的にインクリメントされます。name
、email
、password
属性はそれぞれ文字列で、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/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
# 空のファイル
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
# 空のファイル
someschema.py
では、Pydanticを使用して、APIエンドポイントから受け取るデータを検証するためのスキーマを定義しています。
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エンドポイントで必要なデータを検証するために使用されます。
UserBase
:email
とusername
を必須のフィールドとして持ち、他のクラスで継承されます。
UserCreate
:UserBase
を継承し、password
を必須のフィールドとして追加します。これは、新しいユーザーを作成するときに使用されます。
UserUpdate
:UserBase
を継承し、password
をオプションのフィールドとして追加します。これは、ユーザー情報を更新するときに使用されます。
User
:UserBase
を継承し、id
を必須のフィールドとして追加します。orm_mode
を有効にすることで、SQLAlchemyからのモデルインスタンスを直接Pydanticモデルに変換することができます。これは、データベースから取得されたユーザー情報をAPIエンドポイントのレスポンスに使用する場合に使用されます。
app/tests/
# app/tests/__init__.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}
この例では、pytest
とfastapi.testclient.TestClient
を使用しています。pytest.fixture
でTestClient
を作成し、そのfixture
をtest_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はどうかな?将来のバージョンでの精度向上を期待したい。