Blog | ytyng.comhttps://b.ytyng.com/blog/2024-03-29T13:44:02+00:00BlogDjango の ユニットテストの assertEqual の引数の first, second は実際は expected, actual2024-03-28T00:41:49+00:002024-03-29T13:43:25+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/django-unittest-expected-actual/Django のユニットテストの assertEqual メソッドを見てみると、
```python
def assertEqual(self, first, second, msg=None):
"""Fail if the two objects are unequal as determined by the '=='
operator.
"""
assertion_func = self._getAssertEqualityFunc(first, second)
assertion_func(first, second, msg=msg)
```
第一引数は first, 第二引数が second と命名されており、それぞれの変数に用途の違いは無いように見える。
ただし、下記のようなコードを書いて
```python
from django.test import TestCase
class AssertTest(TestCase):
def test_assert_equal(self):
self.assertEqual(
'expected',
'actual'
)
```
実際にテストが失敗すると、結果に
```
Failure
Expected :'expected'
Actual :'actual'
```
と出力されるので、第一引数が Expected, 第二引数が Actual だと思うべきです。
そのため、ユニットテストにハードコーディングする値は Expected なので第一引数、
メソッド実行結果や結果の変数は Acutal なので 第二引数になる。
```python
self.assertEqual(
110,
get_tax_included_price(100)
)
```
にもかかわらず、実際の Django のユニットテストの例を見ると逆なので、実態としてどっちでも良い。
本当は [Black](https://github.com/psf/black) あたりがどっちかに決めてくれると良いと思う。pipenv install で AttributeError: module 'pkgutil' has no attribute 'ImpImporter'. Did you mean: 'zipimporter'? が出た → まずは pipenv の python バージョンを確認2024-03-24T04:53:50+00:002024-03-29T13:43:34+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/python-pipenv-attributeerror-pkgutil-impimporter-zipimporter/Python3.12 環境を構築するため、Pipenv で仮想環境を新しく構築している際、`pipenv install` をした時に下記のエラーが出た。
```
Traceback (most recent call last):
File "<my-env>/.venv/bin/pip", line 5, in <module>
from pip._internal.cli.main import main
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/cli/main.py", line 9, in <module>
from pip._internal.cli.autocompletion import autocomplete
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/cli/autocompletion.py", line 10, in <module>
from pip._internal.cli.main_parser import create_main_parser
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/cli/main_parser.py", line 8, in <module>
from pip._internal.cli import cmdoptions
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/cli/cmdoptions.py", line 23, in <module>
from pip._internal.cli.parser import ConfigOptionParser
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/cli/parser.py", line 12, in <module>
from pip._internal.configuration import Configuration, ConfigurationError
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/configuration.py", line 20, in <module>
from pip._internal.exceptions import (
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/exceptions.py", line 7, in <module>
from pip._vendor.pkg_resources import Distribution
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_vendor/pkg_resources/__init__.py", line 2164, in <module>
register_finder(pkgutil.ImpImporter, find_on_path)
^^^^^^^^^^^^^^^^^^^
AttributeError: module 'pkgutil' has no attribute 'ImpImporter'. Did you mean: 'zipimporter'?
```
この場合、おそらく
```shell
% python3 -m ensurepip --upgrade
```
をすれば解決する。システムグローバルの python 環境と仮想環境内で両方やっておいたほうが良い。
ただ、そもそも pipenv の環境がおかしかったので pipenv の作り直しをした。
# システム環境の確認
## python3 のデフォルトバージョンの確認 (Mac)
仮想環境(venv)に入っていない状態で
```shell
% which python3
/opt/homebrew/bin/python3
% python3 --version
Python 3.11.6
```
`/opt/homebrew/bin/python3` が使われ、 python3.11 だった。
## pipenv の使用する python のバージョンの確認
仮想環境(venv)に入っていない状態で
```shell
% which pipenv
/opt/homebrew/bin/pipenv
% head /opt/homebrew/bin/pipenv
#!/opt/homebrew/opt/python@3.9/bin/python3.9
# -*- coding: utf-8 -*-
import re
import sys
from pipenv import cli
...
```
pipenv は python3.9 を使うようになっていた。
pipen が使う python3 と、デフォルトの python バージョンが違っていて面倒が発生しそうなのでで、pipenv でデフォルトの python3.11 を使うようにする。
# システムグローバルの pipenv の再インストール
## homebrew でアンインストールしてみる
```shell
% brew uninstall pipenv
Error: No such keg: /opt/homebrew/Cellar/pipenv
```
homebrew でインストールされてるものではなかった
## ファイルを消す
おそらく、python3.9 の pip でインストールされていると考えられる。
後から考えれば、
```shell
% /opt/homebrew/opt/python@3.9/bin/python3.9 -m pip uninstall pipenv
```
するときれいにアンインストールできたのかもしれないが、どうせこの後 Python3.11 環境で作り直されると思ったので、雑に rm した。
```shell
% rm /opt/homebrew/bin/pipenv
```
## システムグローバル環境のデフォルトのpython3 で pipenv をインストールしなおす
```shell
% python3 -m pip install pipenv
```
## インストール結果の確認
```shell
% which pipenv
/opt/homebrew/bin/pipenv
```
さきほど削除したファイルが再生成されている。期待通り。
```shell
% head /opt/homebrew/bin/pipenv
#!/opt/homebrew/opt/python@3.11/bin/python3.11
# -*- coding: utf-8 -*-
import re
import sys
from pipenv import cli
...
```
python3.11 を使うようになっていた。期待通り。
## 念の為 システムグローバルの python 環境で ensurepip upgrade しておく
```shell
% python3 -m ensurepip --upgrade
```
# 仮想環境の構築
python3.12 環境の pipenv 環境構築の覚書
### Pipflie
```
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
[requires]
python_version = "3.12"
```
このファイルを作ってから
```shell
% python3 -m pipenv install
```
※ `head $(which pipenv)` の1行目(shebang)が `which python3` の結果の python のパスと一致している限り、
```shell
% pipenv install
```
でも同じ。
## 仮想環境内に入る
```shell
% python3 -m pipenv shell
```
## 仮想環境内での pip のテスト
仮想環境内で、
```shell
python3 -m pip freeze
```
とか
```shell
pip freeze
```
をしてみて、エラーが出ないかを確認する。
もし、
```
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/cli/main_parser.py", line 8, in <module>
from pip._internal.cli import cmdoptions
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/cli/cmdoptions.py", line 23, in <module>
from pip._internal.cli.parser import ConfigOptionParser
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/cli/parser.py", line 12, in <module>
from pip._internal.configuration import Configuration, ConfigurationError
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/configuration.py", line 20, in <module>
from pip._internal.exceptions import (
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_internal/exceptions.py", line 7, in <module>
from pip._vendor.pkg_resources import Distribution
File "<my-env>/.venv/lib/python3.12/site-packages/pip/_vendor/pkg_resources/__init__.py", line 2164, in <module>
register_finder(pkgutil.ImpImporter, find_on_path)
^^^^^^^^^^^^^^^^^^^
AttributeError: module 'pkgutil' has no attribute 'ImpImporter'. Did you mean: 'zipimporter'?
```
このエラーが出るなら、仮想環境に入っている状態で
```shell
% python3 -m ensurepip --upgrade
```
をしてみる。</module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env></module></my-env>HW-394 と書いてある ESP-WROOM-32 (ESP32) 開発ボードで CircuitPython をする2023-12-24T09:20:13+00:002024-03-29T13:43:48+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/esp32-hw-394-wroom-32-circuit-python/AliExpress で、ESP32 で検索すると出てくる ESP-WROOM-32 搭載の開発ボードで、[基盤に「HW-394」と書いてあるもの](https://www.aliexpress.com/item/1005005495948290.html)を買いました。
4個入で16ドルでした。
![画像](https://media.ytyng.com/20231224/5e52364af9784cf392024a67df0e7ad3.jpg)
USBシリアルコンソールのチップで CH340C が実装されているものです。[HW-394 という印字が無く、パターンが同じタイプ](https://www.aliexpress.com/item/1005005335283537.html)もあるようです。
日本の Amazon でも、ESP-WROOM-32 + CH340C (もしくは別のシリアルコンソールチップ)の(謎メーカーの)同等商品が数多く売られていました。
HW-394 という印字が開発ボードの名前かどうかもわかりませんが、正式名称がわからないため、今後、この製品は HW-394 と呼びます。
後記:
後からわかったのですが、ESP32 DevKitC という規格でいろんなメーカーが作っているらしい
# 選定にあたっての注意
アリエク以外で売ってないですし、型番で検索しても公式ページなどは無いので怪しい商品です。
よく使う小さめのブレッドボードを使った時、基盤が大きすぎるため片側のピンしかアクセスできません。
そのためかなりおすすめしません。
![画像](https://media.ytyng.com/20231224/f98e176df26f4d9e8c76630f34a53cdc.jpg)
(写真では見えにくいですが、左側のピンしか使えません)
ESP32を使う場合、この商品は使わずに、CircuitPython の公式サイトで「ESP32」で検索すると出てくるボードを買うのが良いと思います。
https://circuitpython.org/downloads?q=ESP32
[Seeed Studio XIAO ESP32C3](https://circuitpython.org/board/seeed_xiao_esp32c3/) が、日本でも入手しやすく比較的安価で小さくて良いと思います。
# 開発方法
Mac (Apple Silicon M2) を使って開発します。
ESP32 は、USBシリアル接続を使わずに、ブラウザを使ってファイルを転送したりブラウザ内で開発したりできるようですが、今回はそれは行いません。Python ファイルの転送も USB シリアルポート経由で行います。
PyCharm でコードを書いて、シェルスクリプトでフラッシュの書き込みを行います。
# USB シリアルの接続ポートを確認する
HW-394 を Mac に USB-C-C のケーブルで接続します。
ドライバのインストール等は不要です。
接続後、以下のコマンドでポートを確認します。
```shell
python -m serial.tools.list_ports
```
結果
```
/dev/cu.Bluetooth-Incoming-Port
/dev/cu.usbserial-110
2 ports found
```
`Bluetooth-Incoming-Port` は除外し、検出された `/dev/cu.usbserial-110` が、今回作られた USB シリアルポートです。
ここで、シリアルポートが見えないようであれば、ケーブルを変えて、ハブを通さず直接接続したほうが良いです。途中で USB ハブをはさんでいるとうまく認識されないことがあります。
シェルスクリプト内でポートを扱う際、110 という数字は変動しそうなので、シェルスクリプトから取得する場合は私はこんな感じで探すようにしています。
```shell
port=$(ls -1 /dev/cu.usbserial* |head -n 1)
```
# CircuitPython のファームウェアを書き込む
RP2040(RaspberryPi Pico 等) のように、uf2 をドラッグアンドドロップして書き込むことはできません。ESP32はストレージデバイスとして認識されないためです。
ファームウェア書き込みのために、esptool というライブラリを使います。
## esptool のインストール方法
```shell
python3 -m pip install esptool
```
Pipenv を使う場合、以下のような Pipfile が良いと思います。
#### Pipfile
```
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
esptool = "*"
# 不具合を修正したフォークを使う
adafruit-ampy = {git = "git+ssh://git@github.com/ytyng/ampy.git"}
[requires]
python_version = "3.9"
```
```shell
pipenv install
```
adafruit-ampy については後ほど説明します。ファイルの転送に使います。
## CircuitPython のファームウェアの取得
HW-394 用の CircuitPython のファームウェアは用意されていません。そのため、構成の似ている
[DOIT ESP32 Development Board](https://circuitpython.org/board/doit_esp32_devkit_v1/) のファームウェアを使います。
本来の製品用のものではないため、自己責任での使用をお願いします。
[上記ページ](https://circuitpython.org/board/doit_esp32_devkit_v1/)から DOWNLOAD .BIN NOW を押して、ダウンロードします。
## ファームウェアの書き込み
esptool の `write_flash` を使って書き込みます。
```shell
#!/usr/bin/env zsh
port=$(ls -1 /dev/cu.usbserial* |head -n 1)
esptool.py --chip esp32 --port $port \
--before default_reset --after hard_reset --no-stub \
write_flash --flash_mode dio 0x0 \
adafruit-circuitpython-doit_esp32_devkit_v1-en_US-8.2.9.bin
```
# Python の REPL に接続する
シリアルポートに接続することで、ボード上の Python コンソール (REPL) が使えます。
screen コマンドで接続します。
```shell
#!/usr/bin/env zsh
port=$(ls -1 /dev/cu.usbserial* |head -n 1)
screen $port 115200
```
```
Adafruit CircuitPython 8.2.9 on 2023-12-06; ESP32 Devkit V1 with ESP32
>>> import board
>>> dir(board)
['__class__', '__name__', 'D1', 'D12', 'D13', 'D14', 'D15', 'D16', 'D17', 'D18', 'D19', 'D2', 'D21', 'D22', 'D23', 'D25', 'D26', 'D27', 'D3', 'D32', 'D33', 'D34', 'D35', 'D4', 'D5', 'I2C', 'LED', 'MISO', 'MOSI', 'RX', 'RX0', 'RX2', 'SCK', 'SCL', 'SDA', 'SPI', 'TX', 'TX0', 'TX2', 'UART', 'VN', 'VP', 'board_id']
```
# Mac で書いた Python コードをフラッシュに書き込む
シリアルポートからファイルを書き込むには、 [rshell](https://github.com/dhylands/rshell) ,[mpfshell](https://github.com/wendlers/mpfshell), [ampy](https://pypi.org/project/adafruit-ampy/), および [upydev](https://pypi.org/project/upydev/0.0.9/) といったツールが使えそうですが、どれも開発ボードが MicroPython でないと使えないようです。
例えば、MicroPython では ubinascii というモジュール名なのが、CircuitPython では binascii となっていたりと、モジュール名が違うものがいくつかあるため、MicroPython 用のライブラリはそのまま CircuitPython では使えない場合が多いです。多くは、MicroPython 用のものは先頭に「u」がつく場合が多いです。
この中では ampy が一番シンプルなツールで CircuitPython への対応が容易そうなのでフォークして修正して使いました。本家では既にプルリクエストが上がっていますが、メンテが放置されているらしくマージされていません。
[修正済みampy](https://github.com/ytyng/ampy)
## 修正済みの ampy をインストールする
上で書いた Pipfile でインストールするか、
```shell
python3 -m pip install git+https://github.com/ytyng/ampy.git
```
でインストールしてください。
## Python コードを書く
基盤に実装されている D2 LEDを点滅させるスクリプトを書きます。
### blink_led_d2.py
```
import digitalio
import time
import board
print('brink_led.py loaded.')
led_pin = digitalio.DigitalInOut(board.D2)
led_pin.direction = digitalio.Direction.OUTPUT
while True:
led_pin.value = True
time.sleep(0.1)
led_pin.value = False
time.sleep(1.9)
```
## フラッシュに書き込む
### deploy.sh
```shell
#!/usr/bin/env zsh
export AMPY_PORT=$(ls -1 /dev/cu.usbserial* |head -n 1)
cd $(dirname $0)
ampy put blink_led_d21.py /code.py
````
フラッシュの `/code.py` としてコピーしした後、EN と書いてある基板上のボタン(リセットボタン)を押すと実行され、Lチカします。
![画像](https://media.ytyng.com/20231224/40ae557718e944328b5c2e506f91b701.jpg)
ピン番号を変更することで、他のピンでもLチカできることを確認しました。
![画像](https://media.ytyng.com/20231224/fe1a7bd34cc040288a30179f41c266a8.jpg)Raspberry PI Pico や RP2040 に Mac OS Sonoma でファイルをコピーすると Input/output error になる場合2023-11-21T02:51:56+00:002024-03-29T13:43:57+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/raspberry-pi-pico-rp2040-mac-os-sonoma-copy-input-output-error/# 問題
Mac OS Sononoma で、 Rasberry PI Pico や RP2040 を載せてるマイコンボードの開発をしようとして、
ファイルを RP2040 にコピーした時に、
```
cp: code.py: could not copy extended attributes to /Volumes/CIRCUITPY/code.py: Input/output error
```
となって 0バイトでコピーされる。(中身がコピーされていない)
また、Finder でドラッグアンドドロップした場合は
```
The Finder can’t complete the operation because some data in “” can’t be read or written.
(Error code -36)
```
のエラーダイアログが表示される。
![画像](https://media.ytyng.com/20231120/8d15e80161bc4c3d87b1c48be6f19188.png)
# 解決方法
ボリュームを synchronous でマウントしなおせば良い。
以下のスクリプトでマウントしなおしができる
```shell
#!/usr/bin/env zsh
m=$(mount | grep /Volumes/CIRCUITPY | grep synchronous)
if [ ! "$m" ]; then
devname=$(df | grep CIRCUITPY | cut -d" " -f1)
sudo umount /Volumes/CIRCUITPY
sudo mkdir /Volumes/CIRCUITPY
sleep 2
sudo mount -v -o sync -t msdos $devname /Volumes/CIRCUITPY
fi
```
# 参考情報
[OSError: [Errno 5] Input/output: macOS Sonoma is delaying writes on small filesystems · Issue #8449 · adafruit/circuitpython](https://github.com/adafruit/circuitpython/issues/8449)
# 原因
Sonoma では、ファイルのコピーをする際、まず仮想ファイルを作って、ファイルシステムへのページアウトがトリガーされる。
非同期でマウントされている場合、FAT とメタデータへの更新はフラッシュされるまで遅延されるため、0バイトファイルとなる。
8MB以下、FAT16、uf2、Python ファイルの書き込み の条件下で問題となる。
https://github.com/adafruit/circuitpython/issues/8449#issuecomment-1745372269
# 私の解決策
下記のようなデプロイスクリプトを作りました。
## remount.sh
```shell
#!/usr/bin/env zsh
ret=$(mount | grep /Volumes/CIRCUITPY | grep synchronous)
if [ ! "$ret" ]; then
devname=$(df | grep CIRCUITPY | cut -d" " -f1)
sudo umount /Volumes/CIRCUITPY
sudo mkdir /Volumes/CIRCUITPY
sleep 2
sudo mount -v -o sync -t msdos $devname /Volumes/CIRCUITPY
fi
```
## deploy.sh
```shell
#!/usr/bin/env zsh
cd $(dirname $0)
# Sonoma以降、ファイルのコピーで Input/output error になる問題の対応
./remount.sh
files=(
file_1.py
file_2.py
)
for file in $files; do
echo $file
cp $file /Volumes/CIRCUITPY/
done
# ゴミファイルを消す
dot_clean /Volumes/CIRCUITPY
ls -lhatr /Volumes/CIRCUITPY
```Python の MySQLdb を使う時に symbol not found in flat namespace '_mysql_affected_rows' が出た場合の対応2023-11-09T03:39:23+00:002024-03-29T13:44:02+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/python3-symbol-not-found-in-flat-namespace-_mysql_affected_rows/Apple silicon の Python で MySQL を操作しようとして
```
ImportError: dlopen(/.../site-packages/MySQLdb/_mysql.cpython-310-darwin.so, 0x0002): symbol not found in flat namespace '_mysql_affected_rows'
```
が出た。
[Macos M1 mysqlclient Symbol not found: _mysql_affected_rows ERROR · Issue #496 · PyMySQL/mysqlclient](https://github.com/PyMySQL/mysqlclient/issues/496#issuecomment-1614688099)
上記のコメントを参考にする。
私の環境では
```shell
% echo $MYSQLCLIENT_LDFLAGS
-L/opt/homebrew/opt/openssl/lib -L/opt/homebrew/opt/mysql/lib -L/opt/homebrew/opt/zlib/lib -L/opt/homebrew/lib
```
となっていたので、
```shell
% echo $MYSQLCLIENT_LDFLAGS
-L/opt/homebrew/opt/openssl/lib -L/opt/homebrew/opt/mysql/lib -L/opt/homebrew/opt/zlib/lib -L/opt/homebrew/lib -lmysqlclient -rpath /opt/homebrew/opt/mysql/lib
```
となるように修正した。
その後、
```shell
pip uninstall mysqlclient
pip cache purge
pip install mysqlclient
```
を行った。
```shell
python3
>>> import MySQLdb
>>>
```
でエラーが出なければOKnpm install fibers ( node-fibers ) をインストールしようとして ValueError: invalid mode: 'rU' while trying to load binding.gyp となる場合2023-03-29T05:31:58+00:002024-03-29T05:55:54+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/install-fibers-failed-python-version/```shell
yarn install
```
したら
```
ERROR ## There is an issue with node-fibers ##
`..../node_modules/fibers/bin/darwin-x64-83/fibers.node` is missing.
```
が出て
```shell
npm install fibers
```
したら
```
File "/<home>/.asdf/installs/nodejs/14.7.0/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 2782, in Load
LoadTargetBuildFile(build_file, data, aux_data,
File "/<home>/.asdf/installs/nodejs/14.7.0/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 391, in LoadTargetBuildFile
build_file_data = LoadOneBuildFile(build_file_path, data, aux_data,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/<home>/.asdf/installs/nodejs/14.7.0/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 234, in LoadOneBuildFile
build_file_contents = open(build_file_path, 'rU').read()
^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: invalid mode: 'rU' while trying to load binding.gyp
gyp ERR! configure error
gyp ERR! stack Error: `gyp` failed with exit code: 1
```
が出た。
Github の Issue を見ると、Python のバージョンによるものらしい。
[ValueError: invalid mode: 'rU' while trying to load binding.gyp (Python 3.9 compatibility issue) · Issue #2219 · nodejs/node-gyp](https://github.com/nodejs/node-gyp/issues/2219)
Python のバージョンを変えるのも面倒だったので、コンパイルできないファイル `node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py` をエディタで開き、
```python
build_file_contents = open(build_file_path, 'rU').read()
```
この行から `U` を除去して
```python
build_file_contents = open(build_file_path, 'r').read()
```
こうすると、 `npm install fibers` が成功する。</home></home></home>Python で文字列中の4バイト文字を消す2023-03-06T07:01:43+00:002024-03-26T07:38:09+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/remove-4bytes-char-on-python/```python
def remove_4bytes_char(text):
"""
文字列から4バイト文字を消す
"""
# 文字列を bytearray に変換
byte_string = bytearray(text.encode('utf-8'))
# バイト列から4バイトのUTF-8文字を除去する
while b'\xf0' in byte_string:
index = byte_string.index(b'\xf0')
if index + 3 < len(byte_string):
for _i in range(4):
byte_string.pop(index)
# bytearrayを文字列に変換
return byte_string.decode('utf-8')
```Python で、ターミナルに文字を出力してから消す2023-01-03T02:21:35+00:002024-03-29T13:24:12+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/python-how-to-erase-printed-text/シーケンス \b のバックスペースで、プリントした文字を消すことができる。
```python
print('requesting...', end='', flush=True)
heavy_method(...)...
print('\b' * 13, end='', flush=True)
```
コンテキストマネージャーで書くと
```python
import contextlib
@contextlib.contextmanager
def print_and_erase(text):
print(text, end='', flush=True)
yield
print('\b' * len(text), end='', flush=True)
with print_and_erase('requesting...'):
heavy_method(...)
```Fabric 等 Paramiko を使うライブラリで、サーバログイン時に Authentication (publickey) failed. が出る場合2022-12-02T09:02:19+00:002024-03-27T07:23:02+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/python-paramiko-fabric-authentication-publickey-failed/Python の fabric 等、 Paramiko を使ってサーバに SSH ログインをしようとすると、特定のサーバで Authentication (publickey) failed. エラーで接続できない場合がある。
Github のレポジトリに Issue があり、既に修正されている。
https://github.com/paramiko/paramiko/issues/1915
サーバが openssh 8.8 以上で、クライアントが paramiko 2.8.0 以下の場合に発生します。
fabric (fab) が使っている Python を調べる
```shell
% head $(which fab)
```
```python
#!/usr/local/opt/python@3.9/bin/python3.9
# -*- coding: utf-8 -*-
import re
import sys
from fabric.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
```
fab が使っている Python の paramiko をアップデートする
```shell
% /usr/local/opt/python@3.9/bin/python3.9 -m pip install -U paramiko
```ChromeDriver を自動更新する Python スクリプト2022-10-31T12:43:02+00:002024-03-21T06:16:25+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/download-chromedriver-and-deploy-to-path/最新版の ChromeDrier と適合するテスト版 Chrome をインストールするスクリプトを書いた。
(バージョン115以上対応)
sudo パスワードの入力を求めます。
```python
#!/usr/bin/env python3
"""
ChromeDriver と Chrome テスト版を自動更新するスクリプト
"""
import getpass
import shutil
import subprocess
import pathlib
from contextlib import contextmanager
import requests
import os
USER_AGENT = '@ytyng/upgrade_chromedriver'
default_request_headers = {
'User-Agent': USER_AGENT
}
DOWNLOAD_DIR = os.path.expanduser('~/Downloads')
@contextmanager
def set_directory(path: pathlib.Path):
"""Sets the cwd within the context
Args:
path (Path): The path to the cwd
Yields:
None
"""
origin = pathlib.Path().absolute()
try:
os.chdir(path)
yield
finally:
os.chdir(origin)
def get_cpu_architecture() -> str:
"""
CPU のアーキテクチャを取得する
"""
return subprocess.run(
['uname', '-m'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
).stdout.decode('utf-8').strip()
def get_current_machine_platform():
"""
JSON の platform に適合する文字列を返す。
mac-arm64, mac-x64 の他に、linux64, win32, win64 があるが、
現状は mac のみ対応する。
"""
a = get_cpu_architecture()
if a == 'arm64':
return 'mac-arm64'
else:
return 'mac-x64'
json_url = 'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json'
_rdata = requests.get(json_url, headers=default_request_headers).json()
# chrome と chromedriver が両方存在するもののみにフィルター
_versions = [v for v in _rdata['versions'] if
v['downloads'].get('chromedriver') and v['downloads'].get(
'chrome')]
_versions = sorted(_versions,
key=lambda x: tuple(map(int, x['version'].split('.'))),
reverse=True)
def download_latest_version(version_dict, app_name: str):
"""
ファイルをダウンロードする
:param app_name: str 'chrome' or 'chromedriver'
"""
print(f'Downloading {app_name}...')
_platform = get_current_machine_platform()
target_url = [d for d in version_dict['downloads'][app_name] if
d['platform'] == _platform][0]['url']
file_binary = requests.get(target_url, headers=default_request_headers)
file_name = DOWNLOAD_DIR + '/' + target_url.split('/')[-1]
with open(file_name, 'wb') as f:
f.write(file_binary.content)
print('Downloaded:', file_name)
# 最新版
download_latest_version(_versions[0], 'chrome')
download_latest_version(_versions[0], 'chromedriver')
print('Input sudo password')
sudo_password = (getpass.getpass() + '\n').encode()
def _install_chromedriver(sudo_password):
print('Installing chromedriver...', flush=True)
_platform = get_current_machine_platform()
_zip_file_path = f'{DOWNLOAD_DIR}/chromedriver-{_platform}.zip'
_extracted_dir_path = f'{DOWNLOAD_DIR}/chromedriver-{_platform}'
with set_directory(DOWNLOAD_DIR):
if os.path.exists(_extracted_dir_path):
shutil.rmtree(_extracted_dir_path, ignore_errors=True)
unzip_output = subprocess.run(
['unzip', _zip_file_path],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
).stdout.decode('utf-8').strip()
print(unzip_output)
exists_chromedriver_path = shutil.which('chromedriver')
print('exists_chromedriver_path:', exists_chromedriver_path)
install_output = subprocess.run(
['sudo', '-S', 'mv', f'{_extracted_dir_path}/chromedriver',
exists_chromedriver_path], input=sudo_password, check=True
)
print('Chromedriver installed.', install_output)
_install_chromedriver(sudo_password)
def _install_chrome(sudo_password):
print('Installing chrome...', flush=True)
_platform = get_current_machine_platform()
_zip_file_path = f'{DOWNLOAD_DIR}/chrome-{_platform}.zip'
_extracted_dir_path = f'{DOWNLOAD_DIR}/chrome-{_platform}'
_installed_chrome_path = '/Applications/Google Chrome for Testing.app'
with set_directory(DOWNLOAD_DIR):
if os.path.exists(_extracted_dir_path):
shutil.rmtree(_extracted_dir_path, ignore_errors=True)
if os.path.exists(_installed_chrome_path):
shutil.rmtree(_installed_chrome_path, ignore_errors=True)
unzip_output = subprocess.run(
['unzip', _zip_file_path],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
).stdout.decode('utf-8').strip()
print(unzip_output)
install_output = subprocess.run(
['sudo', '-S', 'mv',
f'chrome-{_platform}/Google Chrome for Testing.app',
'/Applications/'], input=sudo_password, check=True
)
print('Chrome for Testing installed.', install_output)
_install_chrome(sudo_password)
```Python コマンドラインツールを作る時のロガーを作るコード2022-10-09T02:16:35+00:002024-03-27T11:08:12+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/python-create-logger-from-code/Python で簡単なデーモンアプリを作る時などに使う、ロガーをコードで作る時のコードのメモです。
```python
import logging
_logger = logging.getLogger('my-application-name')
def _my_procedure(...):
...
_logger.debug(...)
...
def setup_logger():
_logger.setLevel(logging.DEBUG)
_handler = logging.StreamHandler()
_handler.setFormatter(logging.Formatter(
'%(asctime)s %(thread)d %(name)s %(levelname)s %(message)s'
))
# 重めにフォーマットする時
# _handler.setFormatter(logging.Formatter(
# '%(name)s (%(process)d,%(thread)d) '
# '%(levelname)s %(asctime)s '
# '[%(module)s.%(funcName)s:%(lineno)d] '
# ' %(message)s'
# ))
_logger.addHandler(_handler)
if __name__ == '__main__':
setup_logger()
...
```Mac のフォルダ名を Python から出力する時に濁点が分かれてしまうのを直す2022-09-19T07:31:07+00:002024-03-27T09:16:04+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/mac-%E3%81%AE%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E5%90%8D%E3%82%92-python-%E3%81%8B%E3%82%89%E5%87%BA%E5%8A%9B%E3%81%99%E3%82%8B%E6%99%82%E3%81%AB%E6%BF%81%E7%82%B9%E3%81%8C%E5%88%86%E3%81%8B%E3%82%8C%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86%E3%81%AE%E3%82%92%E7%9B%B4%E3%81%99/<p>Mac で、ディレクトリ名を出力しようとした時に</p>
<pre>フ゜ ロシ゛ ェクト</pre>
<p>みたいになってしまうのを直すには</p>
<pre>import unicodedata<br/>unicodedata.normalize('NFC', folder_name)</pre>
<p>ビルトインの unicodedata で直せる。</p>
<p></p>
<p>Unicode の NFD 正規化からの NFC 正規化への変換、といいます。</p>
<p>参考: <a href="https://t2y.hatenablog.jp/entry/2019/06/29/150125" target="_blank">Python で Unicode 正規化 NFC/NFD の文字列を扱う - forest book </a></p>pipenv install で RuntimeError: location not created nor specified が出る場合2022-09-05T00:49:49+00:002024-03-27T04:38:18+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/pipenv-instal-runtimeerror-location-not-created-nor-specified-resolve/<p>pipenv install で、 python3.10 環境を作ろうとして</p>
<p></p>
<pre>% pipenv install<br/>...<br/>...<br/>Traceback (most recent call last):<br/> File "/usr/local/bin/pipenv", line 8, in <module><br/> sys.exit(cli())<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/vendor/click/core.py", line 829, in __call__<br/> return self.main(*args, **kwargs)<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/vendor/click/core.py", line 782, in main<br/> rv = self.invoke(ctx)<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/vendor/click/core.py", line 1259, in invoke<br/> return _process_result(sub_ctx.command.invoke(sub_ctx))<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/vendor/click/core.py", line 1066, in invoke<br/> return ctx.invoke(self.callback, **ctx.params)<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/vendor/click/core.py", line 610, in invoke<br/> return callback(*args, **kwargs)<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/vendor/click/decorators.py", line 73, in new_func<br/> return ctx.invoke(f, obj, *args, **kwargs)<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/vendor/click/core.py", line 610, in invoke<br/> return callback(*args, **kwargs)<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/vendor/click/decorators.py", line 21, in new_func<br/> return f(get_current_context(), *args, **kwargs)<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/cli/command.py", line 233, in install<br/> retcode = do_install(<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/core.py", line 1920, in do_install<br/> ensure_project(<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/core.py", line 585, in ensure_project<br/> path_to_python = which("python") or which("py")<br/> File "/usr/local/lib/python3.9/site-packages/pipenv/core.py", line 97, in which<br/> raise RuntimeError("location not created nor specified")<br/>RuntimeError: location not created nor specified</pre>
<p>となる場合。</p>
<p></p>
<p>PC 内に、複数の Python 環境がある場合、それぞれの環境ごとに pip や pipenv がインストールされます。</p>
<p>単純に pip というコマンドで、どのバージョンの pip が動くかを確認するには、</p>
<pre>% cat $(which pip)</pre>
<p>としてみて 1行目の Shebang を確認するとわかります。</p>
<pre>% cat $(which pip)<br/>#!/usr/local/opt/python@3.9/bin/python3.9<br/># -*- coding: utf-8 -*-<br/>import re<br/>import sys<br/>...</pre>
<pre>% cat $(which pipenv)<br/>#!/usr/local/opt/python@3.9/bin/python3.9<br/># -*- coding: utf-8 -*-<br/>import re<br/>import sys<br/>...</pre>
<p><br/>今回は、python3.9 環境に入っている pipenv で、 python3.10 の venv を作ろうとしてエラーが出ていました。</p>
<p><br/>バージョンを指定した python 環境化で pip や pipenv を実行するには、 <strong>-m</strong> オプションを使います。</p>
<ul>
<li>pip install ... の代わりに、 python3.10 -m pip install ... を使う</li>
<li>pipenv install ... の代わりに、 python3.10 -m pipenv install ... を使う</li>
</ul>
<p>今回は、pipenv と打った時に、python3.10 環境の pipenv を動作させるようにしたかったため、 python3.10 に pipenv をインストールします。</p>
<pre>% python3.10 -m pip install pipenv</pre>
<p>インストール後</p>
<pre>% cat $(which pipenv)<br/>#!/usr/local/opt/python@3.10/bin/python3.10<br/># -*- coding: utf-8 -*-<br/>import re<br/>import sys<br/>from pipenv import cli<br/>if __name__ == '__main__':<br/> sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])<br/> sys.exit(cli())</pre>Django の Docker 環境を Alpine + uWSGI から Debian + Daphne に変えた → やっぱり uvicorn2022-09-03T11:03:29+00:002024-03-28T00:59:58+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/django-docker-chane-image-alpine-uwsgi-to-debian-daphne/<p>今まで、Alpine Linux + uWSGI で Django のイメージを作ることが多かったのですが、<br/>Alpine Linux で Python を実行すると遅い問題があります。</p>
<ul>
<li><a href="https://applingo.tokyo/article/6860" target="_blank">DockerでPythonを使用する場合はベースイメージにAlpine Linuxを使わない方が良い理由</a></li>
<li><a href="https://qiita.com/kawamou/items/8c3c13c40929dbaaaae3" target="_blank">PythonをAlpineイメージで安易に動かす問題点 - Qiita</a></li>
</ul>
<p>また、その Alpine 上で起動する HTTPサーバには、 uWSGI を使っていたのですが、設定が複雑で、Kubernetes でサービスするには冗長な気になっいました。</p>
<p>そのため、Django サービスの環境を一通り変更しました。</p>
<p>ベースイメージは、Alpine から <strong>Debian</strong> ベースの Python に変更し、HTTP サーバは <strong>Daphne</strong> を使うことにしました。</p>
<p>追記: Daphne だと並列リクエストが受けられないため、後日 <strong>Uvicorn</strong> に変えました</p>
<h1>Docker イメージの変更</h1>
<h3>Alpine → Python (Debian) に変更</h3>
<p>マルチステージビルドにし、Python のフルイメージで pipenv install ( pipenv sync ) を行い、成果物を python のスリムイメージを元とした公開用ステージにコピーする形としました。</p>
<p>今まで Alpine にソースコードや依存関係、uWSGI を含めて入れてビルドした場合、私のアプリでは 277MB。<br/>python:3.10-bullseye + python:3.10-slim-bullseye のマルチステージビルドした場合、最終イメージは 313MB。<br/>容量としては若干増えましたが、ほぼかわらないサイズでした。</p>
<p>Dockerfile は下に書きます。</p>
<h1>HTTPサーバの変更</h1>
<h3>uWSGI → Daphne に変更</h3>
<p>uWSGI は非常に良いライブラリだと思いますが、チューニング項目が多く、使っていく上で少し疲れます。</p>
<p>uWSGI は特性上、レスポンスを繰り返すごとにメモリが増えていく傾向があり、それを防ぐためにある一定回数のリクエストを受け取った時にワーカーを再起動させることができます(max-requests)。メモリリークと言いましたか? 違います。GCです。</p>
<p>ワーカーの再起動時、そのワーカーは一時的にサービス不能になりますが、他のワーカーが生きていれば全体的なサービス停止は防げます。しかし、一定リクエスト回数ごとに動作するため、近いタイミングで発生することが多く、結局サービスが停止してしまします。</p>
<p>再起動のリクエスト数閾値をワーカーごとにずらすオプション(max-requests-delta)があり、それを適用すればサービス停止は避けられるはず…なのですが、この設定は最新ビルドで使えません。何年も前にドキュメントに掲載されているのに、ずっと使えてなかった(効いてるものと思っていた) オプションです。</p>
<ul>
<li><a href="https://qiita.com/mushoku_toumei/items/6bda4e3e077218fbd8d8" target="_blank">あなたの使ってるuWSGI、本当にmax-requests-delta効いてますか? - Qiita</a></li>
<li><a href="https://qiita.com/__m/items/7dcd44e4aeed0b66d4a1">uWSGI の max-requets-delta、マジで効いていない - Qiita</a></li>
</ul>
<p>そのため、これを期に別のアプリケーションサーバに変えることにしました。</p>
<p>候補としては、定番の guinicorn の他に、FastAPI で使われる Uvicorn, Hypercorn, あとは Django チームが開発している Daphne というものがあり、今回は Daphne にしました。</p>
<p>Daphne, Uvicorn, Hypercorn, は、ともに ASGI をサポートしており、Django 3 以降の主流のサーバとなっています。</p>
<p>今回は、 Django を使うため、Django チームが開発している Daphne を採用しました。</p>
<ul>
<li><a href="https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/daphne/" target="_blank">How to use Django with Daphne | Django documentation | Django<br/></a></li>
</ul>
<h3>追記: Daphne → Uvicorn に変えました</h3>
<p>Daphne は、連続のリクエストが来た時に順番にコルーチンで処理します。すべて async の View であれば問題無いと思うのですが、既存サービスではそうなってないため、並列リクエストが処理できない問題がありました。</p>
<p>シングル Pod で動かすアプリも多いため、応答があまり良くなかったので、複数ワーカーの起動が簡単な Uvicorn に変えました。</p>
<h1>静的コンテンツ配信</h1>
<p>uWSGI から Daphne に変更するにあたり、静的コンテンツをどうやってサービスするかの問題があります。</p>
<p>実際に多くのお客さんが使う大規模なサービスでは、静的コンテンツは CloudFront + S3 のような構成でサービスすることが多いと思います。その場合は問題にはなりませんが、社内ツールや管理サイトのような小さなサービスは、よりシンプルな形での静的ファイルサービスをしたい所です。</p>
<p>uWSGI は、static-map という静的ファイルを簡易的にサービスする機能があり、社内ツール等に使う分にはぴったりでいつも使っていました。ただし、 Daphne には静的ファイルのサービスはありません。</p>
<p>Daphne の前に Nginx で受け、Nginx 内で静的コンテンツと Django リクエストを振り分けるのも良いと思いますが、デーモンをあまり増やしたくはなかったため、別の方法を探しました。</p>
<p>比較的最近人気のあるソリューションと思われるのは、WhiteNoise というアプリケーションです。</p>
<ul>
<li><a href="https://whitenoise.evans.io/en/stable/" target="_blank">WhiteNoise documentation</a></li>
</ul>
<p>WhiteNoise は、Python で書かれた静的コンテンツサーバで、Django の MiddleWare に差し込んで簡単に使うことができます。</p>
<p>Python で静的コンテンツサーバを実行するなんてナンセンスじゃないか、という気がしなくもないですが、その回答は公式ドキュメントにあります。</p>
<p><a href="https://whitenoise.evans.io/en/stable/#infrequently-asked-questions" target="_blank">https://whitenoise.evans.io/en/stable/#infrequently-asked-questions</a></p>
<p>S3 でも Nginx でもない選択肢として、まさに私のニーズとしてはぴったりでした。</p>
<h1>Dockerfile</h1>
<p>Dockerfile は以下のようになりました。</p>
<pre>FROM python:3.10-bullseye AS builder<br/><br/># Pipfileをコピー<br/>COPY Pipfile /tmp/Pipfile<br/>COPY Pipfile.lock /tmp/Pipfile.lock<br/><br/># pipenv sync<br/># プロジェクトによっては pipenv install --system --ignore-pipfile --deploy<br/>RUN python3 -m pip install pipenv \<br/> && PIPENV_PIPFILE=/tmp/Pipfile pipenv sync --system \<br/> && python3 -m pip install uvicorn<br/># 前: && python3 -m pip install daphne<br/><br/><br/>FROM python:3.10-slim-bullseye<br/><br/># ビルダーステージから、MySQL クライアントに必要な SO をコピー<br/>COPY --from=builder \<br/> /usr/lib/x86_64-linux-gnu/libmariadb.a \<br/> /usr/lib/x86_64-linux-gnu/libmariadb.so.3 \<br/> /usr/lib/x86_64-linux-gnu/<br/>COPY --from=builder /usr/lib/x86_64-linux-gnu/libmariadb3/ \<br/> /usr/lib/x86_64-linux-gnu/libmariadb3/<br/># ビルダーステージから、Pipenv でインストールしたライブラリをコピー<br/>COPY --from=builder /usr/local/lib/python3.10/site-packages \<br/> /usr/local/lib/python3.10/site-packages<br/>COPY --from=builder /usr/local/lib/python3.10/lib-dynload \<br/> /usr/local/lib/python3.10/lib-dynload<br/>COPY --from=builder /usr/local/bin /usr/local/bin<br/><br/># SOのシンボリックリンクを作っておく<br/>RUN ln -s /usr/lib/x86_64-linux-gnu/libmariadb.a \<br/> /usr/lib/x86_64-linux-gnu/libmariadbclient.a \<br/> && ln -s /usr/lib/x86_64-linux-gnu/libmariadb.so.3 \<br/> /usr/lib/x86_64-linux-gnu/libmariadb.so \<br/> && ln -s /usr/lib/x86_64-linux-gnu/libmariadb.so.3 \<br/> /usr/lib/x86_64-linux-gnu/libmariadbclient.so<br/><br/>COPY my_app /var/app/my_app<br/>RUN chown -R nobody:nogroup /var/app<br/><br/>USER nobody<br/>WORKDIR /var/app/my_app<br/>RUN cd /var/app/my_app && python3 ./manage.py collectstatic --noinput<br/>EXPOSE 8002<br/>CMD ["uvicorn", \<br/> "my_app.asgi:application", \<br/> "--host", "0.0.0.0", \<br/> "--port", "8002", \<br/> "--workers", "4" \<br/>]<br/><br/># 前: CMD ["daphne", "-b", "0.0.0.0", "-p", "8002", "my_app.asgi:application"]<br/><br/></pre>
<p></p>
<h2>Django で WhiteNoise を動かす</h2>
<p>Django の設定の MIDDLEWARE の中に、</p>
<pre>whitenoise.middleware.WhiteNoiseMiddleware</pre>
<p>を追加することで、静的ファイルがホストされます。</p>
<p><a href="https://whitenoise.evans.io/en/stable/django.html" target="_blank">Using WhiteNoise with Django - WhiteNoise 6.2.0 documentation</a></p>
<h3>キャッシュ時間の延長</h3>
<p>WhiteNoise のキャッシュヘッダーの寿命 ( max-age ) は、デフォルトで </p>
<pre>60 if not settings.DEBUG else 0</pre>
<p>となっています。</p>
<p></p>
<p><a href="http://whitenoise.evans.io/en/stable/django.html#WHITENOISE_MAX_AGE">http://whitenoise.evans.io/en/stable/django.html#WHITENOISE_MAX_AGE</a></p>
<p>60秒だと短いと思いますので、7日に変更します。</p>
<pre>WHITENOISE_MAX_AGE = 86400 * 7</pre>
<p>(本番用 settings の中で設定)</p>
<h3>メディアサーバ</h3>
<p>WhiteNoise は、Django の MEDIA_URL をサーブできるようにはなっていません。</p>
<p><a href="http://whitenoise.evans.io/en/stable/django.html#serving-media-files" target="_blank">http://whitenoise.evans.io/en/stable/django.html#serving-media-files</a></p>
<p>理由は上記ページに書いてある通りです。そのため、メディアを扱う場合は、<a href="https://django-storages.readthedocs.io/en/latest/" target="_blank">django-storages</a> などと連携し、S3 や nginx でサービスする仕組みを構築する必要があります。</p>Alpine Linux の Docker イメージに Python 環境を構築する際、cryptgraphy のインストールで失敗する場合の回避方法 3選2022-08-11T15:06:11+00:002024-03-28T12:03:51+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/install-cryptography-to-alpine-linux-docker-image-python-env/<p>私のブログでも同様の記事を昔に書いたことがあるのですが、</p>
<p><a href="https://www.ytyng.com/blog/python-cryptography-pep-517-rust-E0658/">Python cryptography がインストールできなくなったら rustup で rust をインストールする | ytyng.com</a></p>
<p>Pipenv 等のバージョンロックシステムを使い、Alpine Linux の Docker イメージで Python 環境を構築しようとすると、インストールがうまくいかず躓く場合が多くあります。</p>
<p></p>
<p>Intel CPU でビルドがうまくいったとしても、同じ Dockerfile で Apple Silicon チップだとビルドに失敗する場合もあり、なかなか安定しません。</p>
<p>※ Intel CPU、Apple CPU 両方で <code>docker build --platform linux/amd64</code> オプションをつけてビルドしても、結果が安定しない場合があります。</p>
<p>解決方法としていくつかありますので、メモをしておきます。</p>
<p></p>
<h2>A. 環境を整えて Rust でコンパイルする</h2>
<p><a href="https://www.ytyng.com/blog/python-cryptography-pep-517-rust-E0658/">Python cryptography がインストールできなくなったら rustup で rust をインストールする | ytyng.com</a></p>
<p></p>
<p>この、以前書いた方法で行います。ただし、この場合、Intel CPU だと安定してビルドできますが、Apple CPU だと下記のようなエラーが出てビルドができない場合があります。</p>
<pre> Failed to install some dependency or packages. The following have failed installation and attempted retry: [Requirement(_name='cryptography', vcs=None, req=NamedRequirement(name='cryptography', version='==37.0.4', req=Requirement.parse('cryptography==37.0.4; python_version >= "3.6"'), extras=[], editable=False, </pre>
<p>また、Rust のインストールと cryptography のビルドで非常に時間がかかります。</p>
<p></p>
<p>Dockerfile はだいたいこのような形になります。</p>
<pre>FROM alpine:3.15 as my-app-builder<br/><br/>MAINTAINER ytyng<br/><br/># apk --virtual=.build-deps add → apk del .build-deps をすると、<br/># reqests とか six が消えてしまうので行ってはいけない。<br/>RUN apk --no-cache add \<br/> python3 \<br/> py3-pip \<br/> python3-dev \<br/> gcc \<br/> make \<br/> python3-dev \<br/> musl-dev \<br/> libffi-dev \<br/> mariadb-dev \<br/> postgresql-dev \<br/> g++ \<br/> libgcc \<br/> libstdc++ \<br/> libxml2-dev \<br/> libxslt-dev \<br/> jpeg-dev \<br/> git \<br/> openssh \<br/> curl \<br/> && curl https://sh.rustup.rs -sSf | sh -s -- -y \<br/> && pip3 install pipenv --ignore-installed distlib \<br/> && mkdir ~/.ssh/ && ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts<br/><br/>COPY Pipfile /tmp/Pipfile<br/>COPY Pipfile.lock /tmp/Pipfile.lock<br/><br/>RUN --mount=type=ssh source /root/.cargo/env \<br/> && PIPENV_PIPFILE=/tmp/Pipfile pipenv sync --system \<br/> && rustup self uninstall -y \<br/> && rm -rf /var/cache/apk/* \<br/> && rm -rf /tmp/*<br/><br/><br/>FROM alpine:3.15<br/>## ビルドイメージで作ったバイナリをコピー<br/>COPY --from=my-app-builder /usr/lib/python3.9/site-packages/ \<br/> /usr/lib/python3.9/site-packages/<br/><br/>RUN apk --no-cache add \<br/> python3 \<br/> bash \<br/> libxml2 \<br/> libxslt \<br/> libressl \<br/> && ln -s /usr/bin/python3 /usr/bin/python<br/><br/>USER nobody<br/>WORKDIR /var/src/<br/><br/>COPY . /var/src/</pre>
<p></p>
<h2>B. Pipenv.lock から cryptography を消して、apk でインストールする</h2>
<p>もし、上記の Rust で cryptography をインストールする方法がうまくいかない(もしくは、遅いので避けたい) 場合、pipenv で cryptography はインストールせず、apk でインストールするとうまくいきます。</p>
<p>(つまり、cryptography のバージョンロックは諦めます。)</p>
<p>Pipenv.lock を使って、pipenv sync コマンドで環境を作りたい場合は、<strong>事前になんらかの方法で Pipenv.lock から cryptography の項目を削除</strong>し、予め apk で <code>py3-cryptography</code> をインストールするようにすれば、Rust も不要で簡潔な Dockerfile になります。</p>
<p>ただし、Pipenv.lock から cryptography を自動的に消す、もしくは pipenv sync で cryptography だけインストールを除外する、といった方法はわからなかったため、継続して行うには別のスクリプトが必要そうです。</p>
<pre>FROM alpine:3.15 as my-app-builder<br/><br/>RUN apk --no-cache add \<br/> python3 \<br/> py3-pip \<br/> python3-dev \<br/><strong> py3-cryptography</strong> \<br/> gcc \<br/> make \<br/> python3-dev \<br/> musl-dev \<br/> libffi-dev \<br/> mariadb-dev \<br/> postgresql-dev \<br/> g++ \<br/> libgcc \<br/> libstdc++ \<br/> libxml2-dev \<br/> libxslt-dev \<br/> jpeg-dev \<br/> git \<br/> openssh \<br/> curl \<br/> && pip3 install pipenv --ignore-installed distlib<br/><br/>COPY Pipfile /tmp/Pipfile<br/>COPY Pipfile.lock /tmp/Pipfile.lock<br/><br/>RUN <strong>PIPENV_PIPFILE=/tmp/Pipfile pipenv sync --system</strong><br/><br/><br/>FROM alpine:3.15<br/>## ビルドイメージで作ったバイナリをコピー<br/>COPY --from=my-app-builder /usr/lib/python3.9/site-packages/ \<br/> /usr/lib/python3.9/site-packages/<br/><br/>...</pre>
<p></p>
<h2>C. cryptography を apk でインストールして、ロックファイルを使わない</h2>
<p>多少強引な方法ですが、 cryptography は apk でビルド済みのものをインストールし、そのほかの依存関係は Pipfile.lock を使わずに、<strong>Pipfile だけで pipenv install してしまう</strong>方法もありだと思います。依存関係にシビアでない、それほど重要ではない Docker イメージの場合はありだと思います。</p>
<p><span>( cryptography に限らず、Pipfile に記載されていない依存性のバージョンロックをすべて諦めます。)</span></p>
<p>Apple CPU で X86_64 向けの Docker イメージをビルドする場合はこの B か C の方法が今の所安定してビルドできています。</p>
<pre>FROM alpine:3.15 as my-app-builder<br/><br/>RUN apk --no-cache add \<br/> python3 \<br/> py3-pip \<br/> python3-dev \<br/> py3-cryptography \<br/> gcc \<br/> make \<br/> python3-dev \<br/> musl-dev \<br/> libffi-dev \<br/> mariadb-dev \<br/> postgresql-dev \<br/> g++ \<br/> libgcc \<br/> libstdc++ \<br/> libxml2-dev \<br/> libxslt-dev \<br/> jpeg-dev \<br/> git \<br/> openssh \<br/> curl \<br/> && pip3 install pipenv --ignore-installed distlib<br/><br/>COPY Pipfile /tmp/Pipfile<br/><br/>RUN PIPENV_PIPFILE=/tmp/Pipfile pipenv install --system --skip-lock --deploy</pre>
<p></p>
<p></p>デジタルサイネージ … Chrome をキオスクモードで起動する Python Selenium スクリプト2022-04-02T05:32:33+00:002024-03-28T07:57:31+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/digital-signage-chrome-kiosk-mode-python-selenium-script/<p>Python Selenium でサイトを開いて、Google ログインするスクリプト</p>
<p></p>
<pre>#!/usr/bin/env bash<br/>import os<br/>import time<br/><br/>import dotenv<br/>from selenium.webdriver import Chrome, ChromeOptions, DesiredCapabilities<br/><br/><br/>def create_driver():<br/> options = ChromeOptions()<br/> arguments = [<br/> '--lang=ja',<br/> # '--window-size=1920,1080',<br/> # '--window-position=0,0',<br/> '--kiosk',<br/> '--start-fullscreen',<br/> '--noerrdialogs',<br/> '--disable-translate',<br/> '--disable-infobars',<br/> '--disable-features=TranslateUI',<br/> ]<br/> for arg in arguments:<br/> options.add_argument(arg)<br/> # Python スクリプトが終わっても起動しっぱなしにする<br/> options.add_experimental_option('detach', True)<br/> options.add_experimental_option('excludeSwitches', ['enable-automation'])<br/> options.add_experimental_option('useAutomationExtension', False)<br/><br/> # 自己署名証明書を受け入れる場合の設定<br/> capabilities = DesiredCapabilities.CHROME<br/> capabilities['acceptInsecureCerts'] = True<br/> # capabilities['acceptSslCerts'] = True # この設定は意味無いかも<br/><br/> driver = Chrome(options=options, desired_capabilities=capabilities)<br/> return driver<br/><br/><br/>def main():<br/> dotenv.load_dotenv()<br/> driver = create_driver()<br/> driver.get('https://www.example.com/')<br/> time.sleep(2)<br/> driver.find_element_by_xpath("//*[@id='identifierId']") \<br/> .send_keys(os.environ.get('USER_EMAIL'))<br/> driver.find_element_by_xpath("//*[@id='identifierNext']").click()<br/> time.sleep(2)<br/> driver.find_element_by_xpath(<br/> "//*[@id='password']/div[1]/div/div[1]/input"<br/> ).send_keys(os.environ.get('USER_PASSWORD'))<br/> driver.find_element_by_xpath("//*[@id='passwordNext']").click()<br/><br/><br/>if __name__ == '__main__':<br/> main()</pre>M1 Mac で pipenv lock が失敗する場合2022-01-03T11:15:04+00:002024-03-28T01:01:45+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/pipenv-lock-failed-on-m1-mac/<p>Intel Mac だと <code>pipenv lock</code> で問題なくロックファイルができるのに、M1 ( Apple Silicon ) ( ARM ) Mac だとエラーが出てロックファイルが作れなかった。</p>
<pre>arch -x86_64 zsh</pre>
<p>して、ロゼッタ上で x86_64 をエミュレーションする状態にしてから、pipenv lock するとうまくいった。</p>Alpine に Python ライブラリ google-crc32c 1.1.3 以上を入れようとしてエラーが出る場合の解消法2021-11-08T05:52:39+00:002024-03-26T15:27:42+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/alpine-python-google-crc32c-113-error/<pre>Building wheels for collected packages: google-crc32c<br/> Building wheel for google-crc32c (PEP 517) ... error<br/> ERROR: Command errored out with exit status 1:<br/> command: /usr/bin/python3 /usr/lib/python3.8/site-packages/pep517/_in_process.py build_wheel /tmp/tmpa_qqgxr3<br/> cwd: /tmp/pip-install-i8dfak3j/google-crc32c<br/> Complete output (18 lines):<br/> running bdist_wheel<br/> running build<br/> running build_py<br/> creating build<br/> creating build/lib.linux-x86_64-3.8<br/> creating build/lib.linux-x86_64-3.8/google_crc32c<br/> copying src/google_crc32c/_checksum.py -> build/lib.linux-x86_64-3.8/google_crc32c<br/> copying src/google_crc32c/__config__.py -> build/lib.linux-x86_64-3.8/google_crc32c<br/> copying src/google_crc32c/__init__.py -> build/lib.linux-x86_64-3.8/google_crc32c<br/> copying src/google_crc32c/cext.py -> build/lib.linux-x86_64-3.8/google_crc32c<br/> copying src/google_crc32c/python.py -> build/lib.linux-x86_64-3.8/google_crc32c<br/> running build_ext<br/> building 'google_crc32c._crc32c' extension<br/> creating build/temp.linux-x86_64-3.8<br/> creating build/temp.linux-x86_64-3.8/src<br/> creating build/temp.linux-x86_64-3.8/src/google_crc32c<br/> gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fomit-frame-pointer -g -fno-semantic-interposition -fomit-frame-pointer -g -fno-semantic-interposition -fomit-frame-pointer -g -fno-semantic-interposition -DTHREAD_STACK_SIZE=0x100000 -fPIC -I/usr/include/python3.8 -c src/google_crc32c/_crc32c.c -o build/temp.linux-x86_64-3.8/src/google_crc32c/_crc32c.o<br/> error: command 'gcc' failed with exit status 1<br/> ----------------------------------------<br/> ERROR: Failed building wheel for google-crc32c</pre>
<p>となったり</p>
<pre>#10 1190.0 [pipenv.exceptions.InstallError]: gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fomit-frame-pointer -g -fno-semantic-interposition -fomit-frame-pointer -g -fno-semantic-interposition -fomit-frame-pointer -g -fno-semantic-interposition -DTHREAD_STACK_SIZE=0x100000 -fPIC -I/usr/include/python3.8 -c src/google_crc32c/_crc32c.c -o build/temp.linux-x86_64-3.8/src/google_crc32c/_crc32c.o<br/>#10 1190.0 [pipenv.exceptions.InstallError]: src/google_crc32c/_crc32c.c:3:10: fatal error: crc32c/crc32c.h: No such file or directory<br/>#10 1190.0 [pipenv.exceptions.InstallError]: 3 | #include <crc32c/crc32c.h><br/>#10 1190.0 [pipenv.exceptions.InstallError]: | ^~~~~~~~~~~~~~~~~<br/>#10 1190.0 [pipenv.exceptions.InstallError]: compilation terminated.<br/>#10 1190.0 [pipenv.exceptions.InstallError]: error: command 'gcc' failed with exit status 1</pre>
<p>となる場合。</p>
<p>環境変数 CRC32C_PURE_PYTHON=1 を設定すると良い</p>AWS の ElasticSearch を使う時、 Python ElasticSearch で UnsupportedProductError が出場合の解決2021-10-15T11:53:49+00:002024-03-26T10:29:46+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/aws-python-elasticsearch-UnsupportedProductError/<pre>elasticsearch.exceptions.UnsupportedProductError: The client noticed that the server is not a supported distribution of Elasticsearch</pre>
<p>が出る場合</p>
<p></p>
<p>Python の elasticsearch ライブラリが 7.14 で、AWS の OpenSearch / Elasticsearch に接続すると出る。</p>
<p>バージョンダウンすると出ない。</p>
<pre>pip install "<span>elasticsearch<<span>7.14"</span></span></pre>
<p></p>
<p></p>Python で XML を作って Post する2021-10-10T09:45:45+00:002024-03-28T18:25:17+00:00ytynghttps://b.ytyng.com/blog/author/ytyng/https://b.ytyng.com/blog/build-xml-via-xml-etree-elementtree/<p>Python で XML を作って Post する</p>
<p></p>
<pre>from xml.etree import ElementTree<br/><br/>request_xml = ElementTree.Element(<span>'request'</span>)<br/>update_request = ElementTree.SubElement(<br/> request_xml, <span>'itemUpdateRequest'</span>)<br/>item = ElementTree.SubElement(update_request, <span>'item'</span>)<br/>ElementTree.SubElement(item, <span>'itemUrl'</span>).text = itemUrl<br/><br/><span>for </span>key, value <span>in </span>updateData.items():<br/> ElementTree.SubElement(item, key).text = value<br/><br/>response = <span>self</span>.request(<br/> <span>'/es/1.0/item/update'</span>,<br/> <span>method</span>=<span>'post'</span>,<br/> <span>headers</span>={<span>'Content-Type'</span>: <span>'application/xml'</span>},<br/> <span>data</span>=ElementTree.tostring(<br/> request_xml, <span>encoding</span>=<span>'utf-8'</span>, <span>xml_declaration</span>=<span>True</span>)<br/>)</pre>