c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

白色語語根生成器 (sifarU yUsin ru dEku kekurIa) の生成規則を UI 上で變へられるやうにした

白色語とは yUraru 語の主要な祖語で、kAtoriruixis の北の大陸で話されてゐる。白色語語根生成器 (sifarU yUsin ru dEku kekurIa) はこの白色語の研究に資する爲に、有り得さうな白色語の語根を生成してみる道具だ。

meta 的には、yUraru 語を造語する爲の道具である。

子音と母音とを指定し、出現頻度を指定すればそれらしい文字列が作られる。今迄は規定の規則でしか生成出來なかったが、規則の改良を容易とする爲に UI 上で編緝出來るやうにした。保存は出來ない。

白色語語根生成器 (sifarU yUsin ru dEku kekurIa)
白色語語根生成器 (sifarU yUsin ru dEku kekurIa)

規則 頻度 | 規則 頻度 | …と出現頻度を指定する。規則 規則 …連ねると文字列が延びる。DEKUと云ふ規則から生成する。

Google Cloud Load Balancing を ingress-nginx に置き換へて値段を節約した

幾つかの個人 service や chat bot を GKE (Google Kubernetes Engine) で運用してゐる。K8s の機能を試す爲であったり server を運用する上では K8s が一番樂であるので GKE を使ってゐる。

GKE は價格が高い。先月に、古い cluster だと自動更新を有效に出來ないから cluster を作り直した。Cloud Build の workflow を數箇所書き換へたり Route53 を設定し直して、cluster は速やかに移行出來た。しかし作成畫面に言はれるがままに勧められた N1 Predefined Instance (作り直す前の cluster は Small Instance with 1 VCPU だった) を使ってゐたらこれが高い。事業を營むのならこれは安いのだがこちらはほぼ訪問されない Web site を立ててゐるだけだ。50 日ほど放置して氣附いたので Node pool を Small Instance with 1 VCPU のものに作り直した。これで 1 万円/月が 6 千円/月に成る。それでも高いのだが…。

他に高かったのが Network Load Balancing と HTTP Load Balancing である。これは GKE で Ingress の type を LoadBalancer にしておくと作られるもので、Minimum Service Charge と言ってほぼ access が無くても金が掛かる。自前の server を Internet に露出するのは避けたいから何か挟んでおきたいし、TLS を終端してくれ證明書も自動で更新され便利なものだ。5 千円/月は事業を營むのなら安いが、私はさうではない。

Ingress を増やすと HTTP Load Balancing も増える。Network Load Balancing は外せないが、HTTP Load Balancing をingress-nginxに替へれば Ingress を増やしても掛かる金額は殆ど増えない事に氣が附いた。全ての Ingress が同じ Nginx の Service を経由し domain で振り分けられる。殆ど access が無いから Nginx の Pod 數は増えない。TLS は Nginx で終端させ、證明書はcert-managerに更新させる。

Helm を使はずそれぞれを入れる。

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud/deploy.yaml
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v0.14.2/cert-manager.yaml

使ひ方はSecuring NGINX-ingress | cert-managerに全て書いてある。設定例はingress-nginx with cert-manager by ne-sachirou · Pull Request #56 · ne-sachirou/martian_imperial_year_tableだがものによりけりだらう。

さて料金。

Google Cloud Platform Billing Reports
Google Cloud Platform Billing Reports

HTTP Load Balancing: Global Forwarding Rule Minimum Service Charge が無くなり、Static Ip Charge が生えた。これだけで安く成ってゐる。正確な價格は未だ出てゐないが 2 千円/月から 1 千円/月に成るのではないか。これでも高いのだが、Service を増やしてもほぼ値段が上がらない筈だからそれは嬉しい。今一番高いのは Node instance 料金でこれは削れない。

HTTP Response Time
HTTP Response Time

response time もこの通り變はってゐない。

Python で React Web front を書く

Web front は樂だ。UI が組み易い (だから皆 Electron を使ふのだ)。deploy & release し易い。そして今やどんな programming 言語ででも書ける。勿論 Python ででも書ける。

Transcrypt - Python in the browser - Lean, fast, open!

Transcrypt は Python から JavaScript への transpiler。Web server を Flask で書いて Web front をこれで書けば、Python しか讀み書き出來ない member が占めた project でも Web site を運用出來る。non 職業 programmer に餘り多くを義務附けるべきではない。

開發環境はWSL でも macOS でも、Docker で開發する - c4se 記:さっちゃんですよ ☆の手順で調へられてゐるとする。

Python 側で Transcrypt を入れる。私は Poetry を使ってゐる。node.js 側で React 等を入れる。build にはtranscrypt command を直截には使はずに Webpack を使ふ。

[tool.poetry.dev-dependencies]
transcrypt = "^3.7.16"
{
  "dependencies": {
    "create-react-class": "^15.6.3",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "transcrypt-loader": "^1.0.2",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-merge": "^4.2.2"
  }
}
"use static";

module.exports = {
  entry: {
    ui_main: "./ui_main.py",
  },
  module: {
    rules: [
      {
        test: /\.py$/,
        loader: "transcrypt-loader",
        options: {},
      },
    ],
  },
  target: "web",
};

build や配信は普通の Webpack の作法と成るので略する。

さて Python で Web front を書いてゆく。React.createElement()Python で書けばこれは JavaScriptReact.createElement()に成る。document.getElementById()fetch()等と書けばその通りに成る。よって JavaScript 側の API を知ってゐれば普通に書けると思ふ。Python の組み込み函數が使へるし、import すれば import した先も transpile されて Pythonic に呼べる。JavaScript の標準 API が加わった Python と考へれば好い。Python の async/await がそのまま JavaScript の async/await に成るのは嬉しい。Python 標準 library は一部しか使へないから、third party の library は transpile に失敗する事が多い。また JavaScriptAPI に直截 mapping されるから math 等 Python と Transcrypt とで結果が異なり得る。兩方で動く code は餘り夢想し過ぎない方が好いが、私は天文計算で實現したからそれも案外と出來る (# __pragma__("skip"), # __pragma__("noskip")を多用する事には成り、そこの計算は Web server へ任せなければならない)。最も注意すべき違ひは import である。Transcrypt は__init__.pyの中身を讀まず module 名だけから path を計算する。__init__.pyで何も再 export してゐない時と同じ樣に import すれば好い。それと format string f"{v}" が使へないから "{}".format(v) method を使ふ。

node_modules を呼ぶのに工夫が要る。Transcrypt 自體は node_modules の import を中途半端にしか行へない (少し行へてしまふのが罠だ)。そこで Transcrypt には「これは global に定義されてゐる」と誤認させ、Webpack に解決させる。例へば ReactDOM を mount する所では、

"""Main."""
from ui.components.App import App
import typing as t

__pragma__: t.Any = 0  # __:skip
document: t.Any = 0  # __:skip
React: t.Any = 0  # __:skip
ReactDOM: t.Any = 0  # __:skip
window: t.Any = 0  # __:skip

__pragma__(  # noqa: F821
    "js",
    "{}",
    """
    const React = require("react");
    const ReactDOM = require("react-dom");
    """,
)

if __name__ == "__main__":
    window.addEventListener(
        "DOMContentLoaded",
        lambda event: ReactDOM.render(
            React.createElement(App, {}), document.getElementById("app")
        ),
    )
追記 2022-08-06 React 17 で JSX の transpile 方式が變更された。React 18 で React root の作り方が變更された。この二つを踏まへると、以下の記述になる。
"""Main."""
from ui.components.App import App
import typing as t

__pragma__: t.Any = 0  # __:skip
createRoot: t.Any = 0  # __:skip
document: t.Any = 0  # __:skip
jsx: t.Any = 0  # __:skip
jsxs: t.Any = 0  # __:skip
window: t.Any = 0  # __:skip

__pragma__(  # noqa: F821
    "js",
    "{}",
    """
    const { jsx, jsxs } = require("react/jsx-runtime");
    const { createRoot } = require("react-dom/client");
    """,
)

if __name__ == "__main__":
    window.addEventListener(
        "DOMContentLoaded",
        lambda event: createRoot(document.getElementById("app")).render(jsxs(App, {})),
    )

skip pragma に依りそこは JavaScript へは吐かれない。js pragma は Transcrypt の解析を無視して JavaScript へ吐かれる。このrequire()が Webpack に依り處理される。

無理して JSX を使はずReact.createElementを使ってゐる。JSX を書かうとすればそこは Python としては文字列と成り、静的解析で間違ひを見附けられない。h()等の wrapper を書くのは好いと思ふ。

また JavaScript の class を継承する事は出來ない (extendsもであるし、JavaScript で「普通に」継承する - c4se 記:さっちゃんですよ ☆のやり方でも出來ない)。wrapper class を Python で書いてそれを継承する。React にはcreateReactClassが有るからこの方が樂だ。

"""ErrorBoundary component."""
import typing as t

__pragma__: t.Any = 0  # __:skip
console: t.Any = 0  # __:skip
createReactClass: t.Any = 0  # __:skip
React: t.Any = 0  # __:skip

__pragma__(  # noqa: F821
    "js",
    "{}",
    """
    const React = require("react");
    const createReactClass = require("create-react-class");
    """,
)


def renderErrorBoundary():
    """Render a component."""
    this: t.Any = 0  # __:skip
    if this.state.has_error:
        return React.createElement("a", {"href": "/"}, "Reload")
    return this.props.children


ErrorBoundary = createReactClass(
    {
        "componentDidCatch": lambda error, errorInfo: console.error(error, errorInfo),
        "displayName": "ErrorBoundary",
        "getInitialState": lambda: {"has_error": False},
        "render": renderErrorBoundary,
        "statics": {"getDerivedStateFromError": lambda error: {"has_error": True}},
    }
)


def supervise(*children):
    """Supervise children components & give a fallback UI."""
    return React.createElement(ErrorBoundary, {}, *children)

hooks も自然に使へる。[v, set_v] = React.useState(0)等とすれば好い。後は普通の React の世界と變はらない。lambda 構文で複數文を書けないから無名函數を使ふ機會は減るが、それは普通の Pythonic な世界だ。

帝國火星曆で實例が動いてゐる。SSR (server side rendering) は今後行ふ。

WSL でも macOS でも、Docker で開發する

もう WSL 2 も出ると云ふのに WSL 1 の話だが。

Python 3 で開發するとしやう。開發環境を Docker container に閉じ込めたい。と云ふのも Python の依存 library 管理は pip や Pipenv、Poetry と未だ固まってゐず、手元の環境は即座に捨てられるのが望ましい。Python 自體は運用しない事にする。詰り Python 3 と Git と Docker for Desktop とを手元に入れたら、開發出來る樣にする。Windows では WSL を使ってもらふ。

requirements.txt + pip でも好いのだが、ver. lock は當然行いたいし更新を樂にしたいから Poetry を Docker に入れる。

# poetry.toml

[virtualenvs]
create = false
FROM python:3-alpine

SHELL ["/bin/ash", "-ex", "-o", "pipefail", "-c"]

WORKDIR /mnt
VOLUME /mnt

ENV PATH=/root/.poetry/bin:$PATH

RUN apk add --no-cache -t .build-deps \
    curl \
 && apk add --no-cache -t .runtime-deps \
    build-base \
    git \
    python3-dev \
 && curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python \
 && apk del --purge .build-deps \
 && rm -rf /var/cache/apk/*

COPY poetry.lock poetry.toml pyproject.toml ./
RUN poetry install

概ねこう成るのではないか。apk addは足りないかもしれない。venv を Docker VOLUME ではなく container 内に閉じ込めてゐるのは、Docker VOLUME に置くと Poetry が error を吐く爲…。docker-sync 藝も效かない。

さて task runner が要る。これはClojure で task runner を書く - c4se 記:さっちゃんですよ ☆で書いた。題は Clojure だが Python の事も全て書いた。手元には依存 library を入れないからこれはInvoke等を使はず標準 library だけで書く。

この tasks.pyWindowsmacOS 或いは Docker 内の sh から叩かれる。よってどこから叩かれたかを判定し處理を分ける事が要る。殆どは Docker 内で行ふから Docker 内であればそのまま、外であればdocker runを叩く。

Docker container 内であるか否かは./dockerenvが有るか、或いは手づから環境變數を設定してそれで判別する。即ち、

import os
import os.path


def within_docker() -> bool:
    """Detect I'm in a Docker or not."""
    return os.path.exists("/.dockerenv") or os.getenv("CI") == "1"

WSL 内であるか否かはunameLinuxMicrosoftを含む事で判定する。即ち、

import platform


def within_wsl() -> bool:
    """Detect I'm in a WSL or not."""
    uname = platform.uname()
    return uname[0] == "Linux" and "Microsoft" in uname[2]

runと云ふ函數で shell command を叩けるとして、こう使ひたい。

run("echo どの環境でもそのまま叩く")

with docker() as _run:
    _run("echo Docker内で叩く")

with powershell() as _run:
    _run("echo WSL内ならPowershell内で、macOSかDocker内ならそのまま叩く")

それには contextmanager を使ひこうする。

from contextlib import contextmanager
from shlex import quote
import re
import subprocess


@contextmanager
def docker():
    """Run command in Docker."""
    if within_docker():
        yield run
    else:
        yield run_in_docker


@contextmanager
def powershell():
    """Run command in PowerShell if it's present."""
    if within_wsl():
        yield run_in_powershell
    else:
        yield run


def run(command: str, capture_output=False, text=None) -> subprocess.CompletedProcess:
    """Run command."""
    command = command.strip()
    print("+ ", command)
    return subprocess.run(
        command,
        capture_output=capture_output,
        check=True,
        shell=True,
        text=text,
    )


def run_in_docker(
    command: str, docker_options="", capture_output=False, text=None
) -> subprocess.CompletedProcess:
    """Run command in Docker."""
    command = command.strip()
    print("+ ", command)
    return subprocess.run(
        f"{docker_compose_exe()} {docker_options} run --rm web {command}",
        capture_output=capture_output,
        check=True,
        shell=True,
        text=text,
    )


def run_in_powershell(
    command: str, capture_output=False, text=None
) -> subprocess.CompletedProcess:
    """Run command in PowerShell if it's present."""
    command = re.sub(r"\\\n", r"`\n", command.strip())
    print("+ ", command)
    return subprocess.run(
        f"powershell.exe -Command {quote(command)}",
        capture_output=capture_output,
        check=True,
        shell=True,
        text=text,
    )

bash\で改行を escape するが Powershell`で行ふ。

docker_compose_exe()と云ふ函數が見える。これは PowershellmacOS からならdocker-composeで呼べるが、WSL からだとdocker-compose.exeでなければ呼べないからで、次の函數である。

def docker_compose_exe() -> str:
    """Get a DockerCompose executable name."""
    if within_wsl():
        return "docker-compose.exe"
    else:
        return "docker-compose"

これで WSL に Docker を入れなくて好い。dockerdocker.exeでありkubectlkubect.exeだ。

殘る問題は Docker VOLUME を mount する事だ。local 開發環境なので手元の file を VOLUME で同期したい。通常は-v "$(pwd):/mnt"で path を指定する。WSL は pwd/mnt/c/〜だと言ふ。Docker for Windows は WSL ではなく Windows で動くので pwdC:\〜即ち/c/〜だと言ふ。この食ひ違ひは直さなければならない。そして同じ command が macOS でも動かなければならない。これには WSL の/mnt/c/cに mount し直さなくても好い。

def cwd_for_docker_volume() -> str:
    """Get current directory for Docker volume. This works both on macOS & WSL."""
    cwd = os.getcwd()
    if cwd.startswith("/mnt/c"):
        cwd = cwd[4:]
    return cwd

これで Docker for Desktop が使ふ pwd が得られる。docker runであればこれを使って command を組み立てる。docker-compose runであれば環境變數を使って、

---
# docker-compose.yml
version: "3.7"

services:
  web:
    # ...
    volumes:
      - ${PWD:-.}:/mnt:cached
def run_in_docker(
    command: str, docker_options="", capture_output=False, text=None
) -> subprocess.CompletedProcess:
    """Run command in Docker."""
    command = command.strip()
    print("+ ", command)
    env = os.environ.copy()
    env["PWD"] = cwd_for_docker_volume()
    return subprocess.run(
        f"{docker_compose_exe()} {docker_options} run --rm web {command}",
        env=env,
        capture_output=capture_output,
        check=True,
        shell=True,
        text=text,
    )

と指定できる。

實際には更にElixir on Containers - Speaker Deckで紹介した rsync + unison 藝を行ふべきだ。

formatter にはBlackを、静的檢査はflake8flake8-docstringsmypyとを (flake8-mypy は御亡くなりだ)、unit test にはpython -m unittest discover -sを使ってゐる。これはPython の unittest を書かう。其れを自動実行しやう (rake test + guard を置き換へやう) - c4se 記:さっちゃんですよ ☆と餘り替はってゐない。

帝國火星曆の七曜表を見られる樣にした

現在や任意の時刻の帝國火星曆を見られる Web site に、七曜表、所謂 calendar を見られる頁を追加した。

A5 版に印刷して使へる樣にしてある。地球の暦で今日である 2020-05-06(水)は、帝國火星曆の 1425-17 に含まれる。その七曜表はこう成る。

帝國火星曆1425年17月
帝國火星曆1425年17月

今囘帝國火星曆に於ける七曜の規則を明記した。

帝國火星曆に於ける七曜 - 兩河世界

  • 毎月 1 日を日曜日とする
    • 大の月は丁度 4 週間で、月末である 28 日が土曜日と成る
    • 小の月は金曜日である 27 日で終る。翌 1 日は日曜日