2021年7月30日金曜日

Python でカスタムエラーを作成する方法

Python でカスタムエラーを作成する方法

概要

Python で独自のエラークラスを作成する方法を紹介します

環境

  • macOS 11.5
  • Python 3.8.3

サンプルコード

class InvalidStringError(Exception):
    def __init__(self, name):
        self.message = "A name of {} has an invalid character.".format(name)

def check_name(name):
    if 'z' in name:
        raise InvalidStringError(name)
    return True

try:
    check_name("hawk")
    check_name("hawkz")
except InvalidStringError as e:
    print(e.message)

少し解説

Exception を継承して作成します 共通のメッセージや共通のパラメータを管理するのであれば一段 Base を挟んで継承すると良いと思います

コンストラクタは定義しなくても OK ですが message などは最低限必要になるので設定できるようにしておくと良いと思います

使用する場合は普通に raise すれば OK です エラーも execpt で検出することができます

参考サイト

2021年7月29日木曜日

Python の with 句を試してみる

Python の with 句を試してみる

概要

Python の with 句を使うことができるサンプルクラスを作成してみました 使えるようになると便利な機能かなと思います

環境

  • macOS 11.5
  • Python 3.8.3

サンプルコード

class WithSampleUser:
    def __init__(self, name, age=None):
        self.name = name
        self.age = age
        self.tel = None
        self.profile = None

    def gen_profile(self):
        if self.age is None:
            raise
        self.profile = {"name": self.name, "age": self.age, "tel": self.tel}

    def __enter__(self):
        # コンストラクタ以外の初期化処理
        self.tel = '090-1234-5678'
        return self

    def __exit__(self, ex_type, ex_value, trace):
        # エラーが発生した場合
        if ex_value:
            self.name = None
            self.age = None
            self.tel = None
            print("error")
        else:
            print(self.profile)

with WithSampleUser("hawksnowlog", 10) as user:
    user.gen_profile()

with WithSampleUser("hawksnowlog") as user:
    user.gen_profile()

解説

まず必須なのは __entry____exit__ の実装になります with 句に入った場合と出た場合に自動的に呼ばれるメソッドで実装が必須になります

__entry__ 側は初期化的なことを行う感じになります 例えばストリームのオープンやファイル操作などのシリアライズを行います

__exit__ は逆に終了時に呼ばれるのでクローズ処理を行います 開いたものを自動的に閉じるために用意されているメソッドみたいな感じなので終了処理を書きます また __exit__ はエラーが発生した際にも呼ばれます その際にはエラー情報が格納された引数「ex_type」「ex_value」「trace」にエラー情報が格納されています

with 句ないで try catch することで発生したエラーを握りつぶせますがあまり with 句内で try catch することはないのかなと思います

想定ユースケース

以下のようなケースで with 句用にクラスをカスタマイズすると良いかなと思います

  • ファイル操作を行うクラス
  • データベース操作を行うクラス
  • HTTP など外部通信を行うクラス
  • ソケットなどのコネクションを確立するクラス

要するに open -> close の流れが必要になるオブジェクトを扱うクラスの場合には with 句で扱えるように拡張するのが良いかなと思います

2021年7月28日水曜日

ipython 超入門

ipython 超入門

概要

Python 標準の repl を強化した版になります 今回はインストールして試してみました

環境

  • macOS 11.5
  • Python 3.8.3
  • ipython 7.25.0

インストール

  • pipenv install ipython

起動

  • ipython
Python 3.8.3 (default, Jun  7 2021, 23:23:45) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.25.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]:

補完

TAB でできます モジュール名や変数名の他、OS 上にあるファイル名も補完してくれます

オブジェクトの情報をデバッグする

定義したオブジェクトの最後に「?」を記述することでオブジェクトのデバッグ情報を確認できます

In [4]: a = "hoge"

In [5]: a?
Type:        str
String form: hoge
Length:      4
Docstring:  
str(object='') -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or
errors is specified, then the object must expose a data buffer
that will be decoded using the given encoding and error handler.
Otherwise, returns the result of object.__str__() (if defined)
or repr(object).
encoding defaults to sys.getdefaultencoding().
errors defaults to 'strict'.

シェルの実行

コマンドの先頭に「!」を付与します 「!」なしでも動作しますが変数としても使えるのでコマンドということを明示するために付与したほうが良いかなと思います

In [6]: a = !pwd

In [7]: a
Out[7]: ['/Users/username/Documents/work/try_ipython']

In [8]: pwd
Out[8]: '/Users/username/Documents/work/try_ipython'

Magic functions

「%」から始まる関数が良いされておりこれを使うと速度の計測 (%timeit) や python スクリプト自体の実行 (%run) ができます

timeit はデフォルトで 100000 ループするので実行時には注意しましょう

最後に

Jupyter のカーネルにもなっているので JypyterNotebook に慣れている人は iPython も簡単に使えるのかなと思います

参考サイト

2021年7月27日火曜日

emacs で日本語切り替えする方法

emacs で日本語切り替えする方法

概要

OS 側の IME ではなく emacs 内部の IME を使って日本語入力を切り替える方法

環境

  • macOS 11.5
  • emacs 27.1

切替方法

  • M-x toggle-input-method

入力方式を指定する場合は

  • M-x set-input-method

その後入力方式を指定します キーバインドは以下の通りです

  • C-x RET C-\

2021年7月26日月曜日

RedisGraph を使ってみた

RedisGraph を使ってみた

概要

RedisGraph は Redis 上で GQL を使うことができる redis の拡張モジュールです 今回はインストール方法から簡単 GQL の発行まで行ってみました

環境

  • Redis with RedisGraph 6.2.3
  • RedisInsight 1.10.1

RedisGraph モジュールが有効な redis を起動する

docker イメージがあるのでそれを使うと簡単に立ち上げることができます すでに redis が立ち上がっている場合は一旦停止してから redisgraph を起動してください

  • docker run -p 6379:6379 -d redislabs/redisgraph

データの登録

ではデータを登録してみましょう Graph データを登録するには redis-cli で GRAPH.QUERY を使います

127.0.0.1:6379> GRAPH.QUERY MotoGP "CREATE (:Rider {name:'Valentino Rossi'})-[:rides]->(:Team {name:'Yamaha'}), (:Rider {name:'Dani Pedrosa'})-[:rides]->(:Team {name:'Honda'}), (:Rider {name:'Andrea Dovizioso'})-[:rides]->(:Team {name:'Ducati'})"
1) 1) "Labels added: 2"
   2) "Nodes created: 6"
   3) "Properties set: 6"
   4) "Relationships created: 3"
   5) "Cached execution: 0"
   6) "Query internal execution time: 4.416402 milliseconds"

MotoGP というキーに graphdata を登録します 少し複雑に見えますが一つずつ見ていくと単純な構文になっています

データを登録する場合は CREATE を使います SQL で言うところの INSERT になります デーは括弧で囲いリレーションはハイフンを使います

上記のデータは

  • Valentino Rossi はチーム Yamaha に所属
  • Dani Pedrosa はチーム Honda に所属
  • Andrea Dovizioso はチーム Ducati に所属

という 3 つのデータを登録しています グラフデータはある属性がある属性に紐付いているというデータを登録することで可視化や検索を行うことができます

データベースの一覧を取得する

今回は MotoGP というデータベースに登録しています RedisGraph ではデータベースは単純な key として登録されています なのでデータベースの一覧を取得する場合は keys で OK です

127.0.0.1:6379> keys *
1) "MotoGP"

データの検索

例えばライダーの名前だけを取得してみます SQL で言うところの SELECT 文は GQL では MATCH という構文になります

127.0.0.1:6379> GRAPH.QUERY MotoGP "MATCH (r:Rider) RETURN r.name"
1) 1) "r.name"
2) 1) 1) "Valentino Rossi"
   2) 1) "Dani Pedrosa"
   3) 1) "Andrea Dovizioso"
3) 1) "Cached execution: 0"
   2) "Query internal execution time: 0.189014 milliseconds"

(r:Rider) でノードを検索できます 先頭の小文字の r はエイリアスでその後に続くクエリで r で検索したノードを参照できます 今回はすべてのノードを取得してその中の properties の name だけを表示しています

おまけ: RedisInsight で動作確認

RedisInsight を使えばグラフデータを可視化することができます

すでに RedisInsight が構築されているのであれば左メニューにある「RedisGraph」をクリックします そしてデータベースを選択して GQL を入力すれば OK です

ライダーのノード一覧が表示されたらノードをダブルクリックしてみましょう すると紐付いているチームの情報がグラフ上に展開され確認することができます もし紐付きが複数ある場合はいろいろなノードがリアルタイムで展開されていくのでノードの関連性が簡単に確認することができます

最後に

ある属性とある属性が紐付き関係性がグラフデータのような構造のデータがある場合に RedisGraph に入れて可視化すると簡単に関係性を確認することができるようになります

テーブルデータは SQL、グラフデータは GQL というふうに使い分けできるようになると強力なツールになると思います

参考サイト

2021年7月23日金曜日

Redis の管理画面の RedisInsight を試してみた

Redis の管理画面の RedisInsight を試してみた

環境

Redis の管理画面に RedisInsight というのがあります これを使うと画面からキーの登録や削除、取得などができるようになります redis-cli を直接叩いたりすることもできます

環境

  • macOS 11.4
  • Redis 4.0.9
  • RedisInsight 1.10.1

インストール

今回は docker 上で動作させます 以下のコマンドで起動できます

  • docker run -d -v redisinsight:/db -p 8001:8001 redislabs/redisinsight:latest

これでブラウザで 8001 にアクセスすると管理画面が確認できます 一番始めに EULA に同意する必要があるのでトグルボタンを変更して CONFIRM をクリックしましょう

設定

次に接続する Redis を選択します 今回は VM 上で動作している既存の redis があるのでそれを使います その場合は「I already have a database」を選択します

「Connect to Redis Database」を選択します

Redis を追加する画面になるので IP アドレスとポートと好きなデータベース名を入力しましょう

追加が完了すると以下のように一覧に表示されます

動作確認

試しに key/value を登録してみましょう 「ADD KEY」からキーを登録できます 今回は文字列を登録しています

登録が完了すると以下のように一覧に表示されます

最後に

編集機能まで使えると危険なことがあるので slave を用意してそっちを登録するとビューワーとして使うことができると思います (おそらく readonly ユーザは作成できないはず)

参考サイト

2021年7月22日木曜日

Flask 内部で別の API をコールしたい場合はモジュールやメソッドとして切り出す必要がある

Flask 内部で別の API をコールしたい場合はモジュールやメソッドとして切り出す必要がある

概要

Flask で作成した API を他のルーティングからコールしたいケースはあると思います 本来は HTTP Client などを使ってちゃんとコールしたいところですがさすがにできなさそうなのでそういった場合は共通する処理をモジュールやメソッドとして切り出すしかなさそうです

環境

  • macOS 11.4
  • Python 3.8.3
  • flask 1.1.2

サンプルコード

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/abc/<x>/<y>', methods=['GET'])
def abc_route(x, y):
    return jsonify(abc(x, y))

@app.route('/xyz', methods=['POST'])
def xyz():
    x = request.form["x"]
    y = request.form["y"]
    return jsonify(abc(x, y))

def abc(x, y):
    return "{}.{}".format(x, y)

動作確認

  • FLASK_DEBUG=1 pipevn run flask run

GET と POST で同じ結果が返ってきます

  • curl -XPOST localhost:5000/xyz -F 'x=1' -F 'y=2'
  • curl -XGET localhost:5000/abc/1/2

ポイント

共有するモジュールが method やクエリに依存しないようにする

例えば flask では GET と POST ではパラメータの受け取り方が変わります 共有するモジュール内ではパラメータの受け取りなどはせずにパラメータをそれぞれのルーティングで受け取ったあとで引数としてパラメータを受け取るようにしましょう

共有するモジュール内でレスポンスを返さないようにする

あくまでもレスポンスを返すのはルーティング側で行うようにしましょう 同じ処理をさせたいがレスポンスは違うようにしたい場合に対応できなくなります

return などは単純な文字列や辞書型などを返却するようにして flask のレスポンスとして整形するのはルーティング側で行います

別アプリとして切り出すのもありか

例えばサンプルで言うところの abc(x, y) を別の Flask アプリとして切り出します そして requests などを使って RPC 的に abc をコールします この方法であれば例えば外部向けに公開したりもできるのでより汎用的に使えるようになります もしくは PyPi 形式で外部公開するのもありだと思います

最後に

変なことはせずに単純に考えるようにしましょう

参考サイト

2021年7月21日水曜日

Paramiko でコマンドをバックグランド実行する方法

Paramiko でコマンドをバックグランド実行する方法

概要

過去 に paramiko を試してみました 今回はバックグランドで実行する方法を紹介します

環境

  • macOS 11.4
  • Python 3.8.3
  • paramiko 2.7.2

サンプルコード

import paramiko

def execute_cmd():
    with paramiko.SSHClient() as ssh_client:
        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh_client.connect(hostname='192.168.1.2',
                    port=22,
                    username='username',
                    password='xxxxxxxxx')
        transport = ssh_client.get_transport()
        channel = transport.open_session()
        channel.exec_command('for i in `seq 1 10`; do echo $i; sleep 1; done', timeout=5)
        # try: 
        #     RECV_SIZE = 1024 * 32
        #     stdout_data = b''
        #     stderr_data = b''
        #     while not channel.closed or channel.recv_ready() or channel.recv_stderr_ready():
        #         stdout_data += channel.recv(RECV_SIZE)
        #         stderr_data += channel.recv_stderr(RECV_SIZE)
        #     code = channel.recv_exit_status()
        # finally:
        #     channel.close()
        return "ok"

print(execute_cmd())

ポイント

transport.open_session() でセッションを作成してからチャネルに対して exec_command することでバックグランド実行になります

ただこの場合は結果が戻ってこないので別ロジックでチャネルを使って結果を待つ必要があります コメントしている部分を外せば stdout_data に結果が入りますが Python 的には結局結果を待つ必要があるので結果が取得できるまでプロンプトは戻ってきません

バックグランドでコマンドを実行させる場合はコマンド側に結果をどこかに格納してそれを参照するような仕組みにしたほうが良いかなと思います (例えばコマンドの結果をファイルに出力してそのファイルの中身を見るなど)

参考サイト

2021年7月20日火曜日

Flask でファイルのアップロード

Flask でファイルのアップロード

概要

Flask でファイルのアップロードをやってみました

環境

  • macOS 11.4
  • Python 3.8.3
  • flask 1.1.2

サンプルコード

import os
from flask import Flask, flash, request, redirect, url_for, send_from_directory, render_template
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = '/tmp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.secret_key = 'randomstringyoulike'

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

@app.route('/', methods=['POST'])
def upload_file():
    # file パートがない場合はアップロード画面にリダイレクト
    if 'file' not in request.files:
        flash('No file part')
        return redirect(request.url)
    file = request.files['file']
    # ファイルが選択されていない場合はアップロード画面にリダイレクト
    if not file.filename:
        flash('No selected file')
        return redirect(request.url)
    if file and allowed_file(file.filename):
        # マルチバイトなど XSS の可能性のある文字列を変換
        filename = secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return redirect(url_for('uploaded_file', filename=filename))

@app.route('/', methods=['GET'])
def upload_file_view():
    return render_template('index.html')
  • vim templates/index.html
<!doctype html>
<title>Upload new File</title>
{% with messages = get_flashed_messages() %}
  {% if messages %}
    <ul class=flashes>
    {% for message in messages %}
      <li>{{ message }}</li>
    {% endfor %}
    </ul>
  {% endif %}
{% endwith %}
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
  <input type=file name=file>
  <input type=submit value=Upload>
</form>

解説

「/」にブラウザでアクセスした場合にはアップロードフォームを表示します フォームはテンプレートとして管理します

ファイルは multipart/form-data としてアップロードします Flask では file パートがある場合に request.files に格納されるためここをチェックします そもそも file パートがない場合とアップロードフォームでファイルを指定しないでアップロードした場合はエラーとしトップのアップロードフォーム画面にリダイレクトします

今回は flash メソッドを使ってエラー分をテンプレートに表示するような仕組みにしていますがここは好きなようにしてもらって大丈夫です

RuntimeError: The session is unavailable because no secret key was set. が発生する場合は app.secret_key に好きな暗号化文字列を入力します flush が session を使う仕組みなので secret_key を設定していない場合に上記のエラーが発生します

アップロードされたファイルは「/tmp」に保存されます 保存する際に XSS 対策としてファイル名を半角英数字のみのファイル名に変換してあげています ファイルのアップロードに成功した場合はアップロードしたファイルを返しています flask でファイルの内容をそのまま返したい場合には send_from_directory を使うのが便利です

もう少し考えるべきこと

今はサイズ制限などしていません 数GBのファイルをアップロードされてサーバ側がパンクする可能性もあるのでファイルサイズのチェックはサーバ側でもするべきです ちなみにファイルサイズも含めてリクエストのペイロード全体のサイズを制限する場合は app.config['MAX_CONTENT_LENGTH'] を使います このサイズを超えてアップロードしようとした場合は 413 エラーが flask から返却されます

また拡張子のチェックはしていますが正確なファイルの内容を見ているわけではないので本来であればファイルの内容まで確認した上でアップロードさせるべきです

あとは自分で実装せずともそのあたりのお作法などを実装してくれている Flask-Uploads というモジュールがあるのでこれを使うのも手です

動作確認

  • pipenv run flask run

でアプリが起動した localhost:5000 にアクセスするとアップロードフォームが表示されます また curl の場合は以下のようにアップロードできます

  • curl -X POST -F file=@./test.txt localhost:5000

参考サイト

2021年7月19日月曜日

docker-compose で起動した Gitlab でバックアップを取得する方法

docker-compose で起動した Gitlab でバックアップを取得する方法

概要

Gitlab を docker-compose で動作させている場合にバックアップを取得する方法を紹介します

環境

  • Gitlab 13.12.5-ee.0

フルバックアップ

  • docker-compose exec gitlab gitlab-backup

コンテナ内の /var/opt/gitlab/backups に保存されるのでホストマシン側でマウントしたりデータボリュームを使って保存しておきましょう

secret のバックアップ

  • docker-compose exec gitlab gitlab-ctl backup-etc

コンテナ内の /etc/gitlab/config_backup に保存されるのでホストマシン側でマウントしたりデータボリュームを使って保存しておきましょう

最後に

バックアップ時のストラテジーは docker-compose でも普通に指定できます

secret のバックアップは Gitlab のバージョンが 12.3 以上でなければなりません

2021年7月16日金曜日

Python の retry モジュールを使ってみる

Python の retry モジュールを使ってみる

概要

retry はデコレータで簡単にリトライ処理を追加することができる Python のモジュールです 簡単な使い方を紹介します

環境

  • macOS 11.4
  • Python 3.8.3
  • retry 0.9.2

インストール

  • pipenv install retry

サンプルコード

import os
import logging
from retry import retry

@retry(FileNotFoundError, tries=5, delay=10, logger=logging)
def exists_file():
    with open("/tmp/hoge") as f:
        print(f.readlines())

exists_file()

基本的な使い方は @retry デコレータを使います 引数に「捕捉するエラー」「リトライ回数」「待ち時間」を指定します これらが基本的な引数になります

リトライ中のログを出力する場合は logger 引数を指定します

backoff も指定されており一定間隔ではなくリトライするごとに待ち時間を多くすることもできます

基本的には関数のデコレータとして使うので特定の関数をリトライさせたい場合には便利かなと思います あとはエラーに対してリトライ処理を掛けるので関数内で意図的に raise する必要が出てくることもあります

2021年7月15日木曜日

requests を monkeypatch する方法

requests を monkeypatch する方法

概要

requests の get を pytest の monkeypatch でモックしてみたいと思います

環境

  • macOS 11.4
  • Python 3.8.3
  • requests 2.25.1
  • pytest 6.2.4

サンプルコード

import requests

def get_access():
    result = requests.get("http://localhost")
    return result.status_code

テストコード

  • vim test_test.py
import requests
from test import get_access

class MockResponse:

    def __init__(self, code=200):
        self.status_code = code
        self.text = ""

    @staticmethod
    def json():
        return {"": ""}

def mock_get(*args, **kwargs):
    return MockResponse()

def test_get_access(monkeypatch):
    monkeypatch.setattr(requests, "get", mock_get)
    result = get_access()
    assert(result == 200)

解説

ポイントは mock_get と MockResponse です 本来 get が返すべきクラスを自分が作成したクラスにすげ替えてあげる感じになります

これで実際にはアクセスせずテストすることができるようになります

2021年7月14日水曜日

SQLAlchemy で json_set を使う方法

SQLAlchemy で json_set を使う方法

概要

特定のキーだけ更新したい場合に使えます 使用するテーブルは前回の User テーブルを使います

環境

  • macOS 11.4
  • Python 3.8.3
  • MySQL 8.0.25
  • SQLAlchemy 1.3.19

サンプルコード

user_query = User.query.filter(User.id == id)
for key, value in profile.items():
    user_query.update(
        {"profile": db.func.json_set(
            User.profile,
            "$." + key,
            value)
        }, synchronize_session='fetch'
    )
    db.session.commit()

User テーブルの profile カラムが JSON 型です その中にある key を value で更新します json_set を使うと既存のキーはそのままで指定したキーだけ更新します

update を使ってそのまま dict を指定すると上書き更新になるのでキーが消えてしまったりします

参考サイト

2021年7月13日火曜日

Gitlab の docker-compose で SMTP サーバを構築する方法

Gitlab の docker-compose で SMTP サーバを構築する方法

概要

過去に gmail を設定する方法を紹介しました 今回は docker コンテナで postfix サーバを構築してそのメールサーバと gitlab コンテナを連携する方法を紹介します

環境

  • Ubuntu 18.04
  • docker 20.10.7
  • Gitlab 13.12.5
  • postfix

docker-compose.yml

version: "3.6"
services:
  gitlab:
    image: gitlab/gitlab-ee:13.12.5-ee.0
    ports:
      - "22:22"
      - "443:443"
      - "9090:9090"
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'https://gitlab.example.com'
        gitlab_rails['smtp_enable'] = true
        gitlab_rails['smtp_address'] = "mailserver"
        gitlab_rails['smtp_port'] = 587
        gitlab_rails['smtp_domain'] = "gitlab.example.com"
        gitlab_rails['gitlab_email_from'] = 'gitlab@gitlab.example.com'
        gitlab_rails['gitlab_email_reply_to'] = 'noreply@gitlab.example.com'
    restart: always

  mailserver:
    image: boky/postfix
    environment:
      ALLOWED_SENDER_DOMAINS: 'gitlab.example.com'

解説

postfix のイメージは boky/postfix を使っています ALLOWED_SENDER_DOMAINS で送信を許可するドメインを指定します

gitlab 側は smtp で gitlab_rails['smtp_enable'] を true にします そして smtp_address に postfix コンテナのサービス名を指定します gitlab と postfix コンテナは同一ネットワークに属するためこれで postfix にアクセスできます

from や reply-to に好きなアドレスを指定可能ですが基本的には external_url で起動している gitlab と同じドメインを指定しましょう 理由はメールのドメインが IP で解決できる必要があるためですが、それでも受信側の SPF レコードなどに登録されている必要もあるため迷惑メールになる可能性はあります

動作確認

ユーザを招待してメールが受信できるか確認してみましょう ちなみに Gmail では以下のエラーになり postfix から送信できませんでした

mailserver_1  | 2021-07-06T02:21:53.626911+00:00 INFO    postfix/smtp[385]: AFA8B1A45C7: to=<xxx@gmail.com>, relay=gmail-smtp-in.l.google.com[64.233.188.26]:25, delay=2.9, delays=0.05/0.01/1.9/0.97, dsn=5.7.1, status=bounced (host gmail-smtp-in.l.google.com[64.233.188.26] said: 550-5.7.1 [192.168.100.10] The IP you're using to send mail is not authorized to 550-5.7.1 send email directly to our servers. Please use the SMTP relay at your 550-5.7.1 service provider instead. Learn more at 550 5.7.1  https://support.google.com/mail/?p=NotAuthorizedError t5si1621087pjo.91 - gsmtp (in reply to end of DATA command))

最後に

当然ですがメールの受信はできないので注意しましょう

参考サイト

2021年7月12日月曜日

Charts.js でズームする方法

Charts.js でズームする方法

概要

Charts.js でズーム機能を使うには chartjs-plugin-zoom というプラグインを使います 今回は簡単な使い方を紹介します

環境

  • macOS 11.4
  • Chrome 91.0.4472.114
  • Charts.js 3.4.0

プラグインのインストール

head タグに以下の 2 行を追加します

<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js" integrity="sha512-UXumZrZNiOwnTcZSHLOfcTs0aos2MzBWHXOHOuB0J/R44QB0dwY5JgfbvljXcklVf65Gc4El6RjZ+lnwd2az2g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.1.1/chartjs-plugin-zoom.min.js" integrity="sha512-NxlWEbNbTV6acWnTsWRLIiwzOw0IwHQOYUCKBiu/NqZ+5jSy7gjMbpYI+/4KvaNuZ1qolbw+Vnd76pbIUYEG8g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

使ってみる

オプションで zoom というオプションが指定できるようになっています

pan を指定するとドラッグでズームしたグラフを動かすことができます

<html>
<head>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.4.0/chart.min.js" integrity="sha512-JxJpoAvmomz0MbIgw9mx+zZJLEvC6hIgQ6NcpFhVmbK1Uh5WynnRTTSGv3BTZMNBpPbocmdSJfldgV5lVnPtIw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js" integrity="sha512-UXumZrZNiOwnTcZSHLOfcTs0aos2MzBWHXOHOuB0J/R44QB0dwY5JgfbvljXcklVf65Gc4El6RjZ+lnwd2az2g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.1.1/chartjs-plugin-zoom.min.js" integrity="sha512-NxlWEbNbTV6acWnTsWRLIiwzOw0IwHQOYUCKBiu/NqZ+5jSy7gjMbpYI+/4KvaNuZ1qolbw+Vnd76pbIUYEG8g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>

<div>
  <canvas id="myChart"></canvas>
</div>

<script>
var ctx = $('#myChart')[0].getContext("2d");
var data = {
  labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
  datasets: [{
      label: '# of Votes',
      data: [12, 19, 3, 5, 2, 3],
      fill: true,
      borderColor: 'rgb(75, 192, 192)',
      tension: 0.5,
      borderWidth: 5,
  }]
};
var options = {
  scales: {
      y: {
          beginAtZero: true
      }
  },
  plugins: {
    zoom: {
      pan: {
        enabled: true,
        mode: "x"
      },
      zoom: {
        wheel: {
          enabled: true,
        },
        pinch: {
          enabled: true
        },
        mode: 'xy',
      }
    }
  }
};
var config = {
  type: 'line',
  data: data,
  options: options,
};
var myChart = new Chart(ctx, config);
</script>

</body>
</html>

X 軸のみズームさせたい場合には mode: 'x' にします

ズームをリセットするには

chart のオブジェクトに対して resetZoom() をコールするだけです

button を設置して onclick イベントなどでリセットする関数をコールしてあげましょう

<html>
<head>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.4.0/chart.min.js" integrity="sha512-JxJpoAvmomz0MbIgw9mx+zZJLEvC6hIgQ6NcpFhVmbK1Uh5WynnRTTSGv3BTZMNBpPbocmdSJfldgV5lVnPtIw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js" integrity="sha512-UXumZrZNiOwnTcZSHLOfcTs0aos2MzBWHXOHOuB0J/R44QB0dwY5JgfbvljXcklVf65Gc4El6RjZ+lnwd2az2g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.1.1/chartjs-plugin-zoom.min.js" integrity="sha512-NxlWEbNbTV6acWnTsWRLIiwzOw0IwHQOYUCKBiu/NqZ+5jSy7gjMbpYI+/4KvaNuZ1qolbw+Vnd76pbIUYEG8g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>

<div>
  <canvas id="myChart"></canvas>
</div>
<div>
  <button id="reset" onclick="reset()">Reset</button>
</div>

<script>
var ctx = $('#myChart')[0].getContext("2d");
var data = {
  labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
  datasets: [{
      label: '# of Votes',
      data: [12, 19, 3, 5, 2, 3],
      fill: true,
      borderColor: 'rgb(75, 192, 192)',
      tension: 0.5,
      borderWidth: 5,
  }]
};
var options = {
  scales: {
      y: {
          beginAtZero: true
      }
  },
  plugins: {
    zoom: {
      pan: {
        enabled: true,
        mode: "x"
      },
      zoom: {
        wheel: {
          enabled: true,
        },
        pinch: {
          enabled: true
        },
        mode: 'x',
      }
    }
  }
};
var config = {
  type: 'line',
  data: data,
  options: options,
};
var myChart = new Chart(ctx, config);

function reset() {
  myChart.resetZoom();
}
</script>

</body>
</html>

2021年7月9日金曜日

Charts.js 入門

Charts.js 入門

概要

Charts.js はその名の通り JavaScript で様々なグラフやチャートを描画できるライブラリです 今回は HTML + JavaScript のみで使ってみました

環境

  • macOS 11.4
  • Chrome 91.0.4472.114
  • Charts.js 3.4.0

とりあえず棒グラフを作成する

jQuery とCharts.js を head タグで CDN から読み込みます グラフは canvas タグに描画するので canvas タグを id 付きで定義します div タグで囲っておくとサイズなどを調整しやすいのでおすすめです

基本的には new Chart(canvas, config) を使って canvas に設定するグラフデータを渡すだけです config には type, data, options の 3 つが必要になります type はグラフの種類を data はグラフのデータを options はグラフを描画する際の様々なオプションを設定します

あとはこのファイルを nginx のドキュメントルートなどに配置すれば確認できます

<html>
<head>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.4.0/chart.min.js" integrity="sha512-JxJpoAvmomz0MbIgw9mx+zZJLEvC6hIgQ6NcpFhVmbK1Uh5WynnRTTSGv3BTZMNBpPbocmdSJfldgV5lVnPtIw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>

<div>
  <canvas id="myChart"></canvas>
</div>

<script>
var ctx = $('#myChart')[0].getContext("2d");
var data = {
  labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
  datasets: [{
      label: '# of Votes',
      data: [12, 19, 3, 5, 2, 3],
      backgroundColor: [
          'rgba(255, 99, 132, 0.2)',
          'rgba(54, 162, 235, 0.2)',
          'rgba(255, 206, 86, 0.2)',
          'rgba(75, 192, 192, 0.2)',
          'rgba(153, 102, 255, 0.2)',
          'rgba(255, 159, 64, 0.2)'
      ],
      borderColor: [
          'rgba(255, 99, 132, 1)',
          'rgba(54, 162, 235, 1)',
          'rgba(255, 206, 86, 1)',
          'rgba(75, 192, 192, 1)',
          'rgba(153, 102, 255, 1)',
          'rgba(255, 159, 64, 1)'
      ],
      borderWidth: 1
  }]
};
var options = {
  scales: {
      y: {
          beginAtZero: true
      }
  }
};
var config = {
  type: 'bar',
  data: data,
  options: options,
};
var myChart = new Chart(ctx, config);
</script>

</body>
</html>

線グラフを作成する

type で line を使います fill でグラフ内を塗りつぶせます tension はグラフの滑らかさを設定できます

<html>
<head>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.4.0/chart.min.js" integrity="sha512-JxJpoAvmomz0MbIgw9mx+zZJLEvC6hIgQ6NcpFhVmbK1Uh5WynnRTTSGv3BTZMNBpPbocmdSJfldgV5lVnPtIw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>

<div>
  <canvas id="myChart"></canvas>
</div>

<script>
var ctx = $('#myChart')[0].getContext("2d");
var data = {
  labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
  datasets: [{
      label: '# of Votes',
      data: [12, 19, 3, 5, 2, 3],
      fill: true,
      borderColor: 'rgb(75, 192, 192)',
      tension: 0.5,
      borderWidth: 5,
  }]
};
var options = {
  scales: {
      y: {
          beginAtZero: true
      }
  }
};
var config = {
  type: 'line',
  data: data,
  options: options,
};
var myChart = new Chart(ctx, config);
</script>

</body>
</html>

円グラフを作成する

type で doughnut を使います hoverOffset はマウスホバーした際に該当のデータを移動させる距離を指定します

高さに横のサイズに合わせて自動的に高さが決まってしまうので maintainAspectRatio + div で高さを固定すると良いです

<html>
<head>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.4.0/chart.min.js" integrity="sha512-JxJpoAvmomz0MbIgw9mx+zZJLEvC6hIgQ6NcpFhVmbK1Uh5WynnRTTSGv3BTZMNBpPbocmdSJfldgV5lVnPtIw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <style type="text/css">
  <!--
    div {height: 100vh;}
  -->
  </style>
</head>
<body>

<div>
  <canvas id="myChart"></canvas>
</div>

<script>
var ctx = $('#myChart')[0].getContext("2d");
var data = {
  labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
  datasets: [{
      label: 'Votes',
      data: [12, 19, 3, 5, 2, 3],
      backgroundColor: [
          'rgba(255, 99, 132, 0.2)',
          'rgba(54, 162, 235, 0.2)',
          'rgba(255, 206, 86, 0.2)',
          'rgba(75, 192, 192, 0.2)',
          'rgba(153, 102, 255, 0.2)',
          'rgba(255, 159, 64, 0.2)'
      ],
      hoverOffset: 100,
  }]
};
var options = {
  maintainAspectRatio: false
};
var config = {
  type: 'doughnut',
  data: data,
  options: options,
};
var myChart = new Chart(ctx, config);
</script>

</body>
</html>

最後に

その他にも「レーダーチャート」や「鶏頭図」「バブルチャート」「散布図」「面グラフ」「複数グラフの結合」などが行なえます

次回はプラグインを使ってズームなどできるようにしてみます

参考サイト