PyPIのsearch APIが落ちているのでpipのコードを読んでみる

# はじめに

Pythonのパッケージ管理ツールとして提供されている `pip` (Python Installs Packages)ですが、こちらこちらはPython3.4およびPython2.7.9以降でデフォルトで付属するようになりました。
`pip` を使うことで、初心者であっても簡単にパッケージのインストールを行えます。

## この文章の対象

比較的Python歴の浅い方をイメージして記載します。

# 事の始まり

12/16, 22:30JST頃にパッケージの検索をしようとしたら、下記のようにRuntime errorが出ました。

```bash
$ pip search numpy
ERROR: Exception:
Traceback (most recent call last):
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/cli/base_command.py", line 224, in _main
    status = self.run(options, args)
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/commands/search.py", line 62, in run
    pypi_hits = self.search(query, options)
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/commands/search.py", line 82, in search
    hits = pypi.search({'name': query, 'summary': query}, 'or')
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py", line 1112, in __call__
    return self.__send(self.__name, args)
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py", line 1452, in __request
    verbose=self.__verbose
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/network/xmlrpc.py", line 46, in request
    return self.parse_response(response.raw)
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py", line 1342, in parse_response
    return u.close()
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py", line 656, in close
    raise Fault(**self._stack[0])
xmlrpc.client.Fault: <Fault -32500: 'RuntimeError: This API has been temporarily disabled due to unmanageable load and will be deprecated in the near future. Please use the Simple or JSON API instead.'>
```

経験的に `pip` のバージョンが古いとうまくいかないことがあるらしいことがあると知っていたのでアップデートしてみましたが、現象は変わりませんでした。

```bash
$ pip install -U pip
Requirement already satisfied: pip in ./.pyenv/versions/3.7.6/lib/python3.7/site-packages (20.3.1)
Collecting pip
  Downloading pip-20.3.3-py2.py3-none-any.whl (1.5 MB)
     |████████████████████████████████| 1.5 MB 590 kB/s 
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 20.3.1
    Uninstalling pip-20.3.1:
      Successfully uninstalled pip-20.3.1
Successfully installed pip-20.3.3
```

ここまででわかっていること

- `pip search` でエラーが出る
- `pip install` はできる

上記について、[issue](https://github.com/pypa/pip/issues/9292)が起票されていました。

>PyPI has had significant uptick in automated hits to the XMLRPC API, which has resulted in needing to take that offline, as it hampers the rest of the application's ability to function.

`pip` はPyPIにパッケージを検索にいきますが、APIが大量に叩かれるとオフラインになるようです。
より詳細なインシデント状況はこちら [https://status.python.org/incidents/grk0k7sz6zkp](https://status.python.org/incidents/grk0k7sz6zkp) に記載されていますが、参考までにスクリーンショットを添付しておきます。

![スクリーンショット 2020-12-16 22.50.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93354/0e0c1e2e-9143-2eb8-659d-8772ac5755c4.png)

ここに書いてあるように、どうやら大量に `pip search` のAPIを叩いているクライアントがいるために、APIを再度有効にしてもすぐに落ちてしまうようです。そのため、まだAPIをオンラインにできず、結果我々が `pip search` コマンドを実行してもAPIが無効化されているとエラーが出ています。

この問題は1日以上前から起きていたようですが、私は最近開発しているときは自分で `requirements.txt` を用意するなどソラで使えるパッケージばかり利用していました。
そのため `pip search` を叩く機会がなく、気づくのに遅れました。

## 補足：XMLRPCとは

HTTP経由でXMLを使って遠隔手続き呼び出し（Remote Procedure Call）をする方法です。
詳細はこちらの[Pythonのドキュメント](https://docs.python.org/ja/3/library/xmlrpc.html)を御覧ください。

# 実際にpipが行っていること

私はモジュール開発では `venv` を使うなどしていますが、Python自体は `pyenv` で用意しています。
環境は下記のとおりです。

```text
マシン: MacBookAir 13-inch, Early 2015
OS: macOS Catalina (10.15.7)
```

```bash
$ pyenv versions
  system
* 3.7.6 (set by /Users/montblanc18/.pyenv/version)
```

`pip` の中身を追いかけても良いですが、 `pyenv` で入れているため、少々環境に癖があります。
なので、エラーログからさかのぼっていきます。
なお、 `pip` 自体はPythonで書かれているため、Pythonを書いている方ならまだ読めるかなと思います。

```bash
$ pip search numpy
ERROR: Exception:
Traceback (most recent call last):
# pyenvで3.7.6系が有効になっているので、~/.pyenv/shims/pipから3.7.6系のpipが呼び出されオプション解析されます
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/cli/base_command.py", line 224, in _main
    status = self.run(options, args)
# searchがオプションとして与えられているので、search用の関数が呼び出されます
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/commands/search.py", line 62, in run
    pypi_hits = self.search(query, options)
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/commands/search.py", line 82, in search
    hits = pypi.search({'name': query, 'summary': query}, 'or')
# 前のsearch.pyの中で検索クエリを発行しており、その中でXMLRPCが発行呼び出されます。
# 実態としては3.7.6系用のXMLRPCライブラリのクライアント部分がコールされます
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py", line 1112, in __call__
    return self.__send(self.__name, args)
# XMLPRCで受け取った結果をパースし、コネクションをクローズします
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py", line 1452, in __request
    verbose=self.__verbose
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/network/xmlrpc.py", line 46, in request
    return self.parse_response(response.raw)
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py", line 1342, in parse_response
    return u.close()
# エラーが上がっていたので結果を出力して終了しています
  File "/Users/montblanc18/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py", line 656, in close
    raise Fault(**self._stack[0])
xmlrpc.client.Fault: <Fault -32500: 'RuntimeError: This API has been temporarily disabled due to unmanageable load and will be deprecated in the near future. Please use the Simple or JSON API instead.'>
```

もうすこし、具体的にソースコードを見てみます。

```Python:~/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/commands/search.py
# 前略
    def search(self, query, options):
        # type: (List[str], Values) -> List[Dict[str, str]]
        index_url = options.index

        session = self.get_default_session(options)

        transport = PipXmlrpcTransport(index_url, session)
        pypi = xmlrpc_client.ServerProxy(index_url, transport)  # 一行下でエラーメッセージを出しているpypiを定義している場所
        hits = pypi.search({'name': query, 'summary': query}, 'or') # エラーメッセージが出ている場所
        return hits
# 以下略
```

`pypi.search` がエラーメッセージに出ていますが、その中身は直前で定義されている `xmlrpc_client.ServerProxy` です。
このクラスが `~/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py` 内で定義されています。
`~/.pyenv/versions/3.7.6/lib/python3.7/xmlrpc/client.py` 内で行っているのは、引数に与えられたURLを叩く操作です。
実際に叩かれるURLは、 `~/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/commands/search.py` の `def search` 内の `index_url` になります。
この `index_url` は `~/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/cli/base_command.py` から渡ってきた引数ですので、改めてこちらの中身を確認します。

```python:~/.pyenv/versions/3.7.6/lib/python3.7/site-packages/pip/_internal/cli/base_command.py
# 前略
class Command(CommandContextMixIn):
# 中略
    def main(self, args):
        # type: (List[str]) -> int
        try:
            with self.main_context():
                return self._main(args)
        finally:
            logging.shutdown()

    def _main(self, args):
        # type: (List[str]) -> int
        # We must initialize this before the tempdir manager, otherwise the
        # configuration would not be accessible by the time we clean up the
        # tempdir manager.
        self.tempdir_registry = self.enter_context(tempdir_registry())
        # Intentionally set as early as possible so globally-managed temporary
        # directories are available to the rest of the code.
        self.enter_context(global_tempdir_manager())

        options, args = self.parse_args(args)
# 以下略
```

`options` の中には引数をパースした結果が入ってきます。
この中でよしなに接続先のURLが作られ、格納されます。

`pip` は引数にとったコマンド（ `search` や `install` ）によって違う関数がコールされ、結果として裏で叩きに行くPyPIのAPIも異なります。
そのため、 `pip search` に対応するAPIがオフラインでも、 `pip install` に対応するAPIはオンラインであるため、ちゃんとインストールできます。