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..905b3592 --- /dev/null +++ b/src/vercel/functions/deadline.py @@ -0,0 +1,24 @@ +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") + 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 new file mode 100644 index 00000000..cdb7631f --- /dev/null +++ b/src/vercel/tests/unit/test_functions_deadline.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import sys +from datetime import datetime, timezone +from types import SimpleNamespace + +import pytest + +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_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_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) + + with pytest.raises(ValueError, match="invalid deadline"): + get_deadline()