show_package_release_dates.py
1 import asyncio 2 import json 3 import subprocess 4 import sys 5 import traceback 6 from collections.abc import Sequence 7 8 import aiohttp 9 from pydantic import BaseModel 10 11 12 class ReleaseFile(BaseModel): 13 upload_time: str 14 15 16 class PyPIResponse(BaseModel): 17 releases: dict[str, list[ReleaseFile]] 18 19 20 def get_distributions() -> list[tuple[str, str]]: 21 res = subprocess.check_output([sys.executable, "-m", "pip", "list", "--format", "json"]) 22 return [(pkg["name"], pkg["version"]) for pkg in json.loads(res)] 23 24 25 def extract_upload_time(response: PyPIResponse, version: str) -> str | None: 26 for f in response.releases.get(version, []): 27 return f.upload_time.replace("T", " ") 28 return None 29 30 31 async def get_release_date( 32 session: aiohttp.ClientSession, package: str, version: str 33 ) -> str | None: 34 try: 35 async with session.get(f"https://pypi.python.org/pypi/{package}/json", timeout=10) as resp: 36 resp.raise_for_status() 37 resp_json = await resp.json() 38 response = PyPIResponse.model_validate(resp_json) 39 return extract_upload_time(response, version) 40 41 except Exception: 42 traceback.print_exc() 43 return None 44 45 46 def get_longest_string_length(array: Sequence[str]) -> int: 47 return len(max(array, key=len)) 48 49 50 async def main() -> None: 51 distributions = get_distributions() 52 async with aiohttp.ClientSession() as session: 53 tasks = [get_release_date(session, pkg, ver) for pkg, ver in distributions] 54 results = await asyncio.gather(*tasks) 55 56 release_dates = [r or "" for r in results] 57 packages, versions = list(zip(*distributions)) 58 package_length = get_longest_string_length(packages) 59 version_length = get_longest_string_length(versions) 60 release_timestamp_length = get_longest_string_length(release_dates) 61 print( 62 "Package".ljust(package_length), 63 "Version".ljust(version_length), 64 "Release Timestamp".ljust(release_timestamp_length), 65 ) 66 print("-" * (package_length + version_length + release_timestamp_length + 2)) 67 for package, version, release_date in sorted( 68 zip(packages, versions, release_dates), 69 # Sort by release date in descending order 70 key=lambda x: x[2], 71 reverse=True, 72 ): 73 print( 74 package.ljust(package_length), 75 version.ljust(version_length), 76 release_date.ljust(release_timestamp_length), 77 ) 78 79 80 if __name__ == "__main__": 81 asyncio.run(main())