a0c8464e58
Build Package / build (ubuntu-latest) (push) Failing after 1s
CodeQL / Analyze (python) (push) Failing after 1s
Core Typecheck / core-typecheck (push) Failing after 1s
Linting / lint (push) Failing after 1s
llama-dev tests / test-llama-dev (push) Failing after 1s
Publish Sub-Package to PyPI if Needed / publish_subpackage_if_needed (push) Has been skipped
Sync Docs to Developer Hub / sync-docs (push) Failing after 0s
Build Package / build (windows-latest) (push) Has been cancelled
208 lines
6.7 KiB
Python
208 lines
6.7 KiB
Python
import re
|
|
import subprocess
|
|
import sys
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
|
|
import tomli
|
|
from packaging import specifiers, version
|
|
from packaging.version import Version
|
|
|
|
DEP_NAME_REGEX = re.compile(r"([^<>=\[\];\s]+)")
|
|
|
|
|
|
class BumpType(str, Enum):
|
|
MAJOR = "major"
|
|
MINOR = "minor"
|
|
PATCH = "patch"
|
|
|
|
|
|
def bump_version(current_version: str, bump_type: BumpType) -> str:
|
|
"""Bump a version string according to semver rules."""
|
|
v = Version(current_version)
|
|
|
|
# Parse the version components
|
|
release = v.release
|
|
major = release[0] if len(release) > 0 else 0
|
|
minor = release[1] if len(release) > 1 else 0
|
|
micro = release[2] if len(release) > 2 else 0
|
|
|
|
version_str = ""
|
|
if bump_type == BumpType.MAJOR:
|
|
version_str = f"{major + 1}.0.0"
|
|
elif bump_type == BumpType.MINOR:
|
|
version_str = f"{major}.{minor + 1}.0"
|
|
elif bump_type == BumpType.PATCH:
|
|
version_str = f"{major}.{minor}.{micro + 1}"
|
|
|
|
return version_str
|
|
|
|
|
|
def update_pyproject_version(package_path: Path, new_version: str) -> None:
|
|
"""Update the version in a pyproject.toml file."""
|
|
pyproject_path = package_path / "pyproject.toml"
|
|
|
|
# Read the file content
|
|
with open(pyproject_path, "r") as f:
|
|
content = f.read()
|
|
|
|
pattern = r'^version = "[^"]+"'
|
|
new_content = re.sub(
|
|
pattern, f'version = "{new_version}"', content, flags=re.MULTILINE
|
|
)
|
|
|
|
# Write the updated content back
|
|
with open(pyproject_path, "w") as f:
|
|
f.write(new_content)
|
|
|
|
|
|
def package_has_tests(package_path: Path) -> bool:
|
|
"""Returns whether a package folder contains a 'tests' subfolder."""
|
|
tests_folder = package_path / "tests"
|
|
return package_path.is_dir() and tests_folder.exists() and tests_folder.is_dir()
|
|
|
|
|
|
def is_llama_index_package(package_path: Path) -> bool:
|
|
"""Returns whether a folder contains a 'pyproject.toml' file."""
|
|
pyproject = package_path / "pyproject.toml"
|
|
return package_path.is_dir() and pyproject.exists() and pyproject.is_file()
|
|
|
|
|
|
def load_pyproject(package_path: Path) -> dict:
|
|
"""Thin wrapper around tomli.load()."""
|
|
pyproject_path = package_path / "pyproject.toml"
|
|
with open(pyproject_path, "rb") as f:
|
|
return tomli.load(f)
|
|
|
|
|
|
def find_integrations(root_path: Path, recursive=False) -> list[Path]:
|
|
"""Find all integrations packages in the repo."""
|
|
package_roots: list[Path] = []
|
|
integrations_root = root_path
|
|
if not recursive:
|
|
integrations_root = integrations_root / "llama-index-integrations"
|
|
|
|
for category_path in integrations_root.iterdir():
|
|
if not category_path.is_dir():
|
|
continue
|
|
|
|
if category_path.name == "storage":
|
|
# The "storage" category has sub-folders
|
|
package_roots += find_integrations(category_path, recursive=True)
|
|
continue
|
|
|
|
for package_name in category_path.iterdir():
|
|
if is_llama_index_package(package_name):
|
|
package_roots.append(package_name)
|
|
|
|
return package_roots
|
|
|
|
|
|
def find_utils(root_path: Path) -> list[Path]:
|
|
"""Find all llama-index-utils packages in the repo."""
|
|
package_roots: list[Path] = []
|
|
utils_root = root_path / "llama-index-utils"
|
|
|
|
for package_name in utils_root.iterdir():
|
|
if is_llama_index_package(package_name):
|
|
package_roots.append(package_name)
|
|
|
|
return package_roots
|
|
|
|
|
|
def find_all_packages(root_path: Path) -> list[Path]:
|
|
"""Returns a list of all the package folders in the monorepo."""
|
|
return [
|
|
root_path / "llama-index-core",
|
|
*find_integrations(root_path),
|
|
*find_utils(root_path),
|
|
root_path / "llama-index-instrumentation",
|
|
]
|
|
|
|
|
|
def get_changed_files(repo_root: Path, base_ref: str = "main") -> list[Path]:
|
|
"""Use git to get the list of files changed compared to the base branch."""
|
|
try:
|
|
cmd = ["git", "diff", "--name-only", f"{base_ref}...HEAD"]
|
|
result = subprocess.run(cmd, cwd=repo_root, text=True, capture_output=True)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"Git command failed: {result.stderr}")
|
|
|
|
return [repo_root / Path(f) for f in result.stdout.splitlines() if f.strip()]
|
|
except Exception as e:
|
|
print(f"Exception occurred: {e!s}")
|
|
raise
|
|
|
|
|
|
def get_changed_packages(
|
|
changed_files: list[Path], all_packages: list[Path]
|
|
) -> set[Path]:
|
|
"""Get the list of package folders containing the path in 'changed_files'."""
|
|
changed_packages: set[Path] = set()
|
|
|
|
for file_path in changed_files:
|
|
# Find the package containing this file
|
|
for pkg_dir in all_packages:
|
|
if file_path.absolute().is_relative_to(pkg_dir.absolute()):
|
|
changed_packages.add(pkg_dir)
|
|
break
|
|
|
|
return changed_packages
|
|
|
|
|
|
def get_dep_names(pyproject_data: dict) -> set[str]:
|
|
"""Load dependencies from pyproject.toml."""
|
|
dependencies: set[str] = set()
|
|
for dep in pyproject_data["project"]["dependencies"]:
|
|
matches = DEP_NAME_REGEX.findall(dep)
|
|
if not matches:
|
|
continue
|
|
dependencies.add(matches[0])
|
|
return dependencies
|
|
|
|
|
|
def is_python_version_compatible(pyproject_data: dict) -> bool:
|
|
"""Check if the package is compatible with the current Python version using packaging."""
|
|
# Get current Python version
|
|
current_version = version.Version(
|
|
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
)
|
|
|
|
# Get Python version requirements if they exist
|
|
requires_python = pyproject_data.get("project", {}).get("requires-python")
|
|
if requires_python is None:
|
|
# If no Python version is specified, assume it's compatible
|
|
return True
|
|
|
|
try:
|
|
# Parse the version specifier
|
|
spec = specifiers.SpecifierSet(requires_python)
|
|
|
|
# Check if the current version satisfies the specifier
|
|
return spec.contains(str(current_version))
|
|
except Exception as e:
|
|
# If there's any error in parsing, log it and assume compatibility
|
|
print(
|
|
f"Warning: Could not parse Python version specifier '{requires_python}': {e}"
|
|
)
|
|
return True
|
|
|
|
|
|
def get_dependants_packages(
|
|
changed_packages: set[Path], all_packages: list[Path]
|
|
) -> set[Path]:
|
|
"""Get packages containing the files in the changeset."""
|
|
changed_packages_names: set[str] = set()
|
|
for pkg_path in changed_packages:
|
|
pyproject_data = load_pyproject(pkg_path)
|
|
changed_packages_names.add(pyproject_data["project"]["name"])
|
|
|
|
dependants_packages: set[Path] = set()
|
|
for pkg_path in all_packages:
|
|
pyproject_data = load_pyproject(pkg_path)
|
|
for dep_name in get_dep_names(pyproject_data):
|
|
if dep_name in changed_packages_names:
|
|
dependants_packages.add(pkg_path)
|
|
|
|
return dependants_packages
|