snowflake-sandbox-python 0.2.2a2__tar.gz → 0.2.2a3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {snowflake_sandbox_python-0.2.2a2/src/snowflake_sandbox_python.egg-info → snowflake_sandbox_python-0.2.2a3}/PKG-INFO +1 -1
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/pyproject.toml +1 -1
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_version.py +1 -1
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_wire.py +60 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/client.py +43 -3
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/jobs.py +6 -5
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/log_stream.py +8 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/sync_client.py +41 -3
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/sync_log_stream.py +8 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/warm_session.py +10 -7
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3/src/snowflake_sandbox_python.egg-info}/PKG-INFO +1 -1
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/LICENSE +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/README.md +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/setup.cfg +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/setup.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/__init__.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/_adapter.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/_common.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/_egress_flags.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/_get_command.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/_run_command.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/_shell_command.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/_upload_plan.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/commands.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/plugin_spec.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/cli_sandbox/py.typed +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/__init__.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/__main__.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_ansi.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_args.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_assemble.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_bundle.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_connection_resolve.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_deploy_spec.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_diagnostics.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_env.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_files_mixin.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_fs_ops.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_globs.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_hosts.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_mcp_discovery.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_mcp_status.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_retry.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_runtime/__init__.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_runtime/_fs_helper.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_runtime/_job_runner.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_runtime/_protocol.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_runtime/_shims.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_sandbox_state.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_session_registry.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_sse.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_stage.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_sync_files_mixin.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_sync_fs_ops.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_sync_transport.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_sync_watch.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_transport.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_transport_errors.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_upload_plan.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_watch.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/app.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/config.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/connect.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/deploy.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/egress.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/exceptions.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/exec_stream.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/failure.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/files.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/function.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/image.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/lifecycle.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/mcp.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/mount.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/py.typed +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/secret.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/session_app.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/shell.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/sync_exec_stream.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/sync_files.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/sync_shell.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/types.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake_sandbox_python.egg-info/SOURCES.txt +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake_sandbox_python.egg-info/dependency_links.txt +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake_sandbox_python.egg-info/entry_points.txt +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake_sandbox_python.egg-info/requires.txt +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake_sandbox_python.egg-info/top_level.txt +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/tests/test_docs_references.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/tests/test_imports.py +0 -0
- {snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/tests/test_smoke_offline.py +0 -0
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_wire.py
RENAMED
|
@@ -15,6 +15,7 @@ from collections.abc import Mapping, Sequence
|
|
|
15
15
|
from datetime import timedelta
|
|
16
16
|
from typing import TYPE_CHECKING, Any, get_args
|
|
17
17
|
|
|
18
|
+
from snowflake.sandbox._runtime._protocol import _REPLY_SENTINEL, _RESULT_SENTINEL
|
|
18
19
|
from snowflake.sandbox.exceptions import SandboxContractWarning, SandboxError
|
|
19
20
|
from snowflake.sandbox.types import MemoryTier, SandboxStatus
|
|
20
21
|
|
|
@@ -23,6 +24,10 @@ if TYPE_CHECKING:
|
|
|
23
24
|
|
|
24
25
|
__all__ = [
|
|
25
26
|
"_parse_status",
|
|
27
|
+
"_is_internal_sentinel_line",
|
|
28
|
+
"_visible_log_lines",
|
|
29
|
+
"_logs_overfetch",
|
|
30
|
+
"_LOGS_OVERFETCH_CAP",
|
|
26
31
|
"_warn_if_truncated",
|
|
27
32
|
"_exec_session_id",
|
|
28
33
|
"_resolve_exec_budget",
|
|
@@ -428,3 +433,58 @@ def _exec_reconnect_exhausted(attempt: int, *, delivered: bool) -> bool:
|
|
|
428
433
|
never trips this.
|
|
429
434
|
"""
|
|
430
435
|
return not delivered and attempt >= len(_EXEC_RECONNECT_DELAYS)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# --- internal protocol lines that must never reach a caller's logs() -------------
|
|
439
|
+
#
|
|
440
|
+
# The in-sandbox runner and session daemon report over STDOUT, so their markers land in
|
|
441
|
+
# the log stream alongside the workload's own output: `__SANDBOX_RESULT__<b64>` on
|
|
442
|
+
# managed-process exit and `__SANDBOX_REPLY__<...>` per session reply. They are transport,
|
|
443
|
+
# not output -- `logs()` suppressing them is what LogStream already does for the result
|
|
444
|
+
# marker ("suppress the internal marker from output").
|
|
445
|
+
#
|
|
446
|
+
# They CANNOT be stripped server-side: they are an SDK-level protocol the API knows
|
|
447
|
+
# nothing about, and `_protocol.py` calls their values a compatibility surface (a sandbox
|
|
448
|
+
# launched by an older SDK is still running the older wrapper text), so the wrapper cannot
|
|
449
|
+
# stop printing them either. Internal consumers therefore read the UNFILTERED text via
|
|
450
|
+
# `_logs_raw()`; only the public `logs()` filters.
|
|
451
|
+
_INTERNAL_SENTINELS = (_RESULT_SENTINEL, _REPLY_SENTINEL)
|
|
452
|
+
|
|
453
|
+
# How many extra lines to ask the server for when a caller wants the last `tail` VISIBLE
|
|
454
|
+
# lines. `tail` is applied SERVER-side, before filtering, so asking for exactly `tail`
|
|
455
|
+
# and then dropping a sentinel returns fewer lines than requested -- the bug that made
|
|
456
|
+
# logs(tail=2) answer ['line5', '__SANDBOX_RESULT__...'] and then, once filtered,
|
|
457
|
+
# ['line5'] alone. The fetch grows until enough visible lines come back, the server runs
|
|
458
|
+
# out of log, or the cap is hit; the cap keeps a session whose recent history is mostly
|
|
459
|
+
# reply markers from turning one logs() call into an unbounded fetch.
|
|
460
|
+
_LOGS_OVERFETCH_MARGIN = 8
|
|
461
|
+
_LOGS_OVERFETCH_CAP = 4096
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _is_internal_sentinel_line(text: str) -> bool:
|
|
465
|
+
"""True when a log line carries an SDK-internal protocol marker."""
|
|
466
|
+
return any(sentinel in text for sentinel in _INTERNAL_SENTINELS)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _visible_log_lines(raw_text: str) -> list[str]:
|
|
470
|
+
r"""The caller-visible lines of a raw log snapshot, internal markers removed.
|
|
471
|
+
|
|
472
|
+
An empty snapshot yields no lines rather than one empty line, so
|
|
473
|
+
``"\n".join(...)`` round-trips "" instead of inventing a blank line.
|
|
474
|
+
"""
|
|
475
|
+
if not raw_text:
|
|
476
|
+
return []
|
|
477
|
+
return [ln for ln in raw_text.split("\n") if not _is_internal_sentinel_line(ln)]
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _logs_overfetch(tail: int, previous: int = 0) -> int:
|
|
481
|
+
"""Server-side `tail` to request in order to end up with `tail` VISIBLE lines.
|
|
482
|
+
|
|
483
|
+
`tail=0` means "everything", which needs no widening. Otherwise start a margin above
|
|
484
|
+
the request and double on each retry, capped -- see `_LOGS_OVERFETCH_CAP`.
|
|
485
|
+
"""
|
|
486
|
+
if tail <= 0:
|
|
487
|
+
return 0
|
|
488
|
+
if previous <= 0:
|
|
489
|
+
return tail + _LOGS_OVERFETCH_MARGIN
|
|
490
|
+
return min(previous * 2, _LOGS_OVERFETCH_CAP)
|
|
@@ -57,6 +57,7 @@ from snowflake.sandbox._transport import (
|
|
|
57
57
|
from snowflake.sandbox._wire import (
|
|
58
58
|
_EXEC_RECONNECT_DELAYS,
|
|
59
59
|
_EXEC_TIMEOUT_GRACE_S,
|
|
60
|
+
_LOGS_OVERFETCH_CAP,
|
|
60
61
|
_POLL_UNKNOWN_EXIT_CODE,
|
|
61
62
|
_PROBE_BUDGET_S,
|
|
62
63
|
_PROBE_NOT_FOUND_RETRY_S,
|
|
@@ -66,8 +67,10 @@ from snowflake.sandbox._wire import (
|
|
|
66
67
|
_exec_session_id,
|
|
67
68
|
_fresh_exec_body,
|
|
68
69
|
_idle_suspend_minutes,
|
|
70
|
+
_logs_overfetch,
|
|
69
71
|
_parse_status,
|
|
70
72
|
_resolve_exec_budget,
|
|
73
|
+
_visible_log_lines,
|
|
71
74
|
_warn_if_truncated,
|
|
72
75
|
)
|
|
73
76
|
from snowflake.sandbox.egress import Egress, compile_egress
|
|
@@ -772,10 +775,45 @@ class AsyncSandbox(_FilesMixin, _SandboxState):
|
|
|
772
775
|
`tail` limits to the last N lines. Only meaningful for command containers
|
|
773
776
|
(created with `command=`).
|
|
774
777
|
|
|
778
|
+
SDK-internal protocol markers are suppressed: the in-sandbox runner reports a
|
|
779
|
+
run's outcome as `__SANDBOX_RESULT__<b64>` on stdout and the session daemon
|
|
780
|
+
reports replies as `__SANDBOX_REPLY__<...>`, so both land in the log stream
|
|
781
|
+
beside the workload's own output. They are transport, not output. `tail` counts
|
|
782
|
+
VISIBLE lines, so `logs(tail=2)` on a command printing `line1..line5` answers the two
|
|
783
|
+
lines `line4` and `line5`, never spending one of the two on a marker.
|
|
784
|
+
|
|
775
785
|
Example:
|
|
776
786
|
output = await sb.logs(tail=100)
|
|
777
787
|
print(output)
|
|
778
788
|
"""
|
|
789
|
+
fetch = _logs_overfetch(tail)
|
|
790
|
+
while True:
|
|
791
|
+
raw = await self._logs_raw(tail=fetch, since_ts_ms=since_ts_ms)
|
|
792
|
+
raw_count = len(raw.split("\n")) if raw else 0
|
|
793
|
+
visible = _visible_log_lines(raw)
|
|
794
|
+
if tail <= 0 or len(visible) >= tail:
|
|
795
|
+
break
|
|
796
|
+
# Short of `tail` visible lines. Only a WIDER fetch can help, and only while
|
|
797
|
+
# the server still has more to give: `raw_count < fetch` means we already hold
|
|
798
|
+
# the whole log, so filtering -- not the window -- is why there are fewer.
|
|
799
|
+
if raw_count < fetch or fetch >= _LOGS_OVERFETCH_CAP:
|
|
800
|
+
break
|
|
801
|
+
fetch = _logs_overfetch(tail, fetch)
|
|
802
|
+
# `tail > 0`, not `if tail`: a NEGATIVE tail is truthy and visible[-(-1):] is
|
|
803
|
+
# visible[1:], which would silently drop the FIRST lines instead of returning
|
|
804
|
+
# everything. `_logs_overfetch` already treats tail <= 0 as "no window".
|
|
805
|
+
if tail > 0:
|
|
806
|
+
visible = visible[-tail:]
|
|
807
|
+
return "\n".join(visible)
|
|
808
|
+
|
|
809
|
+
async def _logs_raw(self, *, tail: int = 0, since_ts_ms: int = 0) -> str:
|
|
810
|
+
"""The log snapshot EXACTLY as the server returns it, markers included.
|
|
811
|
+
|
|
812
|
+
Internal consumers that read the runner's or daemon's stdout protocol
|
|
813
|
+
(`wait()`, `Job.get`/`Job.status`, `send()`) must use this, not `logs()` --
|
|
814
|
+
`logs()` strips the very lines they parse. Not public: a caller wanting the
|
|
815
|
+
sandbox's output wants `logs()`.
|
|
816
|
+
"""
|
|
779
817
|
if not self._created:
|
|
780
818
|
await self._ensure_created()
|
|
781
819
|
if self._id is None: # pragma: no cover - defensive
|
|
@@ -1852,7 +1890,7 @@ class AsyncSandbox(_FilesMixin, _SandboxState):
|
|
|
1852
1890
|
# it (the connect() reconnect path) fall through to the server-reported
|
|
1853
1891
|
# exit code rather than accepting a forgeable, unauthenticated line.
|
|
1854
1892
|
if self._result_nonce is not None:
|
|
1855
|
-
parsed = _parse_result_sentinel(await self.
|
|
1893
|
+
parsed = _parse_result_sentinel(await self._logs_raw(), nonce=self._result_nonce)
|
|
1856
1894
|
if parsed is not None:
|
|
1857
1895
|
parsed.logs_ref = self._id
|
|
1858
1896
|
return parsed
|
|
@@ -1910,7 +1948,7 @@ class AsyncSandbox(_FilesMixin, _SandboxState):
|
|
|
1910
1948
|
await self.exec(_mailbox_append_cmd(msg_id, message))
|
|
1911
1949
|
deadline = _time.monotonic() + timeout
|
|
1912
1950
|
while True:
|
|
1913
|
-
reply = _parse_reply_sentinel(await self.
|
|
1951
|
+
reply = _parse_reply_sentinel(await self._logs_raw(), msg_id, nonce=self._reply_nonce)
|
|
1914
1952
|
if reply is not None:
|
|
1915
1953
|
return reply
|
|
1916
1954
|
# A terminal container will never reply; surface that rather than spending
|
|
@@ -1918,7 +1956,9 @@ class AsyncSandbox(_FilesMixin, _SandboxState):
|
|
|
1918
1956
|
# the cached status) is the only live read of the container's lifecycle.
|
|
1919
1957
|
if await self.refresh() in TERMINAL_STATUSES:
|
|
1920
1958
|
# One last read: the daemon may have replied just before exiting.
|
|
1921
|
-
reply = _parse_reply_sentinel(
|
|
1959
|
+
reply = _parse_reply_sentinel(
|
|
1960
|
+
await self._logs_raw(), msg_id, nonce=self._reply_nonce
|
|
1961
|
+
)
|
|
1922
1962
|
if reply is not None:
|
|
1923
1963
|
return reply
|
|
1924
1964
|
raise SandboxError(_ended_before_reply(self.status, msg_id))
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/jobs.py
RENAMED
|
@@ -211,7 +211,7 @@ class Job(_JobHandle):
|
|
|
211
211
|
terminal state and server-reported exit code, never a forgeable line."""
|
|
212
212
|
sb = await get_sandbox(self.id, connection=self._connection)
|
|
213
213
|
if self.nonce is not None:
|
|
214
|
-
parsed = _parse_result_sentinel(await sb.
|
|
214
|
+
parsed = _parse_result_sentinel(await sb._logs_raw(), nonce=self.nonce)
|
|
215
215
|
if parsed is not None:
|
|
216
216
|
# A trusted sentinel reports succeeded/failed; status() is a live
|
|
217
217
|
# lifecycle verdict where timed_out is not a member, so fold it in.
|
|
@@ -226,7 +226,8 @@ class Job(_JobHandle):
|
|
|
226
226
|
async def get(self, timeout: float | None = None, poll_s: float = 3.0) -> RunResult:
|
|
227
227
|
"""Poll for the Job's result.
|
|
228
228
|
|
|
229
|
-
Fetches ``get_sandbox(id).
|
|
229
|
+
Fetches ``get_sandbox(id)._logs_raw()`` -- the UNFILTERED channel, because the
|
|
230
|
+
public ``logs()`` strips the very sentinel this scans for -- and takes the LAST one;
|
|
230
231
|
returns the decoded `RunResult` as soon as one appears. If the
|
|
231
232
|
container reaches a terminal state (``dead`` or ``failed``) with no sentinel, returns
|
|
232
233
|
``RunResult(status="failed", ...)``. On *timeout* (when set) before
|
|
@@ -239,7 +240,7 @@ class Job(_JobHandle):
|
|
|
239
240
|
# Only trust the stdout sentinel when we hold the run's nonce;
|
|
240
241
|
# a from_id() handle has none, so fall through to the server state.
|
|
241
242
|
if self.nonce is not None:
|
|
242
|
-
parsed = _parse_result_sentinel(await sb.
|
|
243
|
+
parsed = _parse_result_sentinel(await sb._logs_raw(), nonce=self.nonce)
|
|
243
244
|
if parsed is not None:
|
|
244
245
|
parsed.logs_ref = self.id
|
|
245
246
|
return parsed
|
|
@@ -288,7 +289,7 @@ class SyncJob(_JobHandle):
|
|
|
288
289
|
container's own terminal state."""
|
|
289
290
|
sb = get_sandbox_sync(self.id, connection=self._connection)
|
|
290
291
|
if self.nonce is not None:
|
|
291
|
-
parsed = _parse_result_sentinel(sb.
|
|
292
|
+
parsed = _parse_result_sentinel(sb._logs_raw(), nonce=self.nonce)
|
|
292
293
|
if parsed is not None:
|
|
293
294
|
# See Job.status: fold the non-live timed_out into failed.
|
|
294
295
|
return "failed" if parsed.status == "timed_out" else parsed.status
|
|
@@ -308,7 +309,7 @@ class SyncJob(_JobHandle):
|
|
|
308
309
|
while True:
|
|
309
310
|
sb = get_sandbox_sync(self.id, connection=self._connection)
|
|
310
311
|
if self.nonce is not None:
|
|
311
|
-
parsed = _parse_result_sentinel(sb.
|
|
312
|
+
parsed = _parse_result_sentinel(sb._logs_raw(), nonce=self.nonce)
|
|
312
313
|
if parsed is not None:
|
|
313
314
|
parsed.logs_ref = self.id
|
|
314
315
|
return parsed
|
|
@@ -24,6 +24,7 @@ from collections.abc import AsyncIterator
|
|
|
24
24
|
from typing import TYPE_CHECKING, Any, Self
|
|
25
25
|
|
|
26
26
|
from snowflake.sandbox._runtime._protocol import _RESULT_SENTINEL
|
|
27
|
+
from snowflake.sandbox._wire import _is_internal_sentinel_line
|
|
27
28
|
from snowflake.sandbox.exceptions import SandboxConflictError, SandboxError
|
|
28
29
|
from snowflake.sandbox.types import TERMINAL_STATUSES, StreamName
|
|
29
30
|
|
|
@@ -105,6 +106,13 @@ class LogStream(AsyncIterator[str]):
|
|
|
105
106
|
if _RESULT_SENTINEL in text:
|
|
106
107
|
self._process_done = True
|
|
107
108
|
continue
|
|
109
|
+
# Any OTHER internal marker (the session daemon's __SANDBOX_REPLY__) is
|
|
110
|
+
# suppressed too, but is NOT end-of-stream -- a session keeps running after a
|
|
111
|
+
# reply. Checked after the result marker so that one still sets the flag.
|
|
112
|
+
# Without this the streamed view leaked a marker the snapshot logs() hides:
|
|
113
|
+
# same sandbox, two different answers about what its output was.
|
|
114
|
+
if _is_internal_sentinel_line(text):
|
|
115
|
+
continue
|
|
108
116
|
ts = int(ln.get("ts") or 0)
|
|
109
117
|
if ts:
|
|
110
118
|
# Compare against the batch-start watermark, not one mutated
|
|
@@ -55,6 +55,7 @@ from snowflake.sandbox._sync_transport import (
|
|
|
55
55
|
from snowflake.sandbox._wire import (
|
|
56
56
|
_EXEC_RECONNECT_DELAYS,
|
|
57
57
|
_EXEC_TIMEOUT_GRACE_S,
|
|
58
|
+
_LOGS_OVERFETCH_CAP,
|
|
58
59
|
_POLL_UNKNOWN_EXIT_CODE,
|
|
59
60
|
_PROBE_BUDGET_S,
|
|
60
61
|
_PROBE_NOT_FOUND_RETRY_S,
|
|
@@ -64,8 +65,10 @@ from snowflake.sandbox._wire import (
|
|
|
64
65
|
_exec_session_id,
|
|
65
66
|
_fresh_exec_body,
|
|
66
67
|
_idle_suspend_minutes,
|
|
68
|
+
_logs_overfetch,
|
|
67
69
|
_parse_status,
|
|
68
70
|
_resolve_exec_budget,
|
|
71
|
+
_visible_log_lines,
|
|
69
72
|
_warn_if_truncated,
|
|
70
73
|
)
|
|
71
74
|
from snowflake.sandbox.egress import Egress, compile_egress
|
|
@@ -771,10 +774,45 @@ class Sandbox(_FilesMixin, _SandboxState):
|
|
|
771
774
|
`tail` limits to the last N lines. Only meaningful for command containers
|
|
772
775
|
(created with `command=`).
|
|
773
776
|
|
|
777
|
+
SDK-internal protocol markers are suppressed: the in-sandbox runner reports a
|
|
778
|
+
run's outcome as `__SANDBOX_RESULT__<b64>` on stdout and the session daemon
|
|
779
|
+
reports replies as `__SANDBOX_REPLY__<...>`, so both land in the log stream
|
|
780
|
+
beside the workload's own output. They are transport, not output. `tail` counts
|
|
781
|
+
VISIBLE lines, so `logs(tail=2)` on a command printing `line1..line5` answers the two
|
|
782
|
+
lines `line4` and `line5`, never spending one of the two on a marker.
|
|
783
|
+
|
|
774
784
|
Example:
|
|
775
785
|
output = sb.logs(tail=100)
|
|
776
786
|
print(output)
|
|
777
787
|
"""
|
|
788
|
+
fetch = _logs_overfetch(tail)
|
|
789
|
+
while True:
|
|
790
|
+
raw = self._logs_raw(tail=fetch, since_ts_ms=since_ts_ms)
|
|
791
|
+
raw_count = len(raw.split("\n")) if raw else 0
|
|
792
|
+
visible = _visible_log_lines(raw)
|
|
793
|
+
if tail <= 0 or len(visible) >= tail:
|
|
794
|
+
break
|
|
795
|
+
# Short of `tail` visible lines. Only a WIDER fetch can help, and only while
|
|
796
|
+
# the server still has more to give: `raw_count < fetch` means we already hold
|
|
797
|
+
# the whole log, so filtering -- not the window -- is why there are fewer.
|
|
798
|
+
if raw_count < fetch or fetch >= _LOGS_OVERFETCH_CAP:
|
|
799
|
+
break
|
|
800
|
+
fetch = _logs_overfetch(tail, fetch)
|
|
801
|
+
# `tail > 0`, not `if tail`: a NEGATIVE tail is truthy and visible[-(-1):] is
|
|
802
|
+
# visible[1:], which would silently drop the FIRST lines instead of returning
|
|
803
|
+
# everything. `_logs_overfetch` already treats tail <= 0 as "no window".
|
|
804
|
+
if tail > 0:
|
|
805
|
+
visible = visible[-tail:]
|
|
806
|
+
return "\n".join(visible)
|
|
807
|
+
|
|
808
|
+
def _logs_raw(self, *, tail: int = 0, since_ts_ms: int = 0) -> str:
|
|
809
|
+
"""The log snapshot EXACTLY as the server returns it, markers included.
|
|
810
|
+
|
|
811
|
+
Internal consumers that read the runner's or daemon's stdout protocol
|
|
812
|
+
(`wait()`, `Job.get`/`Job.status`, `send()`) must use this, not `logs()` --
|
|
813
|
+
`logs()` strips the very lines they parse. Not public: a caller wanting the
|
|
814
|
+
sandbox's output wants `logs()`.
|
|
815
|
+
"""
|
|
778
816
|
if not self._created:
|
|
779
817
|
self._ensure_created()
|
|
780
818
|
if self._id is None: # pragma: no cover - defensive
|
|
@@ -1801,7 +1839,7 @@ class Sandbox(_FilesMixin, _SandboxState):
|
|
|
1801
1839
|
# it (the connect() reconnect path) fall through to the server-reported
|
|
1802
1840
|
# exit code rather than accepting a forgeable, unauthenticated line.
|
|
1803
1841
|
if self._result_nonce is not None:
|
|
1804
|
-
parsed = _parse_result_sentinel(self.
|
|
1842
|
+
parsed = _parse_result_sentinel(self._logs_raw(), nonce=self._result_nonce)
|
|
1805
1843
|
if parsed is not None:
|
|
1806
1844
|
parsed.logs_ref = self._id
|
|
1807
1845
|
return parsed
|
|
@@ -1858,7 +1896,7 @@ class Sandbox(_FilesMixin, _SandboxState):
|
|
|
1858
1896
|
self.exec(_mailbox_append_cmd(msg_id, message))
|
|
1859
1897
|
deadline = _time.monotonic() + timeout
|
|
1860
1898
|
while True:
|
|
1861
|
-
reply = _parse_reply_sentinel(self.
|
|
1899
|
+
reply = _parse_reply_sentinel(self._logs_raw(), msg_id, nonce=self._reply_nonce)
|
|
1862
1900
|
if reply is not None:
|
|
1863
1901
|
return reply
|
|
1864
1902
|
# A terminal container will never reply; surface that rather than spending
|
|
@@ -1866,7 +1904,7 @@ class Sandbox(_FilesMixin, _SandboxState):
|
|
|
1866
1904
|
# the cached status) is the only live read of the container's lifecycle.
|
|
1867
1905
|
if self.refresh() in TERMINAL_STATUSES:
|
|
1868
1906
|
# One last read: the daemon may have replied just before exiting.
|
|
1869
|
-
reply = _parse_reply_sentinel(self.
|
|
1907
|
+
reply = _parse_reply_sentinel(self._logs_raw(), msg_id, nonce=self._reply_nonce)
|
|
1870
1908
|
if reply is not None:
|
|
1871
1909
|
return reply
|
|
1872
1910
|
raise SandboxError(_ended_before_reply(self.status, msg_id))
|
|
@@ -23,6 +23,7 @@ from collections.abc import Iterator
|
|
|
23
23
|
from typing import TYPE_CHECKING, Self
|
|
24
24
|
|
|
25
25
|
from snowflake.sandbox._runtime._protocol import _RESULT_SENTINEL
|
|
26
|
+
from snowflake.sandbox._wire import _is_internal_sentinel_line
|
|
26
27
|
from snowflake.sandbox.log_stream import (
|
|
27
28
|
_DRAIN_AFTER_TERMINAL_S,
|
|
28
29
|
_POLL_INTERVAL_S,
|
|
@@ -90,6 +91,13 @@ class SyncLogStream(Iterator[str]):
|
|
|
90
91
|
if _RESULT_SENTINEL in text:
|
|
91
92
|
self._process_done = True
|
|
92
93
|
continue
|
|
94
|
+
# Any OTHER internal marker (the session daemon's __SANDBOX_REPLY__) is
|
|
95
|
+
# suppressed too, but is NOT end-of-stream -- a session keeps running after a
|
|
96
|
+
# reply. Checked after the result marker so that one still sets the flag.
|
|
97
|
+
# Without this the streamed view leaked a marker the snapshot logs() hides:
|
|
98
|
+
# same sandbox, two different answers about what its output was.
|
|
99
|
+
if _is_internal_sentinel_line(text):
|
|
100
|
+
continue
|
|
93
101
|
ts = int(ln.get("ts") or 0)
|
|
94
102
|
if ts:
|
|
95
103
|
if ts < start_ts:
|
|
@@ -28,7 +28,8 @@ redeployed, never adopted — see `agent_session`.
|
|
|
28
28
|
|
|
29
29
|
* `Session.send()` mints a ``msg_id`` (uuid hex), execs a shell-quoted
|
|
30
30
|
``printf … >> /var/tmp/sandbox_mailbox.jsonl`` to append the message, then polls
|
|
31
|
-
``
|
|
31
|
+
``_logs_raw()`` -- the UNFILTERED channel, since the public ``logs()`` suppresses
|
|
32
|
+
internal markers -- until the matching ``__SANDBOX_REPLY__<msg_id>__<b64>`` line
|
|
32
33
|
appears, base64-decodes it, and returns the reply string. A timeout raises
|
|
33
34
|
`SandboxExecTimeoutError`.
|
|
34
35
|
|
|
@@ -304,7 +305,8 @@ class Session:
|
|
|
304
305
|
|
|
305
306
|
Mints a ``msg_id``, execs a shell-quoted append into
|
|
306
307
|
``/var/tmp/sandbox_mailbox.jsonl`` inside the live container, then polls
|
|
307
|
-
``
|
|
308
|
+
``_logs_raw()`` (the unfiltered channel; public ``logs()`` hides the marker)
|
|
309
|
+
until the daemon prints ``__SANDBOX_REPLY__<msg_id>__<b64>``.
|
|
308
310
|
Base64-decodes the payload and returns the reply string.
|
|
309
311
|
|
|
310
312
|
Raises `SandboxError` if the container reaches a terminal state without
|
|
@@ -316,7 +318,7 @@ class Session:
|
|
|
316
318
|
|
|
317
319
|
deadline = time.monotonic() + timeout
|
|
318
320
|
while True:
|
|
319
|
-
log_text = await sb.
|
|
321
|
+
log_text = await sb._logs_raw()
|
|
320
322
|
reply = _parse_reply_sentinel(log_text, msg_id, nonce=self.nonce)
|
|
321
323
|
if reply is not None:
|
|
322
324
|
return reply
|
|
@@ -327,7 +329,7 @@ class Session:
|
|
|
327
329
|
if await sb.refresh() in TERMINAL_STATUSES:
|
|
328
330
|
# One last read first -- the daemon may have replied just before
|
|
329
331
|
# exiting, and that reply is the answer, not an error.
|
|
330
|
-
reply = _parse_reply_sentinel(await sb.
|
|
332
|
+
reply = _parse_reply_sentinel(await sb._logs_raw(), msg_id, nonce=self.nonce)
|
|
331
333
|
if reply is not None:
|
|
332
334
|
return reply
|
|
333
335
|
raise SandboxError(_ended_before_reply(sb.status, msg_id))
|
|
@@ -342,7 +344,8 @@ class Session:
|
|
|
342
344
|
Same mailbox round-trip as `send`, blocking instead of awaiting: mints a
|
|
343
345
|
``msg_id``, execs the shell-quoted append into
|
|
344
346
|
``/var/tmp/sandbox_mailbox.jsonl`` inside the live container (via the
|
|
345
|
-
synchronous `Sandbox`), then polls ``
|
|
347
|
+
synchronous `Sandbox`), then polls ``_logs_raw()`` -- the unfiltered channel,
|
|
348
|
+
since public ``logs()`` hides the marker -- until the daemon prints
|
|
346
349
|
``__SANDBOX_REPLY__<msg_id>__<b64>``, base64-decodes it, and returns the
|
|
347
350
|
reply string. The request built and the reply parsed are byte-identical
|
|
348
351
|
to `send`'s — both go through the shared ``_mailbox_append_cmd`` /
|
|
@@ -365,14 +368,14 @@ class Session:
|
|
|
365
368
|
|
|
366
369
|
deadline = time.monotonic() + timeout
|
|
367
370
|
while True:
|
|
368
|
-
log_text = sb.
|
|
371
|
+
log_text = sb._logs_raw()
|
|
369
372
|
reply = _parse_reply_sentinel(log_text, msg_id, nonce=self.nonce)
|
|
370
373
|
if reply is not None:
|
|
371
374
|
return reply
|
|
372
375
|
# As in `send`: a terminal container is not a timeout, and refresh() is
|
|
373
376
|
# the only live read of its status.
|
|
374
377
|
if sb.refresh() in TERMINAL_STATUSES:
|
|
375
|
-
reply = _parse_reply_sentinel(sb.
|
|
378
|
+
reply = _parse_reply_sentinel(sb._logs_raw(), msg_id, nonce=self.nonce)
|
|
376
379
|
if reply is not None:
|
|
377
380
|
return reply
|
|
378
381
|
raise SandboxError(_ended_before_reply(sb.status, msg_id))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_ansi.py
RENAMED
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_args.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_env.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/_sse.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/files.py
RENAMED
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/image.py
RENAMED
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/mcp.py
RENAMED
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/mount.py
RENAMED
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/shell.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/src/snowflake/sandbox/types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/tests/test_docs_references.py
RENAMED
|
File without changes
|
|
File without changes
|
{snowflake_sandbox_python-0.2.2a2 → snowflake_sandbox_python-0.2.2a3}/tests/test_smoke_offline.py
RENAMED
|
File without changes
|