2023年6月30日金曜日

Python paho-mqtt ライブラリを独自クラスとして定義する

Python paho-mqtt ライブラリを独自クラスとして定義する

概要

mqtt.Client をそのまま使わず継承して使います
その場合 on_connect などをインスタンスメソッドとして定義できます

環境

  • macOS 13.4.1
  • Python 3.11.3
  • paho-mqtt 1.6.1
  • mosquitto 2.0.15

mosquittoの起動

  • brew install mosquitto
  • brew services run mosquitto
  • mosquitto_sub -h localhost -p 1883 -t default

publish するサンプルコード

  • vim ./app.py
from paho.mqtt.client import Client


class CustomClient(Client):
    def __init__(self):
        super().__init__()

    def on_disconnect(self, client, userdata, rc):
        print("on_disconnect")
        print(client)
        print(userdata)
        print(rc)

    def on_publish(self, client, userdata, mid):
        print("on_publish")
        print(client)
        print(userdata)
        print(mid)
        self.disconnect()


if __name__ == "__main__":
    print("start")
    client = CustomClient()
    client.connect("localhost", port=1883, keepalive=60, bind_address="")
    client.publish(topic="default", payload='{"payload": "hoge"}', qos=0, retain=False)
    print("end")
  • pipenv run python app.py
  • mosquitto_sub -h localhost -p 1883 -t default

subscribe するサンプルコード

  • vim ./app.py
from paho.mqtt.client import Client


class CustomClient(Client):
    def __init__(self):
        super().__init__()

    def on_connect(self, client, userdata, flags, rc):
        print("on_connect")
        print(client)
        print(userdata)
        print(flags)
        print(rc)

    def on_disconnect(self, client, userdata, rc):
        print("on_disconnect")
        print(client)
        print(userdata)
        print(rc)

    def on_message(self, client, userdata, message):
        print("on_message")
        print(client)
        print(userdata)
        print(message.topic)
        print(message.payload)
        self.disconnect()

    def on_subscribe(self, client, userdata, mid, granted_qos):
        print("on_subscribe")
        print(client)
        print(userdata)
        print(mid)
        print(granted_qos)


if __name__ == "__main__":
    print("start")
    client = CustomClient()
    client.connect("localhost", port=1883, keepalive=60, bind_address="")
    client.subscribe("default", qos=0)
    client.loop_forever()
    print("end")

loop_forever (loop_start/loop_stop) はメッセージを受信するときに使います
publish するときには不要です

  • pipenv run python app.py
  • mosquitto_sub -h localhost -p 1883 -t default
  • mosquitto_pub -h localhost -p 1883 -t default -m "hoge"

最後に

クラス化したほうが管理は楽になるかなと思います

参考サイト

2023年6月29日木曜日

SQLAlchemyでautomap_baseを使った場合にpytestでmonkeypatchする方法

SQLAlchemyでautomap_baseを使った場合にpytestでmonkeypatchする方法

概要

前回 automap 機能を使ってみました
今回はそのコードに対してデータベースに接続しないで pytest を実行できるように monkeypatch を当ててみました

環境

  • macOS 13.4.1
  • Python 3.11.3
  • sqlalchemy 2.0.17
  • PyMySQL 1.1.0

コード

テストするコードは以下の main メソッドをテストします
実際に mysql が動作していなくてもテストが通るようにします

  • vim app.py
import sqlalchemy.ext.automap
from sqlalchemy import create_engine

# from sqlalchemy.ext.automap import automap_base # monkeypatch が当てられないパターン、必ずコード側とテストコード側では同じ import 方式にする
from sqlalchemy.orm import Session


def main():
    # automap を使用するための準備
    Base = sqlalchemy.ext.automap.automap_base()
    # Base = automap_base() # monkeypatch が当てられないパターン
    # エンジンの作成
    engine = create_engine("mysql+pymysql://root@localhost/test?charset=utf8mb4")
    # テーブル定義の読み込み
    Base.prepare(autoload_with=engine)
    # user テーブルから user モデルの抽出
    User = Base.classes.user
    # CRUD 用セッションの作成
    session = Session(engine)
    # ex) データ全件取得
    users = session.query(User).all()
    for user in users:
        print(user.name)

テストコード

conftest で monkeypatch を当てまくります
ポイントは

  • Base.prepare で接続しに行かせない
  • Session で好きな情報を返却させる

になります

  • vim test/conftest.py
import pytest
import sqlalchemy.ext.automap
from sqlalchemy.orm import Session


# automap_base に関するモック、これでデータベースへの接続を回避する
class DummyUser:
    def __init__(self) -> None:
        self.name = "hoge"


class DummyClasses:
    def __init__(self) -> None:
        self.user = DummyUser()


class DummyBase:
    def __init__(self) -> None:
        self.classes = DummyClasses()

    def prepare(self, autoload_with: bool = True):
        pass


def dummy_automap_base(declarative_base=None, **kwargs):
    return DummyBase()


# session.query 対するモック、データベースからの返却される値はこっちでコントロールする
class DummyQuery:
    def all(self):
        return []


def dummy_query(*args, **kwargs):
    return DummyQuery()


@pytest.fixture(autouse=True)
def mock_automap_base(monkeypatch):
    monkeypatch.setattr(sqlalchemy.ext.automap, "automap_base", dummy_automap_base)
    monkeypatch.setattr(Session, "query", dummy_query)
  • vim test/test_app.py
from app import main


class TestApp:
    def test_app(self):
        main()

動作確認

  • pipenv run pytest -s .

でデータベースが停止していてもテストが通ることを確認します

最後に

かなり無理やりな感じはしますが automap を使ったコードにデータベースに接続しないように monkeypatch を当ててみました
この方式の場合せっかくモデルを自動生成できるのにテスト側ではモデルを手動で生成しないといけないのが辛いです
なので automap を使ったユニットテストを書く場合には データベースが必須という状況でもいいのかもしれません

2023年6月28日水曜日

SQLAlchemyでautomapを使う

SQLAlchemyでautomapを使う

概要

過去に既存のテーブルに対して操作する方法を紹介しました
その際には自分でモデルを作成し操作しています
今回はモデル自体を自動で生成できる automap という機能を紹介します

環境

  • macOS 13.4.1
  • Python 3.11.3
  • sqlalchemy 2.0.17
  • PyMySQL 1.1.0

テーブル作成

なんでも OK です

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50),
  `age` int,
  `profile` varchar(50),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

INSERT INTO user VALUES (null, 'hawk', 10, 'ruby,sinatra');
INSERT INTO user VALUES (null, 'snowlog', 20, 'python,flask');
INSERT INTO user VALUES (null, 'hawksnowlog', 30, 'swift,realm');

automap サンプルコード

MySQL クライアントは今回 pymysql を使っているのでエンジン作成の際に pymysql を指定します

  • vim app.py
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from sqlalchemy import create_engine

# automap を使用するための準備
Base = automap_base()

# エンジンの作成
engine = create_engine("mysql+pymysql://root@localhost/test?charset=utf8mb4")

# テーブル定義の読み込み
Base.prepare(autoload_with=engine)

# user テーブルから user モデルの抽出
User = Base.classes.user

# CRUD 用セッションの作成
session = Session(engine)

# ex) データ登録
session.add(User(name="test", age=99, profile="golang,gin"))
session.commit()

# ex) データ全件取得
users = session.query(User).all()
for user in users:
    print (user.name)

ポイントは Base.prepare の部分になります
モデルを自分で定義する際は declarative_base を使いますが automap の場合は prepare(autoload_with=engine) を使います

メリット・デメリット

メリットとしてはモデルを自分で定義する必要がないので簡単です
デメリットとしては抽出したモデルはクラスとして定義していないのでタイプヒントなどに使えません
また pyright などを使っている場合は抽出したモデルのフィールドやメソッド情報が pyright 側に伝わらないので補完も使えなくなります

automap で自動抽出したモデルからクラスを再定義する方法があるのか気になりました

最後に

SQLAlchemy の automap を使ってみました
ユースケースとしてはすでにあるテーブルに対するツールやライブラリを作成したいときに使う感じかなと思います
モデルを自分で定義するケースはまだテーブルなどがなくマイグレーションも行いたい場合にはモデルを自分で定義したほうが絶対にきれいになります

既存のテーブルに対して automap を使って将来的にマイグレーションスクリプトまで管理したい場合にはどうすれrばいいのかも気になりました

参考サイト

2023年6月27日火曜日

macのtmux上でssh時に何度もパスワードを求められる場合に確認する項目

macのtmux上でssh時に何度もパスワードを求められる場合に確認する項目

概要

mac 上でターミナルを使っていると自動でキーチェインが起動しパスワードを覚えてくれます
2度目以降のログインでは同じパスワードを使ってくれるので入力の必要がありません
しかし tmux を経由しているとなぜかパスワードが何回も求められてしまいます
そんなときの対処方法を紹介します

環境

  • macOS 13.4
  • デフォルトシェル zsh

とりあえず解決方法

  • ssh-add ~/.ssh/id_rsa

これでパスワードを登録すれば次回以降はパスワードを求められなくなります

確認すること

  • echo $SSH_AUTH_SOCK

でちゃんと ssh-agent のパスが表示されることを確認しましょう
echo は tmux を起動した上で実行してください
これが設定されていないとパスワードを覚えてくれません
ちなみに 13.4 だと以下のように表示されます

/private/tmp/com.apple.launchd.undAdGmmSD/Listeners

もし設定されていない場合は .tmux.conf に以下を記載してください

set-environment -g SSH_AUTH_SOCK $SSH_AUTH_SOCK

これで tmux を起動していない zsh 上の SSH_AUTH_SOCK の設定が tmux 上でも引き継がれ ssh-agent がデフォルトで動作するようになります

2023年6月26日月曜日

emacsでruby-lspを使ってみる

emacsでruby-lspを使ってみる

概要

solargraph に変えて ruby-lsp を使ってみました
emacs のクライアントは lsp-mode を使います

環境

  • macOS 11.7.6
  • emacs 28.2
    • lsp-mode 20230524.1820
  • Ruby 3.2.1
    • ruby-lsp 0.5.1

lsp-mode アップデート or インストール

ruby-lsp に対応しているバージョンをインストールしましょう

  • M-x package-list-packages

ruby-lsp

solargraph のアンインストール

  • gem uninstall solargraph

ruby-lsp のインストール

  • gem install ruby-lsp

ruby-lsp コマンドが実行できることを確認してください
また emacs から ruby-lsp コマンドが見つからず Command "ruby-lsp" is not present on the path. という感じでうまく起動できない場合はターミナルや tmux から一度抜けて再度試してみてください

emacs の設定

(require 'lsp-mode)
; (setq lsp-ruby-lsp-use-bundler t)
(add-hook 'ruby-mode-hook 'lsp)

bundler 配下で使用する場合には Gemfile に

gem 'ruby-lsp'

をし bundle install した後上記コメント部分をアウトしてください

動作確認

トラブルシューティング

solargraph から移行した場合には前のセッションが残っておりうまく動作しないことがあるのでセッションを削除して再度 ruby-lsp を起動してみてください

  • rm ~/.emacs.d/.lsp-session-v1
The connected server(s) does not support method textDocument/definition.
To find out what capabilities support your server use ‘M-x lsp-describe-session’
and expand the capabilities section

となり定義へのジャンプはできませんでした
おそらく https://github.com/Shopify/ruby-lsp/pull/195 にあるように特殊なコメントを記載することでそこに飛ぶことはできるようです
またその場合は lsp-mode 側で lsp-enable-links を有効にする必要がありそうです

最後に

まだまだ開発途中という感じがします
また vscode に特化しているのと記述が Spotify 基準になっているので少し特殊な書き方が必要かもしれません
当面は solargraph + robe という気がします

参考サイト

2023年6月24日土曜日

Pythonのrequestsでメソッドを動的に変更する方法とdataにバイト文字列を設定する方法

Pythonのrequestsでメソッドを動的に変更する方法とdataにバイト文字列を設定する方法

概要

get や post は使わずにメソッドを指定します
requests.request("method_name") を使います

また data に bytes が設定できるので試してみました

環境

  • macOS 13.4.1
  • Python 3.11.3
    • requests 2.31.0

サンプルコード

import requests

method = "POST"
url = "https://request-dumper.kakakikikeke.com"
headers = {"x-test-name": "hoge", "content-type": "application/json"}
data = '{"key": "value"}'

response = requests.request(method, url=url, headers=headers, data=data.encode("utf-8"))
print(response.json())

request.request の第一引数でメソッド名を指定できます
また data にバイト文字列を設定していますがこれでちゃんと body に json が設定されて送信されます

2023年6月23日金曜日

xmllintでパイプを使ってフォーマットする方法

xmllintでパイプを使ってフォーマットする方法

忘れるのでメモ

コマンド

echo "<xml><name>hawksnowlog</name></xml>" | xmllint --format -

2023年6月22日木曜日

poetryで急に依存関係の解決やaddができなくなったときの対処方法

poetryで急に依存関係の解決やaddができなくなったときの対処方法

概要

原因は不明ですがなぜか poetry add や install が特定のライブラリでできなくなったときの対象法を紹介します

環境

  • Ubuntu 18.04
  • Python 3.10.2
  • poetry 1.2.2

解決方法

  • poetry config --list

で表示されたキャッシュパスをまるごと削除します

  • rm -rf /home/user01/.cache/pypoetry

2023年6月21日水曜日

Venturaでカメラ連携できないときに試すこと

Venturaでカメラ連携できないときに試すこと

概要

タイトルの通りです
個人的には以下で解決できるケースが多いです

環境

  • M2pro mac mini
    • macOS 13.4
  • iPhoneXR
    • iOS 16.5

mac mini と iPhone をケーブルで接続する

これで復活することがほとんです

(無線の場合) iPhone を横にする

意外とこれを忘れる人がいるのかもしれません
初回接続時には iPhone の向きが横でないと mac が接続しにいかないことがあるようです
騙されたと思って横向きで固定してみましょう

2023年6月20日火曜日

rq のダッシュボードを動かす方法

rq のダッシュボードを動かす方法

概要

rq-dashboard を使います

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1

起動

  • docker run --rm -p 9181:9181 -e RQ_DASHBOARD_REDIS_URL=redis://172.17.0.1:6379 eoranged/rq-dashboard

172.17.0.1 は docker ホストの IP になります

動作確認

localhost:9181 にアクセスするとジョブやキュー、ワーカーの情報が UI で確認できるようになります

参考サイト

2023年6月19日月曜日

M2 mac mini で UTM を動かして Windows が起動するか試してみた

M2 mac mini で UTM を動かして Windows が起動するか試してみた

概要

VirtualBox が動作しないので移行先を探していたところ UTM というツールを見つけたので試してみました

今回は arm 版 Windodws11 を起動するか試します

環境

  • macOS 13.4
  • UTM 4.2.5
  • Windows11 21H2 arm64

UTM

ここから dmg をダウンロードしてインストールします

基本は UTM にギャラリーがあるのでそこにある Windows11 arm64 のガイドに従って進めましょう

arm64版Windows11 21H2 ダウンローダーの取得

https://uupdump.net/known.php?q=22621.674 ここからダウンロードできます

言語、エディションを選択しダウンローダーを取得します
基本は日本語を選択しエディションは Pro を選択すればいいでしょう

また Windows のバージョンは Windows11 21H2 がオススメです
これ以外だと ISO の作成時にエラーになりました

Unable to retrieve data from Windows Update servers. Reason: EMPTY_FILELIST
If this problem persists, most likely the set you are attempting to download was removed from Windows Update servers.

ダウンロードスクリプトの実行、ISO ファイルの取得

取得できた zip を回答すると macOS 用のスクリプトをがあるので実行します

  • brew tap sidneys/homebrew
  • brew install cabextract wimlib cdrtools sidneys/homebrew/chntpw

sidneys/homebrew/chntpw でエラーになる場合は以下を代わりに実行してください

  • brew tap minacle/chntpw
  • brew install aria2 minacle/chntpw/chntpw

必要無ツールのインストールが完了した ISO を作成します

  • sh uup_download_macos.sh

22000.1_PROFESSIONAL_ARM64_JA-JP.ISO というファイルが作成されました

UTM で VM の作成

新規仮想マシンを作成 -> 仮想化 -> Windows と選択します

ISO を選択する画面でのオプションは以下のようにします

CPU、メモリ、ディスクサイズなどは好きな値を設定してください
共有ディレクトリも必要に応じて設定してください

起動

設定を保存して VM を作成したら起動しましょう

startup.nsh の画面になる場合は exit で一回抜けて continue します
そして「press any key to boot from cd or dvd」が表示されているところでエンターを押せば Windows の設定画面に進みます

SPICE Guest tools のインストール

iso がマウントされているのでエクスプローラを開いてインストールしましょう
exe ファイルがあるのでそれを実行すれば自動でインストールされます
これがインストールされていないとドライバがないためネットワークなどに接続できません

最後に

一応これでセットアップ画面に進みライセンスなどを入力したところ問題なく Windows11 が起動しました
ただ起動時になぞのエラーメッセージが出たり SPICE Guest tools がうまくインストールできなかったりとまだまだ動作は不安定のようです

アプリのインストールなどは行っていないのでどこまでアプリが動作するか不明です

Windows インストール後は ISO ファイルのマウントは解除しておきましょう

参考サイト

2023年6月18日日曜日

Windowsで突然IMEが表示されなくなってしまった場合の対処方法

Windowsで突然IMEが表示されなくなってしまった場合の対処方法

概要

Mac から Windows にリモートデスクトップで接続していた際に急に Windows 上で IME が表示されなくなったので対処しました
原因は謎ですが接続元のクライアントの状況が変わったのでそれが原因かもしれません

環境

  • macOS 13.4 (接続元)
    • M2pro mac mini
    • Magic Keyboard2 (US 配列)
  • Windows10 (接続先)

復旧方法

  1. Windows の設定
  2. 時刻と言語
  3. 言語
  4. 優先する言語 -> 日本語 -> オプション
  5. キーボード -> キーボードの追加 -> MicrosoftIME (or Google 日本語入力)

再度なくなってしまった場合は面倒ですが再度追加してください

2023年6月17日土曜日

M2 mac mini (M2pro) で xmrig を動かす方法

M2 mac mini (M2pro) で xmrig を動かす方法

答え

docker を使いましょう

https://hawksnowlog.blogspot.com/2021/09/cpu-mining-monero-on-docker.html

segmentation fault について

Ventura 13.4 + xmrig 6.19.3 だと segmentation fault になります

CPU コア数について

また docker 経由だと cpu-max-threads-hint を 100 にしても 10 コアあるうちの 5 コアしかフルに使わなかったのでまだチューニングが必要そうです

答えとしては docker for mac の設定で Resources から CPU コア数を 10 に上げましょう

ハッシュレート

ログだとだいたい 3000H/s ほどです
かなり少ない印象です
更にプール側で見ると 700H/s になってしまっているので何かおかしいような気がします (原因不明)

↓ 原因判明

Mac mini の場合もデフォルトだとモニタの電源が切れるとスリープモードに入ってしまうためマイニングもそれで停止してしまいます
システム設定 -> ディスプレイ -> 詳細設定 -> ディスプレイがオフのときに自動でスリープさせないにチェック

2023年6月16日金曜日

Flask でフォーム情報を受け取る方法 (jsonまたxmlでも可)

Flask でフォーム情報を受け取る方法 (jsonまたxmlでも可)

概要

前回 json or xml どちらでも受け取れるような仕組みを考えてみました

今回はさらに form でも受け取れるような API に変更します

環境

  • macOS 13.4
  • Python 3.11.3
  • Flask 2.3.2

サンプルコード

レスポンスの変換に関するコードも含まれています
ポイントは Convert クラスの is_form と from_form です
特定のキーが含まれているかでフォームかどうかを判断するようにしています
flask で入れ子なフォームデータを受け取ると Immutablemultidict がなぜか入れ子にならずそのままキー名として使われるようです

リクエスト情報を Converter.to_dict するとどんなデータでも dict に変換してくれるイメージです

  • vim app.py
import copy
import json
import traceback
from json.decoder import JSONDecodeError
from xml.parsers.expat import ExpatError

import dicttoxml
import xmltodict
from flask import Flask, Response, jsonify, request

app = Flask(__name__)


class Profile:
    def __init__(self) -> None:
        self.langs = ["ruby", "python", "swift"]

    def to_dict(self):
        return self.langs


# 各 langs の xml 変換時の属性名を設定
def set_item_name(_: str) -> str:
    return "lang"


class User:
    def __init__(self, name: str = "default_user", **_) -> None:
        self.name = name
        self.age = 10
        self.profile = Profile()

    def to_dict(self):
        return {"name": self.name, "age": self.age, "profile": self.profile.to_dict()}


# dict から User オブジェクトへの変換
def convert_obj(func):
    def wrapper(*args, content_dict: dict, **kwargs):
        try:
            user = User(**content_dict["xml"])
        except KeyError:
            # データ構造が正しくない場合はエラー
            raise ValueError("Root attribute must start with 'xml'.")

        return func(*args, user=user, **kwargs)

    return wrapper


# リクエストの raw xml や raw json を dict に変換するクラス
class Converter:
    def __init__(self, body: bytes, form: dict) -> None:
        self.body = body
        self.form = form

    def to_dict(self) -> dict:
        if self._is_xml():
            return self._from_xml()
        elif self._is_json():
            return self._from_json()
        elif self._is_form():
            return self._from_form()
        return {}

    def _is_xml(self) -> bool:
        try:
            self._from_xml()
        except ExpatError:
            return False
        return True

    def _is_json(self) -> bool:
        try:
            self._from_json()
        except JSONDecodeError:
            return False
        return True

    def _is_form(self) -> bool:
        try:
            self._from_form()
        except KeyError:
            return False
        return True

    def _from_xml(self) -> dict:
        return xmltodict.parse(self.body)

    def _from_json(self) -> dict:
        return json.loads(self.body)

    def _from_form(self) -> dict:
        return {"xml": {"name": self.form["xml[name]"]}}  # なぜか入れ子にならない?


# xml から dict への変換
def convert_dict(func):
    def wrapper(*args, **kwargs):
        body = copy.copy(request.data)
        form = copy.copy(request.form)
        data = Converter(body, form).to_dict()
        if not data:
            raise ValueError("You must be specified request body xml, json or form.")

        return func(*args, content_dict=data, **kwargs)

    return wrapper


@app.route("/", methods=["GET", "POST"])
@convert_dict
@convert_obj
def parse_xml(user: User):
    # デフォルトは json
    return jsonify(user.to_dict())


@app.after_request
def after_request(response: Response):
    accept_header = request.headers.get("Accept")
    if accept_header == "application/xml":
        # xml に変換
        xml = dicttoxml.dicttoxml(
            response.get_json(),
            attr_type=True,
            root=True,
            custom_root="orgRoot",
            item_func=set_item_name,  # type: ignore
        )
        response.headers["Content-Type"] = "applicatoin/xml"
        response.data = xml
    elif accept_header == "application/json":
        # json に変換 (そのまま返却)
        pass
    return response


@app.errorhandler(ValueError)
def handle_bad_request(e):
    return traceback.format_exception_only(e), 400

動作確認

  • pipenv run flask run
curl localhost:5000 -XPOST -d '{"xml": {"name":"hawksnowlog"}}' -H 'content-type: application/json'
curl localhost:5000 -XPOST -d '<xml><name>hawksnowlog</name></xml>' -H 'content-type: application/xml'
curl localhost:5000 -XPOST -d "xml[name]=hawksnowlog" -H 'content-type: application/x-www-form-urlencoded'

結果はすべて同じになります

2023年6月15日木曜日

Flask でレスポンス情報を Accept ヘッダに応じて xml or json に変換して返却する方法

Flask でレスポンス情報を Accept ヘッダに応じて xml or json に変換して返却する方法

概要

Flask でレスポンスを返却する際に xml or json を返却する方法を考えます

flask-restful という拡張を使うと簡単にできそうですが今回は素の flask だけを使って考えます

環境

  • macOS 13.4
  • Python 3.11.3
  • Flask 2.3.2
    • xmltodict 0.13.0
    • dicttoxml 1.7.16

方針

  • Accept ヘッダの内容に応じて返却するタイプを変更する
    • Accept: application/xml -> xml を返却
    • Accept: applicaton/json -> json を返却
  • すべてのルーティングに対応するため after_request を使う
  • 特に指定がない場合は json を返却する

サンプルコード

一部前回のコードを含みます
また xml レスポンスの情報を少し加工するための処理も入っています
ポイントはコード下部にある after_request の部分になります

  • vim app.py
import copy
import json
import traceback
from json.decoder import JSONDecodeError
from xml.parsers.expat import ExpatError

import dicttoxml
import xmltodict
from flask import Flask, Response, jsonify, request

app = Flask(__name__)


class Profile:
    def __init__(self) -> None:
        self.langs = ["ruby", "python", "swift"]

    def to_dict(self):
        return self.langs


# 各 langs の xml 変換時の属性名を設定
def set_item_name(_: str) -> str:
    return "lang"


class User:
    def __init__(self, name: str = "default_user", **_) -> None:
        self.name = name
        self.age = 10
        self.profile = Profile()

    def to_dict(self):
        return {"name": self.name, "age": self.age, "profile": self.profile.to_dict()}


# dict から User オブジェクトへの変換
def convert_obj(func):
    def wrapper(*args, content_dict: dict, **kwargs):
        try:
            user = User(**content_dict["xml"])
        except KeyError:
            # データ構造が正しくない場合はエラー
            raise ValueError("Root attribute must start with 'xml'.")

        return func(*args, user=user, **kwargs)

    return wrapper


# xml から dict への変換、すでに dict の場合は何もしない
def convert_dict(func):
    def wrapper(*args, **kwargs):
        data = copy.copy(request.data)
        try:
            data = xmltodict.parse(data)
        except ExpatError:
            try:
                data = json.loads(data)
            except JSONDecodeError:
                # xml じゃないかつ dict じゃない場合はエラー
                raise ValueError("You xml or dict must be specified.")

        return func(*args, content_dict=data, **kwargs)

    return wrapper


@app.route("/", methods=["GET", "POST"])
@convert_dict
@convert_obj
def parse_xml(user: User):
    # デフォルトは json
    return jsonify(user.to_dict())


@app.after_request
def after_request(response: Response):
    accept_header = request.headers.get("Accept")
    if accept_header == "application/xml":
        # xml に変換
        xml = dicttoxml.dicttoxml(
            response.get_json(),
            attr_type=True,
            root=True,
            custom_root="orgRoot",
            item_func=set_item_name,  # type: ignore
        )
        response.headers["Content-Type"] = "applicatoin/xml"
        response.data = xml
    elif accept_header == "application/json":
        # json に変換 (そのまま返却)
        pass
    return response


@app.errorhandler(ValueError)
def handle_bad_request(e):
    return traceback.format_exception_only(e), 400

動作確認

  • pipenv run python flask run
# xml
curl localhost:5000 -XPOST -d '<xml><name>hawksnowlog</name></xml>' -H 'content-type: application/xml' -H 'Accept: application/xml' 
<?xml version="1.0" encoding="UTF-8" ?><root><age type="int">10</age><name type="str">hawksnowlog</name></root>

# json
curl localhost:5000 -XPOST -d '{"xml": {"name":"hawksnowlog"}}' -H 'content-type: application/xml' -H 'Accept: application/json'
{
  "age": 10,
  "name": "hawksnowlog"
}

# json (指定なし)
curl localhost:5000 -XPOST -d '<xml><name>hawksnowlog</name></xml>' -H 'content-type: application/xml' 
{
  "age": 10,
  "name": "hawksnowlog"
}

最後に

Flask で Accept ヘッダの内容に応じてレスポンスの形式を変更するような API を考えてみました
やっていることは非常に単純ですがうまく作らないとコードが大変なことになりそうなので注意が必要そうです

(こういったマルチメディアタイプの API は実際に活用されているケースがあるのだろうか)

2023年6月14日水曜日

Flask でリクエストボディが xml でも json でもどちらでも受けれるようにするサンプルコード

Flask でリクエストボディが xml でも json でもどちらでも受けれるようにするサンプルコード

概要

content-type: application/xml or content-type: application/json どちらでも受けれるような API を考えます

環境

  • macOS 13.4
  • Python 3.11.3
  • Flask 2.3.2

方針

  • デコレータを使って xml -> dict をいい感じに変換する
  • 最終的には変換はオブジェクトに変換する
  • 各種エラー対応はできる限りしっかりしたい
  • レスポンスに対しては現在は考慮しない

サンプルコード

  • vim app.py
import copy
import json
import traceback
from json.decoder import JSONDecodeError
from xml.parsers.expat import ExpatError

import xmltodict
from flask import Flask, request

app = Flask(__name__)


class User:
    def __init__(self, name: str = "default_user", **_) -> None:
        self.name = name
        self.age = 10


# dict から User オブジェクトへの変換
def convert_obj(func):
    def wrapper(*args, content_dict: dict, **kwargs):
        try:
            user = User(**content_dict["xml"])
        except KeyError:
            # データ構造が正しくない場合はエラー
            raise ValueError("Root attribute must start with 'xml'.")

        return func(*args, user=user, **kwargs)

    return wrapper


# xml から dict への変換、すでに dict の場合は何もしない
def convert_dict(func):
    def wrapper(*args, **kwargs):
        data = copy.copy(request.data)
        try:
            data = xmltodict.parse(data)
        except ExpatError:
            try:
                data = json.loads(data)
            except JSONDecodeError:
                # xml じゃないかつ dict じゃない場合はエラー
                raise ValueError("You xml or dict must be specified.")

        return func(*args, content_dict=data, **kwargs)

    return wrapper


@app.route("/", methods=["GET", "POST"])
@convert_dict
@convert_obj
def parse_xml(user: User):
    return user.name


@app.errorhandler(ValueError)
def handle_bad_request(e):
    return traceback.format_exception_only(e), 400

動作確認

正常系

curl localhost:5000 -XPOST -d '{"xml": {"name":"hawksnowlog"}}' -H 'content-type: application/json'
curl localhost:5000 -XPOST -d '<xml><name>hawksnowlog</name></xml>' -H 'content-type: application/xml' -H 'Accept: application/xml'

異常系

curl localhost:5000 -XPOST -d '{"xml2": {"name":"hawksnowlog"}}' -H 'content-type: application/json'
curl localhost:5000 -XPOST -d 'hoge' -H 'content-type: application/json'

最後に

レスポンスは accept ヘッダのタイプに応じて勝手に変換してくれるようになっているとかっこいい

2023年6月13日火曜日

Vagrant で vmdk を圧縮する方法

Vagrant で vmdk を圧縮する方法

概要

VM 上では容量を使用していないのに知らぬ間に肥大化していることがあります
そんな場合に vmdk ファイルを圧縮する方法を紹介します

ファイル名や UUID の部分は適宜変更してください

環境

  • macOS 11.7.6
  • vagrant 2.3.4
  • VirtualBox 7.0.6

ゼロ埋め

  • sudo dd if=/dev/zero of=zero bs=4k; \rm zero

圧縮

  • cd "/Users/username/VirtualBox VMs/vm_default_xxxx/"
  • VBoxManage clonehd ubuntu-bionic-18.04-cloudimg.vmdk ubuntu-bionic-18.04-cloudimg.vdi --format vdi
  • VBoxManage modifyhd ubuntu-bionic-18.04-cloudimg.vdi compact
  • VBoxManage clonehd ubuntu-bionic-18.04-cloudimg.vdi ubuntu-bionic-18.04-cloudimg_2.vmdk --format vmdk

vdi ファイル削除

  • VBoxManage list hdds
  • VBoxManage closemedium disk cb44d740-7c42-41d2-b0cf-e3938e8e0ee8
  • rm buntu-bionic-18.04-cloudimg.vdi

ディスクデタッチ/アタッチ

  • VBoxManage list vms
  • VBoxManage storageattach 3fffffaa-db44-46b5-b669-77d21b173089 --storagectl "SCSI" --port 0 --device 0 --type hdd --medium "/Users/username/VirtualBox VMs/vm_default_xxxx/ubuntu-bionic-18.04-cloudimg_2.vmdk"

古い vmdk 削除

  • VBoxManage closemedium disk f8f59ec9-2d06-47c2-87c9-29604a72b38f
  • rm ubuntu-bionic-18.04-cloudimg.vmdk

動作確認

  • vagrant up
  • vagrant ssh
  • rm /home/vagrant/zero
  • sudo reboot -h now

で問題なく動作することを確認しましょう

最後に

自分の場合は 17GB -> 4GB ほどになりました

2023年6月12日月曜日

Stable Diffusion WebUI を M2pro mac mini 上で動かす方法

Stable Diffusion WebUI を M2pro mac mini 上で動かす方法

概要

前回 Intel mac 上で動作させました
今回は Apple Silicon の M2 mac mini 上で動作させてみました

環境

  • macOS 13.4
  • M2pro mac mini
  • Stable Diffusion WebUI 1.3.2
  • Python 3.11.3

インストール

前回の手順とほぼ同じです
gpu を使えるので cpu only のフラグは外します

  • git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui
  • cd stable-diffusion-webui
  • sh webui.sh

localhost:7860 にアクセスできれば OK です

1枚の生成にかかる時間

512x512 のサイズでステップ数 20 (すべてデフォルト設定) でだいたい 10 - 20 秒ほどで生成できます

モデルを変える場合は

基本は models/Stable-diffusion 配下に置くだけです

Lora などは models/Lora になります VAE は models/VAE になります

2023年6月11日日曜日

rqではワーカーに渡す関数はクラスのインスタンスメソッドでもよい

rqではワーカーに渡す関数はクラスのインスタンスメソッドでもよい

概要

エンキューする際に func 引数にインスタンスメソッドが指定可能か試してみました
結論としては指定可能です

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1

エンキュー

  • vim app.py
from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler
from lib.util import Message

queue = Queue('bar', connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)

scheduler.cron(
    "*/1 * * * *",
    func=Message("Hello!").say,
    args=[],
)

scheduler.run()

上記の func=Message("Hello!").say のポイントです
コーラブルであればいいのでインスタンスメソッドでも渡すことができます

ワーカー

  • vim lib/util.py
from datetime import datetime

class Message():
    def __init__(self, message: str):
        self.message = message

    def say(self):
        msg = f"{self.message}: {datetime.now()}"
        print(msg)
        return msg
  • vim worker.py
from rq import Worker, Queue
from redis import Redis

redis = Redis()
queue = Queue('bar', connection=redis)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)

動作確認

  • pipenv run python app.py
  • pipenv run python worker.py

最後に

これも pickle を使ってシリアライズしているため実現可能になっています
コンストラクタも呼ぶことができるので便利です
あとは引数と組み合わせればいろいろなケースに応じた処理ができるようになるかなと思います

rq の記事はいくつか紹介していますがワーカー側のコードはほぼ変わっていないのでコマンド実行でもいいかもしれません