xref: /aosp_15_r20/external/grpc-grpc/tools/release/verify_python_release.py (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1#!/usr/bin/env python3
2
3# Copyright 2019 gRPC authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Verifies that all gRPC Python artifacts have been successfully published.
17
18This script is intended to be run from a directory containing the artifacts
19that have been uploaded and only the artifacts that have been uploaded. We use
20PyPI's JSON API to verify that the proper filenames and checksums are present.
21
22Note that PyPI may take several minutes to update its metadata. Don't have a
23heart attack immediately.
24
25This sanity check is a good first step, but ideally, we would automate the
26entire release process.
27"""
28
29import argparse
30import collections
31import hashlib
32import os
33import sys
34
35import requests
36
37_DEFAULT_PACKAGES = [
38    "grpcio",
39    "grpcio-tools",
40    "grpcio-status",
41    "grpcio-health-checking",
42    "grpcio-reflection",
43    "grpcio-channelz",
44    "grpcio-testing",
45    "grpcio-admin",
46    "grpcio-csds",
47    "xds-protos",
48]
49
50Artifact = collections.namedtuple("Artifact", ("filename", "checksum"))
51
52
53def _get_md5_checksum(filename):
54    """Calculate the md5sum for a file."""
55    hash_md5 = hashlib.md5()
56    with open(filename, "rb") as f:
57        for chunk in iter(lambda: f.read(4096), b""):
58            hash_md5.update(chunk)
59        return hash_md5.hexdigest()
60
61
62def _get_local_artifacts():
63    """Get a set of artifacts representing all files in the cwd."""
64    return set(
65        Artifact(f, _get_md5_checksum(f)) for f in os.listdir(os.getcwd())
66    )
67
68
69def _get_remote_artifacts_for_package(package, version):
70    """Get a list of artifacts based on PyPi's json metadata.
71
72    Note that this data will not updated immediately after upload. In my
73    experience, it has taken a minute on average to be fresh.
74    """
75    artifacts = set()
76    payload_resp = requests.get(
77        "https://pypi.org/pypi/{}/{}/json".format(package, version)
78    )
79    payload_resp.raise_for_status()
80    payload = payload_resp.json()
81    for download_info in payload["urls"]:
82        artifacts.add(
83            Artifact(download_info["filename"], download_info["md5_digest"])
84        )
85    return artifacts
86
87
88def _get_remote_artifacts_for_packages(packages, version):
89    artifacts = set()
90    for package in packages:
91        artifacts |= _get_remote_artifacts_for_package(package, version)
92    return artifacts
93
94
95def _verify_release(version, packages):
96    """Compare the local artifacts to the packages uploaded to PyPI."""
97    local_artifacts = _get_local_artifacts()
98    remote_artifacts = _get_remote_artifacts_for_packages(packages, version)
99    if local_artifacts != remote_artifacts:
100        local_but_not_remote = local_artifacts - remote_artifacts
101        remote_but_not_local = remote_artifacts - local_artifacts
102        if local_but_not_remote:
103            print("The following artifacts exist locally but not remotely.")
104            for artifact in local_but_not_remote:
105                print(artifact)
106        if remote_but_not_local:
107            print("The following artifacts exist remotely but not locally.")
108            for artifact in remote_but_not_local:
109                print(artifact)
110        sys.exit(1)
111    print("Release verified successfully.")
112
113
114if __name__ == "__main__":
115    parser = argparse.ArgumentParser(
116        "Verify a release. Run this from a directory containing only the"
117        "artifacts to be uploaded. Note that PyPI may take several minutes"
118        "after the upload to reflect the proper metadata."
119    )
120    parser.add_argument("version")
121    parser.add_argument(
122        "packages", nargs="*", type=str, default=_DEFAULT_PACKAGES
123    )
124    args = parser.parse_args()
125    _verify_release(args.version, args.packages)
126