kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Playwright for Python: PDF 形式で印刷する

Playwright for Python でウェブサイトのスクリーンショット(画像形式)を取得するのではなく「PDF 形式で」取得したいこともあると思う(実際に最近あった〜).Playwright for Python では page.pdf() を使えば簡単に実装できる❗️

そして page.pdf() はデフォルトで「印刷用 CSS : @media printを認識してくれるため,ウェブサイト側のデザインに沿って "整って" 表示された PDF を取得できる.

playwright.dev

page.pdf()

デフォルトの用紙サイズは Letter なので format オプションで A4 など適切な用紙サイズを指定しておくのが良いと思う.

page.pdf(path='pdf/print.pdf', format='A4')

page.emulate_media() + page.pdf()

もし @media print ではなく @media screen で PDF を取得する場合は page.pdf() の前に page.emulate_media() でメディアを変更しておく必要がある.

page.emulate_media(media='screen')
page.pdf(path='pdf/screen.pdf', format='A4')

試す

今回はサンプルとして,MDN のドキュメントを @media print@media screen それぞれで PDF 化した「1ページ目」を載せておく.MDN のドキュメントだと大きな差はないけど「ヘッダー部分」@media print にはなく,ある程度は "整って" 表示されていると思う👌

developer.mozilla.org

今回試したサンプルコード

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto('https://developer.mozilla.org/ja/docs/Web/CSS/@media')

    page.pdf(path='pdf/print.pdf', format='A4')

    page.emulate_media(media='screen')
    page.pdf(path='pdf/screen.pdf', format='A4')

    browser.close()

Playwright for Python: screenshot() に指定できる便利なオプション3選

Playwright for Pythonpage.screenshot() でスクリーンショットを取得するときに指定できる「便利なオプション3選」を紹介する❗️

1. full_page

ウェブサイトのスクリーンショットを「全て」取得する場合 full_page オプションが使える💡デフォルトは False になっているけど,使う機会は多いと思う❗️以下のように実装できる.

page.screenshot(path='images/1_full.png', full_page=True)

2. clip

ウェブサイトのスクリーンショットを「部分的に」取得する場合 clip オプションが使える💡スクリーンショットを長期間残しておくと,アーカイブしておくファイルサイズも増えてくるため,本当に必要な部分に限定してスクリーンショットを取得しておくのは良いプラクティスだと思う❗️

  • x(軸にする x 座標)
  • y(軸にする y 座標)
  • width(横幅 px)
  • height(縦幅 px)

以下のように実装できる.

page.screenshot(path='images/2_clip.png', clip={'x': 150, 'y': 150, 'width': 400, 'height': 400})

3. mask

スクリーンショットの一部を「隠して」取得する場合 mask オプションが使える💡隠す箇所には Playwright for Python の Locator オブジェクトを配列で指定する.例えば kakakakakku blog の "ブログタイトル" と "記事タイトル" を隠すなら,以下のように実装できる.ドキュメントを確認したところ,現状だと色はピンク (#FF00FF) 固定になっていた.

page.screenshot(path='images/4_mask.png', mask=[page.locator('#title > a'), page.locator('.entry-title')])

今回試したサンプルコード

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto('https://kakakakakku.hatenablog.com/')

    page.screenshot(path='images/0_default.png')
    page.screenshot(path='images/1_full.png', full_page=True)
    page.screenshot(path='images/2_clip.png', clip={'x': 150, 'y': 150, 'width': 400, 'height': 400})
    page.screenshot(path='images/3_mask.png', mask=[page.locator('#title > a'), page.locator('.entry-title')])

    browser.close()

GitHub Actions と Playwright を組み合わせて Build Your Own Radar を継続的に構築しよう

「自分だけの Technology Radar」を構築できる「Build Your Own Radar」を以下の記事で紹介した💡

kakakakakku.hatenablog.com

Build Your Own Radar サービスに入力する Technology Radar ファイルとしては Google Sheets / CSV / JSON を使うことができて,もし Build Your Own Radar を企業やチームで構築するなら Technology Radar ファイルを「GitHub 管理」したくなると思う.

今回は検証として,GitHub に CSV ファイルをコミットしたら GitHub ActionsPlaywright for Python を組み合わせて「自動的に Technology Radar を構築する仕組み」を試してみた.構成はザッと以下のような感じで,最終的に Playwright for Python で Build Your Own Radar の "スクリーンショット" と "PDF" を取得して GitHub Actions のアーティファクトに保存する.

.github/workflows/byor.yaml

GitHub Actions のワークフローを以下のように構成した.

まず,1点目のポイントは GitHub Actions の「サービスコンテナ」を使っているところで,ワークフローの中に Build Your Own Radar コンテナを起動して http://localhost/ でアクセスできるようにしている.また CSV ファイルを読み込ませるために volumes を指定してホスト側をマウントしている.

docs.github.com

2点目のポイントは GitHub にコミットした CSV ファイルを sudo cp -p ${GITHUB_WORKSPACE}/files/* /files でマウントしたホスト側のディレクトリにコピーしているところ.検証中にハマったのは Build Your Own Radar コンテナを起動したときに実行される build_and_start_nginx.sh の中に cp /src/build-your-own-radar/spec/end_to_end_tests/resources/localfiles/* ./files/ と実装されていて,デフォルトの radar.csvradar.json がセットアップされることに気付かなかったこと.GitHub にコミットした CSV ファイルが反映されずに結構な時間を溶かした🌀

他は Playwright for Python を実行するために Python 環境をセットアップしたり,GitHub Actions のアーティファクトに "スクリーンショット" と "PDF" を保存したりしている❗️他は自由に変えてもらえればと〜

name: Build Your Own Radar (BYOR)

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    services:
      byor:
        image: wwwthoughtworks/build-your-own-radar
        ports:
          - 80:80
        volumes:
          - /files/:/opt/build-your-own-radar/files
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
      - name: Set up Playwright
        run: |
          playwright install
      - name: Wait for BYOR container running
        run: |
          # Wait for launching container
          sleep 60
          sudo cp -p ${GITHUB_WORKSPACE}/files/* /files
      - name: Build radar
        run: |
          python byor.py
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: byor-artifacts
          path: artifacts/
          retention-days: 90

GitHub Actions のワークフローから実行している Playwright for Python コードも以下に載せておく.もっとキレイに実装できる気もするけど,ポイントとしては "スクリーンショット" と "PDF" を取得しているところ.

byor.py

from playwright.sync_api import sync_playwright
from time import sleep

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()

    page.goto('http://localhost/')

    page.locator('input[name="sheetId"]').fill('http://localhost/files/radar.csv')
    page.locator('a.button').click()

    # Wait for "Your Technology Radar will be available in just a few seconds"
    page.wait_for_selector('div#radar')

    page.screenshot(path='artifacts/byor.png', full_page=True)
    page.pdf(path='artifacts/byor.pdf', format='A4')

    browser.close()

アーティファクト

GitHub Actions の実行が終わると GitHub Actions のアーティファクトに byor-artifacts.zip が保存される.その中に byor.pngbyor.pdf が含まれているので,継続的に Build Your Own Radar の履歴を残せるようになった❗️GitHub Actions の retention-days 設定には注意〜

まとめ

GitHub ActionsPlaywright for Python を組み合わせて「自動的に Technology Radar を構築する仕組み」を試してみた❗️

コードなどは GitHub リポジトリに公開しておいた〜

github.com

Build Your Own Radar で「自分だけの」Technology Radar を構築しよう

「Build Your Own Radar (BYOR)」を使うと Thoughtworks 社が毎年2回ほど出している Technology Radar と同じフォーマットで「自分だけの Technology Radar」を構築できる❗️

企業の技術戦略を策定してどんなテクノロジー組織を目指しているのかを可視化したり,チームでどんなテクノロジーを今後採用したいのかを可視化したりできる.チーム間で Build Your Own Radar を共有して情報交換をするのも良さそう.チームイベントやレトロスペクティブなどの場で Build Your Own Radar を構築してみるのはいかがでしょうか✌

\( 'ω')/ オレオレ Technology Radar を構築するぞー

www.thoughtworks.com

Thoughtworks 社の Technology Radar に関しては以下を見てもらえればと〜

  • Quadrants
    • Programming Languages and Frameworks
    • Tools
    • Platforms
    • Techniques
  • Rings
    • Adopt
    • Trial
    • Assess
    • Hold

www.thoughtworks.com

Build Your Own Radar を構築する

Build Your Own Radar (BYOR) を構築する方法は「大きく2つ」ある.

  1. ホスティングされた Build Your Own Radar サービスを使う
  2. Build Your Own Radar サービスをコンテナで起動する

www.thoughtworks.com

1. ホスティングされた Build Your Own Radar サービスを使う

最初に Technology Radar ファイルを作っておく.フォーマットは大きく以下の3種類から選べる.Google Sheets のサンプルとして ThoughtWorks Technology Radar Vol. 27 と Vol. 26 を載せておく.注意点としては Google Sheets を使う場合は Google 認証を求められることと,CSV / JSON の場合は GitHub などに置いて「公開設定」をしておく必要があること.

次に Build Your Own Radar に Technology Radar ファイルの URL を入力すれば OK❗️

おおおおお!スゴイ👏

2. Build Your Own Radar サービスをコンテナで起動する

今度は Docker を使って Build Your Own Radar サービスをコンテナで起動する.コンテナイメージは Docker Hub に公開されていて,すぐに試せる.macOS / Windows など「ローカル環境で」動かせるのはメリットだと思う❗️

Build Your Own Radar の実装は GitHub で確認できる.

github.com

もし Google Sheets を使うなら Google OAuth Client ID を取得しておく必要がある.今回は Google OAuth Client ID を取得せずお手軽に試せる CSV で試す.コンテナで起動する場合は CSV ファイルを公開しなくても使える.

まず ./files ディレクトリと ./files/radar.csv を作る(もちろん JSON でも OK👌).CSV ファイルは Google Sheets (Thoughtworks Technology Radar Vol. 27) からエクスポートした.そして docker run コマンドを実行して Build Your Own Radar コンテナを起動するときに ./files ディレクトリをマウントしておく.

$ mkdir files
$ touch files/radar.csv
$ docker run -p 8080:80 -v $(pwd)/files/:/opt/build-your-own-radar/files wwwthoughtworks/build-your-own-radar

そして http://localhost:8080/ を開くと Build Your Own Radar にアクセスできる❗️

おおおおお!スゴイ👏

Technology Radar ファイルの URL は http://localhost:8080/files/radar.csv のように localhost を参照できる.

まとめ

「Build Your Own Radar (BYOR)」を使って「自分だけの」Technology Radarを構築しよう❗️

Docker Desktop の Extensions を実装してみよう

2023年1月にリリースされた Docker Desktop v4.16.0 (release notes) で GA になった「Docker Extensions」の Extensions は段々と種類が増えてきて,Disk Usage / Aqua Trivy / LocalStack など,最近使う機会もあったりする.今回は "Quickstart" ドキュメントを読みながら Docker Extensions の実装に入門してみた❗️

\( 'ω')/ オレオレ Docker Extension を実装するぞー

docs.docker.com

🐳 docker extension コマンド

まず,Docker Extensions を実装するためには docker extension コマンドを使う.コマンド一覧を以下に載せておく.

$ docker extension --help

Usage:  docker extension [OPTIONS] COMMAND

Manages Docker extensions

Options:
      --socket string   The Desktop extension manager socket

Management Commands:
  dev             Extension development helpers

Commands:
  init            Create a new Docker Extension based on a template.
  install         Install a Docker extension with the specified image
  ls              List installed Docker extensions
  rm              Remove a Docker extension
  share           Generate a link to share the extension.
  update          Remove and re-install a Docker extension
  validate        Validate an extension image or metadata file
  version         Print the client and server versions

Run 'docker extension COMMAND --help' for more information on a command.

$ docker extension dev --help

Usage:  docker extension dev COMMAND

Extension development helpers

Commands:
  debug       Set the debug mode for an extension
  reset       Reset the source and disables the debug mode for the extension UI
  ui-source   Set a new source for the extension UI

Run 'docker extension dev COMMAND --help' for more information on a command.

🐳 検証環境

今回は Docker Desktop for Mac v4.16.2 と,以下の Docker Extensions を検証環境として使う.

$ docker extension version
Client Version: v0.2.17
Server API Version: 0.3.3

🐳 docker extension init コマンド

まず docker extension init コマンドを実行すると Docker Extensions のプロジェクトを簡単にセットアップできる.Quickstart に沿って,今回は my-extension という名前にする.初期化中のログを見ると Go backendReact app など,馴染みのある技術スタックが使われていることがわかる❗️

$ docker extension init my-extension
? Title: my-extension
? Description: my-extension
? Vendor: kakakakakku
? Image Repository where the extension will be pushed: kakakakakku/my-extension

(中略)

Creating a Go backend service...
Initializing new go module...
Creating a React app...
Copying ui dir...
Renaming some files...
Installing npm packages, this may take a few minutes...

(中略)

プロジェクトディレクトリは以下のようになる(tree コマンドで2階層目まで表示している).

$ tree -L 2 .
.
├── Dockerfile
├── Makefile
├── README.md
├── backend
│   ├── go.mod
│   ├── go.sum
│   └── main.go
├── docker-compose.yaml
├── docker.svg
├── metadata.json
└── ui
    ├── index.html
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── public
    ├── src
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts

6 directories, 15 files

Docker Extensions を実装する前に「構成」を理解しておく必要がある.Docker Extensions は,重要な2つのコンポーネント「UI (例: React x TypeScript x MUI)」「Backend (例: Go)」を組み合わせて実装する❗️以下に UI (Frontend) と Backend の関係性がまとまった図をドキュメントから引用して載せておく.

Extension architecture | Docker Documentation より引用

ちなみに「UI」は React に限定されているわけではなく Vue / Anguler なども使える.「Backend」も Go に限定されているわけではなく Node.js / Python なども使える.よって,Docker Extensions を実装する技術スタックとしては「自由度がある」とも言える.詳しくはドキュメント参照〜

docs.docker.com

さらに React コンポーネントとしてデフォルトで導入されている「MUI(旧 Material UI)」も限定されているわけではなく,他のライブラリを使うこともできる.しかし,Docker Extensions のドキュメントには「React x MUI 推奨」と書いてある.Docker Extensions デザインの一貫性 (look & feel) の観点や今後 MUI を拡張する予定もあるらしく,基本的には MUI を使うのが良さそう.

mui.com

docs.docker.com

🐳 Dockerfile

次に Dockerfile を確認する.初期化したときに作られた Dockerfile は以下のようになっていた.ポイントは Multi-stage builds を使って Backend (Go) と UI (React) をビルドして,最終的に Alpine イメージにまとめているところ.

FROM golang:1.19-alpine AS builder
ENV CGO_ENABLED=0
WORKDIR /backend
COPY backend/go.* .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go mod download
COPY backend/. .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -trimpath -ldflags="-s -w" -o bin/service

FROM --platform=$BUILDPLATFORM node:18.12-alpine3.16 AS client-builder
WORKDIR /ui
# cache packages in layer
COPY ui/package.json /ui/package.json
COPY ui/package-lock.json /ui/package-lock.json
RUN --mount=type=cache,target=/usr/src/app/.npm \
    npm set cache /usr/src/app/.npm && \
    npm ci
# install
COPY ui /ui
RUN npm run build

FROM alpine
LABEL org.opencontainers.image.title="my-extension" \
    org.opencontainers.image.description="my-extension" \
    org.opencontainers.image.vendor="kakakakakku" \
    com.docker.desktop.extension.api.version="0.3.3" \
    com.docker.extension.screenshots="" \
    com.docker.extension.detailed-description="" \
    com.docker.extension.publisher-url="" \
    com.docker.extension.additional-urls="" \
    com.docker.extension.changelog=""

COPY --from=builder /backend/bin/service /
COPY docker-compose.yaml .
COPY metadata.json .
COPY docker.svg .
COPY --from=client-builder /ui/build ui
CMD /service -socket /run/guest-services/backend.sock

🐳 コンテナイメージをビルドする

手順に戻る!今度は docker build コマンドを実行して Dockerfile からコンテナイメージをビルドする.Multi-stage builds を使っているので,最終的に 12.6MB とサイズも小さく抑えられている.

$ docker build -t kakakakakku/my-extension .

$ docker image ls kakakakakku/my-extension
REPOSITORY                 TAG       IMAGE ID       CREATED              SIZE
kakakakakku/my-extension   latest    ef80c4794f8b   About a minute ago   12.6MB

🐳 docker extension install コマンド

最後は docker extension install コマンドを実行して Docker Extensions を Docker Desktop にインストールすれば完了〜

$ docker extension install kakakakakku/my-extension
Extensions can install binaries, invoke commands and access files on your machine.
Are you sure you want to continue? [y/N] y
Installing new extension "kakakakakku/my-extension"
Installing service in Desktop VM...
Setting additional compose attributes
VM service started
Installing Desktop extension UI for tab "My-Extension"...
Extension UI tab "My-Extension" added.
Extension "my-extension" installed successfully

Docker Desktop に my-extension が表示されたぁぁぁぁぁ❗️

🐳 docker extension update コマンド

次に UI (React) と Backend (Go) それぞれのコードを修正して,Docker Extensions に更新を反映する.もう一度 docker build コマンドを実行して,今度は docker extension update コマンドを実行する.すると Docker Extensions に更新が反映されたぁぁぁぁぁ❗️

$ docker build -t kakakakakku/my-extension .
$ docker extension update kakakakakku/my-extension

🐳 ホットリロード

特にフロントエンド側の開発は試行錯誤を伴うため,毎回ビルドせずにコードを修正したらすぐに「ホットリロード」する仕組みがある(正確には ViteHot Module Replacement (HMR) を使っている).手順としては npm run dev コマンドを実行して http://localhost:3000 に UI アプリケーションを起動し,docker extension dev ui-source コマンドを実行して UI コンポーネントの参照先を localhost にする.あとはコードを修正するとほぼリアルタイムに Docker Desktop に反映されるため,開発体験が良くなる❗️詳しくはドキュメント参照〜

$ cd ui
$ npm run dev
$ docker extension dev ui-source kakakakakku/my-extension http://localhost:3000
UI source for the extension "kakakakakku/my-extension" changed to "http://localhost:3000"%

docs.docker.com

🐳 アイコンを変更する

Docker Desktop の Extensions 一覧に表示されるアイコンは初期化したプロジェクトに含まれている docker.svg で,metadata.json で設定する仕組みになっている.

{
  "icon": "docker.svg",
  "vm": {
    "composefile": "docker-compose.yaml",
    "exposes": {
      "socket": "backend.sock"
    }
  },
  "ui": {
    "dashboard-tab": {
      "title": "My-Extension",
      "src": "index.html",
      "root": "ui",
      "backend": {
        "socket": "backend.sock"
      }
    }
  }
}

さらに docker.svgDockerfileCOPY docker.svg . を実行してコンテナイメージに含まれている.metadata.jsonDockerfile を修正すると Docker Extensions のアイコンを変更できる.

ちなみに Dockerfile に記述できる LABELcom.docker.desktop.extension.icon もあって混乱するけど,LABEL は Docker Extensions を Extensions Marketplace に公開するときに使われる.二重に設定してるように感じてややこしく感じたけど,仕様である旨はドキュメントに書いてあった.

In the example Dockerfile, you can see that the image label com.docker.desktop.extension.icon is set to an icon URL. The Extensions Marketplace displays this icon without installing the extension. The Dockerfile also includes COPY docker.svg . to copy an icon file inside the image. This second icon file is used to display the extension UI in the Dashboard, once the extension is installed.
Create an advanced frontend extension | Docker Documentation

docs.docker.com

🐳 Extensions Marketplace に公開する

Docker Extensions の実装と動作確認が終わったら Extensions Marketplace に公開できる.今回は "Quickstart" を試しただけなので割愛する.公開するプロセスは以下のドキュメントに詳しく載っている.

docs.docker.com

🐳 Makefile

今回は docker extension コマンドを実行したけど,初期化したプロジェクトには Makefile も含まれている.よって,make build-extensionmake install-extension など,簡単に実行することもできる❗️

$ make help
Please specify a build target. The choices are:
build-extension                Build service image to be deployed as a desktop extension
install-extension              Install the extension
update-extension               Update the extension
prepare-buildx                 Create buildx builder for multi-arch build, if not exists
push-extension                 Build & Upload extension image to hub. Do not push if tag already exists: make push-extension tag=0.1
help                           Show this help

🐳 まとめ

今回は Docker Extensions の "Quickstart" ドキュメントを読みながら,Hello World レベルではあるけど「オレオレ Docker Extension」を実装してみた❗️実装の流れを把握できて良かった.次はもっと実用的な Docker Extension を実装してみたいぞ〜