FastAPIとOAuth2でユーザログイン機能(備忘録)

何をするか 以下の3つの機能を実装する。 ユーザを作成する。 ユーザを認証してトークンを生成し返す。 ユーザがログインしていないと401を返す:ここでは「ユーザ情報を返すAPI」を作成。 ユーザがログインしているかどうかで異なるレスポンスを返す:ここでは「ログインしているかどうかを真偽値で返すAPI」を作成。 方針 使う技術・フレームワーク、ライブラリなど PythonとMySQLはDocker Composeで動かす。 PythonのパッケージはPoetryで管理する。 APIサーバーはFastAPI + uvicornで動かす。 認可の方式としてOAuth2.0を用いる。認可グラントのタイプはシンプルなROPC。 なぜこれを選んだのかというと、単純にFastAPIのドキュメントに書かれていたのがこれだったため。いつかほかのタイプも実装してみたい。 DBのマイグレーションはalbemicを使用してみる。今回はユーザ情報しか作らないので、alembicの使用は間違いなくオーバーなのだが、勉強のため使ってみる。 プロジェクト構成 プロジェクトディレクトリは次のようにする。 DBに関するCRUDs処理はcrudsモジュールに書く。 DBと対応するモデルはmodelモジュールに書く。 APIのリクエストボディ、レスポンスボディの形式はschemas.pyに書く。 DBのセッションの作成はdb.pyに書く。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 . ├── api │ ├── cruds │ │ ├── __init__.py │ │ └── user.py │ ├── models │ │ ├── __init__.py │ │ └── user.py │ ├── routers │ │ ├── __init__....

2023-06-29 · (updated 2023-06-30) · 7 min · 1388 words

Alembicを使ったDBマイグレーション(備忘録)

経緯 Alembicというライブラリを知ったので、試しに触ってみたメモ。 そもそもAlembicとは Alembicとは、DBマイグレーションライブラリの1つ。SQLAlchemyと一緒に用いる。 今回やってみること 今回行うマイグレーションは、以下の3つ。 空 User(email, password)を作成 User(email, password, name)に変更 使い方 Alembicの初期化 使うDBとの紐づけ エンティティ作成 マイグレーションファイルを作成 マイグレーション 想定する状況と準備 FastAPI上でWeb APIを提供し、その中でDBを操作することを想定する。ただし、メインテーマがマイグレーションのため、この記事でFastAPIの話は一切出ない。プロジェクト作成について、詳しくはFastAPI入門を参照するとよい。この記事では、ディレクトリ構成を大きく参考にしている。 パッケージ管理にはpoetryを使う。 DBにはMySQLを使う。 マイグレーションは同期処理で行うため、入れるのはPyMySQLだけでよい。しかしFastAPIでDBを処理するときに非同期での操作を行うことを見越し、aiomysqlを入れる。この時点でPyMySQLも入る。 1 poetry add sqlalchemy alembic aiomysql alembicの初期化 次のコマンドを実行する。 1 poetry run alembic init alembic プロジェクト配下にalembic/というディレクトリが生成される。 使うDBとの紐づけ alembic.iniを編集する。sqlalchemy.urlの記述を見つけたら以下のようにする。 1 sqlalchemy.url = mysql+pymysql://<url>/<name>?charset=utf8 <url>と<name>にはそれぞれ、mysqlのサーバーのURLとそのDBの名前を指定する。例えばmysqlがdbというDocker Composeのサービスとして稼働しており、3306ポートで受け付けており、そのDBの名前がappdbだった場合、次のようになる。 1 sqlalchemy.url = mysql+pymysql://root@db:3306/appdb?charset=utf8 api/db.pyにDBエンティティのベースを作っておく。 1 2 3 from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() alembic/env.pyのtarget_metadataを以下のようにする。 1 2 3 from api.db import Base target_metadata = Base....

2023-06-20 · (updated 2023-06-25) · 3 min · 588 words

Pythonを使った(静的)ページの画像のURL取得

Webページの画像だけを手っ取り早く取得したい場合にどうすれば良いのかを考えた。 これを行うプログラムをPythonで取得する。 この記事で作成したプログラムはGitHubのRepositoryに公開した。 前提 Pythonのバージョンは3.10を想定。 この記事では外部ライブラリとして Requests 2.26.0 Beautiful Soup 4.10.0 tqdm 4.62.3 w3lib 1.22.0 を使う。この記事のコードを動かす場合はpipコマンドなどでインストールしておく。 方針 やることは案外単純である。 WebページのHTMLデータを取ってくる。 img要素を探して、そのsrc属性を取ってくる。 scheme、netlocが無かったらそれを付加して、完全なURLにする。 1はRequests、2はBeautiful Soupを使えば良いだろう。 3は思ったより複雑である。src属性に入っているパスには、 URL: http://foo.org/bar/hoge.png スキームが省略されている: //foo.org/bar/hoge.png 絶対パス: /bar/hoge.png 相対パス: ../bar/hoge.png データURL: data:image/png;base64,... など色々ある。 これらのフォーマットを統一して完全なURLにするのは面倒であるが、幸運にもurllib.parse.urljoinという関数があったのでこれを使う (余談: 初め、urljoinの存在を知らずに自前でURLの変換機能を実装してしまった。学びにはなったが時間を費やした…)。 ついでの機能として、「特定の要素の中に含まれているimg要素のURLを取得する」ことも考える。 これはCSSセレクタとして指定できるようにする。 まとめると、画像のURLを取得する関数は以下のようなインターフェースとなる。 1 2 def get_img_urls(url: str, selector: Optional[str]=None) -> list[str]: pass # これから実装する URLとセレクタを引数にとり、img要素のURLのリストを返す関数である。 ついでに画像ダウンロードのためのCLIや、画像を閲覧するWebアプリなどが作れたら良い。 プロジェクトの構造 Pythonでモジュールを作ったことがないため、正しい作り方が分からないが、とりあえず以下のような構成にしてみる。 細かいディレクトリの構成は各節で述べる。 1 2 3 4 5 6 7 8 9 10 11 12 /project | +--+ getimg/ | +--+ commandline/ | +--+ viewer/ | +--+ tests/ +-- __init__....

2021-12-09 · (updated 2021-12-16) · 8 min · 1516 words

PythonとFlask(+α)で作るToDoリストAPI

シンプルなToDoリストのWeb APIを作る。 今までWSGIの仕様のみ、Werkzeug の2通りで実装したが、今回はFlaskといくつかのライブラリを使う。使うのは以下の通り。 Flask: WSGIアプリフレームワーク。 peewee: ORMライブラリ。 marshmallow: データの変換やvalidationをするためのライブラリ。 Flaskとは WSGIのWebアプリを作るためのフレームワーク。 フレームワークであるため、Flaskが用意した作法に従ってコードを書くことで比較的手軽にWebアプリが作成できる。 前回との比較でいうならば、例えばルーティングの仕組みをプログラマが書く必要はない。これはFlaskに備わっている。 Djangoのようなフレームワークとは違って、持っている機能は少ない。必要に応じて外部ライブラリを組み合わせる。 例えば、Djangoではデフォルトでデータベースの仕組みが内蔵されているが、Flaskにはない。その代わりに、 データベースのライブラリとしてsqlite3やSQLAlchemy、peeweeなど、好きなものを用いれば良い。 ToDoリストAPIの仕様 今回ToDoのデータは以下キーと値を持つJSONデータとする。 key value id ID content ToDoの内容 created_at 作成日時 (ISO8601) updated_at 更新日時 (ISO8601) APIの仕様は以下の通り。 URL Method 説明 返却値 /todo/ GET 全てのToDoを取得。 ToDoのデータのリスト /todo/ POST ToDoを作成。 なし (LocationヘッダにそのToDoへのURLを乗せる) /todo/<todo_id> GET todo_idのidを持つToDoを取得。 ToDoのデータ /todo/<todo_id> PUT todo_idのidを持つToDoを変更。 なし /todo/<todo_id> DELETE todo_idのidを持つToDoを削除 なし データはSQLiteで管理する。 雛形 todo_listディレクトリを作成。todo_list/__init__.pyを以下のようにする。 1 2 3 4 5 6 7 8 9 10 11 from flask import Flask def create_app(): app = Flask(__name__) @app....

2021-06-03 · (updated 2021-06-04) · 7 min · 1404 words

PythonとWerkzeugで作るToDoリストAPI

シンプルなToDoリストのWeb APIを作る。 前回はWSGIの仕様のみを参考にして作ったが、 今回はWerkzeugというライブラリを利用する。 Werkzeugとは Werkzeugのドキュメントの “Werkzeug is a comprehensive WSGI web application library.“という文面にもある通り、 これはWSGIのWebアプリを作るためのライブラリである。 あくまでフレームワークではなく、ライブラリであることに注意する。 Webアプリを作るうえで、便利なクラスや関数が用意された「道具箱」のようなイメージを持つとよいかもしれない (そもそも"werkzeug"という単語はドイツ語で「道具」という意味)。 あくまで道具があるだけなので、どういう設計を行うかなどを考える余地がある。 ToDoリストAPIの仕様 前回と同じだが、再掲する。 簡単のため、今回ToDoのデータはidと内容のみ持つデータとし、{ id: 0, "content": "やること" }というJSON形式とする。 APIの仕様は以下の通り。 URL Method 説明 返却値 /todo/ GET 全てのToDoを取得。 ToDoのデータのリスト /todo/ POST ToDoを作成。 なし (LocationヘッダにそのToDoへのURLを乗せる) /todo/<todo_id> GET todo_idのidを持つToDoを取得。 ToDoのデータ /todo/<todo_id> PUT todo_idのidを持つToDoを変更。 なし /todo/<todo_id> DELETE todo_idのidを持つToDoを削除 なし データは最終的にはSQLiteで管理する。 雛形 app.pyを以下のようにする。 1 2 3 4 5 6 7 8 9 10 11 from werkzeug.wrappers import Response def app(env, start_response): response = Response('Hello, World') return response(env, start_response) if __name__ == '__main__': from werkzeug....

2021-05-30 · (updated 2021-06-04) · 7 min · 1449 words

PythonとWSGIで作るToDoリストAPI

シンプルなToDoリストを作る。 今回は勉強のため、Webアプリケーションフレームワークは使わずに、 敢えてWSGIの仕様のみ参考にして書く. WSGIとは WSGIとは、WebサーバーとWebアプリとの間の標準的なインターフェース。 WSGIの仕様に沿ってWebアプリを作れば、 WSGI対応のどんなWebサーバーとも連携することができる。 WSGIの仕様はPEP3333に書かれている. ToDoリストAPIの仕様 簡単のため、今回ToDoのデータはidと内容のみ持つデータとし、{ id: 0, "content": "やること" }というJSON形式とする。 APIの仕様は以下の通り。 URL Method 説明 返却値 /todo/ GET 全てのToDoを取得。 ToDoのデータのリスト /todo/ POST ToDoを作成。 なし (LocationヘッダにそのToDoへのURLを乗せる) /todo/<todo_id> GET todo_idのidを持つToDoを取得。 ToDoのデータ /todo/<todo_id> PUT todo_idのidを持つToDoを変更。 なし /todo/<todo_id> DELETE todo_idのidを持つToDoを削除 なし データは最終的にはSQLiteで保存するが、最初は単純にlistで扱う。 雛形 まずはサーバーを作る。この時点ではルーティング処理を書いていない。 どのようなリクエストをしてもHello, Worldをレスポンスとして返す。 1 2 3 4 5 6 7 8 9 10 11 from wsgiref.simple_server import make_server def app(env, start_response): start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')]) return [b'Hello, World....

2021-05-29 · (updated 2021-06-04) · 8 min · 1499 words

Socket通信勉強(3) - 簡易HTTPサーバー作成

1年以上前に書いた記事 で、HTTPサーバーもどき(リクエストを読まず、ただ一方的にレスポンスを返すだけのサーバ)を書いた。 今回はもう少しだけこれを進化させる。 動機 非常にどうでもいいのだが動機を記しておく。 Land of Lispの13章でソケット通信が出てきた。 Land of Lispで扱っているCommon Lispの処理系がCLISPなのに対し、今自分が利用しているのはSBCLなので、 本文中のコードが動かない。そこで色々調べて、usocketを利用しようと思いつく。 その後なんとか書き上げる。ところがChromeやcurlでは動くのに、Safari(現バージョンは14.0.2)では動かない。ページを読み込んだ後、タイムアウトしたかのような挙動を起こす。 その理由を明らかにしたくて「そもそもLisp以外では動くのか。例えばPythonのソケット通信では動くのか」「PythonのWebアプリ、例えばFlaskの開発用サーバーで動くのはなぜか」 など色々調べた。cpythonのsocketserverやhttp.serverなどのソースコードも読んだ。 調べた結果、どうやらSafariがたまに「何も送らない通信(?)」を行うことが原因だった。 何も送ってくれないと、リクエストをrecvで受け取るときに、ブロッキングが働いてサーバー側が固まってしまう。 ただし普通のリクエストも送ってくるので、マルチスレッドなどの多重化を行なっておけば 問題なくSafariでもページが読み込まれる。なのでFlaskの開発用サーバーでは大した問題にならなかった。 Safariがなぜこんな通信をするのかはよく分からない。HTTPの仕様をちゃんと読んでいないので、何か見落としがあるのだろうか。もしくはバグか何かなのか。 何はともあれ、色々ソースコードを読んでいくうちに、リクエストヘッダの取得のやり方など参考になった。 せっかくなのて得た知見を元に再びHTTPサーバを作ってみようと思い立った。 作るもの 以下の条件を満たすHTTPサーバのようなものを作る(そもそも、どこまで実装できたらHTTPサーバと呼べるのだろうか)。 マルチスレッドにする。 HTTPリクエストのリクエストライン、ヘッダ情報をパースする。 リクエストボディは今回は考慮しない。 前回に比べてPythonについての知見が広がったため、 コードにおいてf-stringsやtype-annotation、dataclassなどの機能を使ってみている。 また処理を細かく関数に分ける。 listen用のソケットの作成 待ち受け用のソケットを作成し、それを返す関数を作成する。 bindやlistenは前回説明した通り。 動作確認を何度も行う都合上、TIME_WAIT状態のポートを待つのは面倒なので、setsockopt(...)の記述でそれを解決している。 (この辺りの詳細は"TIME_WAIT"とか"REUSEADDR"あたりのキーワードで検索すれば出てくる) 1 2 3 4 5 6 7 8 9 import socket def server_socket(port: int): soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) soc.bind(('', port)) soc.listen(5) return soc 動作確認 以下のようにrun_serverを作る。 1 2 3 4 5 6 7 8 9 10 def run_server(port: int): with server_socket(port) as soc: while True: conn, addr = soc....

2021-03-06 · (updated 2021-03-06) · 7 min · 1285 words

PythonでPDFの順序並び替えと空白ページ挿入(2種類の方法)

平綴じ印刷ができるように、PDFの順序を入れ替えたり、空白ページを挿入するプログラムを書いた。 方法1はPython + いろんなコマンドで、方法2はPythonのPDFライブラリであるPyPDF4を利用した方法。 実装してみた結果、後者が圧倒的に簡単だった。 動機 平綴じがしたい場面が出てきたが、印刷機に専用の設定が見つからなかった。 なので平綴じになるようにPDFのページ順を1,2,3,4,5,6,7,8,…から4,1,2,3,8,5,6,7,..に変え、それをプリンタで両面刷り(短辺綴じ)・2ページ割付で印刷することを考えた。 平綴じの場合、紙に両面4ページずつ印刷されることになる。するとPDFのページ数は4の倍数でなくてはならない。よって、4の倍数でなかった場合はその分を空白ページで埋めなければならない。 PDFファイルの準備 テスト用にPDFファイルを作っておく。ここはなんでも良いのだが、とりあえず以下のLaTeXのコードから10ページのPDFファイルを作る。名前はinput.pdfとしておく。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \documentclass{jsarticle} \begin{document} \centering \Huge 1 Page \newpage 2 Page \newpage 3 Page \newpage 4 Page \newpage 5 Page \newpage 6 Page \newpage 7 Page \newpage 8 Page \newpage 9 Page \newpage 10 Page \end{document} 方法1:Python + 諸々のコマンドの利用 方針 PDFのページ順を変えるためには、pdftkコマンドを利用すれば良い。pdftkは、Homebrewならbrew install pdftk-javaで使えるようになる)。例えば8ページのPDFファイルinput....

2021-01-07 · (updated 2021-01-07) · 3 min · 492 words

Eular法とRunge-Kutta法をPythonで実装する

備忘のために。数値解析関連の話はほとんど学んだことがないため、何か間違いがあるかも。 Eular法 以下、例に出そうとしている微分方程式が運動方程式なので、文字の使い方を力学っぽくしている(位置、速度、時間を $x, v, t$ みたいな気持ちで書いている)。 導出(1階) まず次の常微分方程式がある。 \[ \frac{dx}{dt} = f(t, x) \] 上の式を以下のように近似する。$h$を十分小さくすれば、微分の定義より上の式に近づく。 \[ \begin{aligned} \frac{x(t + h) - x(t)}{h} \simeq f(t, x) \\ \Rightarrow x(t + h) \simeq x(t) + f(t, x)h \end{aligned} \] これが、$x(t)$の更新式となっている。つまり、ある時刻$t_0$における値$x_0 = x(t_0)$を決めておけば、 \[ \begin{aligned} & t_k := t_{k-1} + h\\ \end{aligned} \] とおいて、 \[ \begin{aligned} & x(t_1) := x(t_0) + f(t_0, x_0)h \\ & x(t_2) := x(t_1) + f(t_1, x_1)h \\ & x(t_3) := x(t_2) + f(t_2, x_2)h \\ & … \end{aligned} \]...

2020-05-28 · (updated 2021-03-31) · 8 min · 1520 words

Socket通信勉強(2) - Pythonでの書き方/HTTPサーバーもどき作成

PythonでのSocket通信 やってることはCでやったときと同じである。サーバーとクライアントの通信手順は同じだし、関数名も同じである。しかしCで書いた場合に比べてシンプルに書ける。エラーは例外として投げられるため、自分で書く必要がない。またsockaddr_inなどの構造体が登場することはなく、Pythonでのbind関数とconnect関数の引数に直接アドレス・ポートを指定する。 server.py 前回と同じく、以下の手順で通信を行う。 listen(待ち受け)用のソケット作成 - socket 「どこからの接続を待つのか」「どのポートにて待ち受けするのか」を決める - bind関数の引数 ソケットにその情報を紐つける - bind 実際に待ち受けする - listen 接続要求が来たら受け入れる - accept 4によって通信用のソケットが得られるので、それを用いてデータのやりとりをする- send/recv 1 2 3 4 5 6 7 8 9 10 import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("", 8000)) s.listen(5) (sock, addr) = s.accept() print("Connected by" + str(addr)) sock.send("Hello, World".encode('utf-8')) sock.close() s.close() 上のコードを見れば各関数がどんな形で引数をとって、どんな値を返すのかがわかると思う。いくつか補足しておく。 bind (受け入れアドレス, ポート)というタプルを引数にとる。受け入れアドレスを空文字列にしておけば、どんなアドレスからの接続も受け入れる。つまりCでやったINADDR_ANYと同じ。 1 s.bind(("", 8000)) encode Pythonのstring型をそのまま送ることはできないので、byte型に変換する。これはstring.encodeで行える。 1 sock.send("Hello, World".encode('utf-8')) client.py サーバーとの通信用のソケット作成 - socket サーバが待ち受けている宛先を設定 - connectの引数 2で設定した宛先に対して接続する - connect 1で作ったソケットを用いてデータのやりとりをする。 - send/recv 1 2 3 4 5 6 7 import socket sock = socket....

2019-12-08 · (updated 2021-03-03) · 3 min · 536 words