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 -