| from collections import defaultdict |
| import sys |
| from typing import Optional |
|
|
| if sys.version_info >= (3, 11): |
| import tomllib |
| else: |
| |
| import tomli as tomllib |
|
|
| from packaging.requirements import Requirement |
| from packaging.specifiers import SpecifierSet |
| from packaging.version import Version |
|
|
|
|
| import requests |
| from packaging.version import parse |
| from typing import List |
|
|
| import re |
|
|
|
|
| MIN_VERSION_LIBS = [ |
| "langchain-core", |
| "langchain-community", |
| "langchain", |
| "langchain-text-splitters", |
| "numpy", |
| "SQLAlchemy", |
| ] |
|
|
| |
| |
| SKIP_IF_PULL_REQUEST = [ |
| "langchain-core", |
| "langchain-text-splitters", |
| "langchain", |
| "langchain-community", |
| ] |
|
|
|
|
| def get_pypi_versions(package_name: str) -> List[str]: |
| """ |
| Fetch all available versions for a package from PyPI. |
| |
| Args: |
| package_name (str): Name of the package |
| |
| Returns: |
| List[str]: List of all available versions |
| |
| Raises: |
| requests.exceptions.RequestException: If PyPI API request fails |
| KeyError: If package not found or response format unexpected |
| """ |
| pypi_url = f"https://pypi.org/pypi/{package_name}/json" |
| response = requests.get(pypi_url) |
| response.raise_for_status() |
| return list(response.json()["releases"].keys()) |
|
|
|
|
| def get_minimum_version(package_name: str, spec_string: str) -> Optional[str]: |
| """ |
| Find the minimum published version that satisfies the given constraints. |
| |
| Args: |
| package_name (str): Name of the package |
| spec_string (str): Version specification string (e.g., ">=0.2.43,<0.4.0,!=0.3.0") |
| |
| Returns: |
| Optional[str]: Minimum compatible version or None if no compatible version found |
| """ |
| |
| spec_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", spec_string) |
| |
| for y in range(1, 10): |
| spec_string = re.sub(rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}", spec_string) |
| |
| for x in range(1, 10): |
| spec_string = re.sub( |
| rf"\^{x}\.(\d+)\.(\d+)", rf">={x}.\1.\2,<{x+1}", spec_string |
| ) |
|
|
| spec_set = SpecifierSet(spec_string) |
| all_versions = get_pypi_versions(package_name) |
|
|
| valid_versions = [] |
| for version_str in all_versions: |
| try: |
| version = parse(version_str) |
| if spec_set.contains(version): |
| valid_versions.append(version) |
| except ValueError: |
| continue |
|
|
| return str(min(valid_versions)) if valid_versions else None |
|
|
|
|
| def _check_python_version_from_requirement( |
| requirement: Requirement, python_version: str |
| ) -> bool: |
| if not requirement.marker: |
| return True |
| else: |
| marker_str = str(requirement.marker) |
| if "python_version" or "python_full_version" in marker_str: |
| python_version_str = "".join( |
| char |
| for char in marker_str |
| if char.isdigit() or char in (".", "<", ">", "=", ",") |
| ) |
| return check_python_version(python_version, python_version_str) |
| return True |
|
|
|
|
| def get_min_version_from_toml( |
| toml_path: str, |
| versions_for: str, |
| python_version: str, |
| *, |
| include: Optional[list] = None, |
| ): |
| |
| with open(toml_path, "rb") as file: |
| toml_data = tomllib.load(file) |
|
|
| dependencies = defaultdict(list) |
| for dep in toml_data["project"]["dependencies"]: |
| requirement = Requirement(dep) |
| dependencies[requirement.name].append(requirement) |
|
|
| |
| min_versions = {} |
|
|
| |
| for lib in set(MIN_VERSION_LIBS + (include or [])): |
| if versions_for == "pull_request" and lib in SKIP_IF_PULL_REQUEST: |
| |
| |
| continue |
| |
| if lib in dependencies: |
| if include and lib not in include: |
| continue |
| requirements = dependencies[lib] |
| for requirement in requirements: |
| if _check_python_version_from_requirement(requirement, python_version): |
| version_string = str(requirement.specifier) |
| break |
|
|
| |
| min_version = get_minimum_version(lib, version_string) |
|
|
| |
| min_versions[lib] = min_version |
|
|
| return min_versions |
|
|
|
|
| def check_python_version(version_string, constraint_string): |
| """ |
| Check if the given Python version matches the given constraints. |
| |
| :param version_string: A string representing the Python version (e.g. "3.8.5"). |
| :param constraint_string: A string representing the package's Python version constraints (e.g. ">=3.6, <4.0"). |
| :return: True if the version matches the constraints, False otherwise. |
| """ |
|
|
| |
| constraint_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", constraint_string) |
| |
| for y in range(1, 10): |
| constraint_string = re.sub( |
| rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}.0", constraint_string |
| ) |
| |
| for x in range(1, 10): |
| constraint_string = re.sub( |
| rf"\^{x}\.0\.(\d+)", rf">={x}.0.\1,<{x+1}.0.0", constraint_string |
| ) |
|
|
| try: |
| version = Version(version_string) |
| constraints = SpecifierSet(constraint_string) |
| return version in constraints |
| except Exception as e: |
| print(f"Error: {e}") |
| return False |
|
|
|
|
| if __name__ == "__main__": |
| |
| toml_file = sys.argv[1] |
| versions_for = sys.argv[2] |
| python_version = sys.argv[3] |
| assert versions_for in ["release", "pull_request"] |
|
|
| |
| min_versions = get_min_version_from_toml(toml_file, versions_for, python_version) |
|
|
| print(" ".join([f"{lib}=={version}" for lib, version in min_versions.items()])) |
|
|