チュートリアル:シンプルなブログの構築#

このチュートリアルでは、データベースにエントリを保存したシンプルなブログを構築します。その後、サーバー上でこれらの投稿をレンダリングし、HTMLを直接ユーザーに配信します。

このチュートリアルは、QuartでのサーバーサイドレンダリングWebサイトの構築入門として役立つことを目的としています。最後にスキップしたい場合は、コードはGithubにあります。

1:プロジェクトの作成#

ブログサーバー用のプロジェクトを作成する必要があります。私はこれを行うためにPoetryを使用することを好みます。Poetryはpip(またはBrew)を介してインストールされます。

pip install poetry

Poetryを使用して新しいブログプロジェクトを作成できます。

poetry new --src blog

これで、プロジェクトはblogディレクトリで開発できるようになり、以降のコマンドはすべてblogディレクトリで実行する必要があります。

2:依存関係の追加#

最初に、ブログサーバーを構築するためにQuartのみが必要であり、次のコマンドを実行してプロジェクトの依存関係としてインストールできます。

poetry add quart

Poetryは、次のコマンドを実行することで、この依存関係が存在し、パスが正しいことを確認します。

poetry install

3:アプリの作成#

WebサーバーとなるQuartアプリが必要です。これは、src/blog/__init__.pyに次の追加を行うことで作成されます。

src/blog/__init__.py#
from quart import Quart

app = Quart(__name__)

def run() -> None:
    app.run()

アプリを簡単に実行するために、pyproject.tomlに次の行を追加して、Poetryスクリプトからrunメソッドを呼び出すことができます。

pyproject.toml#
[tool.poetry.scripts]
start = "blog:run"

これにより、次のコマンドでアプリを起動できます。

poetry run start

4:データベースの作成#

ニーズと要件に応じて、多くのデータベース管理システムから選択できます。この場合、最もシンプルなシステムのみが必要であり、Pythonの標準ライブラリにはSQLiteが含まれているため、最も簡単です。

データベースを初期化するには、src/blog/schema.sqlに追加された次のSQLを使用して、正しいテーブルを作成する必要があります。

src/blog/schema.sql#
DROP TABLE IF EXISTS post;
CREATE TABLE post (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  'text' TEXT NOT NULL
);

次に、コマンドでデータベースを作成できるようにする必要があります。これは、src/blog/__init__.pyにコマンドコードを追加することで実行できます。

src/blog/__init__.py#
from pathlib import Path
from sqlite3 import dbapi2 as sqlite3

app.config.update({
  "DATABASE": app.root_path / "blog.db",
})

def _connect_db():
    engine = sqlite3.connect(app.config["DATABASE"])
    engine.row_factory = sqlite3.Row
    return engine

def init_db():
    db = _connect_db()
    with open(app.root_path / "schema.sql", mode="r") as file_:
        db.cursor().executescript(file_.read())
    db.commit()

次に、pyproject.tomlのPoetryスクリプトを次のように更新する必要があります。

pyproject.toml#
[tool.poetry.scripts]
init_db = "blog:init_db"
start = "blog:run"

これで、次のコマンドを実行してデータベースを作成および更新できます。

poetry run init_db

警告

このコマンドを実行すると、既存のデータはすべて消去されます。

5:データベース内の投稿の表示#

これで、データベースにある投稿を表示できます。そのためには、まず投稿をHTMLとしてレンダリングするためのテンプレートが必要です。これは次のとおりであり、src/blog/templates/posts.htmlに追加する必要があります。

src/blog/templates/posts.html#
<main>
  {% for post in posts %}
    <article>
      <h2>{{ post.title }}</h2>
      <p>{{ post.text|safe }}</p>
    </article>
  {% else %}
    <p>No posts available</p>
  {% endfor %}
</main>

次に、データベースをクエリし、メッセージを取得し、テンプレートをレンダリングするルートが必要です。これは、src/blog/__init__.pyに追加する必要がある次のコードで行います。

src/blog/__init__.py#
from quart import render_template, g

def _get_db():
    if not hasattr(g, "sqlite_db"):
        g.sqlite_db = _connect_db()
    return g.sqlite_db

@app.get("/")
async def posts():
    db = _get_db()
    cur = db.execute(
        """SELECT title, text
             FROM post
         ORDER BY id DESC""",
    )
    posts = cur.fetchall()
    return await render_template("posts.html", posts=posts)

6:新しい投稿の作成#

ブログ投稿を作成するには、まずユーザーが投稿の詳細を入力できるフォームが必要です。これは、src/blog/templates/create.htmlに追加する必要がある次のテンプレートコードを介して行われます。

src/blog/templates/create.html#
<form method="POST" style="display: flex; flex-direction: column; gap: 8px; max-width:400px">
  <label>Title: <input type="text" size="30" name="title" /></label>
  <label>Text: <textarea name="text" rows="5" cols="40"></textarea></label>
  <button type="submit">Create</button>
</form>

スタイル設定により、フォームの要素が適切な最大幅でギャップを空けて垂直に配置されます。

訪問者がブログ投稿を作成できるようにするには、ブラウザでこのフォームによって生成されたPOSTリクエストを受け入れる必要があります。そのためには、src/blog/__init__.pyに次のコードを追加する必要があります。

src/blog/__init__.py#
from quart import redirect, request, url_for

@app.route("/create/", methods=["GET", "POST"])
async def create():
    if request.method == "POST":
        db = _get_db()
        form = await request.form
        db.execute(
            "INSERT INTO post (title, text) VALUES (?, ?)",
            [form["title"], form["text"]],
        )
        db.commit()
        return redirect(url_for("posts"))
    else:
        return await render_template("create.html")

このルートハンドラーは、GETリクエスト(例:ブラウザでのナビゲーション)に応答して作成フォームをレンダリングします。ただし、POSTリクエストの場合は、フォームデータを取り出してブログ投稿を作成し、投稿ページにユーザーをリダイレクトします。

7:テスト#

アプリをテストするには、ブログ投稿を作成でき、作成後、投稿ページに表示されることを確認する必要があります。まず、テスト用のテンポラリデータベースを作成する必要があります。これは、tests/conftest.pyに配置されたpytestフィクスチャを使用して行うことができます。

tests/conftest.py#
import pytest

from blog import app, init_db

@pytest.fixture(autouse=True)
def configure_db(tmpdir):
    app.config['DATABASE'] = str(tmpdir.join('blog.db'))
    init_db()

このフィクスチャはテストの前に自動的に実行され、テストで使用できるデータベースを設定します。

作成と表示をテストするには、tests/test_blog.pyに次のコードを追加できます。

tests/test_blog.py#
from blog import app

async def test_create_post():
    test_client = app.test_client()
    response = await test_client.post("/create/", form={"title": "Post", "text": "Text"})
    assert response.status_code == 302
    response = await test_client.get("/")
    text = await response.get_data()
    assert b"<h2>Post</h2>" in text
    assert b"<p>Text</p>" in text

テストは非同期関数であるため、次のコマンドを実行してpytest-asyncioをインストールする必要があります。

poetry add --dev pytest-asyncio

インストール後、pyproject.tomlに次の行を追加して構成する必要があります。

[tool.pytest.ini_options]
asyncio_mode = "auto"

最後に、次のコマンドでテストを実行できます。

poetry run pytest tests/

Quartの例フォルダでこれを実行している場合は、pytestがQuartのpytest設定を使用しないように、-c pyproject.tomlオプションを追加する必要があります。

8:まとめ#

シンプルなデータベースバックエンドのブログサーバーを構築しました。これは、あらゆる種類のサーバーサイドレンダリングアプリを構築するための良い出発点となるはずです。