This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new d267fbd5cae Bootstrap constraints generation for a Python without a
baseline file (#74596)
d267fbd5cae is described below
commit d267fbd5caee385b0568c60d3695b3cf85b70460
Author: Dev-iL <[email protected]>
AuthorDate: Tue Oct 13 16:00:27 2026 +0300
Bootstrap constraints generation for a Python without a baseline file
(#74596)
The generator always downloads the previous constraints file of the same
Python from the constraints
branch to diff against it, and failed with a 404 for the first generation
of a new Python. When the
branch carries the same constraints mode for other Pythons, a missing file
means a first publication and
the generator diffs against an empty baseline instead. A 404 on a branch
without any file of that mode,
and every other HTTP error, still fails.
---
scripts/in_container/run_generate_constraints.py | 45 +++++++++++---
.../in_container/test_run_generate_constraints.py | 69 ++++++++++++++++++++++
2 files changed, 106 insertions(+), 8 deletions(-)
diff --git a/scripts/in_container/run_generate_constraints.py
b/scripts/in_container/run_generate_constraints.py
index daf0b43c2bf..5ca30fc59f1 100755
--- a/scripts/in_container/run_generate_constraints.py
+++ b/scripts/in_container/run_generate_constraints.py
@@ -20,6 +20,7 @@ from __future__ import annotations
import ast
import json
import os
+import re
import sys
import tomllib
from concurrent.futures import ThreadPoolExecutor
@@ -233,21 +234,49 @@ def freeze_distributions_to_file(
console.print(f"[green]Constraints generated to file: {file.name}. Wrote
{count_lines} lines")
+def _github_headers(accept: str) -> dict[str, str]:
+ headers = {"Accept": accept}
+ if os.environ.get("GITHUB_TOKEN"):
+ headers["Authorization"] = f"Bearer {os.environ.get('GITHUB_TOKEN')}"
+ else:
+ console.print("[bright_blue]No GITHUB_TOKEN - using non-authenticated
request.")
+ return headers
+
+
+def _constraints_branch_has_other_python_files(config_params: ConfigParams) ->
bool:
+ """Tell a first publication for a new Python from a baseline that should
exist but is missing."""
+ listing_url = (
+
f"https://api.github.com/repos/{config_params.constraints_github_repository}/contents/"
+ f"?ref={config_params.default_constraints_branch}"
+ )
+ r = requests.get(listing_url, timeout=60,
headers=_github_headers("application/vnd.github.v3+json"))
+ r.raise_for_status()
+ other_python_file =
re.compile(rf"{re.escape(config_params.airflow_constraints_mode)}-3\.\d+\.txt")
+ return any(
+ other_python_file.fullmatch(entry["name"])
+ and entry["name"] !=
f"{config_params.airflow_constraints_mode}-{config_params.python}.txt"
+ for entry in r.json()
+ )
+
+
def download_latest_constraint_file(config_params: ConfigParams):
constraints_url = (
"https://api.github.com/repos/"
f"{config_params.constraints_github_repository}/contents/"
f"{config_params.airflow_constraints_mode}-{config_params.python}.txt?ref={config_params.default_constraints_branch}"
)
- # download the latest constraints file
- # download using requests
- headers = {"Accept": "application/vnd.github.v3.raw"}
- if os.environ.get("GITHUB_TOKEN"):
- headers["Authorization"] = f"Bearer {os.environ.get('GITHUB_TOKEN')}"
- else:
- console.print("[bright_blue]No GITHUB_TOKEN - using non-authenticated
request.")
console.print(f"[bright_blue]Downloading constraints file from
{constraints_url}")
- r = requests.get(constraints_url, timeout=60, headers=headers)
+ r = requests.get(constraints_url, timeout=60,
headers=_github_headers("application/vnd.github.v3.raw"))
+ if r.status_code == 404 and
_constraints_branch_has_other_python_files(config_params):
+ # The first generation for a new Python has no file to compare with:
diff against an empty
+ # baseline. A 404 on a branch that carries no file of this mode at all
is still an error.
+ console.print(
+ f"[yellow]No {config_params.airflow_constraints_mode} file for
Python {config_params.python} on "
+ f"{config_params.default_constraints_branch}, but other Pythons
have one - first publication, "
+ "using an empty baseline."
+ )
+ config_params.latest_constraints_file.write_text("")
+ return
r.raise_for_status()
with config_params.latest_constraints_file.open("w") as constraints_file:
constraints_file.write(r.text)
diff --git a/scripts/tests/in_container/test_run_generate_constraints.py
b/scripts/tests/in_container/test_run_generate_constraints.py
index e8cc482acc4..f01653a422d 100644
--- a/scripts/tests/in_container/test_run_generate_constraints.py
+++ b/scripts/tests/in_container/test_run_generate_constraints.py
@@ -272,3 +272,72 @@ class TestBuildPinnedProviderRequirements:
assert seen_for_distributions == ["3.14"]
assert seen_for_lookup == [("3.14", False)]
+
+
+class TestDownloadLatestConstraintFile:
+ @staticmethod
+ def _download_config(tmp_path: Path, mode: str =
"constraints-source-providers") -> SimpleNamespace:
+ return SimpleNamespace(
+ latest_constraints_file=tmp_path / "original.txt",
+ airflow_constraints_mode=mode,
+ constraints_github_repository="apache/airflow",
+ default_constraints_branch="constraints-main",
+ python="3.15",
+ )
+
+ @staticmethod
+ def _response(status_code: int = 200, text: str = "", payload: list | None
= None) -> SimpleNamespace:
+ def raise_for_status():
+ if status_code >= 400:
+ raise RuntimeError(f"HTTP {status_code}")
+
+ return SimpleNamespace(
+ status_code=status_code,
+ text=text,
+ json=lambda: payload or [],
+ raise_for_status=raise_for_status,
+ )
+
+ def test_writes_the_downloaded_file(self, tmp_path, monkeypatch):
+ config = self._download_config(tmp_path)
+ monkeypatch.setattr(m.requests, "get", lambda *args, **kwargs:
self._response(text="a==1\n"))
+
+ m.download_latest_constraint_file(config)
+
+ assert config.latest_constraints_file.read_text() == "a==1\n"
+
+ def test_missing_file_for_a_new_python_bootstraps_an_empty_baseline(self,
tmp_path, monkeypatch):
+ config = self._download_config(tmp_path)
+ listing = [
+ {"name": "constraints-source-providers-3.14.txt"},
+ {"name": "constraints-3.15.txt"},
+ {"name": "README.md"},
+ ]
+ responses = iter([self._response(404),
self._response(payload=listing)])
+ monkeypatch.setattr(m.requests, "get", lambda *args, **kwargs:
next(responses))
+
+ m.download_latest_constraint_file(config)
+
+ assert config.latest_constraints_file.read_text() == ""
+
+ def test_missing_file_is_an_error_when_no_other_python_has_one(self,
tmp_path, monkeypatch):
+ config = self._download_config(tmp_path)
+ listing = [
+ {"name": "constraints-3.14.txt"},
+ {"name": "constraints-no-providers-3.14.txt"},
+ {"name": "constraints-source-providers-3.15.txt"},
+ ]
+ responses = iter([self._response(404),
self._response(payload=listing)])
+ monkeypatch.setattr(m.requests, "get", lambda *args, **kwargs:
next(responses))
+
+ with pytest.raises(RuntimeError, match="HTTP 404"):
+ m.download_latest_constraint_file(config)
+
+ assert not config.latest_constraints_file.exists()
+
+ def test_other_http_errors_are_not_treated_as_a_first_publication(self,
tmp_path, monkeypatch):
+ config = self._download_config(tmp_path)
+ monkeypatch.setattr(m.requests, "get", lambda *args, **kwargs:
self._response(500))
+
+ with pytest.raises(RuntimeError, match="HTTP 500"):
+ m.download_latest_constraint_file(config)