/ dev / update_requirements.py
update_requirements.py
  1  """
  2  This script updates the `max_major_version` attribute of each package in a YAML dependencies
  3  specification (e.g. requirements/core-requirements.yaml) to the maximum available version on PyPI.
  4  """
  5  
  6  import os
  7  import re
  8  from datetime import datetime, timedelta, timezone
  9  
 10  import requests
 11  import yaml
 12  from packaging.version import InvalidVersion, Version
 13  
 14  PACKAGE_NAMES = ["tracing", "skinny", "core", "gateway"]
 15  RELEASE_CUTOFF_DAYS = 14
 16  PYPI_URL = os.environ.get("PYPI_URL", "https://pypi.org").rstrip("/")
 17  
 18  
 19  def check_pypi_accessibility() -> None:
 20      try:
 21          response = requests.head(PYPI_URL, timeout=5)
 22          response.raise_for_status()
 23      except requests.exceptions.RequestException:
 24          raise SystemExit(
 25              f"Error: Cannot connect to {PYPI_URL}. "
 26              "If it's not accessible, set the PYPI_URL environment variable to a PyPI proxy URL."
 27          )
 28  
 29  
 30  def get_latest_major_version(package_name: str) -> int | None:
 31      url = f"{PYPI_URL}/pypi/{package_name}/json"
 32      response = requests.get(url)
 33      response.raise_for_status()
 34      data = response.json()
 35      cutoff = datetime.now(tz=timezone.utc) - timedelta(days=RELEASE_CUTOFF_DAYS)
 36      versions = []
 37      for version, distributions in data["releases"].items():
 38          if len(distributions) == 0 or any(d.get("yanked", False) for d in distributions):
 39              continue
 40  
 41          upload_times = [
 42              datetime.fromisoformat(ut.replace("Z", "+00:00"))
 43              for dist in distributions
 44              if (ut := dist.get("upload_time_iso_8601"))
 45          ]
 46          release_date = min(upload_times) if upload_times else None
 47          if not release_date or release_date >= cutoff:
 48              continue
 49  
 50          try:
 51              version = Version(version)
 52          except InvalidVersion:
 53              # Ignore invalid versions such as https://pypi.org/project/pytz/2004d
 54              continue
 55  
 56          if version.is_devrelease or version.is_prerelease:
 57              continue
 58  
 59          versions.append(version)
 60  
 61      return max(versions).major if versions else None
 62  
 63  
 64  def update_max_major_version(raw: str, key: str, old_value: int, new_value: int) -> str:
 65      """
 66      Update the max_major_version value for a specific package using regex.
 67      This preserves comments and formatting exactly as they appear in the file.
 68      """
 69      # Use word boundaries to ensure we match the exact number, not a substring
 70      pattern = rf"(^{re.escape(key)}:.*?max_major_version:)\s+\b{old_value}\b"
 71      updated, count = re.subn(
 72          pattern, rf"\1 {new_value}", raw, count=1, flags=re.DOTALL | re.MULTILINE
 73      )
 74      if count == 0:
 75          raise ValueError(
 76              f"Failed to update {key}.max_major_version from {old_value} to {new_value}. "
 77              "The pattern may not match the YAML structure."
 78          )
 79      return updated
 80  
 81  
 82  def main() -> None:
 83      check_pypi_accessibility()
 84      for package_name in PACKAGE_NAMES:
 85          req_file_path = os.path.join("requirements", package_name + "-requirements.yaml")
 86          with open(req_file_path) as f:
 87              requirements_src = f.read()
 88  
 89          requirements = yaml.safe_load(requirements_src)
 90          updated_src = requirements_src
 91          changes_made = False
 92  
 93          for key, req_info in requirements.items():
 94              pip_release = req_info["pip_release"]
 95              max_major_version = req_info["max_major_version"]
 96              if req_info.get("freeze", False):
 97                  continue
 98              latest_major_version = get_latest_major_version(pip_release)
 99              if latest_major_version is None:
100                  print(f"Skipping {key}: no releases older than {RELEASE_CUTOFF_DAYS}d found")
101                  continue
102              if latest_major_version > max_major_version:
103                  updated_src = update_max_major_version(
104                      updated_src, key, max_major_version, latest_major_version
105                  )
106                  print(f"Updated {key}.max_major_version to {latest_major_version}")
107                  changes_made = True
108  
109          if changes_made:
110              with open(req_file_path, "w") as f:
111                  f.write(updated_src)
112  
113  
114  if __name__ == "__main__":
115      main()