Skip to content

Commit 460ae50

Browse files
Merge pull request #174 from kernel/release-please--branches--main--changes--next
release: 0.108.0
2 parents fdeb71f + 13781cb commit 460ae50

7 files changed

Lines changed: 361 additions & 3 deletions

File tree

‎.release-please-manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.107.0"
2+
".": "0.108.0"
33
}

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [0.108.0](https://github.com/kernel/kernel-python-sdk/compare/v0.107.0...v0.108.0) (2026-09-17)
4+
5+
6+
### Features
7+
8+
* feat: add config registry analysis waiter ([e38c887](https://github.com/kernel/kernel-python-sdk/commit/e38c8878f9f80f5d28fce9f92df6f0a18a48e2bb))
9+
310
## [0.107.0](https://github.com/kernel/kernel-python-sdk/compare/v0.106.0...v0.107.0) (2026-09-16)
411

512

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "kernel"
3-
version = "0.107.0"
3+
version = "0.108.0"
44
description = "The official Python library for the kernel API"
55
dynamic = ["readme"]
66
license = "Apache-2.0"

‎src/kernel/_version.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "kernel"
4-
__version__ = "0.107.0" # x-release-please-version
4+
__version__ = "0.108.0" # x-release-please-version
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import annotations
2+
3+
import math
4+
import time
5+
import random
6+
from datetime import datetime
7+
8+
from .._types import Omit, Headers
9+
from .._exceptions import KernelError
10+
from ..types.config_registry_response import ConfigRegistryResponse
11+
12+
DEFAULT_CONFIG_REGISTRY_POLL_INTERVAL = 5.0
13+
_TERMINAL_STATUSES = frozenset({"completed", "failed", "canceled", "expired"})
14+
15+
16+
def validate_wait_options(poll_interval: float, max_wait_seconds: float | None) -> None:
17+
if not math.isfinite(poll_interval) or poll_interval <= 0:
18+
raise ValueError("Expected a finite, positive value for `poll_interval`")
19+
if max_wait_seconds is not None and (not math.isfinite(max_wait_seconds) or max_wait_seconds < 0):
20+
raise ValueError("Expected a finite, non-negative value for `max_wait_seconds`")
21+
22+
23+
def poll_headers(extra_headers: Headers | None) -> Headers:
24+
headers: dict[str, str | Omit] = dict(extra_headers or {})
25+
headers["X-Stainless-Poll-Helper"] = "true"
26+
return headers
27+
28+
29+
def poll_delay(poll_interval: float) -> float:
30+
return poll_interval * random.uniform(0.9, 1.1)
31+
32+
33+
def wait_timeout_error(id: str, polls: int, last_status: str | None, started_at: float) -> TimeoutError:
34+
elapsed = time.monotonic() - started_at
35+
return TimeoutError(
36+
f"Timed out waiting for config registry analysis {id!r} after {elapsed:.1f}s "
37+
f"and {polls} polls; last status was {last_status!r}"
38+
)
39+
40+
41+
def analysis_finished(response: ConfigRegistryResponse, requested_id: str) -> tuple[bool, str]:
42+
analysis = response.analysis
43+
if analysis is None:
44+
raise KernelError(f"Config registry response for {requested_id!r} is missing an analysis")
45+
46+
analysis_id = getattr(analysis, "id", None)
47+
if not isinstance(analysis_id, str) or not analysis_id:
48+
raise KernelError(f"Config registry response for {requested_id!r} has no valid analysis ID")
49+
if analysis_id != requested_id:
50+
raise KernelError(f"Config registry response for {requested_id!r} returned analysis {analysis_id!r}")
51+
52+
status = getattr(analysis, "status", None)
53+
if not isinstance(status, str) or not status:
54+
raise KernelError(f"Config registry analysis {requested_id!r} has no valid status")
55+
56+
if "finished_at" not in analysis.model_fields_set:
57+
raise KernelError(f"Config registry analysis {requested_id!r} is missing `finished_at`")
58+
59+
finished_at = getattr(analysis, "finished_at", None)
60+
if finished_at is not None and not isinstance(finished_at, datetime):
61+
raise KernelError(f"Config registry analysis {requested_id!r} has an invalid `finished_at`")
62+
63+
return finished_at is not None or status in _TERMINAL_STATUSES, status

‎src/kernel/resources/config_registry/analyses.py‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import time
6+
57
import httpx
68

79
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
@@ -18,6 +20,14 @@
1820
from ..._base_client import AsyncPaginator, make_request_options
1921
from ...types.config_registry import analysis_list_params
2022
from ...types.analysis_summary import AnalysisSummary
23+
from ...lib.config_registry_wait import (
24+
DEFAULT_CONFIG_REGISTRY_POLL_INTERVAL,
25+
poll_delay,
26+
poll_headers,
27+
analysis_finished,
28+
wait_timeout_error,
29+
validate_wait_options,
30+
)
2131
from ...types.config_registry_response import ConfigRegistryResponse
2232

2333
__all__ = ["AnalysesResource", "AsyncAnalysesResource"]
@@ -79,6 +89,54 @@ def retrieve(
7989
cast_to=ConfigRegistryResponse,
8090
)
8191

92+
def wait_for_result(
93+
self,
94+
id: str,
95+
*,
96+
poll_interval: float = DEFAULT_CONFIG_REGISTRY_POLL_INTERVAL,
97+
max_wait_seconds: float | None = None,
98+
extra_headers: Headers | None = None,
99+
extra_query: Query | None = None,
100+
extra_body: Body | None = None,
101+
timeout: float | httpx.Timeout | None | NotGiven = not_given,
102+
) -> ConfigRegistryResponse:
103+
"""Wait for an analysis to finish and return its complete result.
104+
105+
The first retrieval happens immediately. ``max_wait_seconds`` is a soft
106+
polling deadline: an in-flight request and its normal retries may finish
107+
after it. Timing out does not cancel the remote analysis.
108+
"""
109+
validate_wait_options(poll_interval, max_wait_seconds)
110+
started_at = time.monotonic()
111+
deadline = started_at + max_wait_seconds if max_wait_seconds is not None else None
112+
headers = poll_headers(extra_headers)
113+
polls = 0
114+
last_status: str | None = None
115+
116+
while True:
117+
if polls > 0 and deadline is not None and time.monotonic() >= deadline:
118+
raise wait_timeout_error(id, polls, last_status, started_at)
119+
120+
response = self.retrieve(
121+
id,
122+
extra_headers=headers,
123+
extra_query=extra_query,
124+
extra_body=extra_body,
125+
timeout=timeout,
126+
)
127+
polls += 1
128+
finished, last_status = analysis_finished(response, id)
129+
if finished:
130+
return response
131+
132+
delay = poll_delay(poll_interval)
133+
if deadline is not None:
134+
remaining = deadline - time.monotonic()
135+
if remaining <= 0:
136+
raise wait_timeout_error(id, polls, last_status, started_at)
137+
delay = min(delay, remaining)
138+
self._sleep(delay)
139+
82140
def list(
83141
self,
84142
*,
@@ -220,6 +278,55 @@ async def retrieve(
220278
cast_to=ConfigRegistryResponse,
221279
)
222280

281+
async def wait_for_result(
282+
self,
283+
id: str,
284+
*,
285+
poll_interval: float = DEFAULT_CONFIG_REGISTRY_POLL_INTERVAL,
286+
max_wait_seconds: float | None = None,
287+
extra_headers: Headers | None = None,
288+
extra_query: Query | None = None,
289+
extra_body: Body | None = None,
290+
timeout: float | httpx.Timeout | None | NotGiven = not_given,
291+
) -> ConfigRegistryResponse:
292+
"""Wait for an analysis to finish and return its complete result.
293+
294+
The first retrieval happens immediately. ``max_wait_seconds`` is a soft
295+
polling deadline: an in-flight request and its normal retries may finish
296+
after it. Timing out does not cancel the remote analysis. Cancelling the
297+
calling task stops the wait without cancelling the remote analysis.
298+
"""
299+
validate_wait_options(poll_interval, max_wait_seconds)
300+
started_at = time.monotonic()
301+
deadline = started_at + max_wait_seconds if max_wait_seconds is not None else None
302+
headers = poll_headers(extra_headers)
303+
polls = 0
304+
last_status: str | None = None
305+
306+
while True:
307+
if polls > 0 and deadline is not None and time.monotonic() >= deadline:
308+
raise wait_timeout_error(id, polls, last_status, started_at)
309+
310+
response = await self.retrieve(
311+
id,
312+
extra_headers=headers,
313+
extra_query=extra_query,
314+
extra_body=extra_body,
315+
timeout=timeout,
316+
)
317+
polls += 1
318+
finished, last_status = analysis_finished(response, id)
319+
if finished:
320+
return response
321+
322+
delay = poll_delay(poll_interval)
323+
if deadline is not None:
324+
remaining = deadline - time.monotonic()
325+
if remaining <= 0:
326+
raise wait_timeout_error(id, polls, last_status, started_at)
327+
delay = min(delay, remaining)
328+
await self._sleep(delay)
329+
223330
def list(
224331
self,
225332
*,

0 commit comments

Comments
 (0)