diff --git a/docs/api-reference/xml-rpc.rst b/docs/api-reference/xml-rpc.rst index 10fa8b57f017..52b61eb1228b 100644 --- a/docs/api-reference/xml-rpc.rst +++ b/docs/api-reference/xml-rpc.rst @@ -2,7 +2,7 @@ PyPI's XML-RPC methods ====================== -.. note:: +.. warning:: The XML-RPC API will be deprecated in the future. Use of this API is not recommended, and existing consumers of the API should migrate to the RSS and/or JSON APIs instead. @@ -41,166 +41,217 @@ Example usage (Python 3):: .. _changes-to-legacy-api: -Changes to Legacy API +Changes to XMLRPC API --------------------- -``package_releases`` As Warehouse does not support the concept of hidden -releases, the `show_hidden` flag now controls whether the latest version or all -versions are returned. +- ``search`` Permanently deprecated and disabled due to excessive traffic + driven by unidentified traffic, presumably automated. `See historical + incident `_. -``release_data`` The `stable_version` flag is always an empty string. It was -never fully supported anyway. +- ``package_releases`` As Warehouse does not support the concept of hidden + releases, the `show_hidden` flag now controls whether the latest version or + all versions are returned. -``release_downloads`` and ``top_packages`` No longer supported. Use -`Google BigQuery -`_ -instead (`guidance -`_, -`tips `_). +- ``release_data`` The `stable_version` flag is always an empty string. It was + never fully supported anyway. -Package querying ----------------- +- ``release_downloads`` and ``top_packages`` No longer supported. Use + :doc:`Google BigQuery ` instead (`guidance + `_, + `tips `_). -``list_packages()`` - Retrieve a list of the package names registered with the package index. - Returns a list of name strings. -``package_releases(package_name, show_hidden=False)`` - Retrieve a list of the releases registered for the given `package_name`, - ordered by version. +.. _changelog-since: - If `show_hidden` is `False` (the default), only the latest version is - returned. Otherwise, all versions are returned. +Mirroring Support +----------------- -``package_roles(package_name)`` - Retrieve a list of `[role, user]` for a given `package_name`. - Role is either `Maintainer` or `Owner`. +.. note:: + XML-RPC methods for mirroring support are currently the only methods we + consider fully supported, until an improved mechanism for mirroring is + implemented. Users of these methods should **certainly** subscribe to the + pypi-announce_ mailing list to ensure they are aware of changes or + deprecations related to these methods. -``user_packages(user)`` - Retrieve a list of `[role, package_name]` for a given `user`. - Role is either `Maintainer` or `Owner`. +``changelog(since, with_ids=False)`` +++++++++++++++++++++++++++++++++++++ -``release_urls(package_name, release_version)`` - Retrieve a list of download URLs for the given `release_version`. - Returns a list of dicts with the following keys: - - * filename - * packagetype ('sdist', 'bdist_wheel', etc) - * python_version (required version, or 'source', or 'any') - * size (an ``int``) - * md5_digest - * digests (a dict with two keys, "md5" and "sha256") - * has_sig (a boolean) - * upload_time_iso_8601 (a ``DateTime`` object) - * comment_text - * downloads (always says "-1") - * url +Retrieve a list of `[name, version, timestamp, action]`, or `[name, +version, timestamp, action, id]` if `with_ids=True`, since the given +`since`. All `since` timestamps are UTC values. The argument is a +UTC integer seconds since the epoch (e.g., the ``timestamp`` method +to a ``datetime.datetime`` object). -``release_data(package_name, release_version)`` - Retrieve metadata describing a specific `release_version`. - Returns a dict with keys for: - - * name - * version - * stable_version (always an empty string or None) - * bugtrack_url - * package_url - * release_url - * docs_url (URL of the packages.python.org docs if they've been supplied) - * home_page - * download_url - * project_url - * author - * author_email - * maintainer - * maintainer_email - * summary - * description (string, sometimes the entirety of a ``README``) - * license - * keywords - * platform - * classifiers (list of classifier strings) - * requires - * requires_dist - * provides - * provides_dist - * obsoletes - * obsoletes_dist - * requires_python - * requires_external - * _pypi_ordering - * _pypi_hidden - * downloads (``{'last_day': 0, 'last_week': 0, 'last_month': 0}``) - - If the release does not exist, an empty dictionary is returned. +``changelog_last_serial()`` ++++++++++++++++++++++++++++ -``search(spec[, operator])`` - Search the package database using the indicated search `spec`. - - Returns at most 100 results. - - The `spec` may include any of the keywords described in the above list - (except 'stable_version' and 'classifiers'), for example: - {'description': 'spam'} will search description fields. Within the spec, a - field's value can be a string or a list of strings (the values within the - list are combined with an OR), for example: {'name': ['foo', 'bar']}. Valid - keys for the spec dict are listed here. Invalid keys are ignored: - - * name - * version - * author - * author_email - * maintainer - * maintainer_email - * home_page - * license - * summary - * description - * keywords - * platform - * download_url - - Arguments for different fields are combined using either "and" (the default) - or "or". Example: `search({'name': 'foo', 'description': 'bar'}, 'or')`. - The results are returned as a list of dicts `{'name': package name, - 'version': package release version, 'summary': package release summary}` +Retrieve the last event's serial id (an ``int``). + +``changelog_since_serial(since_serial)`` +++++++++++++++++++++++++++++++++++++++++ + +Retrieve a list of `(name, version, timestamp, action, serial)` since the +event identified by the given ``since_serial``. All timestamps are UTC +values. + +``list_packages_with_serial()`` ++++++++++++++++++++++++++++++++ + +Retrieve a dictionary mapping package names to the last serial for each +package. + + +Package querying +---------------- + +``package_roles(package_name)`` ++++++++++++++++++++++++++++++++ + +Retrieve a list of `[role, user]` for a given `package_name`. +Role is either `Maintainer` or `Owner`. + +``user_packages(user)`` ++++++++++++++++++++++++ + +Retrieve a list of `[role, package_name]` for a given `user`. +Role is either `Maintainer` or `Owner`. ``browse(classifiers)`` - Retrieve a list of `[name, version]` of all releases classified with all of - the given classifiers. `classifiers` must be a list of Trove classifier - strings. ++++++++++++++++++++++++ + +Retrieve a list of `[name, version]` of all releases classified with all of +the given classifiers. `classifiers` must be a list of Trove classifier +strings. ``updated_releases(since)`` - Retrieve a list of package releases made since the given timestamp. The - releases will be listed in descending release date. ++++++++++++++++++++++++++++ + +Retrieve a list of package releases made since the given timestamp. The +releases will be listed in descending release date. ``changed_packages(since)`` - Retrieve a list of package names where those packages have been changed - since the given timestamp. The packages will be listed in descending date - of most recent change. ++++++++++++++++++++++++++++ -.. _changelog-since: +Retrieve a list of package names where those packages have been changed +since the given timestamp. The packages will be listed in descending date +of most recent change. -Mirroring Support ------------------ -``changelog(since, with_ids=False)`` - Retrieve a list of `[name, version, timestamp, action]`, or `[name, - version, timestamp, action, id]` if `with_ids=True`, since the given - `since`. All `since` timestamps are UTC values. The argument is a - UTC integer seconds since the epoch (e.g., the ``timestamp`` method - to a ``datetime.datetime`` object). +``list_packages()`` ++++++++++++++++++++ -``changelog_last_serial()`` - Retrieve the last event's serial id (an ``int``). +.. warning:: + Migrate to using the :doc:`Simple API `. -``changelog_since_serial(since_serial)`` - Retrieve a list of `(name, version, timestamp, action, serial)` since the - event identified by the given ``since_serial``. All timestamps are UTC - values. +Retrieve a list of the package names registered with the package index. +Returns a list of name strings. -``list_packages_with_serial()`` - Retrieve a dictionary mapping package names to the last serial for each - package. +``package_releases(package_name, show_hidden=False)`` ++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. warning:: + Migrate to using the :doc:`json`. + +Retrieve a list of the releases registered for the given `package_name`, +ordered by version. + +If `show_hidden` is `False` (the default), only the latest version is +returned. Otherwise, all versions are returned. + +``release_urls(package_name, release_version)`` ++++++++++++++++++++++++++++++++++++++++++++++++ + +.. warning:: + Migrate to using the :doc:`json`. + +Retrieve a list of download URLs for the given `release_version`. +Returns a list of dicts with the following keys: + +* filename +* packagetype ('sdist', 'bdist_wheel', etc) +* python_version (required version, or 'source', or 'any') +* size (an ``int``) +* md5_digest +* digests (a dict with two keys, "md5" and "sha256") +* has_sig (a boolean) +* upload_time_iso_8601 (a ``DateTime`` object) +* comment_text +* downloads (always says "-1") +* url + +``release_data(package_name, release_version)`` ++++++++++++++++++++++++++++++++++++++++++++++++ + +.. warning:: + Migrate to using the :doc:`json`. + +Retrieve metadata describing a specific `release_version`. +Returns a dict with keys for: + +* name +* version +* stable_version (always an empty string or None) +* bugtrack_url +* package_url +* release_url +* docs_url (URL of the packages.python.org docs if they've been supplied) +* home_page +* download_url +* project_url +* author +* author_email +* maintainer +* maintainer_email +* summary +* description (string, sometimes the entirety of a ``README``) +* license +* keywords +* platform +* classifiers (list of classifier strings) +* requires +* requires_dist +* provides +* provides_dist +* obsoletes +* obsoletes_dist +* requires_python +* requires_external +* _pypi_ordering +* _pypi_hidden +* downloads (``{'last_day': 0, 'last_week': 0, 'last_month': 0}``) + +If the release does not exist, an empty dictionary is returned. + + +Deprecated Methods +------------------ + +.. warning:: + The following methods are permanently deprecated and will return a + `RuntimeError` + +``package_data(package_name, version)`` ++++++++++++++++++++++++++++++++++++++++ + +Deprecated in favor of ``release_data``, :doc:`json` should be used. + +``package_urls(package_name, version)`` ++++++++++++++++++++++++++++++++++++++++ + +Deprecated in favor of ``release_urls``, :doc:`json` should be used. + +``top_packages(num=None)`` +++++++++++++++++++++++++++ + +Use :doc:`Google BigQuery ` +instead (`guidance `_, +`tips `_). + +``search(spec[, operator])`` +++++++++++++++++++++++++++++ + +Permanently deprecated and disabled due to excessive traffic +driven by unidentified traffic, presumably automated. `See historical incident +`_. .. _pypi-announce: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ diff --git a/tests/unit/legacy/api/xmlrpc/test_xmlrpc.py b/tests/unit/legacy/api/xmlrpc/test_xmlrpc.py index ccd9ef5451dd..79c9bd7ba333 100644 --- a/tests/unit/legacy/api/xmlrpc/test_xmlrpc.py +++ b/tests/unit/legacy/api/xmlrpc/test_xmlrpc.py @@ -12,7 +12,6 @@ import datetime -import elasticsearch import pretend import pytest @@ -114,434 +113,25 @@ def view(context, request): class TestSearch: - def test_error_when_disabled(self, pyramid_request, metrics, monkeypatch): - monkeypatch.setattr( - pyramid_request.registry, - "settings", - {"warehouse.xmlrpc.search.enabled": False}, - ) + @pytest.mark.parametrize("domain", [None, "example.com"]) + def test_error(self, pyramid_request, metrics, monkeypatch, domain): + registry_settings = {} + if domain: + registry_settings["warehouse.domain"] = domain + monkeypatch.setattr(pyramid_request.registry, "settings", registry_settings) + monkeypatch.setattr(pyramid_request, "domain", "example.org") + with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc: xmlrpc.search(pyramid_request, {"name": "foo", "summary": ["one", "two"]}) assert exc.value.faultString == ( - "RuntimeError: PyPI's XMLRPC API is currently disabled due to " - "unmanageable load and will be deprecated in the near future. See " - "https://status.python.org/ for more information." - ) - assert metrics.increment.calls == [ - pretend.call("warehouse.xmlrpc.search.deprecated") - ] - - def test_fails_with_invalid_operator(self, pyramid_request, metrics): - with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc: - xmlrpc.search(pyramid_request, {}, "lol nope") - - assert ( - exc.value.faultString - == "ValueError: Invalid operator, must be one of 'and' or 'or'." - ) - assert metrics.histogram.calls == [] - - def test_default_search_operator(self, pyramid_request, metrics): - class FakeQuery: - def __init__(self, type, must): - self.type = type - self.must = must - - def __getitem__(self, name): - self.offset = name.start - self.limit = name.stop - self.step = name.step - return self - - def execute(self): - assert self.type == "bool" - assert [q.to_dict() for q in self.must] == [ - {"match": {"name": {"query": "foo", "boost": 10}}}, - { - "bool": { - "should": [ - {"match": {"summary": {"query": "one", "boost": 5}}}, - {"match": {"summary": {"query": "two", "boost": 5}}}, - ] - } - }, - ] - assert self.offset is None - assert self.limit == 100 - assert self.step is None - return [ - pretend.stub( - name="foo", - summary="my summary", - latest_version="1.0", - version=["1.0"], - ), - pretend.stub( - name="foo-bar", - summary="other summary", - latest_version="2.0", - version=["2.0", "1.0"], - ), - ] - - pyramid_request.es = pretend.stub(query=FakeQuery) - results = xmlrpc.search( - pyramid_request, {"name": "foo", "summary": ["one", "two"]} - ) - assert results == [ - { - "_pypi_ordering": False, - "name": "foo", - "summary": "my summary", - "version": "1.0", - }, - { - "_pypi_ordering": False, - "name": "foo-bar", - "summary": "other summary", - "version": "2.0", - }, - ] - assert metrics.histogram.calls == [ - pretend.call("warehouse.xmlrpc.search.results", 2) - ] - - def test_default_search_operator_with_spaces_in_values( - self, pyramid_request, metrics - ): - class FakeQuery: - def __init__(self, type, must): - self.type = type - self.must = must - - def __getitem__(self, name): - self.offset = name.start - self.limit = name.stop - self.step = name.step - return self - - def execute(self): - assert self.type == "bool" - assert [q.to_dict() for q in self.must] == [ - { - "bool": { - "should": [ - { - "match": { - "summary": {"boost": 5, "query": "fix code"} - } - }, - { - "match": { - "summary": {"boost": 5, "query": "like this"} - } - }, - ] - } - } - ] - assert self.offset is None - assert self.limit == 100 - assert self.step is None - return [ - pretend.stub( - name="foo", - summary="fix code", - latest_version="1.0", - version=["1.0"], - ), - pretend.stub( - name="foo-bar", - summary="like this", - latest_version="2.0", - version=["2.0", "1.0"], - ), - ] - - pyramid_request.es = pretend.stub(query=FakeQuery) - results = xmlrpc.search(pyramid_request, {"summary": ["fix code", "like this"]}) - assert results == [ - { - "_pypi_ordering": False, - "name": "foo", - "summary": "fix code", - "version": "1.0", - }, - { - "_pypi_ordering": False, - "name": "foo-bar", - "summary": "like this", - "version": "2.0", - }, - ] - assert metrics.histogram.calls == [ - pretend.call("warehouse.xmlrpc.search.results", 2) - ] - - def test_searches_with_and(self, pyramid_request, metrics): - class FakeQuery: - def __init__(self, type, must): - self.type = type - self.must = must - - def __getitem__(self, name): - self.offset = name.start - self.limit = name.stop - self.step = name.step - return self - - def execute(self): - assert self.type == "bool" - assert [q.to_dict() for q in self.must] == [ - {"match": {"name": {"query": "foo", "boost": 10}}}, - { - "bool": { - "should": [ - {"match": {"summary": {"query": "one", "boost": 5}}}, - {"match": {"summary": {"query": "two", "boost": 5}}}, - ] - } - }, - ] - assert self.offset is None - assert self.limit == 100 - assert self.step is None - return [ - pretend.stub( - name="foo", - summary="my summary", - latest_version="1.0", - version=["1.0"], - ), - pretend.stub( - name="foo-bar", - summary="other summary", - latest_version="2.0", - version=["2.0", "1.0"], - ), - ] - - pyramid_request.es = pretend.stub(query=FakeQuery) - results = xmlrpc.search( - pyramid_request, {"name": "foo", "summary": ["one", "two"]}, "and" - ) - assert results == [ - { - "_pypi_ordering": False, - "name": "foo", - "summary": "my summary", - "version": "1.0", - }, - { - "_pypi_ordering": False, - "name": "foo-bar", - "summary": "other summary", - "version": "2.0", - }, - ] - assert metrics.histogram.calls == [ - pretend.call("warehouse.xmlrpc.search.results", 2) - ] - - def test_searches_with_or(self, pyramid_request, metrics): - class FakeQuery: - def __init__(self, type, should): - self.type = type - self.should = should - - def __getitem__(self, name): - self.offset = name.start - self.limit = name.stop - self.step = name.step - return self - - def execute(self): - assert self.type == "bool" - assert [q.to_dict() for q in self.should] == [ - {"match": {"name": {"query": "foo", "boost": 10}}}, - { - "bool": { - "should": [ - {"match": {"summary": {"query": "one", "boost": 5}}}, - {"match": {"summary": {"query": "two", "boost": 5}}}, - ] - } - }, - ] - assert self.offset is None - assert self.limit == 100 - assert self.step is None - return [ - pretend.stub( - name="foo", - summary="my summary", - latest_version="1.0", - version=["1.0"], - ), - pretend.stub( - name="foo-bar", - summary="other summary", - latest_version="2.0", - version=["2.0", "1.0"], - ), - ] - - pyramid_request.es = pretend.stub(query=FakeQuery) - results = xmlrpc.search( - pyramid_request, {"name": "foo", "summary": ["one", "two"]}, "or" - ) - assert results == [ - { - "_pypi_ordering": False, - "name": "foo", - "summary": "my summary", - "version": "1.0", - }, - { - "_pypi_ordering": False, - "name": "foo-bar", - "summary": "other summary", - "version": "2.0", - }, - ] - assert metrics.histogram.calls == [ - pretend.call("warehouse.xmlrpc.search.results", 2) - ] - - def test_version_search(self, pyramid_request, metrics): - class FakeQuery: - def __init__(self, type, must): - self.type = type - self.must = must - - def __getitem__(self, name): - self.offset = name.start - self.limit = name.stop - self.step = name.step - return self - - def execute(self): - assert self.type == "bool" - assert [q.to_dict() for q in self.must] == [ - {"match": {"name": {"boost": 10, "query": "foo"}}}, - {"match": {"version": {"query": "1.0"}}}, - ] - assert self.offset is None - assert self.limit == 100 - assert self.step is None - return [ - pretend.stub( - name="foo", - summary="my summary", - latest_version="1.0", - version=["1.0"], - ), - pretend.stub( - name="foo-bar", - summary="other summary", - latest_version="2.0", - version=["2.0", "1.0"], - ), - ] - - pyramid_request.es = pretend.stub(query=FakeQuery) - results = xmlrpc.search( - pyramid_request, {"name": "foo", "version": "1.0"}, "and" + "RuntimeError: PyPI no longer supports 'pip search' (or XML-RPC search). " + f"Please use https://{domain if domain else 'example.org'}/search " + "(via a browser) instead. See " + "https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods " + "for more information." ) - assert results == [ - { - "_pypi_ordering": False, - "name": "foo", - "summary": "my summary", - "version": "1.0", - }, - { - "_pypi_ordering": False, - "name": "foo-bar", - "summary": "other summary", - "version": "1.0", - }, - ] - assert metrics.histogram.calls == [ - pretend.call("warehouse.xmlrpc.search.results", 2) - ] - - def test_version_search_returns_latest(self, pyramid_request, metrics): - class FakeQuery: - def __init__(self, type, must): - self.type = type - self.must = must - - def __getitem__(self, name): - self.offset = name.start - self.limit = name.stop - self.step = name.step - return self - - def execute(self): - assert self.type == "bool" - assert [q.to_dict() for q in self.must] == [ - {"match": {"name": {"query": "foo", "boost": 10}}} - ] - assert self.offset is None - assert self.limit == 100 - assert self.step is None - return [ - pretend.stub( - name="foo", - summary="my summary", - latest_version="1.0", - version=["1.0"], - ), - pretend.stub( - name="foo-bar", - summary="other summary", - latest_version="2.0", - version=["3.0a1", "2.0", "1.0"], - ), - ] - - pyramid_request.es = pretend.stub(query=FakeQuery) - results = xmlrpc.search(pyramid_request, {"name": "foo"}, "and") - assert results == [ - { - "_pypi_ordering": False, - "name": "foo", - "summary": "my summary", - "version": "1.0", - }, - { - "_pypi_ordering": False, - "name": "foo-bar", - "summary": "other summary", - "version": "2.0", - }, - ] - assert metrics.histogram.calls == [ - pretend.call("warehouse.xmlrpc.search.results", 2) - ] - - def test_version_search_wraps_connection_error(self, pyramid_request, metrics): - class FakeQuery: - def __init__(self, type, must): - pass - - def __getitem__(self, name): - return self - - def execute(self): - raise elasticsearch.TransportError() - - pyramid_request.es = pretend.stub(query=FakeQuery) - - with pytest.raises(xmlrpc.XMLRPCServiceUnavailable): - xmlrpc.search(pyramid_request, {"name": "foo"}, "and") - - assert metrics.increment.calls == [ - pretend.call("warehouse.xmlrpc.search.error") - ] - assert metrics.histogram.calls == [] + assert metrics.increment.calls == [] def test_list_packages(db_request): @@ -597,9 +187,10 @@ def test_top_packages(num, pyramid_request): with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc: xmlrpc.top_packages(pyramid_request, num) - assert ( - exc.value.faultString - == "RuntimeError: This API has been removed. Use BigQuery instead." + assert exc.value.faultString == ( + "RuntimeError: This API has been removed. Use BigQuery instead. " + "See https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods " + "for more information." ) @@ -613,10 +204,9 @@ def test_package_urls(domain, db_request): xmlrpc.package_urls(db_request, "foo", "1.0.0") assert exc.value.faultString == ( - "RuntimeError: This API has been deprecated. Use " - f"https://{domain if domain else 'example.org'}/foo/1.0.0/json " - "instead. The XMLRPC method release_urls can be used in the " - "interim, but will be deprecated in the future." + "RuntimeError: This API has been deprecated. " + "See https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods " + "for more information." ) @@ -630,10 +220,9 @@ def test_package_data(domain, db_request): xmlrpc.package_data(db_request, "foo", "1.0.0") assert exc.value.faultString == ( - "RuntimeError: This API has been deprecated. Use " - f"https://{domain if domain else 'example.org'}/foo/1.0.0/json " - "instead. The XMLRPC method release_data can be used in the " - "interim, but will be deprecated in the future." + "RuntimeError: This API has been deprecated. " + "See https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods " + "for more information." ) diff --git a/warehouse/legacy/api/xmlrpc/views.py b/warehouse/legacy/api/xmlrpc/views.py index c2123ea96e65..619f79731f37 100644 --- a/warehouse/legacy/api/xmlrpc/views.py +++ b/warehouse/legacy/api/xmlrpc/views.py @@ -18,10 +18,8 @@ from typing import List, Mapping, Union -import elasticsearch import typeguard -from elasticsearch_dsl import Q from packaging.utils import canonicalize_name from pyramid.httpexceptions import HTTPTooManyRequests from pyramid.view import view_config @@ -47,7 +45,6 @@ release_classifiers, ) from warehouse.rate_limiting import IRateLimiter -from warehouse.search.queries import SEARCH_BOOSTS # From https://stackoverflow.com/a/22273639 _illegal_ranges = [ @@ -77,6 +74,10 @@ ] _illegal_xml_chars_re = re.compile("[%s]" % "".join(_illegal_ranges)) +XMLRPC_DEPRECATION_URL = ( + "https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods" +) + def _clean_for_xml(data): """Sanitize any user-submitted data to ensure that it can be used in XML""" @@ -227,100 +228,14 @@ def exception_view(exc, request): @xmlrpc_method(method="search") def search(request, spec: Mapping[str, Union[str, List[str]]], operator: str = "and"): - metrics = request.find_service(IMetricsService, context=None) - - # This uses a setting instead of an admin flag to avoid hitting the DB/Elasticsearch - # at all since the broad purpose of this flag is to enable us to control the load to - # our backend servers. This does mean that turning search on or off requires a - # deploy, but it should be infrequent enough to not matter. - if not request.registry.settings.get("warehouse.xmlrpc.search.enabled", True): - metrics.increment("warehouse.xmlrpc.search.deprecated") - raise XMLRPCWrappedError( - RuntimeError( - ( - "PyPI's XMLRPC API is currently disabled due to " - "unmanageable load and will be deprecated in the near " - "future. See https://status.python.org/ for more " - "information." - ) - ) - ) - - if operator not in {"and", "or"}: - raise XMLRPCWrappedError( - ValueError("Invalid operator, must be one of 'and' or 'or'.") + domain = request.registry.settings.get("warehouse.domain", request.domain) + raise XMLRPCWrappedError( + RuntimeError( + "PyPI no longer supports 'pip search' (or XML-RPC search). " + f"Please use https://{domain}/search (via a browser) instead. " + f"See {XMLRPC_DEPRECATION_URL} for more information." ) - - # Remove any invalid spec fields - spec = { - k: [v] if isinstance(v, str) else v - for k, v in spec.items() - if v - and k - in { - "name", - "version", - "author", - "author_email", - "maintainer", - "maintainer_email", - "home_page", - "license", - "summary", - "description", - "keywords", - "platform", - "download_url", - } - } - - queries = [] - for field, value in sorted(spec.items()): - q = None - for item in value: - kw = {"query": item} - if field in SEARCH_BOOSTS: - kw["boost"] = SEARCH_BOOSTS[field] # type: ignore - if q is None: - q = Q("match", **{field: kw}) - else: - q |= Q("match", **{field: kw}) - queries.append(q) - - if operator == "and": - query = request.es.query("bool", must=queries) - else: - query = request.es.query("bool", should=queries) - - try: - results = query[:100].execute() - except elasticsearch.TransportError: - metrics.increment("warehouse.xmlrpc.search.error") - raise XMLRPCServiceUnavailable - - metrics.histogram("warehouse.xmlrpc.search.results", len(results)) - - if "version" in spec.keys(): - return [ - { - "name": r.name, - "summary": _clean_for_xml(getattr(r, "summary", None)), - "version": v, - "_pypi_ordering": False, - } - for r in results - for v in r.version - if v in spec.get("version", [v]) - ] - return [ - { - "name": r.name, - "summary": _clean_for_xml(getattr(r, "summary", None)), - "version": r.latest_version, - "_pypi_ordering": False, - } - for r in results - ] + ) @xmlrpc_cache_all_projects(method="list_packages") @@ -355,7 +270,10 @@ def user_packages(request, username: str): @xmlrpc_method(method="top_packages") def top_packages(request, num=None): raise XMLRPCWrappedError( - RuntimeError("This API has been removed. Use BigQuery instead.") + RuntimeError( + "This API has been removed. Use BigQuery instead. " + f"See {XMLRPC_DEPRECATION_URL} for more information." + ) ) @@ -385,15 +303,11 @@ def package_releases(request, package_name: str, show_hidden: bool = False): @xmlrpc_method(method="package_data") def package_data(request, package_name, version): - settings = request.registry.settings - domain = settings.get("warehouse.domain", request.domain) raise XMLRPCWrappedError( RuntimeError( ( - "This API has been deprecated. Use " - f"https://{domain}/{package_name}/{version}/json " - "instead. The XMLRPC method release_data can be used in the " - "interim, but will be deprecated in the future." + "This API has been deprecated. " + f"See {XMLRPC_DEPRECATION_URL} for more information." ) ) ) @@ -461,15 +375,11 @@ def release_data(request, package_name: str, version: str): @xmlrpc_method(method="package_urls") def package_urls(request, package_name, version): - settings = request.registry.settings - domain = settings.get("warehouse.domain", request.domain) raise XMLRPCWrappedError( RuntimeError( ( - "This API has been deprecated. Use " - f"https://{domain}/{package_name}/{version}/json " - "instead. The XMLRPC method release_urls can be used in the " - "interim, but will be deprecated in the future." + "This API has been deprecated. " + f"See {XMLRPC_DEPRECATION_URL} for more information." ) ) )