From 8b6708d9b5c64869304d1abb90a0e5ae0f12cfe1 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Tue, 18 Aug 2026 21:03:20 -0700 Subject: [PATCH 1/2] Expose Function invocation deadlines --- changes/vercel/deadline.feature.md | 1 + src/vercel/functions/README.md | 6 ++++ src/vercel/functions/__init__.py | 2 ++ src/vercel/functions/deadline.py | 18 +++++++++++ .../tests/unit/test_functions_deadline.py | 31 +++++++++++++++++++ 5 files changed, 58 insertions(+) create mode 100644 changes/vercel/deadline.feature.md create mode 100644 src/vercel/functions/deadline.py create mode 100644 src/vercel/tests/unit/test_functions_deadline.py diff --git a/changes/vercel/deadline.feature.md b/changes/vercel/deadline.feature.md new file mode 100644 index 00000000..f7b3692c --- /dev/null +++ b/changes/vercel/deadline.feature.md @@ -0,0 +1 @@ +Expose `get_deadline()` for reading the current Function invocation deadline. diff --git a/src/vercel/functions/README.md b/src/vercel/functions/README.md index 5eeec9f3..4e4a4b49 100644 --- a/src/vercel/functions/README.md +++ b/src/vercel/functions/README.md @@ -7,6 +7,7 @@ Functions. from vercel.functions import ( AsyncRuntimeCache, geolocation, + get_deadline, get_env, ip_address, set_headers, @@ -19,6 +20,7 @@ async def handler(request): wait_until(record_request_analytics(request)) env = get_env() + deadline = get_deadline() cache = AsyncRuntimeCache(namespace="api") await cache.set("last_region", env.VERCEL_REGION, {"ttl": 60}) @@ -26,6 +28,7 @@ async def handler(request): "ip": ip_address(request), "geo": geolocation(request), "region": env.VERCEL_REGION, + "deadline": deadline.isoformat() if deadline else None, } ``` @@ -33,6 +36,9 @@ Exports include environment helpers from `vercel.env`, header and geolocation helpers from `vercel.headers`, cache clients from `vercel.cache`, and `wait_until()` for work that should finish after the response is sent. +`get_deadline()` returns the current invocation deadline as a timezone-aware UTC +`datetime`, or `None` when the runtime does not provide one. + `wait_until()` is not a durable task queue. Its work must finish within the Function's configured maximum duration, and it is not retried if the invocation terminates. It accepts awaitables only; run synchronous work with diff --git a/src/vercel/functions/__init__.py b/src/vercel/functions/__init__.py index 8fe6d7df..60a0d298 100644 --- a/src/vercel/functions/__init__.py +++ b/src/vercel/functions/__init__.py @@ -1,6 +1,7 @@ from ..cache import AsyncRuntimeCache, RuntimeCache, get_cache from ..env import Env, get_env from ..headers import Geo, geolocation, get_headers, ip_address, set_headers +from .deadline import get_deadline from .wait_until import wait_until __all__ = [ @@ -15,4 +16,5 @@ "RuntimeCache", "AsyncRuntimeCache", "wait_until", + "get_deadline", ] diff --git a/src/vercel/functions/deadline.py b/src/vercel/functions/deadline.py new file mode 100644 index 00000000..2278c3a3 --- /dev/null +++ b/src/vercel/functions/deadline.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from datetime import datetime +from importlib import import_module + + +def get_deadline() -> datetime | None: + """Return the current Function invocation deadline, if available.""" + try: + runtime = import_module("vercel_runtime") + runtime_get_deadline = getattr(runtime, "get_deadline", None) + if not callable(runtime_get_deadline): + return None + value = runtime_get_deadline() + except Exception: + return None + + return value if isinstance(value, datetime) else None diff --git a/src/vercel/tests/unit/test_functions_deadline.py b/src/vercel/tests/unit/test_functions_deadline.py new file mode 100644 index 00000000..411fde56 --- /dev/null +++ b/src/vercel/tests/unit/test_functions_deadline.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import sys +from datetime import datetime, timezone +from types import SimpleNamespace + +from vercel.functions import get_deadline + + +def test_get_deadline_reads_runtime_context(monkeypatch) -> None: + expected = datetime(2026, 8, 18, 12, 30, tzinfo=timezone.utc) + runtime = SimpleNamespace(get_deadline=lambda: expected) + monkeypatch.setitem(sys.modules, "vercel_runtime", runtime) + + assert get_deadline() == expected + + +def test_get_deadline_returns_none_without_runtime_support(monkeypatch) -> None: + monkeypatch.setitem(sys.modules, "vercel_runtime", SimpleNamespace()) + + assert get_deadline() is None + + +def test_get_deadline_never_propagates_runtime_errors(monkeypatch) -> None: + def raise_error() -> None: + raise ValueError("invalid deadline") + + runtime = SimpleNamespace(get_deadline=raise_error) + monkeypatch.setitem(sys.modules, "vercel_runtime", runtime) + + assert get_deadline() is None From bb601cdeb7382dc3598d80b406dbbb9f7a4ec157 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Tue, 18 Aug 2026 22:02:35 -0700 Subject: [PATCH 2/2] Silence only expected deadline absence Narrow the get_deadline() accessor so only two expected states return None: vercel_runtime absent, or an older runtime without the accessor. Broken installs and runtime bugs now surface instead of reading as a missing deadline. --- src/vercel/functions/deadline.py | 16 +++++++++++----- src/vercel/tests/unit/test_functions_deadline.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/vercel/functions/deadline.py b/src/vercel/functions/deadline.py index 2278c3a3..905b3592 100644 --- a/src/vercel/functions/deadline.py +++ b/src/vercel/functions/deadline.py @@ -8,11 +8,17 @@ def get_deadline() -> datetime | None: """Return the current Function invocation deadline, if available.""" try: runtime = import_module("vercel_runtime") - runtime_get_deadline = getattr(runtime, "get_deadline", None) - if not callable(runtime_get_deadline): - return None - value = runtime_get_deadline() - except Exception: + except ModuleNotFoundError as exc: + # Only the runtime itself being absent means "not on Vercel". A + # missing transitive dependency is a broken install and must surface. + if exc.name != "vercel_runtime": + raise return None + accessor = getattr(runtime, "get_deadline", None) + if not callable(accessor): + # Older runtime without deadline support. + return None + + value = accessor() return value if isinstance(value, datetime) else None diff --git a/src/vercel/tests/unit/test_functions_deadline.py b/src/vercel/tests/unit/test_functions_deadline.py index 411fde56..cdb7631f 100644 --- a/src/vercel/tests/unit/test_functions_deadline.py +++ b/src/vercel/tests/unit/test_functions_deadline.py @@ -4,6 +4,8 @@ from datetime import datetime, timezone from types import SimpleNamespace +import pytest + from vercel.functions import get_deadline @@ -15,17 +17,24 @@ def test_get_deadline_reads_runtime_context(monkeypatch) -> None: assert get_deadline() == expected +def test_get_deadline_returns_none_outside_vercel(monkeypatch) -> None: + monkeypatch.delitem(sys.modules, "vercel_runtime", raising=False) + + assert get_deadline() is None + + def test_get_deadline_returns_none_without_runtime_support(monkeypatch) -> None: monkeypatch.setitem(sys.modules, "vercel_runtime", SimpleNamespace()) assert get_deadline() is None -def test_get_deadline_never_propagates_runtime_errors(monkeypatch) -> None: +def test_get_deadline_propagates_runtime_bugs(monkeypatch) -> None: def raise_error() -> None: raise ValueError("invalid deadline") runtime = SimpleNamespace(get_deadline=raise_error) monkeypatch.setitem(sys.modules, "vercel_runtime", runtime) - assert get_deadline() is None + with pytest.raises(ValueError, match="invalid deadline"): + get_deadline()