はじめに
Pythonのパッケージ管理ツールとして提供されている pip
(Python Installs Packages)ですが、こちらこちらはPython3.4およびPython2.7.9以降でデフォルトで付属するようになりました。
pip
を使うことで、初心者であっても簡単にパッケージのインストールを行えます。
この文章の対象
比較的Python歴の浅い方をイメージして記載します。
事の始まり
12/16, 22:30JST頃にパッケージの検索をしようとしたら、下記のようにRuntime errorが出ました。
$ 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
のバージョンが古いとうまくいかないことがあるらしいことがあると知っていたのでアップデートしてみましたが、現象は変わりませんでした。
$ 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が起票されていました。
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 に記載されていますが、参考までにスクリーンショットを添付しておきます。
ここに書いてあるように、どうやら大量に pip search
のAPIを叩いているクライアントがいるために、APIを再度有効にしてもすぐに落ちてしまうようです。そのため、まだAPIをオンラインにできず、結果我々が pip search
コマンドを実行してもAPIが無効化されているとエラーが出ています。
この問題は1日以上前から起きていたようですが、私は最近開発しているときは自分で requirements.txt
を用意するなどソラで使えるパッケージばかり利用していました。
そのため pip search
を叩く機会がなく、気づくのに遅れました。
補足:XMLRPCとは
HTTP経由でXMLを使って遠隔手続き呼び出し(Remote Procedure Call)をする方法です。
詳細はこちらのPythonのドキュメントを御覧ください。
実際にpipが行っていること
私はモジュール開発では venv
を使うなどしていますが、Python自体は pyenv
で用意しています。
環境は下記のとおりです。
マシン: MacBookAir 13-inch, Early 2015
OS: macOS Catalina (10.15.7)
$ pyenv versions
system
* 3.7.6 (set by /Users/montblanc18/.pyenv/version)
pip
の中身を追いかけても良いですが、 pyenv
で入れているため、少々環境に癖があります。
なので、エラーログからさかのぼっていきます。
なお、 pip
自体はPythonで書かれているため、Pythonを書いている方ならまだ読めるかなと思います。
$ 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.'>
もうすこし、具体的にソースコードを見てみます。
# 前略
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
から渡ってきた引数ですので、改めてこちらの中身を確認します。
# 前略
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はオンラインであるため、ちゃんとインストールできます。