Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions python/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

- Fix a test-results log that could never finish uploading. An update whose fields were all `None`, or whose keys were not fields of the update model, produced an empty field mask. The client logged it without complaint, then the API rejected it at import. The replay cursor only advances past a line that succeeded, so every retry stopped at the same line and stranded the rest of the log. Such an update is now a no-op that is neither logged nor sent, and a logged one is skipped at import. Re-run `import-test-result-log` to finish a log written by an earlier version.
- A key that is not a field of a create or update model now warns, instead of being dropped in silence. The warning suggests a near match when the key resembles a real field. Both this and an update that names no fields raise the new `SiftIgnoredInputWarning`. Add `error::sift_client.errors.SiftIgnoredInputWarning` to `filterwarnings` to make either one fail the call.
- Uploading a test-results log is faster. A run of consecutive measurement lines now reaches the server through `CreateTestMeasurements` as one call, rather than one call per measurement. On a 13,631-line log this cut the calls from 13,628 to 4,021 and the upload from about 12 minutes to about 3.5. Step creates and updates still take one call each.

## [v0.22.1] - September 29, 2026

- Fix streaming ingestion processes crashing with SIGSEGV or SIGABRT at interpreter exit. The `sift-stream-bindings` tokio runtime was never shut down, so a runtime thread that finished work while Python was finalizing re-entered the interpreter and killed the process after the program had completed. The bindings now stop the runtime from an `atexit` hook and expose `shutdown(timeout=5.0)` for callers that exit with `os._exit()`. If runtime threads outlive the timeout, `shutdown()` issues a `RuntimeWarning`. Requires `sift-stream-bindings` 0.5.2. ([#804](https://github.com/sift-stack/sift/pull/804))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ class LogTracking:
file itself is append-only and stores only API-call data lines.

* ``last_uploaded_line`` is the count of data lines that have been
successfully replayed against the server, in log order. Each data line
corresponds to a single API call, so line granularity matches the atomic
unit of work: a line is either fully replayed or must be retried in its
entirety. Data lines are strictly append-only, so this counter is stable
across runs. A batch upload creates in collapsed order rather than log
order and so leaves it at zero; ``complete`` is what marks that upload
finished.
successfully replayed against the server, in log order. A line is either
fully replayed or must be retried in its entirety. Most lines are one API
call, and a run of consecutive measurement lines is one batch call, so the
counter advances by the whole run once that call returns. Data lines are
strictly append-only, so this counter is stable across runs. A batch upload
creates in collapsed order rather than log order and so leaves it at zero;
``complete`` is what marks that upload finished.
* ``id_map`` maps simulated response IDs (created during the original test
run) to the real IDs assigned by the server during replay. Subsequent
``Update*`` entries consult this map to translate IDs, and a resumed
Expand Down
334 changes: 293 additions & 41 deletions python/lib/sift_client/_internal/low_level_wrappers/test_results.py

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions python/lib/sift_client/_internal/util/util.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
from __future__ import annotations

import inspect
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from collections.abc import Iterator

_PACKAGE_ROOT = str(Path(__file__).resolve().parent.parent.parent)
_PYDANTIC_PATH = f"{os.sep}pydantic{os.sep}"


def count_non_none(*args: Any) -> int:
"""Count the number of non-none arguments."""
return sum(1 for arg in args if arg is not None)


def caller_stacklevel() -> int:
"""Return the ``warnings.warn`` stacklevel of the first frame outside the SDK.

A fixed stacklevel points at SDK or pydantic internals, which tells the
caller nothing about which of their lines caused the warning.

Call this from the function that calls ``warnings.warn``.
"""
frame = inspect.currentframe()
frame = frame.f_back if frame is not None else None # the function calling warn()
level = 1
while frame is not None:
filename = frame.f_code.co_filename
if not filename.startswith(_PACKAGE_ROOT) and _PYDANTIC_PATH not in filename:
return level
frame = frame.f_back
level += 1
return 2


def chunked(items: list[Any], size: int) -> Iterator[list[Any]]:
"""Yield successive chunks of at most ``size`` items."""
for i in range(0, len(items), size):
Expand Down
Loading
Loading