Skip to content

Commit 209cddf

Browse files
authored
Initial commit
0 parents  commit 209cddf

File tree

14 files changed

+1042
-0
lines changed

14 files changed

+1042
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# TODO: https://docs.astral.sh/uv/guides/integration/github/#caching
2+
3+
name: uv-install
4+
description: Set up Python and uv
5+
6+
inputs:
7+
python-version:
8+
description: Python version, supporting MAJOR.MINOR only
9+
required: true
10+
11+
env:
12+
UV_VERSION: "0.5.25"
13+
14+
runs:
15+
using: composite
16+
steps:
17+
- name: Install uv and set the python version
18+
uses: astral-sh/setup-uv@v5
19+
with:
20+
version: ${{ env.UV_VERSION }}
21+
python-version: ${{ inputs.python-version }}

.github/scripts/check_diff.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import json
2+
import sys
3+
from typing import Dict
4+
5+
LIB_DIRS = ["libs/{lib}"]
6+
7+
if __name__ == "__main__":
8+
files = sys.argv[1:]
9+
10+
dirs_to_run: Dict[str, set] = {
11+
"lint": set(),
12+
"test": set(),
13+
}
14+
15+
if len(files) == 300:
16+
# max diff length is 300 files - there are likely files missing
17+
raise ValueError("Max diff reached. Please manually run CI on changed libs.")
18+
19+
for file in files:
20+
if any(
21+
file.startswith(dir_)
22+
for dir_ in (
23+
".github/workflows",
24+
".github/tools",
25+
".github/actions",
26+
".github/scripts/check_diff.py",
27+
)
28+
):
29+
# add all LANGCHAIN_DIRS for infra changes
30+
dirs_to_run["test"].update(LIB_DIRS)
31+
32+
if any(file.startswith(dir_) for dir_ in LIB_DIRS):
33+
for dir_ in LIB_DIRS:
34+
if file.startswith(dir_):
35+
dirs_to_run["test"].add(dir_)
36+
elif file.startswith("libs/"):
37+
raise ValueError(
38+
f"Unknown lib: {file}. check_diff.py likely needs "
39+
"an update for this new library!"
40+
)
41+
42+
outputs = {
43+
"dirs-to-lint": list(dirs_to_run["lint"] | dirs_to_run["test"]),
44+
"dirs-to-test": list(dirs_to_run["test"]),
45+
}
46+
for key, value in outputs.items():
47+
json_output = json.dumps(value)
48+
print(f"{key}={json_output}") # noqa: T201
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
from collections import defaultdict
2+
import sys
3+
from typing import Optional
4+
5+
if sys.version_info >= (3, 11):
6+
import tomllib
7+
else:
8+
# for python 3.10 and below, which doesnt have stdlib tomllib
9+
import tomli as tomllib
10+
11+
from packaging.requirements import Requirement
12+
from packaging.specifiers import SpecifierSet
13+
from packaging.version import Version
14+
15+
16+
import requests
17+
from packaging.version import parse
18+
from typing import List
19+
20+
import re
21+
22+
23+
MIN_VERSION_LIBS = ["langchain-core"]
24+
25+
# some libs only get checked on release because of simultaneous changes in
26+
# multiple libs
27+
SKIP_IF_PULL_REQUEST = ["langchain-core"]
28+
29+
30+
def get_pypi_versions(package_name: str) -> List[str]:
31+
"""
32+
Fetch all available versions for a package from PyPI.
33+
34+
Args:
35+
package_name (str): Name of the package
36+
37+
Returns:
38+
List[str]: List of all available versions
39+
40+
Raises:
41+
requests.exceptions.RequestException: If PyPI API request fails
42+
KeyError: If package not found or response format unexpected
43+
"""
44+
pypi_url = f"https://pypi.org/pypi/{package_name}/json"
45+
response = requests.get(pypi_url)
46+
response.raise_for_status()
47+
return list(response.json()["releases"].keys())
48+
49+
50+
def get_minimum_version(package_name: str, spec_string: str) -> Optional[str]:
51+
"""
52+
Find the minimum published version that satisfies the given constraints.
53+
54+
Args:
55+
package_name (str): Name of the package
56+
spec_string (str): Version specification string (e.g., ">=0.2.43,<0.4.0,!=0.3.0")
57+
58+
Returns:
59+
Optional[str]: Minimum compatible version or None if no compatible version found
60+
"""
61+
# rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string)
62+
spec_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", spec_string)
63+
# rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1 (can be anywhere in constraint string)
64+
for y in range(1, 10):
65+
spec_string = re.sub(rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}", spec_string)
66+
# rewrite occurrences of ^x.y.z to >=x.y.z,<x+1.0.0 (can be anywhere in constraint string)
67+
for x in range(1, 10):
68+
spec_string = re.sub(
69+
rf"\^{x}\.(\d+)\.(\d+)", rf">={x}.\1.\2,<{x+1}", spec_string
70+
)
71+
72+
spec_set = SpecifierSet(spec_string)
73+
all_versions = get_pypi_versions(package_name)
74+
75+
valid_versions = []
76+
for version_str in all_versions:
77+
try:
78+
version = parse(version_str)
79+
if spec_set.contains(version):
80+
valid_versions.append(version)
81+
except ValueError:
82+
continue
83+
84+
return str(min(valid_versions)) if valid_versions else None
85+
86+
87+
def _check_python_version_from_requirement(
88+
requirement: Requirement, python_version: str
89+
) -> bool:
90+
if not requirement.marker:
91+
return True
92+
else:
93+
marker_str = str(requirement.marker)
94+
if "python_version" or "python_full_version" in marker_str:
95+
python_version_str = "".join(
96+
char
97+
for char in marker_str
98+
if char.isdigit() or char in (".", "<", ">", "=", ",")
99+
)
100+
return check_python_version(python_version, python_version_str)
101+
return True
102+
103+
104+
def get_min_version_from_toml(
105+
toml_path: str,
106+
versions_for: str,
107+
python_version: str,
108+
*,
109+
include: Optional[list] = None,
110+
):
111+
# Parse the TOML file
112+
with open(toml_path, "rb") as file:
113+
toml_data = tomllib.load(file)
114+
115+
dependencies = defaultdict(list)
116+
for dep in toml_data["project"]["dependencies"]:
117+
requirement = Requirement(dep)
118+
dependencies[requirement.name].append(requirement)
119+
120+
# Initialize a dictionary to store the minimum versions
121+
min_versions = {}
122+
123+
# Iterate over the libs in MIN_VERSION_LIBS
124+
for lib in set(MIN_VERSION_LIBS + (include or [])):
125+
if versions_for == "pull_request" and lib in SKIP_IF_PULL_REQUEST:
126+
# some libs only get checked on release because of simultaneous
127+
# changes in multiple libs
128+
continue
129+
# Check if the lib is present in the dependencies
130+
if lib in dependencies:
131+
if include and lib not in include:
132+
continue
133+
requirements = dependencies[lib]
134+
for requirement in requirements:
135+
if _check_python_version_from_requirement(requirement, python_version):
136+
version_string = str(requirement.specifier)
137+
break
138+
139+
# Use parse_version to get the minimum supported version from version_string
140+
min_version = get_minimum_version(lib, version_string)
141+
142+
# Store the minimum version in the min_versions dictionary
143+
min_versions[lib] = min_version
144+
145+
return min_versions
146+
147+
148+
def check_python_version(version_string, constraint_string):
149+
"""
150+
Check if the given Python version matches the given constraints.
151+
152+
:param version_string: A string representing the Python version (e.g. "3.8.5").
153+
:param constraint_string: A string representing the package's Python version constraints (e.g. ">=3.6, <4.0").
154+
:return: True if the version matches the constraints, False otherwise.
155+
"""
156+
157+
# rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string)
158+
constraint_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", constraint_string)
159+
# rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1.0 (can be anywhere in constraint string)
160+
for y in range(1, 10):
161+
constraint_string = re.sub(
162+
rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}.0", constraint_string
163+
)
164+
# rewrite occurrences of ^x.y.z to >=x.y.z,<x+1.0.0 (can be anywhere in constraint string)
165+
for x in range(1, 10):
166+
constraint_string = re.sub(
167+
rf"\^{x}\.0\.(\d+)", rf">={x}.0.\1,<{x+1}.0.0", constraint_string
168+
)
169+
170+
try:
171+
version = Version(version_string)
172+
constraints = SpecifierSet(constraint_string)
173+
return version in constraints
174+
except Exception as e:
175+
print(f"Error: {e}")
176+
return False
177+
178+
179+
if __name__ == "__main__":
180+
# Get the TOML file path from the command line argument
181+
toml_file = sys.argv[1]
182+
versions_for = sys.argv[2]
183+
python_version = sys.argv[3]
184+
assert versions_for in ["release", "pull_request"]
185+
186+
# Call the function to get the minimum versions
187+
min_versions = get_min_version_from_toml(toml_file, versions_for, python_version)
188+
189+
print(" ".join([f"{lib}=={version}" for lib, version in min_versions.items()]))

.github/workflows/_codespell.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
name: make spell_check
3+
4+
on:
5+
workflow_call:
6+
inputs:
7+
working-directory:
8+
required: true
9+
type: string
10+
description: "From which folder this pipeline executes"
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
codespell:
17+
name: (Check for spelling errors)
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Install Dependencies
25+
run: |
26+
pip install toml
27+
28+
- name: Extract Ignore Words List
29+
working-directory: ${{ inputs.working-directory }}
30+
run: |
31+
# Use a Python script to extract the ignore words list from pyproject.toml
32+
python ../../.github/workflows/extract_ignored_words_list.py
33+
id: extract_ignore_words
34+
35+
- name: Codespell
36+
uses: codespell-project/actions-codespell@v2
37+
with:
38+
skip: guide_imports.json
39+
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: compile-integration-test
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
working-directory:
7+
required: true
8+
type: string
9+
description: "From which folder this pipeline executes"
10+
11+
env:
12+
UV_FROZEN: "true"
13+
14+
jobs:
15+
build:
16+
defaults:
17+
run:
18+
working-directory: ${{ inputs.working-directory }}
19+
runs-on: ubuntu-latest
20+
strategy:
21+
matrix:
22+
python-version:
23+
- "3.9"
24+
- "3.12"
25+
name: "uv run pytest -m compile tests/integration_tests"
26+
steps:
27+
- uses: actions/checkout@v4
28+
29+
- name: Set up Python ${{ matrix.python-version }} + uv
30+
uses: "./.github/actions/uv_setup"
31+
with:
32+
python-version: ${{ matrix.python-version }}
33+
34+
- name: Install integration dependencies
35+
shell: bash
36+
run: uv sync --group test --group test_integration
37+
38+
- name: Check integration tests compile
39+
shell: bash
40+
run: uv run pytest -m compile tests/integration_tests
41+
42+
- name: Ensure the tests did not create any additional files
43+
shell: bash
44+
run: |
45+
set -eu
46+
47+
STATUS="$(git status)"
48+
echo "$STATUS"
49+
50+
# grep will exit non-zero if the target message isn't found,
51+
# and `set -e` above will cause the step to fail.
52+
echo "$STATUS" | grep 'nothing to commit, working tree clean'

0 commit comments

Comments
 (0)