jqfpyにloadfile(),dumpfile()を追加していた

github.com

loadfile(),dumpfile()がほしくなったので追加した。

jqfpy

jqfpyは、

jsonをparseするためにDSLを覚えるのがめんどくさい(jq)。 各自自分の慣れた言語のワンライナーで十分なのでは?

という発想のもとのpackage(自分はpython)。

重要なことは誰でも最初から使えるということ(pythonの知識があるなら)。

例えば以下のようなファイルがある時に、category情報の一覧を取り出したいときは以下の様に書く。

flask.json

{
  "data": [
    {
      "category": "2.6",
      "downloads": 5287
    },
    {
      "category": "2.7",
      "downloads": 2274964
    },
    {
      "category": "3.2",
      "downloads": 23
    },
    {
      "category": "3.3",
      "downloads": 723
    },
    {
      "category": "3.4",
      "downloads": 120625
    },
    {
      "category": "3.5",
      "downloads": 899583
    },
    {
      "category": "3.6",
      "downloads": 4135489
    },
    {
      "category": "3.7",
      "downloads": 787076
    },
    {
      "category": "3.8",
      "downloads": 3190
    },
    {
      "category": "null",
      "downloads": 44777
    }
  ],
  "package": "flask",
  "type": "python_minor_downloads"
}

これをjqfpyで取り出すのは以下のような形。

$ jqfpy -c '[row["category"] for row in get("data")]' flask.json
["2.6", "2.7", "3.2", "3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "null"]

ただのリスト内包表記。ただのリスト内包表記なので誰でも書ける。 get() は渡されたファイルをloadした結果を返す。loadした結果をどうこうするということなので、単にdictをイジるだけの操作ということになる。

ショートカット

とはいえ毎回書くのがめんどくさいと思うこともあるのでショートカットを用意したくなる。

$ jqfpy -c 'get("data[]/category")' flask.json
["2.6", "2.7", "3.2", "3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "null"]

個人的にはなるべく対応関係を意識せずにすむようにしたいので、JSON Referenceに近いかたちの表記でアクセスできるようにしている ("[", "]"などは対応関係を意識する必要がある)。

複数ファイル

複数のファイルが渡されていたときの挙動。これはおそらくjqなどと同様。

$ jqfpy -c 'get("data[]/category")' flask.json django.json
["2.6", "2.7", "3.2", "3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "null"]
["2.6", "2.7", "3.2", "3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "null"]

というところまでが復習。

loadfile()

ところで、直接JSONデータを渡すのではなくファイル名を渡したい場合がある、そして複数のファイル名を組み合わせたJSONを元に変換を書きたいことがある。そんなわけで loadfile()が欲しくなった。

$ echo '["django.json", "flask.json"]' | jqfpy '{name: h.loadfile(name) for name in get()}'
{
  "django.json": {
    "data": [
      {
        "category": "2.6",
        "downloads": 1320
      },
      {
        "category": "2.7",
        "downloads": 762206
      },
      {
        "category": "3.2",
        "downloads": 24
      },
      {
        "category": "3.3",
        "downloads": 381
      },
      {
        "category": "3.4",
        "downloads": 56461
      },
      {
        "category": "3.5",
        "downloads": 250227
      },
      {
        "category": "3.6",
        "downloads": 1078975
      },
      {
        "category": "3.7",
        "downloads": 595086
      },
      {
        "category": "3.8",
        "downloads": 1764
      },
      {
        "category": "null",
        "downloads": 30137
      }
    ],
    "package": "django",
    "type": "python_minor_downloads"
  },
  "flask.json": {
    "data": [
      {
        "category": "2.6",
        "downloads": 5287
      },
      {
        "category": "2.7",
        "downloads": 2274964
      },
      {
        "category": "3.2",
        "downloads": 23
      },
      {
        "category": "3.3",
        "downloads": 723
      },
      {
        "category": "3.4",
        "downloads": 120625
      },
      {
        "category": "3.5",
        "downloads": 899583
      },
      {
        "category": "3.6",
        "downloads": 4135489
      },
      {
        "category": "3.7",
        "downloads": 787076
      },
      {
        "category": "3.8",
        "downloads": 3190
      },
      {
        "category": "null",
        "downloads": 44777
      }
    ],
    "package": "flask",
    "type": "python_minor_downloads"
  }
}

--here

相対位置をどのように解釈するかのoptionを追加した。defaultはcurrent working directoryからの相対位置。--here を付けると、その位置からの相対位置になる。

# ./x.jsonを見る
$ jqfpy 'h.loadfile("x.json")' a/b/main.json

# ./a/b/x.jsonを見る
$ jqfpy --here a/b/ 'h.loadfile("x.json")' a/b/main.json

--relative-path

--relative-path を付けると渡されたファイルからの相対位置になる。

# ./a/b/x.jsonを見る
$ jqfpy --relative-path 'h.loadfile("x.json")' a/b/main.json

dumpfile()

loadfile() と同様に dumpfile() もある。こちらは逆に複数のファイルに分割して出力したくなることがある。このとき出力は不要なので /dev/null にリダイレクトしたくなるかもしれない。

$ echo '["flask.json", "django.json"]' | jqfpy '[h.dumpfile(get("data[]/category", d=h.loadfile(name)), f"""{name.replace(".json", ".path")}""") for name in get()]; None'
null
$ ls *.path
django.path  flask.path

python3.8 だとさらに便利に。

pypiの特定のpython packageを利用しているversionの割合を見てみる方法について

python2.xのサポートが2020-01-01T00:00:00でおわるということで、最近は drop python2 というようなissueも増えてきています(node-gypはどうするんでしょうね..)。

そのようなissueの中で、使われているversionの情報を調べて共有しくれたりする人がだいたいどこかに居るのですが、その人たちがどのようにしてそれを調べているのか気になったので手順などを調べることにしてみました。

pypistats

一番手軽に行えるのはpypistatsを見ることかもしれません。

こちらはウェブのインターフェイスもあるようです。例えばflaskを例に調べてみると以下の様なページが見つかります。

pypistats-flask

時系列的な遷移を知りたい場合にはグラフの方がありがたいですが、直近1ヶ月のサマリー程度が見えれば十分ですよね。issueの上で議論する上では。

そういうときにはCLIでテキトーに調べるのが楽です。このpypistatsのCLIを作ってくれている人もいてこちらを使うのが手軽です。

github.com

先程のとおりにflaskを、そして直近一ヶ月という期間で調べるには以下の様なコマンドを実行すれば良いようです。formatも指定できるので更に他のコマンドで整形したい場合などはjsonで、issueなどで共有したい場合にはmarkdownで(defaultはmarkdownです)出力してあげれば良さそうです。

$ pip install pypistats
$ pypistats python_minor --last-month flask
| category | percent | downloads |
|----------|--------:|----------:|
|      3.6 |  50.00% | 4,135,489 |
|      2.7 |  27.50% | 2,274,964 |
|      3.5 |  10.88% |   899,583 |
|      3.7 |   9.52% |   787,076 |
|      3.4 |   1.46% |   120,625 |
| null     |   0.54% |    44,777 |
|      2.6 |   0.06% |     5,287 |
|      3.8 |   0.04% |     3,190 |
|      3.3 |   0.01% |       723 |
|      3.2 |   0.00% |        23 |
| Total    |         | 8,271,737 |

つまりこういう形で共有できるわけです。

category percent downloads
3.6 50.00% 4,135,489
2.7 27.50% 2,274,964
3.5 10.88% 899,583
3.7 9.52% 787,076
3.4 1.46% 120,625
null 0.54% 44,777
2.6 0.06% 5,287
3.8 0.04% 3,190
3.3 0.01% 723
3.2 0.00% 23
Total 8,271,737

python_minor以外にも幾つかのsub commandが用意されています。

$ pypistats -h
usage: pypistats [-h] [-V] {recent,overall,python_major,python_minor,system} ...

positional arguments:
  {recent,overall,python_major,python_minor,system}
...

データの在り処

数値だけが分かってもデータの在り処がはっきりしていないとスッキリしないですよね。実はFAQに書いてあります。

https://pypistats.org/faqs#what-is-the-source-of-the-download-data

What is the source of the download data?

PyPI provides download records as a publicly available dataset on Google's BigQuery. You can access the data with a Google Cloud account here.

そんなわけでpypi自身がbigquery上にデータセットを公開してくれていたりしています。これを使っていたようです。ここです。

https://bigquery.cloud.google.com/table/the-psf:pypi.downloads

そしてデイリーでバッチを起動して集めているようです。そのあたりのこともFAQに書いてあります。

pypinfo

似たようなコマンドでpypinfoというコマンドを使っているひともいました。こちらは自分自身でbigqueryにアクセスしてデータを取ってくるようです。

github.com

ちなみにbigqueryへのアクセスの仕方がすごく丁寧にreadmeに書いてあるので親切です。

$ pip install pypinfo
$ pypinfo --auth=<path/to/your_credentials.json>
# or GOOGLE_APPLICATION_CREDENTIALS=<path/to/your_credentials.json>
$ pypistats python_minor flask --last-month
| category | percent | downloads |
|----------|--------:|----------:|
|      3.6 |  50.00% | 4,135,489 |
|      2.7 |  27.50% | 2,274,964 |
|      3.5 |  10.88% |   899,583 |
|      3.7 |   9.52% |   787,076 |
|      3.4 |   1.46% |   120,625 |
| null     |   0.54% |    44,777 |
|      2.6 |   0.06% |     5,287 |
|      3.8 |   0.04% |     3,190 |
|      3.3 |   0.01% |       723 |
|      3.2 |   0.00% |        23 |
| Total    |         | 8,271,737 |

同じようにmarkdownで出力してくれます。

category percent downloads
3.6 50.00% 4,135,489
2.7 27.50% 2,274,964
3.5 10.88% 899,583
3.7 9.52% 787,076
3.4 1.46% 120,625
null 0.54% 44,777
2.6 0.06% 5,287
3.8 0.04% 3,190
3.3 0.01% 723
3.2 0.00% 23
Total 8,271,737

pypistatsの方が手軽なので日常的にはpypistatsで済ませることが多いかもしれません。そしてもう少し細かく気にしたくなったら自分でbigqueryを叩きに行くかpypinfoを使う感じになりそうです。

おまけ

ちなみにflaskとdjangoの比較

flaskの方がdjangoの4倍くらいdownloadされている?(2019-04月のデータ)

flaskとdjango

category percent downloads category percent downloads
3.6 50.00% 4,135,489 3.6 38.86% 1,078,975
2.7 27.50% 2,274,964 2.7 27.45% 762,206
3.5 10.88% 899,583 3.7 21.43% 595,086
3.7 9.52% 787,076 3.5 9.01% 250,227
3.4 1.46% 120,625 3.4 2.03% 56,461
null 0.54% 44,777 null 1.09% 30,137
2.6 0.06% 5,287 3.8 0.06% 1,764
3.8 0.04% 3,190 2.6 0.05% 1,320
3.3 0.01% 723 3.3 0.01% 381
3.2 0.00% 23 3.2 0.00% 24
Total 8,271,737 Total 2,776,581

code

# ちょっと手抜きだけれど。。
$ pypistats python_minor --month 2019-04 -f markdown flask > flask.md
$ pypistats python_minor --month 2019-04 -f markdown django > django.md
$ paste flask.md django.md | sed 's/|[ \t]|/|/g' > diff.md

ちなみにautopep8とyapfとblack

今度はformatter

autopep8とyapfとblack

category percent downloads category percent downloads category percent downloads
2.7 45.09% 383,360 3.6 38.69% 165,974 3.6 56.17% 258,994
3.6 29.36% 249,583 2.7 32.29% 138,492 3.7 40.72% 187,724
3.7 14.17% 120,421 3.7 16.93% 72,631 null 2.86% 13,187
3.5 5.88% 49,984 3.5 7.88% 33,783 3.8 0.14% 659
3.4 5.05% 42,938 null 3.50% 15,021 2.7 0.06% 255
null 0.35% 2,956 3.4 0.65% 2,794 3.5 0.05% 216
2.6 0.08% 678 3.8 0.06% 252 3.4 0.00% 15
3.8 0.02% 178 3.3 0.00% 8 3.3 0.00% 8
3.3 0.00% 24 2.6 0.00% 6 2.6 0.00% 6
3.2 0.00% 2 Total 428,961 Total 461,064
Total 850,124

code

$ pypistats python_minor --month 2019-04 -f markdown autopep8 > autopep8.md
$ pypistats python_minor --month 2019-04 -f markdown yapf > yapf.md
$ pypistats python_minor --month 2019-04 -f markdown black > black.md
$ paste autopep8.md yapf.md black.md | sed 's/|[ \t]|/|/g' > diff2.md

まじめに取り扱える便利ななにかがほしいかも(pandasを取り出す気力がなかった)