Summary
uv.lock can silently resolve protobuf to a version that is incompatible
with the checked-in generated code in app/events/generated/event_pb2.py,
breaking app, email_handler.py and job_runner.py at import time with:
ImportError: cannot import name 'runtime_version' from 'google.protobuf'
This is not a hypothetical: it reproduces on a clean checkout of master.
Root cause
app/events/generated/event_pb2.py is generated code that requires
google.protobuf.runtime_version, which only exists starting with
protobuf >= 4.25.
protobuf is not a direct dependency in pyproject.toml — it is only
pulled in transitively through google-api-python-client ~= 1.12.3 (an
~2020-era pin) via its google-api-core dependency chain.
google-api-python-client ~= 1.12.3's transitive constraints permit
(and, once nudged, actively prefer) protobuf < 4, i.e. the latest 3.x
release (3.20.3), which does not have the runtime_version module at
all.
- Separately,
app/email_utils.py does from cachetools import cached, TTLCache, but cachetools is also not declared as a direct
dependency — it only happens to be present transitively (currently via
google-auth).
Because protobuf and cachetools both float as undeclared/transitive
dependencies, the exact versions that get resolved depend entirely on
whatever else is going on in the rest of the dependency graph at lock time.
The currently committed uv.lock happens to have protobuf==5.27.1, which
works — but that is incidental, not guaranteed by pyproject.toml.
Reproduction (on a clean checkout of master)
Add any new, unrelated top-level dependency and re-lock, e.g.:
"google-auth-httplib2 ~= 0.0.4",
+ "cachetools ~= 5.3.3",
Result:
Updated cachetools v4.1.1 -> v5.3.3
Updated google-api-core v1.22.2 -> v1.34.1
Updated google-auth v1.22.0 -> v2.47.0
Updated googleapis-common-protos v1.52.0 -> v1.73.0
Updated protobuf v5.27.1 -> v3.20.3
protobuf drops from 5.27.1 to 3.20.3. Booting the app (or running
python -c "import email_handler") with this lockfile immediately fails
with the ImportError above.
We hit this for real: adding a dependency for an unrelated feature on a
fork triggered exactly this, taking down a production deployment until we
tracked it down (the failure mode gives no indication that protobuf, of
all things, is the problem).
Suggested fix
- Declare
protobuf as a direct dependency with a floor that matches what
the generated event_pb2.py actually requires, e.g. protobuf >= 4.25, < 6.
- Declare
cachetools as a direct dependency (it's directly imported in
app/email_utils.py), e.g. cachetools >= 5.0, < 6.
- Bump
google-api-python-client off the 2020-era ~= 1.12.3 pin to a
current 2.x release (>= 2.100, < 3) and google-auth-httplib2 to >= 0.2.0, < 1.0 accordingly — the only call site
(app/api/views/auth.py, googleapiclient.discovery.build("oauth2", "v2", ...)) is unaffected by the major version bump.
This resolves cleanly to stable (non-prerelease) versions across the board
(protobuf 5.27.1, google-api-core 2.30.3) and removes the landmine for
good, rather than just re-locking back to a lucky state.
I'll open a PR with this fix, verified locally (dependency resolution +
container build + import smoke test for app, email_handler.py,
job_runner.py, and the Google OAuth code path).
Summary
uv.lockcan silently resolveprotobufto a version that is incompatiblewith the checked-in generated code in
app/events/generated/event_pb2.py,breaking
app,email_handler.pyandjob_runner.pyat import time with:This is not a hypothetical: it reproduces on a clean checkout of
master.Root cause
app/events/generated/event_pb2.pyis generated code that requiresgoogle.protobuf.runtime_version, which only exists starting withprotobuf >= 4.25.protobufis not a direct dependency inpyproject.toml— it is onlypulled in transitively through
google-api-python-client ~= 1.12.3(an~2020-era pin) via its
google-api-coredependency chain.google-api-python-client ~= 1.12.3's transitive constraints permit(and, once nudged, actively prefer)
protobuf < 4, i.e. the latest 3.xrelease (
3.20.3), which does not have theruntime_versionmodule atall.
app/email_utils.pydoesfrom cachetools import cached, TTLCache, butcachetoolsis also not declared as a directdependency — it only happens to be present transitively (currently via
google-auth).Because
protobufandcachetoolsboth float as undeclared/transitivedependencies, the exact versions that get resolved depend entirely on
whatever else is going on in the rest of the dependency graph at lock time.
The currently committed
uv.lockhappens to haveprotobuf==5.27.1, whichworks — but that is incidental, not guaranteed by
pyproject.toml.Reproduction (on a clean checkout of
master)Add any new, unrelated top-level dependency and re-lock, e.g.:
"google-auth-httplib2 ~= 0.0.4", + "cachetools ~= 5.3.3",Result:
protobufdrops from5.27.1to3.20.3. Booting the app (or runningpython -c "import email_handler") with this lockfile immediately failswith the
ImportErrorabove.We hit this for real: adding a dependency for an unrelated feature on a
fork triggered exactly this, taking down a production deployment until we
tracked it down (the failure mode gives no indication that
protobuf, ofall things, is the problem).
Suggested fix
protobufas a direct dependency with a floor that matches whatthe generated
event_pb2.pyactually requires, e.g.protobuf >= 4.25, < 6.cachetoolsas a direct dependency (it's directly imported inapp/email_utils.py), e.g.cachetools >= 5.0, < 6.google-api-python-clientoff the 2020-era~= 1.12.3pin to acurrent 2.x release (
>= 2.100, < 3) andgoogle-auth-httplib2to>= 0.2.0, < 1.0accordingly — the only call site(
app/api/views/auth.py,googleapiclient.discovery.build("oauth2", "v2", ...)) is unaffected by the major version bump.This resolves cleanly to stable (non-prerelease) versions across the board
(
protobuf 5.27.1,google-api-core 2.30.3) and removes the landmine forgood, rather than just re-locking back to a lucky state.
I'll open a PR with this fix, verified locally (dependency resolution +
container build + import smoke test for
app,email_handler.py,job_runner.py, and the Google OAuth code path).