|
6 | 6 |
|
7 | 7 | import functools
|
8 | 8 | import hashlib
|
| 9 | +import json |
9 | 10 | import os
|
10 | 11 | import sys
|
11 | 12 | import time
|
|
36 | 37 | # CUTOFF = datetime.now(tz=timezone.utc) - timedelta(days=365 * 5)
|
37 | 38 |
|
38 | 39 | TOX_FILE = Path(__file__).resolve().parent.parent.parent / "tox.ini"
|
| 40 | +RELEASES_CACHE_FILE = Path(__file__).resolve().parent / "releases.jsonl" |
39 | 41 | ENV = Environment(
|
40 | 42 | loader=FileSystemLoader(Path(__file__).resolve().parent),
|
41 | 43 | trim_blocks=True,
|
42 | 44 | lstrip_blocks=True,
|
43 | 45 | )
|
44 | 46 |
|
45 |
| -PYPI_COOLDOWN = 0.1 # seconds to wait between requests to PyPI |
| 47 | +PYPI_COOLDOWN = 0.05 # seconds to wait between requests to PyPI |
46 | 48 |
|
47 | 49 | PYPI_PROJECT_URL = "https://pypi.python.org/pypi/{project}/json"
|
48 | 50 | PYPI_VERSION_URL = "https://pypi.python.org/pypi/{project}/{version}/json"
|
49 | 51 | CLASSIFIER_PREFIX = "Programming Language :: Python :: "
|
50 | 52 |
|
| 53 | +CACHE = defaultdict(dict) |
51 | 54 |
|
52 | 55 | IGNORE = {
|
53 | 56 | # Do not try auto-generating the tox entries for these. They will be
|
@@ -96,9 +99,33 @@ def fetch_package(package: str) -> Optional[dict]:
|
96 | 99 |
|
97 | 100 | @functools.cache
|
98 | 101 | def fetch_release(package: str, version: Version) -> Optional[dict]:
|
99 |
| - """Fetch release metadata from PyPI.""" |
| 102 | + """Fetch release metadata from cache or, failing that, PyPI.""" |
| 103 | + release = _fetch_from_cache(package, version) |
| 104 | + if release is not None: |
| 105 | + return release |
| 106 | + |
100 | 107 | url = PYPI_VERSION_URL.format(project=package, version=version)
|
101 |
| - return fetch_url(url) |
| 108 | + release = fetch_url(url) |
| 109 | + if release is not None: |
| 110 | + _save_to_cache(package, version, release) |
| 111 | + return release |
| 112 | + |
| 113 | + |
| 114 | +def _fetch_from_cache(package: str, version: Version) -> Optional[dict]: |
| 115 | + package = _normalize_name(package) |
| 116 | + if package in CACHE and str(version) in CACHE[package]: |
| 117 | + CACHE[package][str(version)]["_accessed"] = True |
| 118 | + return CACHE[package][str(version)] |
| 119 | + |
| 120 | + return None |
| 121 | + |
| 122 | + |
| 123 | +def _save_to_cache(package: str, version: Version, release: Optional[dict]) -> None: |
| 124 | + with open(RELEASES_CACHE_FILE, "a") as releases_cache: |
| 125 | + releases_cache.write(json.dumps(_normalize_release(release)) + "\n") |
| 126 | + |
| 127 | + CACHE[_normalize_name(package)][str(version)] = release |
| 128 | + CACHE[_normalize_name(package)][str(version)]["_accessed"] = True |
102 | 129 |
|
103 | 130 |
|
104 | 131 | def _prefilter_releases(
|
@@ -612,6 +639,24 @@ def get_last_updated() -> Optional[datetime]:
|
612 | 639 | return timestamp
|
613 | 640 |
|
614 | 641 |
|
| 642 | +def _normalize_name(package: str) -> str: |
| 643 | + return package.lower().replace("-", "_") |
| 644 | + |
| 645 | + |
| 646 | +def _normalize_release(release: dict) -> dict: |
| 647 | + """Filter out unneeded parts of the release JSON.""" |
| 648 | + normalized = { |
| 649 | + "info": { |
| 650 | + "classifiers": release["info"]["classifiers"], |
| 651 | + "name": release["info"]["name"], |
| 652 | + "requires_python": release["info"]["requires_python"], |
| 653 | + "version": release["info"]["version"], |
| 654 | + "yanked": release["info"]["yanked"], |
| 655 | + }, |
| 656 | + } |
| 657 | + return normalized |
| 658 | + |
| 659 | + |
615 | 660 | def main(fail_on_changes: bool = False) -> None:
|
616 | 661 | """
|
617 | 662 | Generate tox.ini from the tox.jinja template.
|
@@ -649,6 +694,20 @@ def main(fail_on_changes: bool = False) -> None:
|
649 | 694 | f"The SDK supports Python versions {MIN_PYTHON_VERSION} - {MAX_PYTHON_VERSION}."
|
650 | 695 | )
|
651 | 696 |
|
| 697 | + # Load file cache |
| 698 | + global CACHE |
| 699 | + |
| 700 | + with open(RELEASES_CACHE_FILE) as releases_cache: |
| 701 | + for line in releases_cache: |
| 702 | + release = json.loads(line) |
| 703 | + name = _normalize_name(release["info"]["name"]) |
| 704 | + version = release["info"]["version"] |
| 705 | + CACHE[name][version] = release |
| 706 | + CACHE[name][version][ |
| 707 | + "_accessed" |
| 708 | + ] = False # for cleaning up unused cache entries |
| 709 | + |
| 710 | + # Process packages |
652 | 711 | packages = defaultdict(list)
|
653 | 712 |
|
654 | 713 | for group, integrations in GROUPS.items():
|
@@ -714,6 +773,21 @@ def main(fail_on_changes: bool = False) -> None:
|
714 | 773 | packages, update_timestamp=not fail_on_changes, last_updated=last_updated
|
715 | 774 | )
|
716 | 775 |
|
| 776 | + # Sort the release cache file |
| 777 | + releases = [] |
| 778 | + with open(RELEASES_CACHE_FILE) as releases_cache: |
| 779 | + releases = [json.loads(line) for line in releases_cache] |
| 780 | + releases.sort(key=lambda r: (r["info"]["name"], r["info"]["version"])) |
| 781 | + with open(RELEASES_CACHE_FILE, "w") as releases_cache: |
| 782 | + for release in releases: |
| 783 | + if ( |
| 784 | + CACHE[_normalize_name(release["info"]["name"])][ |
| 785 | + release["info"]["version"] |
| 786 | + ]["_accessed"] |
| 787 | + is True |
| 788 | + ): |
| 789 | + releases_cache.write(json.dumps(release) + "\n") |
| 790 | + |
717 | 791 | if fail_on_changes:
|
718 | 792 | new_file_hash = get_file_hash()
|
719 | 793 | if old_file_hash != new_file_hash:
|
|
0 commit comments