|
#!/usr/bin/env python3 |
|
""" |
|
Sage Version Analyzer - Compare package versions across Sage installations |
|
|
|
Usage: python3 analyze_sage.py <path_to_sage_10_7> <path_to_sage_10_8> |
|
""" |
|
|
|
import os |
|
import json |
|
import sys |
|
from collections import defaultdict |
|
|
|
|
|
def get_packages(site_packages_path): |
|
"""Extract package versions from dist-info METADATA files""" |
|
packages = {} |
|
|
|
if not os.path.exists(site_packages_path): |
|
return packages |
|
|
|
for dist_info in os.listdir(site_packages_path): |
|
if dist_info.endswith('.dist-info'): |
|
pkg_name = dist_info.replace('.dist-info', '') |
|
metadata_file = os.path.join(site_packages_path, dist_info, 'METADATA') |
|
|
|
if os.path.exists(metadata_file): |
|
with open(metadata_file) as f: |
|
for line in f: |
|
if line.startswith('Version:'): |
|
version = line.split(':', 1)[1].strip() |
|
packages[pkg_name] = version |
|
break |
|
|
|
return packages |
|
|
|
|
|
def categorize(packages): |
|
"""Categorize packages by type""" |
|
categories = { |
|
'Security': ['pwntools', 'paramiko', 'capstone', 'ropgadget', 'pyelftools', 'unicorn', 'rpyc', 'cryptography', 'bcrypt', 'pycryptodome', 'pynacl', 'pysocks'], |
|
'Optimization': ['z3', 'ortools', 'igraph', 'cuso', 'flatn', 'gf2bv', 'lll_cvp', 'pycryptosat'], |
|
'Data': ['pandas', 'tqdm', 'rich', 'markdown'], |
|
'Tools': ['mako', 'invoke', 'plumbum', 'protobuf', 'zstandard', 'pyserial'], |
|
} |
|
|
|
categorized = defaultdict(dict) |
|
|
|
for pkg, version in packages.items(): |
|
found = False |
|
for cat, keywords in categories.items(): |
|
if any(kw.lower() in pkg.lower() for kw in keywords): |
|
categorized[cat][pkg] = version |
|
found = True |
|
break |
|
if not found: |
|
categorized['Other'][pkg] = version |
|
|
|
return categorized |
|
|
|
|
|
def main(): |
|
if len(sys.argv) != 3: |
|
print("Usage: python3 analyze_sage.py <path_10_7> <path_10_8>") |
|
sys.exit(1) |
|
|
|
path_10_7 = sys.argv[1] |
|
path_10_8 = sys.argv[2] |
|
|
|
if not os.path.isdir(path_10_7) or not os.path.isdir(path_10_8): |
|
print("Error: Invalid paths", file=sys.stderr) |
|
sys.exit(1) |
|
|
|
print("Analyzing Sage versions...", file=sys.stderr) |
|
|
|
pkgs_10_7 = get_packages(path_10_7) |
|
pkgs_10_8 = get_packages(path_10_8) |
|
|
|
# Find packages only in 10.7 |
|
only_10_7 = {} |
|
for pkg, version in pkgs_10_7.items(): |
|
if not any(pkg.split('-')[0].lower() == p.split('-')[0].lower() for p in pkgs_10_8): |
|
only_10_7[pkg] = version |
|
|
|
# Build result |
|
result = { |
|
'Sage_10.7_total': len(pkgs_10_7), |
|
'Sage_10.8_total': len(pkgs_10_8), |
|
'only_in_10.7_count': len(only_10_7), |
|
'growth_rate': f"+{((len(pkgs_10_8) - len(pkgs_10_7)) / len(pkgs_10_7) * 100):.1f}%", |
|
'packages_only_in_10.7': only_10_7, |
|
'categorized': dict(categorize(only_10_7)) |
|
} |
|
|
|
# Print summary to stderr |
|
print(f"\n{'='*60}", file=sys.stderr) |
|
print(f"Sage 10.7: {len(pkgs_10_7)} packages", file=sys.stderr) |
|
print(f"Sage 10.8: {len(pkgs_10_8)} packages", file=sys.stderr) |
|
print(f"Only in 10.7: {len(only_10_7)} packages", file=sys.stderr) |
|
print(f"Growth: {result['growth_rate']}", file=sys.stderr) |
|
print(f"\nTop packages removed:\n", file=sys.stderr) |
|
|
|
for pkg, version in sorted(only_10_7.items())[:10]: |
|
print(f" β’ {pkg} ({version})", file=sys.stderr) |
|
|
|
print(f"{'='*60}\n", file=sys.stderr) |
|
|
|
# Output JSON to stdout |
|
print(json.dumps(result, indent=2, ensure_ascii=False)) |
|
|
|
|
|
if __name__ == '__main__': |
|
main() |