feat: Support constraints in pip_compile (#2916)

This adds in support to pass in a constraints file to pip-compile.
This is extremly useful when you want to uprade an indirect/intermediate
dependency to pull in security fixes but don't want to add said
dependency to
the requirements.in file.

---------

Signed-off-by: Vihang Mehta <vihang@gimletlabs.ai>
Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a113c74..355f1fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -111,6 +111,9 @@
   and activated with custom flags. See the [Registering custom runtimes]
   docs and {obj}`single_version_platform_override()` API docs for more
   information.
+* (rules) Added support for a using constraints files with `compile_pip_requirements`.
+  Useful when an intermediate dependency needs to be upgraded to pull in
+  security patches.
 
 {#v0-0-0-removed}
 ### Removed
diff --git a/examples/pip_parse/BUILD.bazel b/examples/pip_parse/BUILD.bazel
index 8bdbd94..6ed8d26 100644
--- a/examples/pip_parse/BUILD.bazel
+++ b/examples/pip_parse/BUILD.bazel
@@ -57,6 +57,10 @@
 compile_pip_requirements(
     name = "requirements",
     src = "requirements.in",
+    constraints = [
+        "constraints_certifi.txt",
+        "constraints_urllib3.txt",
+    ],
     requirements_txt = "requirements_lock.txt",
     requirements_windows = "requirements_windows.txt",
 )
diff --git a/examples/pip_parse/constraints_certifi.txt b/examples/pip_parse/constraints_certifi.txt
new file mode 100644
index 0000000..7dc4eac
--- /dev/null
+++ b/examples/pip_parse/constraints_certifi.txt
@@ -0,0 +1 @@
+certifi>=2025.1.31
\ No newline at end of file
diff --git a/examples/pip_parse/constraints_urllib3.txt b/examples/pip_parse/constraints_urllib3.txt
new file mode 100644
index 0000000..3818262
--- /dev/null
+++ b/examples/pip_parse/constraints_urllib3.txt
@@ -0,0 +1 @@
+urllib3>1.26.18
diff --git a/examples/pip_parse/requirements_lock.txt b/examples/pip_parse/requirements_lock.txt
index aeac61e..dc34b45 100644
--- a/examples/pip_parse/requirements_lock.txt
+++ b/examples/pip_parse/requirements_lock.txt
@@ -12,10 +12,12 @@
     --hash=sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900 \
     --hash=sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed
     # via sphinx
-certifi==2024.7.4 \
-    --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
-    --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
-    # via requests
+certifi==2025.4.26 \
+    --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \
+    --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3
+    # via
+    #   -c ./constraints_certifi.txt
+    #   requests
 chardet==4.0.0 \
     --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
     --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5
@@ -218,10 +220,12 @@
     # via
     #   -r requirements.in
     #   sphinx
-urllib3==1.26.18 \
-    --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \
-    --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0
-    # via requests
+urllib3==1.26.20 \
+    --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
+    --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
+    # via
+    #   -c ./constraints_urllib3.txt
+    #   requests
 yamllint==1.28.0 \
     --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \
     --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b
diff --git a/examples/pip_parse/requirements_windows.txt b/examples/pip_parse/requirements_windows.txt
index 61a6682..78c1a45 100644
--- a/examples/pip_parse/requirements_windows.txt
+++ b/examples/pip_parse/requirements_windows.txt
@@ -12,10 +12,12 @@
     --hash=sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900 \
     --hash=sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed
     # via sphinx
-certifi==2024.7.4 \
-    --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
-    --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
-    # via requests
+certifi==2025.4.26 \
+    --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \
+    --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3
+    # via
+    #   -c ./constraints_certifi.txt
+    #   requests
 chardet==4.0.0 \
     --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
     --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5
@@ -222,10 +224,12 @@
     # via
     #   -r requirements.in
     #   sphinx
-urllib3==1.26.18 \
-    --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \
-    --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0
-    # via requests
+urllib3==1.26.20 \
+    --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
+    --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
+    # via
+    #   -c ./constraints_urllib3.txt
+    #   requests
 yamllint==1.28.0 \
     --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \
     --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b
diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl
index 9782d3c..c989950 100644
--- a/python/private/pypi/pip_compile.bzl
+++ b/python/private/pypi/pip_compile.bzl
@@ -38,6 +38,7 @@
         requirements_windows = None,
         visibility = ["//visibility:private"],
         tags = None,
+        constraints = [],
         **kwargs):
     """Generates targets for managing pip dependencies with pip-compile.
 
@@ -77,6 +78,7 @@
         requirements_windows: File of windows specific resolve output to check validate if requirement.in has changes.
         tags: tagging attribute common to all build rules, passed to both the _test and .update rules.
         visibility: passed to both the _test and .update rules.
+        constraints: a list of files containing constraints to pass to pip-compile with `--constraint`.
         **kwargs: other bazel attributes passed to the "_test" rule.
     """
     if len([x for x in [srcs, src, requirements_in] if x != None]) > 1:
@@ -100,7 +102,7 @@
         visibility = visibility,
     )
 
-    data = [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None]
+    data = [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None] + constraints
 
     # Use the Label constructor so this is expanded in the context of the file
     # where it appears, which is to say, in @rules_python
@@ -122,6 +124,8 @@
         args.append("--requirements-darwin={}".format(loc.format(requirements_darwin)))
     if requirements_windows:
         args.append("--requirements-windows={}".format(loc.format(requirements_windows)))
+    for constraint in constraints:
+        args.append("--constraint=$(location {})".format(constraint))
     args.extend(extra_args)
 
     deps = [