From 5ad264e9b79aadf8cd19244d8e354d45189076c4 Mon Sep 17 00:00:00 2001 From: Ee Durbin Date: Sun, 8 Sep 2024 09:17:17 -0400 Subject: [PATCH] finalize deprecation of xmlrpc list_packages, package_releases, release_urls, and release_data Also re-organizes the methods in the file to match the docs --- tests/unit/legacy/api/xmlrpc/test_xmlrpc.py | 170 ++-------- warehouse/legacy/api/xmlrpc/views.py | 347 ++++++++------------ 2 files changed, 172 insertions(+), 345 deletions(-) diff --git a/tests/unit/legacy/api/xmlrpc/test_xmlrpc.py b/tests/unit/legacy/api/xmlrpc/test_xmlrpc.py index 24dccd70ce05..8261029fd373 100644 --- a/tests/unit/legacy/api/xmlrpc/test_xmlrpc.py +++ b/tests/unit/legacy/api/xmlrpc/test_xmlrpc.py @@ -24,7 +24,6 @@ from .....common.db.accounts import UserFactory from .....common.db.packaging import ( - FileFactory, JournalEntryFactory, ProjectFactory, ReleaseFactory, @@ -137,9 +136,16 @@ def test_error(self, pyramid_request, metrics, monkeypatch, domain): assert metrics.increment.calls == [] -def test_list_packages(db_request): - projects = ProjectFactory.create_batch(10) - assert set(xmlrpc.list_packages(db_request)) == {p.name for p in projects} +def test_list_packages(pyramid_request): + with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc: + xmlrpc.list_packages(pyramid_request) + + assert exc.value.faultString == ( + "RuntimeError: PyPI no longer supports the XMLRPC list_packages method. " + "Use Simple API instead. " + "See https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods " + "for more information." + ) def test_list_packages_with_serial(db_request): @@ -154,15 +160,6 @@ def test_list_packages_with_serial(db_request): assert xmlrpc.list_packages_with_serial(db_request) == expected -def test_package_hosting_mode_shows_none(db_request): - assert xmlrpc.package_hosting_mode(db_request, "nope") == "pypi-only" - - -def test_package_hosting_mode_results(db_request): - project = ProjectFactory.create() - assert xmlrpc.package_hosting_mode(db_request, project.name) == "pypi-only" - - def test_user_packages(db_request): user = UserFactory.create() other_user = UserFactory.create() @@ -229,136 +226,41 @@ def test_package_data(domain, db_request): ) -def test_package_releases(db_request): - project1 = ProjectFactory.create() - releases1 = ReleaseFactory.create_batch(10, project=project1) - project2 = ProjectFactory.create() - ReleaseFactory.create_batch(10, project=project2) - result = xmlrpc.package_releases(db_request, project1.name, show_hidden=False) - assert ( - result - == [ - r.version - for r in reversed(sorted(releases1, key=lambda x: x._pypi_ordering)) - ][:1] +def test_package_releases(pyramid_request): + with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc: + xmlrpc.package_releases(pyramid_request, "project") + + assert exc.value.faultString == ( + "RuntimeError: PyPI no longer supports the XMLRPC package_releases method. " + "Use JSON or Simple API instead. " + "See https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods " + "for more information." ) -def test_package_releases_hidden(db_request): - project1 = ProjectFactory.create() - releases1 = ReleaseFactory.create_batch(10, project=project1) - project2 = ProjectFactory.create() - ReleaseFactory.create_batch(10, project=project2) - result = xmlrpc.package_releases(db_request, project1.name, show_hidden=True) - assert result == [ - r.version for r in reversed(sorted(releases1, key=lambda x: x._pypi_ordering)) - ] +def test_release_data(pyramid_request): + with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc: + xmlrpc.release_data(pyramid_request, "project", "version") + assert exc.value.faultString == ( + "RuntimeError: PyPI no longer supports the XMLRPC release_data method. " + "Use JSON or Simple API instead. " + "See https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods " + "for more information." + ) -def test_package_releases_no_project(db_request): - result = xmlrpc.package_releases(db_request, "foo") - assert result == [] - - -def test_package_releases_no_releases(db_request): - project = ProjectFactory.create() - result = xmlrpc.package_releases(db_request, project.name) - assert result == [] - - -def test_release_data_no_project(db_request): - assert xmlrpc.release_data(db_request, "foo", "1.0") == {} - - -def test_release_data_no_release(db_request): - project = ProjectFactory.create() - assert xmlrpc.release_data(db_request, project.name, "1.0") == {} - - -def test_release_data(db_request): - project = ProjectFactory.create() - release = ReleaseFactory.create(project=project) - - urls = [pretend.stub(), pretend.stub()] - urls_iter = iter(urls) - db_request.route_url = pretend.call_recorder(lambda r, **kw: next(urls_iter)) - - assert xmlrpc.release_data(db_request, project.name, release.version) == { - "name": release.project.name, - "version": release.version, - "stable_version": None, - "bugtrack_url": None, - "package_url": urls[0], - "release_url": urls[1], - "docs_url": release.project.documentation_url, - "home_page": release.home_page, - "download_url": release.download_url, - "project_url": list(release.project_urls), - "author": release.author, - "author_email": release.author_email, - "maintainer": release.maintainer, - "maintainer_email": release.maintainer_email, - "summary": release.summary, - "description": release.description.raw, - "license": release.license, - "keywords": release.keywords, - "platform": release.platform, - "classifiers": list(release.classifiers), - "requires": list(release.requires), - "requires_dist": list(release.requires_dist), - "provides": list(release.provides), - "provides_dist": list(release.provides_dist), - "obsoletes": list(release.obsoletes), - "obsoletes_dist": list(release.obsoletes_dist), - "requires_python": release.requires_python, - "requires_external": list(release.requires_external), - "_pypi_ordering": release._pypi_ordering, - "downloads": {"last_day": -1, "last_week": -1, "last_month": -1}, - "cheesecake_code_kwalitee_id": None, - "cheesecake_documentation_id": None, - "cheesecake_installability_id": None, - } - assert db_request.route_url.calls == [ - pretend.call("packaging.project", name=project.name), - pretend.call("packaging.release", name=project.name, version=release.version), - ] +def test_release_urls(pyramid_request): + with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc: + xmlrpc.release_urls(pyramid_request, "project", "version") -def test_release_urls(db_request): - project = ProjectFactory.create() - release = ReleaseFactory.create(project=project) - file_ = FileFactory.create( - release=release, - filename=f"{project.name}-{release.version}.tar.gz", - python_version="source", + assert exc.value.faultString == ( + "RuntimeError: PyPI no longer supports the XMLRPC release_urls method. " + "Use JSON or Simple API instead. " + "See https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods " + "for more information." ) - urls = [pretend.stub()] - urls_iter = iter(urls) - db_request.route_url = pretend.call_recorder(lambda r, **kw: next(urls_iter)) - - assert xmlrpc.release_urls(db_request, project.name, release.version) == [ - { - "filename": file_.filename, - "packagetype": file_.packagetype, - "python_version": file_.python_version, - "size": file_.size, - "md5_digest": file_.md5_digest, - "sha256_digest": file_.sha256_digest, - "digests": {"md5": file_.md5_digest, "sha256": file_.sha256_digest}, - "has_sig": False, - "upload_time": file_.upload_time.isoformat() + "Z", - "upload_time_iso_8601": file_.upload_time.isoformat() + "Z", - "comment_text": file_.comment_text, - "downloads": -1, - "path": file_.path, - "url": urls[0], - } - ] - assert db_request.route_url.calls == [ - pretend.call("packaging.file", path=file_.path) - ] - def test_package_roles(db_request): project1, project2 = ProjectFactory.create_batch(2) diff --git a/warehouse/legacy/api/xmlrpc/views.py b/warehouse/legacy/api/xmlrpc/views.py index 5e0aa126f428..1435caba9608 100644 --- a/warehouse/legacy/api/xmlrpc/views.py +++ b/warehouse/legacy/api/xmlrpc/views.py @@ -31,14 +31,12 @@ exception_view as _exception_view, xmlrpc_method as _xmlrpc_method, ) -from sqlalchemy import func, orm, select -from sqlalchemy.exc import NoResultFound +from sqlalchemy import func, select from warehouse.accounts.models import User from warehouse.classifiers.models import Classifier from warehouse.metrics import IMetricsService from warehouse.packaging.models import ( - File, JournalEntry, Project, Release, @@ -242,26 +240,33 @@ def exception_view(exc, request): return _exception_view(exc, request) -@xmlrpc_method(method="search") -def search( - request, - spec: Mapping[StrictStr, StrictStr | list[StrictStr]], - operator: StrictStr = "and", -): - 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." - ) - ) +# Mirroring Support -@xmlrpc_cache_all_projects(method="list_packages") -def list_packages(request): - names = request.db.query(Project.name).all() - return [n[0] for n in names] +@xmlrpc_method(method="changelog_last_serial") +def changelog_last_serial(request): + return request.db.query(func.max(JournalEntry.id)).scalar() + + +@xmlrpc_method(method="changelog_since_serial") +def changelog_since_serial(request, serial: StrictInt): + entries = ( + request.db.query(JournalEntry) + .filter(JournalEntry.id > serial) + .order_by(JournalEntry.id) + .limit(50000) + ) + + return [ + ( + e.name, + e.version, + int(e.submitted_date.replace(tzinfo=datetime.UTC).timestamp()), + _clean_for_xml(e.action), + e.id, + ) + for e in entries + ] @xmlrpc_cache_all_projects(method="list_packages_with_serial") @@ -270,9 +275,20 @@ def list_packages_with_serial(request): return {serial[0]: serial[1] for serial in serials} -@xmlrpc_method(method="package_hosting_mode") -def package_hosting_mode(request, package_name: StrictStr): - return "pypi-only" +# Package querying methods + + +@xmlrpc_cache_by_project(method="package_roles") +def package_roles(request, package_name: StrictStr): + roles = ( + request.db.query(Role) + .join(User) + .join(Project) + .filter(Project.normalized_name == func.normalize_pep426_name(package_name)) + .order_by(Role.role_name.desc(), User.username) + .all() + ) + return [(r.role_name, r.user.username) for r in roles] @xmlrpc_method(method="user_packages") @@ -288,38 +304,57 @@ def user_packages(request, username: StrictStr): return [(r.role_name, r.project.name) for r in roles] -@xmlrpc_method(method="top_packages") -def top_packages(request, num: StrictInt | None = None): +@xmlrpc_method(method="browse") +def browse(request, classifiers: list[StrictStr]): + classifiers_q = ( + request.db.query(Classifier) + .filter(Classifier.classifier.in_(classifiers)) + .subquery() + ) + + release_classifiers_q = ( + select(ReleaseClassifiers) + .where(ReleaseClassifiers.trove_id == classifiers_q.c.id) + .alias("rc") + ) + + releases = ( + request.db.query(Project.name, Release.version) + .join(Release) + .join(release_classifiers_q, Release.id == release_classifiers_q.c.release_id) + .group_by(Project.name, Release.version) + .having(func.count() == len(classifiers)) + .order_by(Project.name, Release.version) + .all() + ) + + return [(r.name, r.version) for r in releases] + + +# Synthetic methods + + +@xmlrpc_method(method="system.multicall") +def multicall(request, args): raise XMLRPCWrappedError( - RuntimeError( - "This API has been removed. Use BigQuery instead. " - f"See {XMLRPC_DEPRECATION_URL} for more information." + ValueError( + "MultiCall requests have been deprecated, use individual " + "requests instead." ) ) -@xmlrpc_cache_by_project(method="package_releases") -def package_releases(request, package_name: StrictStr, show_hidden: StrictBool = False): - try: - project = ( - request.db.query(Project) - .filter(Project.normalized_name == func.normalize_pep426_name(package_name)) - .one() +# Deprecated Methods + + +@xmlrpc_method(method="changelog") +def changelog(request, since: StrictInt, with_ids: StrictBool = False): + raise XMLRPCWrappedError( + ValueError( + "The changelog method has been deprecated, use changelog_since_serial " + "instead." ) - except NoResultFound: - return [] - - # This used to support the show_hidden parameter to determine if it should - # show hidden releases or not. However, Warehouse doesn't support the - # concept of hidden releases, so this parameter controls if the latest - # version or all_versions are returned. - if show_hidden: - return [v.version for v in project.all_versions] - else: - latest_version = project.latest_version - if latest_version is None: - return [] - return [latest_version.version] + ) @xmlrpc_method(method="package_data") @@ -332,66 +367,6 @@ def package_data(request, package_name, version): ) -@xmlrpc_cache_by_project(method="release_data") -def release_data(request, package_name: StrictStr, version: StrictStr): - try: - release = ( - request.db.query(Release) - .options(orm.joinedload(Release.description)) - .join(Project) - .filter( - (Project.normalized_name == func.normalize_pep426_name(package_name)) - & (Release.version == version) - ) - .one() - ) - except NoResultFound: - return {} - - return { - "name": release.project.name, - "version": release.version, - "stable_version": None, - "bugtrack_url": None, - "package_url": request.route_url( - "packaging.project", name=release.project.name - ), - "release_url": request.route_url( - "packaging.release", name=release.project.name, version=release.version - ), - "docs_url": _clean_for_xml(release.project.documentation_url), - "home_page": _clean_for_xml(release.home_page), - "download_url": _clean_for_xml(release.download_url), - "project_url": [ - _clean_for_xml(f"{label}, {url}") - for label, url in release.project_urls.items() - ], - "author": _clean_for_xml(release.author), - "author_email": _clean_for_xml(release.author_email), - "maintainer": _clean_for_xml(release.maintainer), - "maintainer_email": _clean_for_xml(release.maintainer_email), - "summary": _clean_for_xml(release.summary), - "description": _clean_for_xml(release.description.raw), - "license": _clean_for_xml(release.license), - "keywords": _clean_for_xml(release.keywords), - "platform": release.platform, - "classifiers": list(release.classifiers), - "requires": list(release.requires), - "requires_dist": list(release.requires_dist), - "provides": list(release.provides), - "provides_dist": list(release.provides_dist), - "obsoletes": list(release.obsoletes), - "obsoletes_dist": list(release.obsoletes_dist), - "requires_python": release.requires_python, - "requires_external": list(release.requires_external), - "_pypi_ordering": release._pypi_ordering, - "downloads": {"last_day": -1, "last_week": -1, "last_month": -1}, - "cheesecake_code_kwalitee_id": None, - "cheesecake_documentation_id": None, - "cheesecake_installability_id": None, - } - - @xmlrpc_method(method="package_urls") def package_urls(request, package_name, version): raise XMLRPCWrappedError( @@ -402,125 +377,75 @@ def package_urls(request, package_name, version): ) -@xmlrpc_cache_by_project(method="release_urls") -def release_urls(request, package_name: StrictStr, version: StrictStr): - files = ( - request.db.query(File) - .join(Release) - .join(Project) - .filter( - (Project.normalized_name == func.normalize_pep426_name(package_name)) - & (Release.version == version) +@xmlrpc_method(method="top_packages") +def top_packages(request, num: StrictInt | None = None): + raise XMLRPCWrappedError( + RuntimeError( + "This API has been removed. Use BigQuery instead. " + f"See {XMLRPC_DEPRECATION_URL} for more information." ) - .all() ) - return [ - { - "filename": f.filename, - "packagetype": f.packagetype.value, - "python_version": f.python_version, - "size": f.size, - "md5_digest": f.md5_digest, - "sha256_digest": f.sha256_digest, - "digests": {"md5": f.md5_digest, "sha256": f.sha256_digest}, - # TODO: Remove this once we've had a long enough time with it - # here to consider it no longer in use. - "has_sig": False, - "upload_time": f.upload_time.isoformat() + "Z", - "upload_time_iso_8601": f.upload_time.isoformat() + "Z", - "comment_text": f.comment_text, - # TODO: Remove this once we've had a long enough time with it - # here to consider it no longer in use. - "downloads": -1, - "path": f.path, - "url": request.route_url("packaging.file", path=f.path), - } - for f in files - ] - -@xmlrpc_cache_by_project(method="package_roles") -def package_roles(request, package_name: StrictStr): - roles = ( - request.db.query(Role) - .join(User) - .join(Project) - .filter(Project.normalized_name == func.normalize_pep426_name(package_name)) - .order_by(Role.role_name.desc(), User.username) - .all() - ) - return [(r.role_name, r.user.username) for r in roles] - - -@xmlrpc_method(method="changelog_last_serial") -def changelog_last_serial(request): - return request.db.query(func.max(JournalEntry.id)).scalar() - - -@xmlrpc_method(method="changelog_since_serial") -def changelog_since_serial(request, serial: StrictInt): - entries = ( - request.db.query(JournalEntry) - .filter(JournalEntry.id > serial) - .order_by(JournalEntry.id) - .limit(50000) - ) - - return [ - ( - e.name, - e.version, - int(e.submitted_date.replace(tzinfo=datetime.UTC).timestamp()), - _clean_for_xml(e.action), - e.id, +@xmlrpc_method(method="search") +def search( + request, + spec: Mapping[StrictStr, StrictStr | list[StrictStr]], + operator: StrictStr = "and", +): + 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." ) - for e in entries - ] + ) -@xmlrpc_method(method="changelog") -def changelog(request, since: StrictInt, with_ids: StrictBool = False): +@xmlrpc_method(method="list_packages") +def list_packages(request): raise XMLRPCWrappedError( - ValueError( - "The changelog method has been deprecated, use changelog_since_serial " - "instead." + RuntimeError( + "PyPI no longer supports the XMLRPC list_packages method. " + "Use Simple API instead. " + f"See {XMLRPC_DEPRECATION_URL} " + "for more information." ) ) -@xmlrpc_method(method="browse") -def browse(request, classifiers: list[StrictStr]): - classifiers_q = ( - request.db.query(Classifier) - .filter(Classifier.classifier.in_(classifiers)) - .subquery() +@xmlrpc_method(method="package_releases") +def package_releases(request, package_name: StrictStr, show_hidden: StrictBool = False): + raise XMLRPCWrappedError( + RuntimeError( + "PyPI no longer supports the XMLRPC package_releases method. " + "Use JSON or Simple API instead. " + f"See {XMLRPC_DEPRECATION_URL} " + "for more information." + ) ) - release_classifiers_q = ( - select(ReleaseClassifiers) - .where(ReleaseClassifiers.trove_id == classifiers_q.c.id) - .alias("rc") - ) - releases = ( - request.db.query(Project.name, Release.version) - .join(Release) - .join(release_classifiers_q, Release.id == release_classifiers_q.c.release_id) - .group_by(Project.name, Release.version) - .having(func.count() == len(classifiers)) - .order_by(Project.name, Release.version) - .all() +@xmlrpc_method(method="release_urls") +def release_urls(request, package_name: StrictStr, version: StrictStr): + raise XMLRPCWrappedError( + RuntimeError( + "PyPI no longer supports the XMLRPC release_urls method. " + "Use JSON or Simple API instead. " + f"See {XMLRPC_DEPRECATION_URL} " + "for more information." + ) ) - return [(r.name, r.version) for r in releases] - -@xmlrpc_method(method="system.multicall") -def multicall(request, args): +@xmlrpc_method(method="release_data") +def release_data(request, package_name: StrictStr, version: StrictStr): raise XMLRPCWrappedError( - ValueError( - "MultiCall requests have been deprecated, use individual " - "requests instead." + RuntimeError( + "PyPI no longer supports the XMLRPC release_data method. " + "Use JSON or Simple API instead. " + f"See {XMLRPC_DEPRECATION_URL} " + "for more information." ) )