2024年8月

育児

3 歳 9 ヶ月 になった。

  • メガネつくりにいった
    • 店員さんが良い方ですんなり終わった
  • 串カツ田中いった
    • チンチロリンに大興奮
    • アイスつくれてご満悦
  • メガネかけたがらない
    • 少しずつかけさせていこう
  • パパとショッピングモールデート
    • たまたまやってた江ノ水の催しで楽しんだ
    • グローバルワークのおもちゃでも遊び
    • 噴水で遊んで楽しそうだった
  • パパと 2 人で沖縄にお盆帰省した
    • 移動は全然よゆう
    • 初日からおうちに帰りたいと言う
    • じぃじばぁばに新幹線のおもちゃ買ってもらった
    • 米しか食わん
    • こどもの国でおおはしゃぎ
    • ひいおばあちゃんのお家では甘やかされる
    • 初のエイサー
    • 帰宅後翌日の保育園登園はギャン泣き
  • メガネかけない
  • 夜寝る前に暗いのが嫌っぽくて近くにあったスタンドライトをつける
    • 目悪くなるから消してといっても消さなかったので強制取り上げギャン泣き
    • すぐ寝た
  • バナナを上手に半分こできなくてギャン泣き
  • 久々の嘔吐
    • 5 回くらい吐いてキツそうだった
    • 翌日小児科いって胃腸炎と診断された
    • 座薬ですぐ落ち着いた
  • 少しずつメガネかける時間長くなってきた
    • 朝食後〜お昼まで

初めて 2 人で沖縄に帰省した!これまで小田原やスカイツリーなど 2 人で遠くにいったことはあるが、泊まることはなかったのでオレがレベルアップした気分。グズグズはあったけど 1 度も泣くことなく 4 泊 5 日を過ごすことができた。こどもの国とか従姉妹と遊んだりして本人も楽しそうでなによりだった。 メガネは最初ぜんぜんかけてくれなかったが、少しずつかける時間が長くなっていったのでコツコツかけさせていきたい。そろそろトイレでうんちできるようになってほしい。。親もがんばろ。

仕事

  • フロントエンド
  • バックエンド
    • ロギング微修正
    • バグ修正
    • エラーハンドリング修正
  • インフラ
    • CDK と格闘
      • crossRegionReferences 難しい
    • 不要リソース削除
    • CI/CD 整備
      • 二重でテスト・デプロイが走ってしまう問題を Actions の Concurrency 設定で修正
      • ジョブの並列化

その他

  • 話題の uv で遊んでみた便利
  • GitHub CI/CD実践ガイド』を購入した
    • さっそく業務で実践できて最高
  • 珍しくブログ熱が沸いて 3 本かいた

github.com

gihyo.jp

enokawa.hatenablog.jp

enokawa.hatenablog.jp

enokawa.hatenablog.jp

pytestでプライベートメソッドをテストする

毎回ググっているのでここでアウトプットしておきます。

MyClass のプライベートメソッド __bar のテストをしたいとします。

class MyClass:
    def foo(self) -> str:
        return "foo"

    def __bar(self) -> str:
        return "bar"

これをテストしたい場合は以下のようなコードを書きがちですが AttributeError が発生します。

from main import MyClass


class TestMyClass:
    foo = MyClass()

    def test_foo(self) -> None:
        assert self.foo.foo() == "foo"

    def test_bar(self) -> None:
        assert self.foo.__bar() == "bar"  # => E       AttributeError: 'MyClass' object has no attribute '_TestMyClass__bar'

こうすれば解決します。Mypy から attr-defined で怒られてしまうので ignore コメントを入れています。

     def test_bar(self) -> None:
-        assert self.foo.__bar() == "bar"
+        assert self.foo._MyClass__bar() == "bar"  # type: ignore[attr-defined]

「本当にプライベートメソッドのテストが必要か?」「パブリックメソッド経由でテストできるのでは?」という視点は持っておきたいですね。

t-wada.hatenablog.jp

Lambda Web AdapterでrequestIdをログ出力する

素の Python Lambda だと logging モジュールを利用することで簡単に requestId をログに出力することができます。requestId があると、CloudWatch Logs Insights で requestId でフィルターできるためエラーが発生した際のログ調査を迅速に行うことができて便利です。

import logging

logger = logging.getLogger()
logger.setLevel("INFO")
  
def lambda_handler(event, context):
    logger.info("test")

しかし、Lambda Web Adapter だとそうはいきません。FastAPI を利用して同じように実装しても requestId は出力されません。

import logging

from fastapi import FastAPI


logger = logging.getLogger("uvicorn")
logger.setLevel("INFO")

app = FastAPI()


@app.get("/")
def read_root():
    logger.info("test")
    return {"Hello": "World"}

今回は Lambda Web Adapter で requestId を出力してみます。FastAPI での例ですが、他の言語・フレームワークでも似たようなロジックで実装可能ですので適宜読み替えてもらえればと思います。

どのようにして requestId を取得するか

Lambda Web Adapter の README を見ると以下の記載があります。リクエストヘッダーの "x-amzn-lambda-context" に何かしらの情報があるように読み取れます。

Lambda Web Adapter forwards this information to the web application in a Http Header named "x-amzn-lambda-context". In the web application, you can retrieve the value of this http header and deserialize it into a JSON object. Check out Express.js in Zip on how to use it.

github.com

実際にリクエストヘッダーをのぞいてみます。

@app.get("/")
def read_root(req: Request):
    logger.info(req.headers)
    return {"Hello": "World"}

以下のようなログが出力されました。

JSON に整形すると以下の形式です。ドキュメントに記載されている通り、"x-amzn-lambda-context" フィールド内の request_id が Lambda の requestId と一致しています。ちなみに "x-amzn-request-context" は API Gateway から Lambda に送信するメタデータのようです。

当初は "x-amzn-request-context" の requestId を参照すればよいのかなと思いましたが、Lambda 起動時に出力される requestId とは一致しなかったため "x-amzn-lambda-context" を参照するのが正しいと判断しました。今回用意した環境では Lambda Function URL を利用しているため "x-amzn-request-context" の requestId と "x-amzn-lambda-context" の request_id と一致しているものと考えられます。

{
  "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256",
  "x-amzn-tls-version": "TLSv1.3",
  "x-amzn-trace-id": "Root=1-66b4ead5-533d2c0e3c4e9f076fe8a483;Parent=371a72bb3eefa053;Sampled=0;Lineage=01ffc78c:0",
  "x-forwarded-proto": "https",
  "host": "<host>",
  "x-forwarded-port": "443",
  "x-forwarded-for": "<x-forwarded-for>",
  "accept": "*/*",
  "user-agent": "curl/7.88.1",
  "x-amzn-request-context": "{\"routeKey\":\"$default\",\"accountId\":\"anonymous\",\"stage\":\"$default\",\"requestId\":\"85203a83-e02c-4284-b6dd-3f0def1ae128\",\"apiId\":\"<apiId>\",\"domainName\":\"<host>\",\"domainPrefix\":\"<domainPrefix>\",\"time\":\"08/Aug/2024:15:57:09 +0000\",\"timeEpoch\":1723132629444,\"http\":{\"method\":\"GET\",\"path\":\"/\",\"protocol\":\"HTTP/1.1\",\"sourceIp\":\"<sourceIp>\",\"userAgent\":\"curl/7.88.1\"}}",
  "x-amzn-lambda-context": "{\"request_id\":\"85203a83-e02c-4284-b6dd-3f0def1ae128\",\"deadline\":1723132633704,\"invoked_function_arn\":\"<invoked_function_arn>\",\"xray_trace_id\":\"Root=1-66b4ead5-533d2c0e3c4e9f076fe8a483;Parent=371a72bb3eefa053;Sampled=0;Lineage=01ffc78c:0\",\"client_context\":null,\"identity\":null,\"env_config\":{\"function_name\":\"lambda-web-adapter-sample\",\"memory\":128,\"version\":\"$LATEST\",\"log_stream\":\"\",\"log_group\":\"\"}}",
  "content-length": "0"
}

実装

それでは requestId を取得してログ出力してみます。"x-amzn-lambda-context" の値は文字列型のため、いちど json.loads して dict 型に変換し、request_id を取り出す関数を用意します。ローカルでの動作確認も想定されるため、"x-amzn-lambda-context" が存在しない場合は None で出力します。

def get_request_id(headers: Headers) -> str | None:
    lambda_context = headers.get("x-amzn-lambda-context")
    if not lambda_context:
        return None

    request_id = json.loads(lambda_context).get("request_id")
    return request_id


@app.get("/")
def read_root(req: Request):
    request_id = get_request_id(req.headers)
    logger.info(f"request_id: {request_id}")

    return {"Hello": "World"}

ローカル(Docker Compose) では以下のようにログに出力されました。想定通りです。

app-1  | INFO:     request_id: None
app-1  | INFO:     192.168.65.1:55473 - "GET / HTTP/1.1" 200 OK

では Lambda をデプロイしてログを確認してみます。無事 requestId が出力されました。やったね!

おわりに

Lambda Web Adapter で requestId を取得する方法を紹介しました。今回はシンプルに requestId を出力するのみでしたが、実運用では logger のレコードに組み込んだ方がよいでしょう。僕がいま関わっているプロジェクトでは共通 logger を用意してそこで requestId や request URL、HTTP Method などを JSON で出力していて、CloudWatch Logs Insights での調査が容易に行えるようになりました。その話もまたどこかで紹介できればと思います。

余談ですが今回用意した Lambda は CDK で作成しています。最初は API Gateway もつくろうかなと思ったのですが面倒だったので Lambda Function URL を有効化してみました。想像以上に簡単に設定できたので、このようなテンポラリな環境を立ち上げるぶんには十分ですね。

アプリケーションコードと CDK もふくめたサンプルコード全文は以下を参照してください。

github.com

PydanticでHTTPリクエストのJSONレスポンスに型情報を付与する

Requests や HTTPX などのライブラリを利用して HTTP リクエストを送り、JSON レスポンスをアプリケーション内で参照するといったケースはよくあると思います。

docs.python-requests.org

www.python-httpx.org

例えば HTTPX を利用して愚直に実装すると以下のように記述できます。

import httpx

r = httpx.get("https://jsonplaceholder.typicode.com/users/1")
r_json = r.json()

print(r_json.get("name"))  # => Leanne Graham

# OR

print(r_json["name"])

しかしネストした複雑な JSON を扱う場合は更に記述量が増えますし、エディタによる型補完もありません。何重にもネストしたフィールドの値を取り出す場合はさらに .get が増えて辛いですね。

import httpx

r = httpx.get("https://jsonplaceholder.typicode.com/users/1")
r_json = r.json()

print(r_json.get("company").get("name"))  # => Leanne Graham

# OR

print(r_json["company"]["name"])

今回は Pydantic を利用して JSON レスポンスに型情報を付与してあげて、エディタの型補完が可能となるようにしてみます。 サンプルとして JSONPlaceholder の /users リソースを利用します。

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

jsonplaceholder.typicode.com

前提

  • MacOS Ventra
  • Python 3.11.3
  • Pydantic 2.8.2
  • httpx 0.27.0

クラスの定義

まずは下準備として BaseModel を継承したクラスを作成してあげます。

from pydantic import BaseModel


class Geo(BaseModel):
    lat: str
    lng: str


class Address(BaseModel):
    street: str
    suite: str
    city: str
    zipcode: str
    geo: Geo


class Company(BaseModel):
    name: str
    catchPhrase: str
    bs: str


class User(BaseModel):
    id: int
    name: str
    username: str
    email: str
    address: Address
    phone: str
    website: str
    company: Company

User インスタンスの生成・インスタンスの利用

/users/1 にリクエストすると、r_json の型は dict になります。そのため r_json の値をアンパックして User インスタンスを生成します。これで user の型は User となります。JSON 内の company フィールドの name フィールドを参照したい場合は、user.company.name でアクセスすることが可能で、かつエディタの型補完も効きます。

r = httpx.get("https://jsonplaceholder.typicode.com/users/1")
r_json = r.json()
user = User(**r_json)
print(user.company.name)  # => Romaguera-Crona

エディタ補完(1)

エディタ補完(2)

注意点

APIスキーマが変わってしまうと Pydantic で ValidationError が発生してしまうため注意が必要です。APIスキーマ変更とあわせてクラスの変更も忘れないようにしましょう。以下はリクエスト URL を /user/1 から /posts/1 に変更した場合のエラー内容です。

Traceback (most recent call last):
  File "/Users/enokawa/workspace/github.com/enokawa/python-sandbox/httpx-sample/httpx_sample/main.py", line 43, in <module>
    main()
  File "/Users/enokawa/workspace/github.com/enokawa/python-sandbox/httpx-sample/httpx_sample/main.py", line 38, in main
    user = User(**r_json)
           ^^^^^^^^^^^^^^
  File "/Users/enokawa/.anyenv/envs/pyenv/versions/3.11.3/lib/python3.11/site-packages/pydantic/main.py", line 171, in __init__
    self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 7 validation errors for User

docs.pydantic.dev

おわりに

Pydantic で JSON レスポンスに型情報を付与する方法を紹介しました。その他の利用例として、boto3 で Secrets Manager の get_secret_value のレスポンスにも型情報を付与するといったことも可能になります。僕がいま関わっているプロジェクトでも多段 .get を利用している部分が多くあるため、少しずつこの方式に変えていきたいと企んでいます。

boto3.amazonaws.com

サンプルコード全文は以下を参照してください。

github.com

2024年7月

育児

3 歳 8 ヶ月 になった。

  • ショッピングモールのトイレでおしっこできた
    • 子どもようの小便器で立ちションできた
    • たくさん褒めたトミカかった
  • 久々に声枯れた
    • ハスキーボイス
  • ねごとを言う
    • ふみきりかんかんかん
  • 3 歳半健診で乱視ぎみと診断された
    • 眼科で再検査をすることに
    • YouTube の観せすぎがよくないか
  • 外で盛大にコケた
    • 絆創膏だいすき
    • ちょっと剥がれた程度で貼り直したがる
    • 治ったところでバイバイした
  • プリンつくった
    • 豆腐みたいな舌触りになった完食できず
  • フルグラ食べなくなった
    • 食パンにマーマレードはさんであげたら食べた
    • オレも小さい時たべていたので懐かしい
  • エレベーターで得意げに「お先にどうぞ」
  • はじめてのルマンドうまそうだった
  • 夜寝るときグズり気味
    • ささいなことでグズる
    • 直近だとパパが寝室にいったら鍵が閉められててママが「ダメだよ〜」とやわで言ったらグズり
  • 眼科の検査で乱視と判定
    • 瞳孔(黒目)を大きくする目薬を 5 日間うって再検査することに
    • イヤイヤだからやむなく押さえつけて点眼ギャン泣き
    • 再検査したら遠視 & 乱視のダブルパンチでメガネ確定..
  • 下痢で初の保育園から呼び出し
    • 基本元気
  • 小児科に運良くセラピードッグがいてお触りできた
  • パパの誕生日に似顔絵を描いてくれた
  • 登園時に園のなかにダンゴムシがいて捕まえてクラスの虫かごにいれる
  • 「保育園きらい!」「友達がいるから!」「ひとりが好き!」といってゲラゲラ笑った
    • オレやん。。となった

メガネ確定がショックすぎる。。大人と同じように寝る時やお風呂はいるとき以外は基本かけるようにとのこと。やはり YouTube の観せすぎか。。時既に遅し。視力じたいは 1.0 はあるらしい。開き直って向き合いたい。

仕事

  • フロントエンド
    • とある画面のマークアップ・ロジック実装
      • 時間かかったけどバックエンドも含めてできたから達成感ある
    • Storybook が Codespaces 環境だとホットリロードされない問題の調査
      • 結局原因わからず
      • フロントエンドのアプリケーションでは効いてるんだけどな〜
  • バックエンド
    • 軽微なバグ修正
    • Mypy 推進
    • マッピング処理の追加
    • リクエストヘッダの一部をログに出力
  • インフラ
    • Amplify と格闘
    • ロギング設計・設定

その他

  • 奥さんの誕生日を忘れてすっぽかした
    • そして奥さんもオレの誕生日を忘れていた
  • 新卒の方がチームにジョインした
    • 久々にインフラの話で盛り上がったので楽しかった
  • ライト、ついてますか ―問題発見の人間学―』を購入した
    • TL で話題になっていたので
    • まだ読み切っていないが良書の予感

www.kyoritsu-pub.co.jp

2024年6月

育児

3 歳 7 ヶ月 になった。

  • バナナアイスにはまる
  • ダンゴムシを手で潰す
    • 「かわいそうだね〜」
  • 水鉄砲かってお風呂で遊んだ
    • めちゃめちゃ楽しんでる
    • オレがかけまくってもオレにはかけない優しい
      • 日頃のうっぷんを水鉄砲で晴らした
  • いすゞプラザにいった
    • ジオラマ見たりトラックに乗ったり
    • 塗装ゲームしたり自分のトラックをパネルでつくったり
    • オレの方が楽しんでた
    • ブレーキの仕組みがわかって嬉しい
  • 「言ったのに」を「言ったに〜」という
  • 手伝って欲しい時は「パパ手伝ってあげる?」と言う
  • パパ出張いくときに寂しがってたけどじゃがりこで合意
  • 江ノ電のった
    • 途中下車はせず鎌倉までいってマクドナルドたべて神社でお参りして帰宅
    • 人が多くて大変だった
      • 電車もあえて 1 本待ってのったのでなんとか座れた
    • 藤沢駅江ノ電ショップでプラレール江ノ電を買ってご満悦
  • バナナの皮を自分でむきたくない
    • 手が汚れるから
  • スリコで折りたたみ式のおまる購入
    • トイレうんちがなかなか進まないので期待
  • クッキー作りをした
    • 楽しそうにつくってたし美味しかったし最高

保育園いきたくないムーブが頻発したりおもちゃ投げまくったりでオレもイライラして息子氏の目の前で舌打ちとかため息ついたりして自己嫌悪。。親の都合もあるのでもう少し時間に余裕を持って行動しようと思う。が、どうしても上手くいかない時もあるので都度ちゃんと話して伝えていこうと思う。

仕事

  • フロントエンド
    • favicon の差し替え
      • テンションあがった
    • 軽微なバグ修正
    • GET API の呼び出し処理を POST API に書き換え
      • 割と大きな書き換えで達成感あった
  • バックエンド
    • 軽微なバグ修正
    • Mypy 推進
    • テスト拡充
    • マッピング処理の追加
  • インフラ

その他

  • いま開発中のプロダクトをトライアル利用しているエンドユーザに会いにいって使っているところ見せてもらった
    • 自分たちが作り上げたサービスを使っている姿を見れて課題も見つかったし開発のモチベーションがめっちゃ上がった
    • 今後も機会があれば伺おうと思う
    • その直後に id:naoya さんの記事見てわかる〜となった
      • もともとオーナーシップもって開発していたが更に「いいモノつくっていくぞ」という気持ちに
  • 『雰囲気でOAuth2.0を使っているエンジニアがOAuth2.0を整理して、手を動かしながら学べる本[2023年改訂版]』読み切った

findy-code.io

authya.booth.pm

2024年5月

育児

3 歳 6 ヶ月 になった。

  • 自転車の鍵を開けたり自分でヘルメット被ったりベルトしたり
  • 東京タワーにいった
    • トーマスのイベントで遊んだり
    • 頑張って階段でメインデッキまで登った
  • 下の階に住む双子のお友達と遊んだ
    • 一緒にホットケーキづくりしたり
    • もう 5 回目? くらいだしだいぶ慣れたっぽい
  • 厚木基地近くの公園でピクニックした
    • 小川もあっていっぱい濡れて遊んだ楽しそうだった
    • 1 時間以上は小川で遊んでた
  • 寝る前に「ママ嫌い」と言ったので 1 人で寝室で寝てもらった
    • ギャン泣きしたあまりよくないなこのやり方は
  • 1 人で半袖着られた
  • パパと小田原デート
    • ミナカで足湯したり
    • 小田原城のこども遊園地で遊んだりして楽しかった
  • 初めての尿検査
    • 前日に練習がてらお風呂場でコップに入れた上手
  • バナナアイス好き
    • 熟れたバナナを冷凍しただけ
  • 足が痛いとお風呂入りたがらず
    • 無理やりいれてギャン泣きすまんな
    • 「パパにありがとうは?」ときくと満面の笑みで「ありがと」

徐々に保育園に慣れてきた息子氏。ほぼ毎日「ピンクのおうち(保育園)いかない!」と言っているがなんだかんだ楽しんでいるようで安心している。奥さんの仕事の関係で朝はオレと 2 人きりで朝ご飯たべたり登園準備するようになってバタバタだけどなんとか生きている。東京タワーに行けたのは相当嬉しかったらしく、1 週間ほど東京タワー熱が続いた。オレとも 2 人で遠出したり、ママがいなくても大丈夫になってきた。今度はもっと遠くに行きたいな。

仕事

  • バックエンドのリファクタやバグ対応
    • バグが多くなってきたので mypy いれたいな
      • 凡ミスで TypeError や ValueError が起きて 500 status を返すことが多くなってきた
      • 例外処理も甘い
  • ログイン処理の修正
  • Amplify Auth(Cognito) と格闘

その他

  • 認証・認可に疎いので『雰囲気でOAuth2.0を使っているエンジニアがOAuth2.0を整理して、手を動かしながら学べる本[2023年改訂版]』を購入した
    • スラスラよめる。OAuth2.0 少しずつわかってきた
    • いま Cognito と外部 IdP を利用していてログインの流れが読めていなかったので参考になる

authya.booth.pm