Skip to content
Open
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
1 change: 1 addition & 0 deletions python/changelog.d/+ingestion-backups-opt-in.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ingestion streaming clients now default to `StreamingMode.LIVE_ONLY`; pass `streaming_mode=StreamingMode.LIVE_WITH_BACKUPS` to enable local telemetry backups.
6 changes: 6 additions & 0 deletions python/docs/examples/ingestion.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
"pip install sift-stack-py[sift-stream]\n",
"```\n",
"\n",
"## Streaming backups\n",
"\n",
"Both streaming client factories default to `StreamingMode.LIVE_ONLY`: telemetry streams to Sift without local disk backups.\n",
"\n",
"To enable local backups and replay after delivery failures, import `StreamingMode` from `sift_client.resources.ingestion` and pass `streaming_mode=StreamingMode.LIVE_WITH_BACKUPS` to `create_ingestion_config_streaming_client` or `create_auto_register_streaming_client`.\n",
"\n",
"## Topics Covered\n",
"\n",
"1. **Basic Example**: Simple single flow sending using FlowConfig\n",
Expand Down
4 changes: 4 additions & 0 deletions python/examples/ingestion-tutorial/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ async def main() -> None:
# All telemetry sent within this context will appear inside
# this Run in Sift.
#
# Streaming defaults to LIVE_ONLY (no local telemetry backups).
# To opt in, import StreamingMode from sift_client.resources.ingestion
# and pass streaming_mode=StreamingMode.LIVE_WITH_BACKUPS below.
#
# WARNING: SiftStream should be reused for the entire program duration.
# Do NOT recreate the streaming client per batch or per iteration —
# each instantiation creates a new ingestion config, which introduces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ async def _build_sift_stream_instance(
_default_logfile_prefix = "sift_stream_bindings.log"

if streaming_mode is ...: # type: ignore[comparison-overlap]
streaming_mode = StreamingMode.LIVE_WITH_BACKUPS
streaming_mode = StreamingMode.LIVE_ONLY

# None disables metrics streaming, so it passes through to the builder as-is. Zero is
# rejected here because SiftStream cannot schedule metrics on a zero interval.
Expand Down Expand Up @@ -328,7 +328,7 @@ async def _build_sift_stream_instance(
fb_builder.disk_backup_policy = disk_backup_policy
return await fb_builder.build()

else: # LIVE_WITH_BACKUPS (default)
else: # LIVE_WITH_BACKUPS
lwb_builder = config_builder.live_with_backups()
lwb_builder.metrics_streaming_interval = metrics_interval
if retry_policy is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ async def _build(**kwargs):
)


class TestStreamingMode:
@pytest.mark.asyncio
async def test_defaults_to_live_only(self, config_builder):
await _build()

config_builder.live_only.return_value.build.assert_awaited_once()
config_builder.live_with_backups.assert_not_called()
config_builder.file_backup.assert_not_called()

@pytest.mark.asyncio
async def test_backups_are_explicit_opt_in(self, config_builder):
backup_policy = sift_stream_bindings.DiskBackupPolicyPy.default()
retry_policy = sift_stream_bindings.RetryPolicyPy.default()

await _build(
streaming_mode=StreamingMode.LIVE_WITH_BACKUPS,
disk_backup_policy=backup_policy,
retry_policy=retry_policy,
checkpoint_interval_seconds=30,
)

mode_builder = config_builder.live_with_backups.return_value
mode_builder.build.assert_awaited_once()
assert mode_builder.disk_backup_policy is backup_policy
assert mode_builder.retry_policy is retry_policy
assert mode_builder.checkpoint_interval.to_timedelta() == timedelta(seconds=30)
config_builder.live_only.assert_not_called()
config_builder.file_backup.assert_not_called()


@pytest.mark.parametrize("streaming_mode", list(MODE_BUILDER_METHODS))
class TestMetricsStreamingInterval:
@pytest.mark.asyncio
Expand Down
35 changes: 34 additions & 1 deletion python/lib/sift_client/_tests/resources/test_ingestion.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests that rate-limit errors from SiftStream reach callers of the streaming clients unchanged."""
"""Tests for streaming client configuration and rate-limit error propagation."""

from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock
Expand All @@ -7,12 +7,45 @@

from sift_client.resources.ingestion import (
AutoRegisterStreamingClient,
IngestionAPIAsync,
IngestionConfigStreamingClient,
StreamingMode,
)

sift_stream_bindings = pytest.importorskip("sift_stream_bindings")


@pytest.mark.asyncio
@pytest.mark.parametrize(
"factory_name",
["create_ingestion_config_streaming_client", "create_auto_register_streaming_client"],
)
@pytest.mark.parametrize("streaming_mode", [None, StreamingMode.LIVE_WITH_BACKUPS])
async def test_streaming_mode_default_and_opt_in(
mock_client, monkeypatch, factory_name, streaming_mode
):
build_stream = AsyncMock()
monkeypatch.setattr(
"sift_client._internal.low_level_wrappers.ingestion._build_sift_stream_instance",
build_stream,
)
monkeypatch.setattr("sift_client.resources.ingestion._build_sift_stream_instance", build_stream)
monkeypatch.setattr(
sift_stream_bindings,
"SiftStreamAutoRegisterPy",
MagicMock(from_stream=AsyncMock()),
)
api = IngestionAPIAsync(mock_client)
kwargs = {} if streaming_mode is None else {"streaming_mode": streaming_mode}

await getattr(api, factory_name)(ingestion_config=MagicMock(), **kwargs)

build_stream.assert_awaited_once()
assert build_stream.call_args.kwargs["streaming_mode"] == (
StreamingMode.LIVE_ONLY if streaming_mode is None else streaming_mode
)


def _rate_limited_error():
return sift_stream_bindings.RateLimitedErrorPy(
"[RateLimitedError] rate limited",
Expand Down
17 changes: 10 additions & 7 deletions python/lib/sift_client/resources/ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ async def create_ingestion_config_streaming_client(
run: RunCreate | dict | str | Run | None = None,
asset_tags: list[str] | list[Tag] | None = None,
asset_metadata: dict[str, str | float | bool] | None = None,
streaming_mode: StreamingMode = StreamingMode.LIVE_WITH_BACKUPS,
streaming_mode: StreamingMode = StreamingMode.LIVE_ONLY,
retry_policy: RetryPolicyPy | None = None,
disk_backup_policy: DiskBackupPolicyPy | None = None,
checkpoint_interval_seconds: int | None = None,
Expand All @@ -175,7 +175,8 @@ async def create_ingestion_config_streaming_client(
run: The run to associate with ingestion. Can be a Run, RunCreate, dict, or run ID string.
asset_tags: Tags to associate with the asset.
asset_metadata: Metadata to associate with the asset.
streaming_mode: Transport mode for the stream. Defaults to LIVE_WITH_BACKUPS.
streaming_mode: Transport mode for the stream. Defaults to LIVE_ONLY, without disk
backups. Set to LIVE_WITH_BACKUPS to opt in to local disk backups.
retry_policy: Retry policy for LIVE_WITH_BACKUPS mode.
disk_backup_policy: Disk backup policy for LIVE_WITH_BACKUPS or FILE_BACKUP mode.
checkpoint_interval_seconds: Checkpoint interval in seconds (LIVE_WITH_BACKUPS only).
Expand Down Expand Up @@ -223,7 +224,7 @@ async def create_auto_register_streaming_client(
run: RunCreate | dict | str | Run | None = None,
asset_tags: list[str] | list[Tag] | None = None,
asset_metadata: dict[str, str | float | bool] | None = None,
streaming_mode: StreamingMode = StreamingMode.LIVE_WITH_BACKUPS,
streaming_mode: StreamingMode = StreamingMode.LIVE_ONLY,
retry_policy: RetryPolicyPy | None = None,
disk_backup_policy: DiskBackupPolicyPy | None = None,
checkpoint_interval_seconds: int | None = None,
Expand Down Expand Up @@ -256,7 +257,8 @@ async def create_auto_register_streaming_client(
run: The run to associate with ingestion. Can be a Run, RunCreate, dict, or run ID string.
asset_tags: Tags to associate with the asset.
asset_metadata: Metadata to associate with the asset.
streaming_mode: Transport mode for the stream. Defaults to LIVE_WITH_BACKUPS.
streaming_mode: Transport mode for the stream. Defaults to LIVE_ONLY, without disk
backups. Set to LIVE_WITH_BACKUPS to opt in to local disk backups.
retry_policy: Retry policy for LIVE_WITH_BACKUPS mode.
disk_backup_policy: Disk backup policy for LIVE_WITH_BACKUPS or FILE_BACKUP mode.
checkpoint_interval_seconds: Checkpoint interval in seconds (LIVE_WITH_BACKUPS only).
Expand Down Expand Up @@ -327,7 +329,7 @@ async def _create(
run: RunCreate | dict | str | Run | RunFormPy | None = None,
asset_tags: list[str] | list[Tag] | None = None,
asset_metadata: dict[str, str | float | bool] | None = None,
streaming_mode: StreamingMode = StreamingMode.LIVE_WITH_BACKUPS,
streaming_mode: StreamingMode = StreamingMode.LIVE_ONLY,
retry_policy: RetryPolicyPy | None = None,
disk_backup_policy: DiskBackupPolicyPy | None = None,
checkpoint_interval_seconds: int | None = None,
Expand All @@ -344,7 +346,8 @@ async def _create(
run: The run to associate with ingestion. Can be a Run, RunCreate, dict, or run ID string.
asset_tags: Tags to associate with the asset.
asset_metadata: Metadata to associate with the asset.
streaming_mode: Transport mode for the stream. Defaults to LIVE_WITH_BACKUPS.
streaming_mode: Transport mode for the stream. Defaults to LIVE_ONLY, without disk
backups. Set to LIVE_WITH_BACKUPS to opt in to local disk backups.
retry_policy: Retry policy for LIVE_WITH_BACKUPS mode.
disk_backup_policy: Disk backup policy for LIVE_WITH_BACKUPS or FILE_BACKUP mode.
checkpoint_interval_seconds: Checkpoint interval in seconds (LIVE_WITH_BACKUPS only).
Expand Down Expand Up @@ -686,7 +689,7 @@ async def _create(
run: RunCreate | dict | str | Run | RunFormPy | None = None,
asset_tags: list[str] | list[Tag] | None = None,
asset_metadata: dict[str, str | float | bool] | None = None,
streaming_mode: StreamingMode = StreamingMode.LIVE_WITH_BACKUPS,
streaming_mode: StreamingMode = StreamingMode.LIVE_ONLY,
retry_policy: RetryPolicyPy | None = None,
disk_backup_policy: DiskBackupPolicyPy | None = None,
checkpoint_interval_seconds: int | None = None,
Expand Down
Loading