NLQueries GitHub

Dependency locking (SEC-11)

The image installs its Python dependencies from a committed, hashed lock file rather than re-resolving the >= floors in pyproject.toml at build time. Two builds of the same commit install the same bytes, and a yanked or compromised upstream release cannot be adopted silently.

The base image is pinned by digest for the same reason: a tag is mutable, so without a digest "the same Dockerfile" does not mean the same image.

Files

File Purpose
pyproject.toml Source of truth for direct dependencies, with >= floors.
requirements/core.lock Compiled, fully pinned, hashed closure. Committed.
scripts/lock-deps.sh Regenerates the lock in a container matching the runtime.

Regenerating

pip-tools locks are specific to the resolving platform and Python version, so the lock must be compiled for the runtime target — linux, Python 3.11. Do not run pip-compile from a development machine; the script compiles inside a throwaway python:3.11-slim container so the result does not depend on your OS.

./scripts/lock-deps.sh
git add requirements/core.lock

Regenerate whenever pyproject.toml changes. tests/test_dependency_lock.py fails if a declared dependency is missing from the lock, which is the case the rest of the pipeline would not catch: only the release workflow builds the image, so a stale lock would otherwise install fine for every developer and fail at the release.

Two flags that are not optional

--allow-unsafe is required despite the name. It tells pip-tools to pin setuptools, which it otherwise leaves floating. A lock installed with --require-hashes must pin everything, so without it pip either falls back to whatever setuptools the base image happens to carry, or refuses to install outright. The symptom is a # WARNING block written into the lock naming the unpinned package, which is a comment and therefore fails nothing on its own — test_the_lock_carries_no_unresolved_warnings exists for that reason.

--generate-hashes is what makes this a supply-chain control rather than a version pin. Without it the lock fixes the version but not the bytes, and a re-upload of the same version number would install.

What is not in the closure

The connector extras — snowflake, bigquery, mssql, redshift, mysql, onnx — are deliberately excluded. They are optional installs whose wheels are large and partly platform-specific, and a deployment that needs one pins it in its own lock; nlqueries-enterprise does exactly that. Locking them here would make every core build carry drivers almost nobody uses.

The dev extra is included, so CI and the image agree about the tooling.

pip-tools version

scripts/lock-deps.sh pins pip-tools itself. Version 8 changes the default for --strip-extras, which changes how names are written in the lock (httpx[http2]== versus httpx==). Pinning keeps the format stable; changing it is a deliberate edit, not something that happens because a developer's machine had a newer pip-tools.

Updating the base image

docker buildx imagetools inspect python:3.11-slim-bookworm

Take the Digest line and update the FROM in the Dockerfile. Do this deliberately and on purpose — the value of the pin is that it does not move on its own.