16 """Verifies that all gRPC Python artifacts have been successfully published.
18 This script is intended to be run from a directory containing the artifacts
19 that have been uploaded and only the artifacts that have been uploaded. We use
20 PyPI's JSON API to verify that the proper filenames and checksums are present.
22 Note that PyPI may take several minutes to update its metadata. Don't have a
23 heart attack immediately.
25 This sanity check is a good first step, but ideally, we would automate the
26 entire release process.
41 "grpcio-health-checking",
49 Artifact = collections.namedtuple(
"Artifact", (
"filename",
"checksum"))
53 """Calculate the md5sum for a file."""
54 hash_md5 = hashlib.md5()
55 with open(filename,
'rb')
as f:
56 for chunk
in iter(
lambda: f.read(4096), b
""):
57 hash_md5.update(chunk)
58 return hash_md5.hexdigest()
62 """Get a set of artifacts representing all files in the cwd."""
68 """Get a list of artifacts based on PyPi's json metadata.
70 Note that this data will not updated immediately after upload. In my
71 experience, it has taken a minute on average to be fresh.
74 payload = requests.get(
"https://pypi.org/pypi/{}/{}/json".
format(
75 package, version)).json()
76 for download_info
in payload[
'releases'][version]:
78 Artifact(download_info[
'filename'], download_info[
'md5_digest']))
84 for package
in packages:
90 """Compare the local artifacts to the packages uploaded to PyPI."""
93 if local_artifacts != remote_artifacts:
94 local_but_not_remote = local_artifacts - remote_artifacts
95 remote_but_not_local = remote_artifacts - local_artifacts
96 if local_but_not_remote:
97 print(
"The following artifacts exist locally but not remotely.")
98 for artifact
in local_but_not_remote:
100 if remote_but_not_local:
101 print(
"The following artifacts exist remotely but not locally.")
102 for artifact
in remote_but_not_local:
105 print(
"Release verified successfully.")
108 if __name__ ==
"__main__":
109 parser = argparse.ArgumentParser(
110 "Verify a release. Run this from a directory containing only the"
111 "artifacts to be uploaded. Note that PyPI may take several minutes"
112 "after the upload to reflect the proper metadata.")
113 parser.add_argument(
"version")
114 parser.add_argument(
"packages",
117 default=_DEFAULT_PACKAGES)
118 args = parser.parse_args()