Let’s EncryptとcertbotとDockerを使ってwebアプリをSSL化する

Let’s Encrypt はフリーで利用できる認証局だ。

今回はこの Let’s Encrypt を利用して、既存の web アプリを SSL 化(つまり、https://〜でアクセスできるように)してみた。

Let’s Encrypt を利用するには certbot というツールをインストールして、サーバ証明書の発行や更新ができる。だけど、webアプリやwebサーバは Docker のコンテナ上で動かしているので、せっかくだから certbot も Docker でできないかと調べてみたら、ちゃんとやり方があった。certbot は Docker Hub にイメージが公開されている。

以下、ウチのwebアプリはこのやり方でできたよ、という記録。

前提

  • 既存のwebアプリ(http://webapp.panicblanket.com/)をSSL化する
  • webアプリ、webサーバ(Nginx)は Docker コンテナ上で動いている
  • Docker と docker-compose がインストール済み

付け加えると、webアプリの / (ルート)は他のページへリダイレクトされている。ここが最初よくわからなかったところだ。

certbot を使うには、対象のサーバの / に Let’s Encrypt からアクセスできるようにしておく必要がある。というのも certbot はサーバ認証のために一時的なファイルを作って、Let’s Encrypt からアクセスできるのを確認することでサーバの存在を確認しているらしいからだ。だけど、webアプリでは他のページにリダイレクトしているので、うまく行かないんじゃないかと思っていた。

結論からいうと、webアプリのリバースプロキシになっているwebサーバをいったん停めて、認証取得用に別のwebサーバをたてることで対処することができた。

手順

大まかな手順は次の通り:

  1. 認証取得用の設定ファイルを書く
  2. 既存webアプリのリバースプロキシになってるwebサーバを停める
  3. certbot の Dockerコンテナを使って認証取得
  4. 既存のwebサーバの設定ファイルをSSL対応にして再起動

認証取得用の設定ファイル

Docker、というか docker-compose を使うので docker-compose.yml ファイルを書く。

ersion: "3"

services:
  nginx:
    image: nginx:1.19.2-alpine
    ports:
      - 80:80
    volumes:
      - ./conf.d:/etc/nginx/conf.d
      - ./html:/var/www/html
      - /etc/letsencrypt:/etc/letsencrypt
      - /var/lib/letsencrypt:/var/lib/letsencrypt

  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./html:/var/www/html
      - /etc/letsencrypt:/etc/letsencrypt
      - /var/lib/letsencrypt:/var/lib/letsencrypt
    command: ["--version"]

Nginx と certbot のイメージを利用する。この Nginx は認証取得のための一時的なものだ。

で、その Nginx の設定ファイルはこう:

server {
    listen      80;
    listen      [::]:80;

    server_name webapp.panicblanket.com;

    root        /var/www/html;
}

80番ポートで待ち受けるだけ。このファイルを、docker-compose.yml ファイルからみて ./conf.d/webapp.panicblanket.com.conf に保存する。ドキュメントルートはディレクトリを作っておけば、何もなくて構わない。

認証取得

取得する前に既存のwebサーバを停めておくこと。

次に、認証用のwebサーバだけ起動する。

$ docker-compose up -d nginx

webサーバが起動したら、certbot を使って認証取得する。

$ docker-compose run --rm certbot certonly --webroot -w /var/www/html -d webapp.panicblanket.com

これで証明書が /etc/letsencrypt 以下に保存される。

確認できたら Nginx も停める。

$ docker-compose down

webアプリとサーバの設定、再起動

証明書が取得できたので、それを利用して https://〜 でアクセスできるように設定ファイルを書き換える。

まずは、Nginx のバーチャルホストの設定ファイル。

upstream webapp {
    server webapp:9000;
}

server {
    listen      80;
    listen      [::]:80;

    server_name webapp.panicblanket.com;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen      443 ssl http2;
    listen      [::]:443 ssl http2;

    server_name webapp.panicblanket.com;

    ssl_certificate     /etc/letsencrypt/live/webapp.panicblanket.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/webapp.panicblanket.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers off;

    access_log /var/log/nginx/webapp.panicblanket.com.access.log main;
    error_log  /var/log/nginx/webapp.panicblanket.com.error.log warn;

    keepalive_timeout     60;
    proxy_connect_timeout 60;
    proxy_read_timeout    60;
    proxy_send_timeout    60;

    location / {
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_pass       http://webapp;
    }
}

80番ポートにアクセスしてきたらすべて https://~ にリダイレクトしている。

あと、docker-compose.yml(の該当部分)。

  httpserver:
    image: nginx:1.18.0
    container_name: httpserver
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - /home/takatoh/docker-environment/nginx/etc:/etc/nginx/conf.d
      - /home/takatoh/docker-environment/nginx/log:/var/log/nginx
      - /etc/letsencrypt:/etc/letsencrypt

443番ポートと /etc/letsencrypt を追加。

これでコンテナを再起動すればOK。

$ docker-compose restart webapp httpserver

参考にしたページ

古いRailsアプリをDockerコンテナに乗せた

もう1年も前になるけどこんな記事を書いた。

ローカルネットワークのサーバ置き換えに際して、古いサーバ(Ubuntu 16.04 LTS)で動かしていた Rails アプリが、新しいサーバ(Ubuntu 20.04 LTS)上で動かせなくてどうしよう……っていう記事だ。

あれから1年も過ぎてしまったけど、今年に入ってから週末ごとにちまちまと作業をして、なんとか新しいサーバの Dockerコンテナで動かせるまでになった。

ベースにした Docker イメージは、Ruby の公式イメージのうち一番古いバージョン、2.6.9。とにかく Ruby の公式 Docker イメージをベースにしたコンテナで動かせることを目標にした。

Rails は 5.0.7.2まで上げた。これだって今となっては古いバージョン(RubyGems.org を見ると2019/3/13リリース)だけど、ローカルネットワークでしか使ってない web アプリなのでひとまず妥協する。

作業は開発用メインマシン(Ubuntu 20.04 LTS)の rbenv で Ruby そのもののバージョンも変えながらやった。作業を始めた時点では Rails 4.1.4 で、最後のコミットはなんと 2016/3/3 だった。6年近くもメンテしてなかったってことだよ。アプリを使う分には不自由してなかったからだけど、だからってほったらかしだとこうやって後になってツケが来るんだよな。Rails にさわるもの6年ぶりってわけで、少しずつバージョンを上げるたびに出るエラー(主に依存関係)で苦労した。これからはもう少し面倒を見て、追いつくようにしよう。

さて、そういうわけで、動かしていたアプリがなくなって、めでたく古いサーバが空いた。春になれば Ubuntu 22.04 LTS もリリースされるだろうから、そのテスト用にしようかな。

Windows11へのGNU Fortranのインストールと算術IFについて

古いFortran(FORTRAN77だ!)のプログラムをコンパイルする必要があって、GNU Fortran をインストールした。

GNU Fortran の公式サイトは↓ここにあるんだけど、どこからダウンロードすればいいんだか、ひどくわかりにくい。

さいわい、丁寧に説明してくれているページを見つけた。日付が古いが内容は古くなってない。

公式サイトのここをクリックして、次の画面のここをクリックして……みたいなことをグダグダと書かれているが――という言い方はよくないな。丁寧に説明してくれてるんだからありがたい。悪いのはわかりにくい公式サイトだ――要するに TDM GCC をインストールすれば GNU Fortran もインストールできる。TDM GCC のサイトは↓ここ。

バージョン10.3.0が最新だった。ダウンロードしたのは64ビット版のtdm64-gcc-10.3.0-2.exe。

このファイルは Windows のインストーラになってるので、普通にインストール作業すれば何も困ることはない。ただ、デフォルトでは Fortran はインストールされないので、途中で Fortran をインストールするようにチェックを入れる必要がある。

さて、インストールできたので早速試してみた。コマンドは gfortran

takatoh@sofa: sample > gfortran --version
GNU Fortran (tdm64-1) 10.3.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

コンパイルするには引数にソースファイルの名前を指定すればいい。-o オプションはコンパイルされた実行ファイルの名前の指定。省略すると a.exe になる。

takatoh@sofa: sample > gfortran -o auto auto.for

ソースファイルの拡張子が .for だと FORTRAN77 だと見做すらしい。.f90 なら Fortran90。

こんな調子で20個ほどコンパイルしたんだけど、そのひとつで次のような warning がでた。

takatoh@sofa: sample > gfortran -o ohsp ohsp.for
ohsp.for:70:72:

   70 |       IF(RLOG-R1) 170,180,180
      |                                                                        1
Warning: Fortran 2018 deleted feature: Arithmetic IF statement at (1)

Arithmetic IF statement ってなんだと思って調べたら、算術IFっていうんだと。

↓ここに説明がある。

条件式の値が負かゼロか正かで分岐して指定された番号の文へジャンプする。条件式は論理式じゃなくて算術式、つまり評価すると数値になる式。こんな感じ:

      IF(X-10) 100,200,300
  100 ...
      GO TO 500
  200 ...
      GO TO 500
  300 ...
      GO TO 500
  500 ...

X-10 が負なら100へ、ゼロなら200へ、正なら300へジャンプする。

もう少し調べてみたら、論理IF よりも 算術IF のほうが先にあったらしい。

FORTRAN77 って学生の時にやったんだけど、こんなの覚えてないよ。

それはさておき、こういう古いソースコードでもコンパイルできるってのはすごいことだよな。

Python: Larkが1.0.0になったので試してみたんだけど挙動がおかしい

年が明けたら Lark の 1.0.0 がリリースされていたのに気が付いたので、以前作ったコマンド形式の入力ファイルをパースするのを、再度やってみた。フレームワーク的なパッケージにまとめて再利用できるようにしてみようと思う……んだけど、どうも挙動がおかしい。

要点を先に書くと、書いたパーサも入力ファイルも同じなのに、実行するたびに結果が異なる。原因はわからない。パーサの書き方が悪いのか、Lark のバグなのか。

順を追って説明する。

コマンド形式の入力ファイル

プログラムには何らかの入力が必要で、入力ファイルにも何らかのフォーマットが必要だ。コマンド形式というのは、入力データの内部表現を、ひとつずつコマンドを実行しながら組み立てようというものだ。この記事で扱うのは、そのフレームワーク的なライブラリで、ファイルからの入力である文字列を、コマンド(と引数)の列にパースするもの。

サンプルとして次のような入力ファイルを使う。

TITLE "example"
ADD 1 2
MUL
    3
//    5
    7  8  
    9
// this line is comment.
REPEAT "Hello!" 5  
    // this is comment too.
JOIN "Hello," "world!"  // in-line comment.
ADD-2  5  .7e03
NO-ARG-COMMAND
COMMAND-WITH-KEYWORD
    KW1  1
    KW2  2
    KW3  on
    KW4  off

文法

全体を「スクリプト」と呼ぶ。文法は次の通り。

  • 「スクリプト」は1つ以上の「ステートメント」からなる
  • 「ステートメント」は「コマンド」と0個以上の「引数」からなり、空白文字で区切られ、改行文字で終わる
  • ただし、空白文字で始まる行は前の行の続きとみなす。なので「コマンド」は必ず行頭からはじまる
  • 「引数」には数値、文字列、キーワード、真偽値の4種類がある
  • //から行末まではコメント

「引数」について補足しておく。数値は文字通りの数値だけど、整数/実数の区別はなし。文字列はダブルークォートで囲む。真偽値は true / falseyes / noon / off のいずれか(すべて小文字)。

で、キーワードが今日の主題。「引数」のリストの中で、次の引数が何のデータかを示す目印に使う。要するに Python のメソッドのキーワード引数のような使い方だ。大文字のラテン文字と数字からなり、行頭には来ないことでコマンドと区別できる。実装としては Ruby なら Symbol をつかうところだけど、Python にはそういうのがないので(ないよね?)、Keyword クラスを定義した。

文法ファイルは次のようになった。

?script : statement+

statement : line continued*

line : command _WS_INLINE arglist _WS_INLINE? _NL
     | command _WS_INLINE? _NL

continued : _INDENT arglist _WS_INLINE? _NL

command : CMDNAME

CMDNAME : UCASE_LETTER ("-"|UCASE_LETTER|DIGIT)*

arglist : arg
        | arglist _WS_INLINE arg

arg : number
    | string
    | keyword
    | boolean

number : SIGNED_NUMBER

string : ESCAPED_STRING

keyword : KWORD

KWORD : UCASE_LETTER (UCASE_LETTER|DIGIT)*

boolean : true
        | false

true : "true" | "yes" | "on"

false : "false" | "no" | "off"

_WS_INLINE : WS_INLINE

_NL : NEWLINE

_INDENT : WS_INLINE


%import common.UCASE_LETTER
%import common.DIGIT
%import common.SIGNED_NUMBER
%import common.ESCAPED_STRING
%import common.NEWLINE
%import common.WS_INLINE
%import common.CPP_COMMENT

COMMENT : WS_INLINE? CPP_COMMENT NEWLINE
COMMENT_INLINE : WS_INLINE? CPP_COMMENT

%ignore COMMENT
%ignore COMMENT_INLINE

パーサ

from lark import Lark
from lark.exceptions import UnexpectedInput
from lark.visitors import Interpreter


class Parser():
    def __init__(self):
        with open('grammer.lark', 'r') as f:
            grammer = f.read()
        self.parser = Lark(grammer, start='script')

    def parse(self, input_data):
        try:
            tree = self.parser.parse(input_data)
        except UnexpectedInput as e:
            context = e.get_context(input_data)
            print(f'Syntax error:  line = {e.line}  column = {e.column}\n')
            print(context)
            exit(1)

        script = ScriptInterpreter().visit(tree)
        return script


class ScriptInterpreter(Interpreter):
    def script(self, tree):
        return [self.visit(c) for c  in tree.children]

    def statement(self, tree):
        (cmd, args1) = self.visit(tree.children[0])
        args2 = flatten([ self.visit(a) for a in tree.children[1:] ])
        return (cmd, args1 + args2)

    def line(self, tree):
        cmd = self.visit(tree.children[0])
        if len(tree.children) > 1:
            args = flatten(self.visit(tree.children[1]))
        else:
            args = []
        return (cmd, args)

    def continued(self, tree):
        return self.visit(tree.children[0])

    def command(self, tree):
        return tree.children[0]

    def arglist(self, tree):
        args = [ self.visit(a) for a in tree.children ]
        return args

    def arg(self, tree):
        return self.visit(tree.children[0])

    def number(self, tree):
        return float(tree.children[0])

    def string(self, tree):
        return tree.children[0].strip('"')

    def keyword(self, tree):
        return Keyword(str(tree.children[0]))

    def boolean(self, tree):
        return self.visit(tree.children[0])

    def true(self, tree):
        return True

    def false(self, tree):
        return False


class Keyword():
    def __init__(self, val):
        self.val = val

    def __str__(self):
        return f'Keyword<{self.val}>'

    def __repr__(self):
        return f'Keyword<{self.val}>'


def flatten(lis):
    result = []
    for elem in lis:
        if isinstance(elem, list):
            result += flatten(elem)
        else:
            result.append(elem)
    return result

テスト用のスクリプトと実行結果

テスト用なので、入力データ(の内部表現)を組み立てる代わりにコマンドと引数リストを出力する。

from parsers import Parser
import sys


def main():
    parser = Parser()

    with open(sys.argv[1], 'r') as f:
        input_data = f.read()

    script = parser.parse(input_data)

    print('SCRIPT')
    for (cmd, args) in script:
        print('  COMMAND: ' + cmd)
        print('     ARGS: ' + repr(args))



main()

これを実行すると次のようになる。

takatoh@sofa: inputscriptparser-sample > python main.py example.dat
SCRIPT
  COMMAND: TITLE
     ARGS: ['example']
  COMMAND: ADD
     ARGS: [1.0, 2.0]
  COMMAND: MUL
     ARGS: [3.0, 7.0, 8.0, 9.0]
  COMMAND: REPEAT
     ARGS: ['Hello!', 5.0]
  COMMAND: JOIN
     ARGS: ['Hello,', 'world!']
  COMMAND: ADD-2
     ARGS: [5.0, 700.0]
  COMMAND: NO-ARG-COMMAND
     ARGS: []
  COMMAND: COMMAND-WITH-KEYWORD
     ARGS: [Keyword<KW1>, 1.0, Keyword<KW2>, 2.0, Keyword<KW3>, True, Keyword<KW4>, False]

これは期待通り。ところが、何度か実行を続けると、時々次のような結果になる。

takatoh@sofa: inputscriptparser-sample > python main.py example.dat
SCRIPT
  COMMAND: TITLE
     ARGS: ['example']
  COMMAND: ADD
     ARGS: [1.0, 2.0]
  COMMAND: MUL
     ARGS: [3.0, 7.0, 8.0, 9.0]
  COMMAND: REPEAT
     ARGS: ['Hello!', 5.0, Keyword<JOIN>, 'Hello,', 'world!']
  COMMAND: ADD-2
     ARGS: [5.0, 700.0]
  COMMAND: NO-ARG-COMMAND
     ARGS: []
  COMMAND: COMMAND-WITH-KEYWORD
     ARGS: [Keyword<KW1>, 1.0, Keyword<KW2>, 2.0, Keyword<KW3>, True, Keyword<KW4>, False]

コマンドであるべき JOIN が、REPEAT コマンドの引数リストの中に、JOIN というキーワードとして含まれてしまっている(JOIN に続く引数もろとも)。

最初に書いたとおり、文法ファイルも入力ファイルも、パーサも何も変えてない。なのに実行するたびに、JOIN だけコマンドになったりキーワードになったりする。

今のところ全くの原因不明。

気になるところといえば、入力ファイルの JOIN コマンドの前の行が空白文字とコメントであること。さらにもう一つ前の行の行末(5 のうしろ)に空白文字があること(この記事では見えないけど)だ。だけどこれは文法上は問題ないはずに思える。実際期待通りの結果になることもあるんだし。

というわけで、このままでは安心して使えない。どうしようか。

AWS CLIでAmazon S3のバケットにファイルをアップロードする

AWS CLI は AWS の各サービスをコマンドラインから使えるツールだ。Amazon S3 をデータのバックアップ用に使おうと思って試してみた。

インストールと設定

AWS の Web ページからダウンロードする。↓のページから。

ダウンロードした awscliv2.zip を解凍する。

takatoh@apostrophe:~$ unzip awscliv2.zip

できた aws ディレクトリにあるインストールスクリプトを実行。

takatoh@apostrophe:~$ sudo aws/install
You can now run: /usr/local/bin/aws --version

メッセージにあるとおりコマンド名は aws だ。バージョンを確認してみよう。

takatoh@apostrophe:~$ aws --version
aws-cli/2.4.4 Python/3.8.8 Linux/5.4.0-91-generic exe/x86_64.ubuntu.20 prompt/off

続いて設定。aws configure コマンドを実行。

takatoh@apostrophe:~$ aws configure
AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXX
AWS Secret Access Key [None]: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Default region name [None]: us-west-2
Default output format [None]: json

AWS Access Key ID と AWS Secret Access Key はあらかじめ取得しておくこと。

S3にファイルをアップロード

aws s3 cp コマンドを利用する。今回はテスト用の panicblanket-test というバケットにファイルをアップロードする。

takatoh@apostrophe:~$ aws s3 cp sample.zip s3://panicblanket-test/sample.zip

ディレクトリごとアップロードするには aws s3 sync コマンドが便利。バックアップ用途にはこちらがいいだろう。

takatoh@apostrophe:~$ aws s3 sync sample s3://panicblanket-test/sample
upload: sample/sample-1.zip to s3://panicblanket-test/sample/sample-1.zip
upload: sample/sample-2.zip to s3://panicblanket-test/sample/sample-2.zip

sample ディレクトリにあった2つのファイルがアップロードされた。

あとはシェルスクリプトを書いて定期的に事項するようにしておけば良さそうだ。

Python: 複数の文字列の共通する部分文字列をとりだす

今日は Python。何がしたいかというと、要するに ‘abcde’、’abcxz’、’ab-op’ という文字列から、共通する部分文字列(ただし先頭から) ‘ab’ をとりだしたい。

探せばいいのがあるかと思ったけど、見つからないので書いた。こんなふうになった。

import itertools

def common_substring(*strings):
    def same_all(*args):
        piv = args[0]
        return all([piv == e for e in args[1:]])
    return ''.join([s[0] for s in itertools.takewhile(lambda x: same_all(*x), zip(*strings))])

試してみよう。

>>> s1 = 'abcde'
>>> s2 = 'abcyz'
>>> s3 = 'ab-op'
>>> common_substring(s1, s2, s3)
'ab'

日本語でもいける。

>>> common_substring('こんにちは', 'こんばんは', 'こんちは')
'こん'

可変長引数をとるので、文字列は何個あってもいい。

Windows11にJDK17をインストールした

Java についてはもう何年も前から、ライセンスで有料化だとか、開発主体が Oracle から離れたとかいう話を聞いていて、部外者から見ると何がどうなってんだかわからない状態なんだけど、Java で書かなきゃいけなさそうな事案に遭遇したので調べてみた。

といっても、ググっても情報が新旧入り混じっててむしろ混乱した。

なんとなくわかったことは:

  • Java SE 17 が最新
  • コミュニティによって OpenJDK として開発されてて、Oracle はじめいろんなところからバイナリ配布されてるらしい
  • 以前はランタイムだけの JRE ってのもあったけど今はない。開発ツールを含んだ JDK だけ

くらいなとこか。

結局 Oracle の JDK に msi インストーラがあったのでこれをインストールした。

Windows 用のインストーラなので指示通りにクリックしていけばOK。試しにコンソールから実行してみるとこんな感じ。

takatoh@sofa: Documents > javac -version
javac 17.0.1
takatoh@sofa: Documents > java -version
java version "17.0.1" 2021-10-19 LTS
Java(TM) SE Runtime Environment (build 17.0.1+12-LTS-39)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.1+12-LTS-39, mixed mode, sharing)

まぁ、何とかなるだろ。

Python: ローカルネットワー上のプライベートなPyPIリポジトリからpoetry addする on Windows

一昨日、Windows 10 上に pypiserver で立てたプライベートリポジトリから pip コマンドでパッケージをインストールできたので、今日は Poetry が使えるか試してみた。

まずは pypi-server コマンドでサーバを起動してプライベートリポジトリを作っておく。

takatoh@montana: Documents > pypi-server -p 8080 ./py-packages

別のターミナルを開いて、Poetry で新しいプロジェクトを作って中に移動する。

takatoh@montana: Documents > poetry new py-sample
Created package py_sample in py-sample
takatoh@montana: Documents > cd py-sample

Poetry にプライベートリポジトリを登録する。

takatoh@montana: py-sample > poetry config repositories.local http://localhost:8080/simple/

リポジトリの名前が local で、その URL が http://localhost:8080/simple/ だ。

つづいて pyproject.toml ファイルにプライベートリポジトリを参照するように次を追記する。

[[tool.poetry.source]]
name = "local"
url = "http://localhost:8080/simple/"

これで準備は完了。poetry install や poetry add でパッケージをインストールできるはず。あと、キャッシュを削除しておくのを忘れずに(まだ直ってない)。今回は PyPI.org には公開してない、自作の outputdatareader っていうパッケージを追加してみた。

takatoh@montana: py-sample > poetry add outputdatareader
Using version ^0.1.0 for outputdatareader

Updating dependencies
Resolving dependencies...

Writing lock file

Package operations: 11 installs, 0 updates, 0 removals

  • Installing pyparsing (2.4.7)
  • Installing atomicwrites (1.4.0)
  • Installing attrs (21.2.0)
  • Installing colorama (0.4.4)
  • Installing more-itertools (8.10.0)
  • Installing packaging (21.0)
  • Installing pluggy (0.13.1)
  • Installing py (1.10.0)
  • Installing wcwidth (0.2.5)
  • Installing outputdatareader (0.1.0)
  • Installing pytest (5.4.3)

無事インストールできた。

ちなみに、プライベートリポジトリには outputdatareader パッケージしか置いてないから、ほかのパッケージは PyPI.org からインストールしてる。

サーバ側のターミナルにはこんなふうに出力されている。

127.0.0.1 - - [22/Oct/2021 22:41:31] "GET /simple/outputdatareader/ HTTP/1.1" 200 353
127.0.0.1 - - [22/Oct/2021 22:41:33] "GET /simple/outputdatareader/ HTTP/1.1" 200 353
127.0.0.1 - - [22/Oct/2021 22:41:36] "GET /simple/pyparsing/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/atomicwrites/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/packaging/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/more-itertools/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/py/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/attrs/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/pluggy/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/colorama/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/wcwidth/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:48] "GET /simple/outputdatareader/ HTTP/1.1" 200 353
127.0.0.1 - - [22/Oct/2021 22:41:48] "GET /simple/pytest/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:50] "GET /packages/outputdatareader-0.1.0-py3-none-any.whl HTTP/1.1" 200 2016

outputdatareader 以外のパッケージではステータスコードが 303 (See Other)になってる。つまりプライベートリポジトリには無いってことで、そうすると Poetry は PyPI.org に探しに行く。最後の行から、プライベートリポジトリからは outputdatareader-0.1.0-py3-none-any.whl ファイルだけがダウンロードされてるのがわかる。

というわけで、今日はここまで。

SQLite3のデータベースファイルからSQLにダンプする

たいしたネタじゃないけど、ときどき必要になる割にいつも調べてる気がするのでメモしておく。

SQLite3 のデータベースは1つのファイルになってる。このデータベースの中身を SQL にダンプするには sqlite3 コマンドをつかって、次の2通りのやり方がある。

対話的インターフェイスでダンプする方法

データベースを開くと、コマンド入力待ち状態になる。

takatoh@wplj:db$ sqlite3 production.sqlite3
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite>

ここで .dump コマンドでダンプすることができるけど、このままだと画面に出力されてしまうので、先に .output コマンドで出力先をファイルに変更しておく。

sqlite> .output ./dump.sql

これでカレントディレクトリの dump.sql ファイルに出力されるようになる。そしたら .dump コマンド。

sqlite> .dump

引数なしだとデータベース内のすべてのテーブルをダンプする。特定のテーブルだけをダンプするには、そのテーブル名を引数に与えればいい。

終わったら .exit

sqlite> .exit

コマンドラインから直接ダンプする方法

パイプを使って sqlite3 コマンドにダンプを指示する .dump コマンドを流し込んでやればいい。デフォルトでは標準出力に書き出すので、ファイルにリダイレクトする。

takatoh@wplj:db$ echo '.dump' | sqlite3 production.sqlite3 > dump2.sql

定期的にバックアップするような場合にはこっちのほうが便利。

Python: ローカルネットワーク上のプライベートなPyPIリポジトリからインストールする on Windows

先月同じようなタイトルの記事を書いたけど……

pip コマンドの --find-links オプションを使えば、パッケージファイルを放り込んだディレクトリを指定するだけでインストールすることができた。のだけれど、poetry を使うときにはこの方法ではダメだった。結局のところ PyPI.org 相当のプレイべートなリポジトリサーバを立ててやる必要がある。

選択肢としては pypiserver っていうパッケージがあって、それは知ってたんだけど、設定とか面倒そうだと勝手に思ったので今までやってみなかった。ところが今日試してみたら存外に簡単にできたのでメモしておくことにする。記事のタイトルにもあるように環境は Windows。

pypiserver 自体は pip コマンドでインストールできる。

takatoh@montana: Documents > pip install pypiserver
Looking in links: http://nightschool/py-packages/
Collecting pypiserver
  Using cached pypiserver-1.4.2-py2.py3-none-any.whl (77 kB)
Installing collected packages: pypiserver
Successfully installed pypiserver-1.4.2

これで完了。以下のようにインストールされてるのが確認できる。

takatoh@montana: Documents > pip list | grep pypiserver
pypiserver                        1.4.2

つぎに、パッケージファイルを置いておくフォルダを作る。

takatoh@montana: Documents > mkdir py-packages

    Directory: C:\Users\takatoh\Documents

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2021/10/19    20:32                py-packages

で、ここに自作のパッケージファイルを放り込んだ。

takatoh@montana: Documents > ls py-packages

    Directory: C:\Users\takatoh\Documents\py-packages

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2021/10/17    18:51           4035 brs-0.8.1-py3-none-any.whl

あとは pypi-server コマンドで起動すれば準備は完了。

takatoh@montana: Documents > pypi-server -p 8080 ./py-packages

-p 8080 オプションでポートを、引数でパッケージファイルの入っているフォルダを指定している。

さて、別のコンソールを立ち上げて pip install してみる。--extra-index-url http://localhost:8080/simple/ オプションでローカルホストに立てた pypiserver の URL を指定している。

takatoh@montana: takatoh > pip install --extra-index-url http://localhost:8080/simple/ brs
Looking in indexes: https://pypi.org/simple, http://localhost:8080/simple/
Looking in links: http://nightschool/py-packages/
Collecting brs
  Downloading http://localhost:8080/packages/brs-0.8.1-py3-none-any.whl (4.0 kB)
Requirement already satisfied: click<9.0.0,>=8.0.1 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from brs) (8.0.1)
Requirement already satisfied: PyYAML<6.0.0,>=5.4.1 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from brs) (5.4.1)
Requirement already satisfied: requests<3.0.0,>=2.26.0 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from brs) (2.26.0)
Requirement already satisfied: colorama in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from click<9.0.0,>=8.0.1->brs) (0.4.4)
Requirement already satisfied: idna<4,>=2.5 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from requests<3.0.0,>=2.26.0->brs) (3.2)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from requests<3.0.0,>=2.26.0->brs) (2021.5.30)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from requests<3.0.0,>=2.26.0->brs) (1.26.7)
Requirement already satisfied: charset-normalizer~=2.0.0 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from requests<3.0.0,>=2.26.0->brs) (2.0.6)
Installing collected packages: brs
Successfully installed brs-0.8.1

Looking in indexes: で始まる行で、PyPI.org とローカルに立てた pypiserver を見に行ってるのがわかる。つづく Looking in links: で始まる行では、先日の記事で設定したパッケージファイルの置いてある場所を探しに行っている。結果として、ローカルの pypiserver からダウンロードしてインストールしてるのがわかる。

サーバ側にはこんなふうに出力されていた。

127.0.0.1 - - [19/Oct/2021 20:35:27] "GET /simple/ HTTP/1.1" 200 207
127.0.0.1 - - [19/Oct/2021 20:35:27] "GET /favicon.ico HTTP/1.1" 404 711
127.0.0.1 - - [19/Oct/2021 20:36:42] "GET /simple/brs/ HTTP/1.1" 200 301
127.0.0.1 - - [19/Oct/2021 20:36:45] "GET /packages/brs-0.8.1-py3-none-any.whl HTTP/1.1" 200 4035

というわけで、Windows でもローカルに pypiserver を立てるのは簡単だということが分かった。

あとはこれをサービスとしてバックグラウンドで動かせれば、poetry からも使えるようになる(はず)。それはまた今度。