modAL 1.5.5.dev0__py3-none-any.whl → 1.5.5.dev2__py3-none-any.whl

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.
@@ -145,7 +145,7 @@ def call_function(
145
145
  item_count += 1
146
146
 
147
147
  await container_io_manager._send_outputs.aio(
148
- io_context.output_items_generator_done(started_at, item_count)
148
+ io_context, io_context.output_items_generator_done(started_at, item_count)
149
149
  )
150
150
  else:
151
151
  value = await io_context.call_function_async()
@@ -182,7 +182,9 @@ def call_function(
182
182
  container_io_manager._queue_put(generator_queue, value)
183
183
  item_count += 1
184
184
 
185
- container_io_manager._send_outputs(io_context.output_items_generator_done(started_at, item_count))
185
+ container_io_manager._send_outputs(
186
+ io_context, io_context.output_items_generator_done(started_at, item_count)
187
+ )
186
188
  else:
187
189
  values = io_context.call_function_sync()
188
190
  container_io_manager.push_outputs(io_context, started_at, values)
modal/_logs_manager.py CHANGED
@@ -93,6 +93,8 @@ def _entry_context_ids(object_id: str, item: api_pb2.TaskLogs, batch: api_pb2.Ta
93
93
  ]
94
94
  case "fc":
95
95
  context_ids = [item.input_id or batch.input_id, item.container_id or batch.task_id]
96
+ case "sb":
97
+ context_ids = [item.container_id or batch.task_id]
96
98
  case _:
97
99
  context_ids = []
98
100
  # filter for empty strings, can happen for Server which have function_ids but no input/container
@@ -802,6 +804,71 @@ class _ImageLogsManager:
802
804
  ImageLogsManager = synchronize_api(_ImageLogsManager, target_module=__name__)
803
805
 
804
806
 
807
+ class _SandboxLogsManager:
808
+ """mdmd:namespace"""
809
+
810
+ def __init__(self, source: _SupportsLogs):
811
+ """mdmd:hidden"""
812
+ self._manager = _LogsManager(source)
813
+
814
+ async def fetch(
815
+ self,
816
+ *,
817
+ since: datetime,
818
+ until: datetime | None = None,
819
+ source: LogSource | None = None,
820
+ search_text: str = "",
821
+ ) -> AsyncGenerator[LogEntry, None]:
822
+ """Fetch Sandbox logs corresponding to the date range and filters.
823
+
824
+ Args:
825
+ since: Start date to fetch logs from. Must be in UTC or timezone-naive,
826
+ which is interpreted as local time.
827
+ until: Defaults to current date if None. Must be in UTC or timezone-naive, which is interpreted
828
+ as local time.
829
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
830
+ search_text: Filter by search text.
831
+
832
+ Yields:
833
+ `LogEntry` objects in chronological order.
834
+
835
+ Examples:
836
+
837
+ ```python notest
838
+ sandbox = modal.Sandbox.from_name("my-app", "sandbox")
839
+
840
+ for entry in sandbox.logs.fetch(
841
+ since=datetime.now() - timedelta(minutes=25),
842
+ source="stdout",
843
+ ):
844
+ print(entry.message, end="")
845
+ ```
846
+ """
847
+ async for log_entry in self._manager.fetch(since=since, until=until, source=source, search_text=search_text):
848
+ yield log_entry
849
+
850
+ async def tail(
851
+ self,
852
+ entries: int = 100,
853
+ *,
854
+ source: LogSource | None = None,
855
+ ) -> AsyncGenerator[LogEntry, None]:
856
+ """Fetch the most recent Sandbox logs.
857
+
858
+ Args:
859
+ entries: The number of log entries to return.
860
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
861
+
862
+ Yields:
863
+ `LogEntry` objects in chronological order.
864
+ """
865
+ async for log_entry in self._manager.tail(entries, source=source):
866
+ yield log_entry
867
+
868
+
869
+ SandboxLogsManager = synchronize_api(_SandboxLogsManager, target_module=__name__)
870
+
871
+
805
872
  class _AppLogsManager:
806
873
  """mdmd:namespace"""
807
874
 
modal/_logs_manager.pyi CHANGED
@@ -996,6 +996,173 @@ class ImageLogsManager:
996
996
 
997
997
  tail: __tail_spec
998
998
 
999
+ class _SandboxLogsManager:
1000
+ """mdmd:namespace"""
1001
+ def __init__(self, source: modal._supports_logs._SupportsLogs):
1002
+ """mdmd:hidden"""
1003
+ ...
1004
+
1005
+ def fetch(
1006
+ self,
1007
+ *,
1008
+ since: datetime.datetime,
1009
+ until: typing.Optional[datetime.datetime] = None,
1010
+ source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None,
1011
+ search_text: str = "",
1012
+ ) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
1013
+ """Fetch Sandbox logs corresponding to the date range and filters.
1014
+
1015
+ Args:
1016
+ since: Start date to fetch logs from. Must be in UTC or timezone-naive,
1017
+ which is interpreted as local time.
1018
+ until: Defaults to current date if None. Must be in UTC or timezone-naive, which is interpreted
1019
+ as local time.
1020
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1021
+ search_text: Filter by search text.
1022
+
1023
+ Yields:
1024
+ `LogEntry` objects in chronological order.
1025
+
1026
+ Examples:
1027
+
1028
+ ```python notest
1029
+ sandbox = modal.Sandbox.from_name("my-app", "sandbox")
1030
+
1031
+ for entry in sandbox.logs.fetch(
1032
+ since=datetime.now() - timedelta(minutes=25),
1033
+ source="stdout",
1034
+ ):
1035
+ print(entry.message, end="")
1036
+ ```
1037
+ """
1038
+ ...
1039
+
1040
+ def tail(
1041
+ self, entries: int = 100, *, source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None
1042
+ ) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
1043
+ """Fetch the most recent Sandbox logs.
1044
+
1045
+ Args:
1046
+ entries: The number of log entries to return.
1047
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1048
+
1049
+ Yields:
1050
+ `LogEntry` objects in chronological order.
1051
+ """
1052
+ ...
1053
+
1054
+ class SandboxLogsManager:
1055
+ """mdmd:namespace"""
1056
+ def __init__(self, source: modal._supports_logs._SupportsLogs):
1057
+ """mdmd:hidden"""
1058
+ ...
1059
+
1060
+ class __fetch_spec(typing_extensions.Protocol):
1061
+ def __call__(
1062
+ self,
1063
+ /,
1064
+ *,
1065
+ since: datetime.datetime,
1066
+ until: typing.Optional[datetime.datetime] = None,
1067
+ source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None,
1068
+ search_text: str = "",
1069
+ ) -> typing.Generator[modal.types.LogEntry, None, None]:
1070
+ """Fetch Sandbox logs corresponding to the date range and filters.
1071
+
1072
+ Args:
1073
+ since: Start date to fetch logs from. Must be in UTC or timezone-naive,
1074
+ which is interpreted as local time.
1075
+ until: Defaults to current date if None. Must be in UTC or timezone-naive, which is interpreted
1076
+ as local time.
1077
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1078
+ search_text: Filter by search text.
1079
+
1080
+ Yields:
1081
+ `LogEntry` objects in chronological order.
1082
+
1083
+ Examples:
1084
+
1085
+ ```python notest
1086
+ sandbox = modal.Sandbox.from_name("my-app", "sandbox")
1087
+
1088
+ for entry in sandbox.logs.fetch(
1089
+ since=datetime.now() - timedelta(minutes=25),
1090
+ source="stdout",
1091
+ ):
1092
+ print(entry.message, end="")
1093
+ ```
1094
+ """
1095
+ ...
1096
+
1097
+ def aio(
1098
+ self,
1099
+ /,
1100
+ *,
1101
+ since: datetime.datetime,
1102
+ until: typing.Optional[datetime.datetime] = None,
1103
+ source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None,
1104
+ search_text: str = "",
1105
+ ) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
1106
+ """Fetch Sandbox logs corresponding to the date range and filters.
1107
+
1108
+ Args:
1109
+ since: Start date to fetch logs from. Must be in UTC or timezone-naive,
1110
+ which is interpreted as local time.
1111
+ until: Defaults to current date if None. Must be in UTC or timezone-naive, which is interpreted
1112
+ as local time.
1113
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1114
+ search_text: Filter by search text.
1115
+
1116
+ Yields:
1117
+ `LogEntry` objects in chronological order.
1118
+
1119
+ Examples:
1120
+
1121
+ ```python notest
1122
+ sandbox = modal.Sandbox.from_name("my-app", "sandbox")
1123
+
1124
+ for entry in sandbox.logs.fetch(
1125
+ since=datetime.now() - timedelta(minutes=25),
1126
+ source="stdout",
1127
+ ):
1128
+ print(entry.message, end="")
1129
+ ```
1130
+ """
1131
+ ...
1132
+
1133
+ fetch: __fetch_spec
1134
+
1135
+ class __tail_spec(typing_extensions.Protocol):
1136
+ def __call__(
1137
+ self, /, entries: int = 100, *, source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None
1138
+ ) -> typing.Generator[modal.types.LogEntry, None, None]:
1139
+ """Fetch the most recent Sandbox logs.
1140
+
1141
+ Args:
1142
+ entries: The number of log entries to return.
1143
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1144
+
1145
+ Yields:
1146
+ `LogEntry` objects in chronological order.
1147
+ """
1148
+ ...
1149
+
1150
+ def aio(
1151
+ self, /, entries: int = 100, *, source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None
1152
+ ) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
1153
+ """Fetch the most recent Sandbox logs.
1154
+
1155
+ Args:
1156
+ entries: The number of log entries to return.
1157
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1158
+
1159
+ Yields:
1160
+ `LogEntry` objects in chronological order.
1161
+ """
1162
+ ...
1163
+
1164
+ tail: __tail_spec
1165
+
999
1166
  class _AppLogsManager:
1000
1167
  """mdmd:namespace"""
1001
1168
  def __init__(self, source: modal._supports_logs._SupportsLogs):
@@ -96,6 +96,7 @@ class IOContext:
96
96
 
97
97
  _cancel_issued: bool = False
98
98
  _cancel_callback: Callable[[], None] | None = None
99
+ _exited: bool = False
99
100
 
100
101
  def __init__(
101
102
  self,
@@ -889,25 +890,35 @@ class _ContainerIOManager:
889
890
  if item.kill_switch:
890
891
  logger.debug(f"Task {self.task_id} input kill signal input.")
891
892
  return
892
- inputs.append(
893
- (
894
- item.input_id,
895
- item.retry_count,
896
- item.function_call_id,
897
- item.attempt_token,
898
- item.input,
899
- item.function_call_invocation_type,
893
+ live_io_context = self.current_inputs.get(item.input_id)
894
+ if live_io_context is not None and (
895
+ live_io_context.retry_counts[live_io_context.input_ids.index(item.input_id)]
896
+ == item.retry_count
897
+ ):
898
+ # An expired fetch lease redelivers a still-running attempt under the same
899
+ # input id and retry count; a retry of a failed attempt gets a new retry count.
900
+ logger.warning(f"Skipping duplicate delivery of input {item.input_id}")
901
+ else:
902
+ inputs.append(
903
+ (
904
+ item.input_id,
905
+ item.retry_count,
906
+ item.function_call_id,
907
+ item.attempt_token,
908
+ item.input,
909
+ item.function_call_invocation_type,
910
+ )
900
911
  )
901
- )
902
912
  if item.input.final_input:
903
913
  if request.batch_max_size > 0:
904
914
  logger.debug(f"Task {self.task_id} Final input not expected in batch input stream")
905
915
  final_input_received = True
906
916
  break
907
917
 
908
- # If yielded, allow input slots to be released via exit_context
909
- yield inputs
910
- yielded = True
918
+ if inputs:
919
+ # If yielded, allow input slots to be released via exit_context
920
+ yield inputs
921
+ yielded = True
911
922
 
912
923
  # TODO(michael): Remove use of max_inputs after worker rollover
913
924
  single_use_container = self.function_def.single_use_containers or self.function_def.max_inputs == 1
@@ -950,7 +961,7 @@ class _ContainerIOManager:
950
961
  # collect all active input slots, meaning all inputs have wrapped up.
951
962
  await self._input_slots.close()
952
963
 
953
- async def _send_outputs(self, outputs: list[api_pb2.FunctionPutOutputsItem]) -> None:
964
+ async def _send_outputs(self, io_context: IOContext, outputs: list[api_pb2.FunctionPutOutputsItem]) -> None:
954
965
  """Send pre-built output items with retry and chunking."""
955
966
  # There are multiple outputs for a single IOContext in the case of @modal.batched.
956
967
  # Limit the batch size to 20 to stay within message size limits and buffer size limits.
@@ -963,8 +974,7 @@ class _ContainerIOManager:
963
974
  max_retries=None, # Retry indefinitely, trying every 1s.
964
975
  ),
965
976
  )
966
- input_ids = [output.input_id for output in outputs]
967
- self.exit_context(input_ids)
977
+ self.exit_context(io_context)
968
978
 
969
979
  @asynccontextmanager
970
980
  async def handle_input_exception(
@@ -984,7 +994,7 @@ class _ContainerIOManager:
984
994
  raise
985
995
  except (InputCancellation, asyncio.CancelledError):
986
996
  outputs = await io_context.output_items_cancellation(started_at)
987
- await self._send_outputs(outputs)
997
+ await self._send_outputs(io_context, outputs)
988
998
  logger.warning(f"Successfully canceled input {io_context.input_ids}")
989
999
  return
990
1000
  except BaseException as exc:
@@ -995,13 +1005,24 @@ class _ContainerIOManager:
995
1005
  # print exception so it's logged
996
1006
  print_exception(*sys.exc_info())
997
1007
  outputs = await io_context.output_items_exception(started_at, self.task_id, exc)
998
- await self._send_outputs(outputs)
1008
+ await self._send_outputs(io_context, outputs)
999
1009
 
1000
- def exit_context(self, input_ids: list[str]):
1001
- for input_id in input_ids:
1002
- self.current_inputs.pop(input_id)
1003
-
1004
- self._input_slots.release()
1010
+ def exit_context(self, io_context: IOContext):
1011
+ # A cancellation can land after outputs were already sent, re-entering here for the same
1012
+ # context; exit at most once so the input slot is not released twice.
1013
+ if io_context._exited:
1014
+ return
1015
+ io_context._exited = True
1016
+ try:
1017
+ for input_id in io_context.input_ids:
1018
+ # A retry admitted before its predecessor finished exiting takes over the
1019
+ # tracking entry, so remove it only if this context still owns it.
1020
+ if self.current_inputs.get(input_id) is io_context:
1021
+ self.current_inputs.pop(input_id)
1022
+ else:
1023
+ logger.warning(f"Input {input_id} missing from active input tracking")
1024
+ finally:
1025
+ self._input_slots.release()
1005
1026
 
1006
1027
  # skip inspection of user-generated output_data for synchronicity input translation
1007
1028
  @synchronizer.no_io_translation
@@ -1013,7 +1034,7 @@ class _ContainerIOManager:
1013
1034
  ) -> None:
1014
1035
  # The standard output encoding+sending method for successful function outputs
1015
1036
  outputs = await io_context.output_items(started_at, output_data)
1016
- await self._send_outputs(outputs)
1037
+ await self._send_outputs(io_context, outputs)
1017
1038
 
1018
1039
  @asynccontextmanager
1019
1040
  async def snapshot_context_manager(self) -> AsyncGenerator[None, None]:
@@ -33,6 +33,7 @@ class IOContext:
33
33
  finalized_function: modal._runtime.user_code_imports.FinalizedFunction
34
34
  _cancel_issued: bool
35
35
  _cancel_callback: typing.Optional[collections.abc.Callable[[], None]]
36
+ _exited: bool
36
37
 
37
38
  def __init__(
38
39
  self,
@@ -202,7 +203,9 @@ class _ContainerIOManager:
202
203
  batch_max_size: int = 0,
203
204
  batch_wait_ms: int = 0,
204
205
  ) -> collections.abc.AsyncIterator[IOContext]: ...
205
- async def _send_outputs(self, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
206
+ async def _send_outputs(
207
+ self, io_context: IOContext, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]
208
+ ) -> None:
206
209
  """Send pre-built output items with retry and chunking."""
207
210
  ...
208
211
 
@@ -210,7 +213,7 @@ class _ContainerIOManager:
210
213
  """Handle an exception while processing a function input."""
211
214
  ...
212
215
 
213
- def exit_context(self, input_ids: list[str]): ...
216
+ def exit_context(self, io_context: IOContext): ...
214
217
  async def push_outputs(self, io_context: IOContext, started_at: float, output_data: list[typing.Any]) -> None: ...
215
218
  def snapshot_context_manager(self) -> typing.AsyncContextManager[None]: ...
216
219
  async def interact(self, from_breakpoint: bool = False): ...
@@ -445,11 +448,13 @@ class ContainerIOManager:
445
448
  run_inputs_outputs: __run_inputs_outputs_spec
446
449
 
447
450
  class ___send_outputs_spec(typing_extensions.Protocol):
448
- def __call__(self, /, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
451
+ def __call__(self, /, io_context: IOContext, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
449
452
  """Send pre-built output items with retry and chunking."""
450
453
  ...
451
454
 
452
- async def aio(self, /, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
455
+ async def aio(
456
+ self, /, io_context: IOContext, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]
457
+ ) -> None:
453
458
  """Send pre-built output items with retry and chunking."""
454
459
  ...
455
460
 
@@ -468,7 +473,7 @@ class ContainerIOManager:
468
473
 
469
474
  handle_input_exception: __handle_input_exception_spec
470
475
 
471
- def exit_context(self, input_ids: list[str]): ...
476
+ def exit_context(self, io_context: IOContext): ...
472
477
 
473
478
  class __push_outputs_spec(typing_extensions.Protocol):
474
479
  def __call__(self, /, io_context: IOContext, started_at: float, output_data: list[typing.Any]) -> None: ...
modal/client.pyi CHANGED
@@ -38,7 +38,7 @@ class _Client:
38
38
  server_url: str,
39
39
  client_type: int,
40
40
  credentials: typing.Optional[tuple[str, str]],
41
- version: str = "1.5.5.dev0",
41
+ version: str = "1.5.5.dev2",
42
42
  ):
43
43
  """mdmd:hidden
44
44
  The Modal client object is not intended to be instantiated directly by users.
@@ -209,7 +209,7 @@ class Client:
209
209
  server_url: str,
210
210
  client_type: int,
211
211
  credentials: typing.Optional[tuple[str, str]],
212
- version: str = "1.5.5.dev0",
212
+ version: str = "1.5.5.dev2",
213
213
  ):
214
214
  """mdmd:hidden
215
215
  The Modal client object is not intended to be instantiated directly by users.
modal/functions.pyi CHANGED
@@ -546,7 +546,7 @@ class Function(
546
546
 
547
547
  _call_generator: ___call_generator_spec
548
548
 
549
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
549
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
550
550
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
551
551
  """Calls the function remotely, executing it with the given arguments and returning the execution's result.
552
552
 
@@ -571,7 +571,7 @@ class Function(
571
571
  """
572
572
  ...
573
573
 
574
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType]
574
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
575
575
 
576
576
  class __remote_gen_spec(typing_extensions.Protocol):
577
577
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -621,7 +621,7 @@ class Function(
621
621
  """
622
622
  ...
623
623
 
624
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
624
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
625
625
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
626
626
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
627
627
 
@@ -654,7 +654,7 @@ class Function(
654
654
  """
655
655
  ...
656
656
 
657
- _experimental_spawn: ___experimental_spawn_spec[modal._functions.P, modal._functions.ReturnType]
657
+ _experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
658
658
 
659
659
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
660
660
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
@@ -662,7 +662,7 @@ class Function(
662
662
 
663
663
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
664
664
 
665
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
665
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
666
666
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
667
667
  """Calls the function with the given arguments, without waiting for the results.
668
668
 
@@ -695,7 +695,7 @@ class Function(
695
695
  """
696
696
  ...
697
697
 
698
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType]
698
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
699
699
 
700
700
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
701
701
  """Return the inner Python object wrapped by this Modal Function.
modal/sandbox.py CHANGED
@@ -15,6 +15,8 @@ from dataclasses import dataclass
15
15
  from pathlib import PurePosixPath
16
16
  from typing import TYPE_CHECKING, Any, Literal, Union, overload
17
17
 
18
+ from modal._logs_manager import _SandboxLogsManager
19
+ from modal._supports_logs import LogsFilters, _LogQueryData
18
20
  from modal.secret import _split_env_dict_and_resolvable_secrets
19
21
 
20
22
  from ._output.pty import get_pty_info
@@ -1931,6 +1933,27 @@ class _Sandbox(_Object, type_prefix="sb"):
1931
1933
  await asyncio.sleep(0.5)
1932
1934
  return self._task_id
1933
1935
 
1936
+ async def _resolve_task_id_for_logs(self) -> str:
1937
+ """Try to resolve the task ID in one shot for historical log fetches.
1938
+
1939
+ For a historical log api, we should not wait for the task to be created.
1940
+ If the Sandbox created logs worth fetching, the task ID should be available.
1941
+ """
1942
+ if self._task_id:
1943
+ return self._task_id
1944
+
1945
+ req = api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id)
1946
+ stub = self.__client.stub
1947
+ if self._is_v2:
1948
+ assert self.__client._auth_token_manager
1949
+ auth_token = await self.__client._auth_token_manager.get_token()
1950
+ resp = await stub.SandboxGetTaskIdV2(req, metadata=[("x-modal-auth-token", auth_token)])
1951
+ else:
1952
+ resp = await stub.SandboxGetTaskId(req)
1953
+ if not resp.task_id:
1954
+ raise ExecutionError("Sandbox task_id is not available yet.")
1955
+ return resp.task_id
1956
+
1934
1957
  async def _get_command_router_client(self, task_id: str) -> TaskCommandRouterClient:
1935
1958
  if self._command_router_client is None:
1936
1959
  try:
@@ -2605,6 +2628,36 @@ class _Sandbox(_Object, type_prefix="sb"):
2605
2628
  # Fetch the next batch starting from the end of the current one.
2606
2629
  before_timestamp = resp.sandboxes[-1].created_at
2607
2630
 
2631
+ async def _get_log_query_data(self) -> _LogQueryData:
2632
+ if not self._app_id:
2633
+ if not self._attached:
2634
+ raise ExecutionError("Cannot fetch logs for a detached Sandbox without hydrated app_id")
2635
+ await self.hydrate()
2636
+
2637
+ if not self._app_id:
2638
+ raise ExecutionError("app_id should have been set during sandbox hydration")
2639
+
2640
+ task_id = await self._resolve_task_id_for_logs()
2641
+ return _LogQueryData(self.__client, self._app_id, LogsFilters(task_id=task_id), self.object_id)
2642
+
2643
+ @property
2644
+ def logs(self) -> _SandboxLogsManager:
2645
+ """Access logs for a `Sandbox` entrypoint.
2646
+
2647
+ Useful for inspecting logs after a Sandbox terminates.
2648
+ Use [`fetch()`](#logsfetch)
2649
+ to read logs from a UTC time range, [`tail()`](#logstail)
2650
+ to read the most recent logs.
2651
+
2652
+ Note that the logs from executed commands in the sandbox (via `exec()`) are not included in the
2653
+ entrypoint logs.
2654
+
2655
+ See also:
2656
+ - [`modal app logs`](https://modal.com/docs/cli/latest/app#modal-app-logs):
2657
+ CLI access to logs for an App.
2658
+ """
2659
+ return _SandboxLogsManager(self)
2660
+
2608
2661
 
2609
2662
  class _SidecarContainer:
2610
2663
  """Handle to an additional container running in a Sandbox."""
modal/sandbox.pyi CHANGED
@@ -3,7 +3,9 @@ import collections.abc
3
3
  import enum
4
4
  import google.protobuf.message
5
5
  import modal._image
6
+ import modal._logs_manager
6
7
  import modal._object
8
+ import modal._supports_logs
7
9
  import modal._tunnel
8
10
  import modal._utils.task_command_router_client
9
11
  import modal.app
@@ -808,6 +810,14 @@ class _Sandbox(modal._object._Object):
808
810
  ...
809
811
 
810
812
  async def _get_task_id(self, raise_if_task_complete=False) -> str: ...
813
+ async def _resolve_task_id_for_logs(self) -> str:
814
+ """Try to resolve the task ID in one shot for historical log fetches.
815
+
816
+ For a historical log api, we should not wait for the task to be created.
817
+ If the Sandbox created logs worth fetching, the task ID should be available.
818
+ """
819
+ ...
820
+
811
821
  async def _get_command_router_client(
812
822
  self, task_id: str
813
823
  ) -> modal._utils.task_command_router_client.TaskCommandRouterClient: ...
@@ -1052,6 +1062,25 @@ class _Sandbox(modal._object._Object):
1052
1062
  """
1053
1063
  ...
1054
1064
 
1065
+ async def _get_log_query_data(self) -> modal._supports_logs._LogQueryData: ...
1066
+ @property
1067
+ def logs(self) -> modal._logs_manager._SandboxLogsManager:
1068
+ """Access logs for a `Sandbox` entrypoint.
1069
+
1070
+ Useful for inspecting logs after a Sandbox terminates.
1071
+ Use [`fetch()`](#logsfetch)
1072
+ to read logs from a UTC time range, [`tail()`](#logstail)
1073
+ to read the most recent logs.
1074
+
1075
+ Note that the logs from executed commands in the sandbox (via `exec()`) are not included in the
1076
+ entrypoint logs.
1077
+
1078
+ See also:
1079
+ - [`modal app logs`](https://modal.com/docs/cli/latest/app#modal-app-logs):
1080
+ CLI access to logs for an App.
1081
+ """
1082
+ ...
1083
+
1055
1084
  class _SidecarContainer:
1056
1085
  """Handle to an additional container running in a Sandbox."""
1057
1086
 
@@ -2841,6 +2870,25 @@ class Sandbox(modal.object.Object):
2841
2870
 
2842
2871
  _get_task_id: ___get_task_id_spec
2843
2872
 
2873
+ class ___resolve_task_id_for_logs_spec(typing_extensions.Protocol):
2874
+ def __call__(self, /) -> str:
2875
+ """Try to resolve the task ID in one shot for historical log fetches.
2876
+
2877
+ For a historical log api, we should not wait for the task to be created.
2878
+ If the Sandbox created logs worth fetching, the task ID should be available.
2879
+ """
2880
+ ...
2881
+
2882
+ async def aio(self, /) -> str:
2883
+ """Try to resolve the task ID in one shot for historical log fetches.
2884
+
2885
+ For a historical log api, we should not wait for the task to be created.
2886
+ If the Sandbox created logs worth fetching, the task ID should be available.
2887
+ """
2888
+ ...
2889
+
2890
+ _resolve_task_id_for_logs: ___resolve_task_id_for_logs_spec
2891
+
2844
2892
  class ___get_command_router_client_spec(typing_extensions.Protocol):
2845
2893
  def __call__(self, /, task_id: str) -> modal._utils.task_command_router_client.TaskCommandRouterClient: ...
2846
2894
  async def aio(self, /, task_id: str) -> modal._utils.task_command_router_client.TaskCommandRouterClient: ...
@@ -3349,6 +3397,30 @@ class Sandbox(modal.object.Object):
3349
3397
 
3350
3398
  _experimental_list: typing.ClassVar[___experimental_list_spec]
3351
3399
 
3400
+ class ___get_log_query_data_spec(typing_extensions.Protocol):
3401
+ def __call__(self, /) -> modal._supports_logs._LogQueryData: ...
3402
+ async def aio(self, /) -> modal._supports_logs._LogQueryData: ...
3403
+
3404
+ _get_log_query_data: ___get_log_query_data_spec
3405
+
3406
+ @property
3407
+ def logs(self) -> modal._logs_manager.SandboxLogsManager:
3408
+ """Access logs for a `Sandbox` entrypoint.
3409
+
3410
+ Useful for inspecting logs after a Sandbox terminates.
3411
+ Use [`fetch()`](#logsfetch)
3412
+ to read logs from a UTC time range, [`tail()`](#logstail)
3413
+ to read the most recent logs.
3414
+
3415
+ Note that the logs from executed commands in the sandbox (via `exec()`) are not included in the
3416
+ entrypoint logs.
3417
+
3418
+ See also:
3419
+ - [`modal app logs`](https://modal.com/docs/cli/latest/app#modal-app-logs):
3420
+ CLI access to logs for an App.
3421
+ """
3422
+ ...
3423
+
3352
3424
  _default_image: modal._image._Image
3353
3425
 
3354
3426
  _MAIN_CONTAINER_NAME: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.5.dev0
3
+ Version: 1.5.5.dev2
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -3,7 +3,7 @@ modal/__main__.py,sha256=CqxDUv1PzwZTi7sYOzxgSqfgEiDievza0pbrUCiyLTM,1591
3
3
  modal/_billing.py,sha256=VEa-YKFsuKmRfWW1SPiXGiY84RzTTRvo8ILi0V10R28,3483
4
4
  modal/_clustered_functions.py,sha256=3OcPCcgEI2YRKb4-ApAIsl7_w7-NLpLCineb691pvPA,3504
5
5
  modal/_clustered_functions.pyi,sha256=TvFV1n6bJcSVzRfk3WEfZJYC_bTXBnHt_DorMsaOU2s,1273
6
- modal/_container_entrypoint.py,sha256=AyhMFBiaApVn1mueur2yrVy7IXR9qdXdwY4IePtVv74,22760
6
+ modal/_container_entrypoint.py,sha256=QQoLeFpBjQ6HX0P1a3Ho2YBJWVKI1Knh_lVAz4R2lUo,22822
7
7
  modal/_environments.py,sha256=drnm5rkQdCbHIlxb7VyKwAwUyYZ0XWlAuQq1uvETuZo,25225
8
8
  modal/_function_variants.py,sha256=tKS1zJJCBJcj9_mKn2s5CdEWcvcskr3usaqEHKZddDg,12552
9
9
  modal/_functions.py,sha256=Qci-v-isCV1SxFebZ1gW9Qp1tdkRShdRvKTprGymWGs,99579
@@ -13,8 +13,8 @@ modal/_ipython.py,sha256=Bu9fc3hm3xt2O47qLe_d-Mu2jS81LwMsmXNJ1sawBA0,575
13
13
  modal/_load_context.py,sha256=wrDqD3KehhFepp0vBhyz_PoLWoRVD0gZHP8dlLHCTiw,4818
14
14
  modal/_location.py,sha256=joiX-0ZeutEUDTrrqLF1GHXCdVLF-rHzstocbMcd_-k,366
15
15
  modal/_logs.py,sha256=vr4cyWiwYGN1AWwZI9MQfERmdKjQxMupFH3Ay6mo-Fw,17208
16
- modal/_logs_manager.py,sha256=To7IcBcnz_HthX5tVyw4BxNeQvaylvg3fiwnGhrxrsw,33741
17
- modal/_logs_manager.pyi,sha256=WfkSj1HyrProeDYL935grys3vyGTP5DZMIzYu7uqar4,44333
16
+ modal/_logs_manager.py,sha256=og7BTDw-ayXYCcNjtytZ-D6pnm4WpbtiGgi505ddK_o,35885
17
+ modal/_logs_manager.pyi,sha256=UpX4CteeBJWzA9UDbg2l9TLkrmVHHyOmvrB7Nm1Fxr0,50296
18
18
  modal/_object.py,sha256=E24RIV3ZRhq-TFnir7cURERrFJnF5HXgNGATas7Q1o4,13557
19
19
  modal/_partial_function.py,sha256=VWjYZCVmHsQpad7MvVifHUQn4ElGKm5cSsyOLxDQ9Yk,35371
20
20
  modal/_resolver.py,sha256=LLDM_84aaJfBdgMZ_50CpXJtQzTw5CNRkbLDOiLZEZ0,5309
@@ -34,7 +34,7 @@ modal/billing.py,sha256=E1-P_P8bJFLp-bTaggYd4BaaViImPB5UdtXiY8VeAZQ,390
34
34
  modal/billing.pyi,sha256=4m7VLYNS8IiJ2I3CJx0awvrOM6BILWRVLfdeqtAYr8M,7189
35
35
  modal/call_graph.py,sha256=XGALIDn1bLchWo_goyX23bcPjzCZcfqKUo8INIfDapo,1775
36
36
  modal/client.py,sha256=h0RyigMQxlD2Xf4Rrr95GBLKENMJxSvNayAmU-aXeJM,16380
37
- modal/client.pyi,sha256=tq4x81_X1-FDw2yWnynEkPzqWyeg3PaQqHJdXCSg_EU,18037
37
+ modal/client.pyi,sha256=FCtXVgu9bjGpucrhfd8-53dOGI0vTvrnfd4tzEWMkj8,18037
38
38
  modal/cloud_bucket_mount.py,sha256=NrdNV9r9aL3QFrlt-Dhtd7_K3f8v1igrHf2FRwgJ1CI,6827
39
39
  modal/cloud_bucket_mount.pyi,sha256=0E8d53pMh2KsFa-rMh_i6LxlzW4cMV8drVmmkPHSqZk,8149
40
40
  modal/cls.py,sha256=TxRxHdY4L6AZ6I16DNPgDryAkGtlFadD-Mm0ovtfN58,41206
@@ -51,7 +51,7 @@ modal/file_io.py,sha256=GYTa67mrmbTTTBsYQnb51VmwGIo0YY-1qQ3YBhagkmA,21795
51
51
  modal/file_io.pyi,sha256=3181-DS07flFWLf2BKjqCOiWIjieEOolf33wx6fBp5c,15377
52
52
  modal/file_pattern_matcher.py,sha256=CpaNNj7oBlOSewX-AAUZCF8KDxH0_DmgwzfpVGbPYJ4,8138
53
53
  modal/functions.py,sha256=EZRcHFfnoYbvawUJnAHF5nIvoo9GX2Djy7mUmOCXmXc,258
54
- modal/functions.pyi,sha256=QoHzhWw69ta8RP2mYonGDqaU-5BtzAkSjUIijuEKmAU,50874
54
+ modal/functions.pyi,sha256=BZY0C6G1t3eQbJaxPphL9l1dVur4ZHs0IGHyk5Fir2E,50874
55
55
  modal/image.py,sha256=c0lUSAoU-Ip3g5DGZ0uhdCzoWSQWh1g0i-7Rxo-Sz-I,160
56
56
  modal/image.pyi,sha256=R5rdY4P30IEm-F7A2S9CdXa6QYNk-pQwsEun9BKTyLw,61094
57
57
  modal/io_streams.py,sha256=3fhhb7SDJ69pVUu9J3PiMLBePD9bpiIxPN05SCzf8FE,29118
@@ -76,8 +76,8 @@ modal/retries.py,sha256=tSGB5bGM5xZZUDXMkuelIEoQJY0vOukOssZhwc6tXAo,5014
76
76
  modal/runner.py,sha256=rSeKWgv-gcUlThTzw4oLvRcVW7C8FGwAQGpDS34Z0gI,31347
77
77
  modal/runner.pyi,sha256=jIdnTwYv5AH-c8weyeCFzJ0hIekw81QqUsxVzXbgkcc,9954
78
78
  modal/running_app.py,sha256=xSR1VVlhxEFytmjx2x697_0GpD25Vc9crhp6r117_QY,1145
79
- modal/sandbox.py,sha256=l98lE-c0wK56fdOIT0KDLL9oKaKIHAWZcUh_6SRlPWM,128145
80
- modal/sandbox.pyi,sha256=is9AiA7NY2RLgaOIQC0VPC7Kf_Z1vYTKCRhv0o4Q4Lo,150923
79
+ modal/sandbox.py,sha256=rEQyBrSwKRXaHUwiOwbU03j9mxZ0-NUzY88suqAilWU,130378
80
+ modal/sandbox.pyi,sha256=lLXCU-vlmPHyx8K1S3sipyXzRhve3BCCyob7k5M16Gk,153723
81
81
  modal/sandbox_fs.py,sha256=GSuPlrLSdQ7fG7c_TGMd-VWs_s6RqQxMvLivdvC6s4s,27002
82
82
  modal/sandbox_fs.pyi,sha256=TJWx8h7x3yRC7KUxeF7q62_0a7w1IHufnJL7AYEojAg,45285
83
83
  modal/schedule.py,sha256=J0rpNAVcoQjCCzobYFQXHXv1BBPLYATUwGS5prCHp4c,3242
@@ -105,8 +105,8 @@ modal/_output/rich.py,sha256=K6dUEqeeyytqUR-78eiS1GNmvuFW7HuKhws_41pjCRQ,32205
105
105
  modal/_output/status.py,sha256=c0rdvjoYKtIG7XJqq42TlcKawkYqZWtod38RmRY5IWA,4492
106
106
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
107
107
  modal/_runtime/asgi.py,sha256=0-z0dqTLBtgT9PR_XCkRbL9Fi9G7Dhor5HGVTlMwaEM,22831
108
- modal/_runtime/container_io_manager.py,sha256=bp2XVDJfuD9Roj0y8_2T4Cskq_wmvy8IoiGTg5HoZZA,49212
109
- modal/_runtime/container_io_manager.pyi,sha256=gzdk-g2tFHOWRArWv9xgLwLgLLbZwMZkXuKegJwIASA,20315
108
+ modal/_runtime/container_io_manager.py,sha256=3wO0oqdIjf7Vi6oSM3ky1aPZPsv7Kw6lAQZwWlGi6JU,50654
109
+ modal/_runtime/container_io_manager.pyi,sha256=BTfu6uE4U25uXtfOPvuz6dmyJy8Lh6ApbozNJ2scduc,20440
110
110
  modal/_runtime/execution_context.py,sha256=P0MHKv7HBHJn4T8VV1p4UgBT-4ww6uegieYUI-L23Qs,3946
111
111
  modal/_runtime/execution_context.pyi,sha256=CmOsFbsN22douEypIe88AtzfegoalH0IBaWpPZBhUcc,2607
112
112
  modal/_runtime/gpu_memory_snapshot.py,sha256=Evn-pEMjvUQ-lTzFYj8IGVfcznTUDq_rORuklWEmDsk,12370
@@ -198,7 +198,7 @@ modal/experimental/flash.py,sha256=HC-GW4FpRXyaVFSBw4yxneS1eLvW5j19rwc16AsqrXQ,3
198
198
  modal/experimental/flash.pyi,sha256=gpN4lDv4na2nVTUOCRxFu52o3-Ed0HrJISLxACUe7Ls,17892
199
199
  modal/experimental/ipython.py,sha256=kJ6o9wkAY_MiSb7paewI7f1MJAeqdi8TX5pBdhr_mUI,2925
200
200
  modal/skills/modal/SKILL.md,sha256=WzLjYJ1utjc_ZOf6qZTPwB6wxxwRYB5PibdvIf_FLy0,5316
201
- modal-1.5.5.dev0.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
201
+ modal-1.5.5.dev2.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
202
202
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
203
203
  modal_docs/gen_cli_docs.py,sha256=9-b8vl2EsLWD9zJqpCK8IUi5h44Bjn3i3rSdedZV4bA,6415
204
204
  modal_docs/gen_cli_docs_main.py,sha256=UPGr7O1DYTIRLwfINe2rbdh89J3cPVTU1OWL-SjxnOQ,198
@@ -222,10 +222,10 @@ modal_proto/task_command_router_pb2.py,sha256=tZm_pN94vDNVom4WL4T5id3_mxWoia3U1C
222
222
  modal_proto/task_command_router_pb2.pyi,sha256=ojB6XuvvoUEzZUPwGedjTCZtkImdcm1lPJAXzKmfbis,46716
223
223
  modal_proto/task_command_router_pb2_grpc.py,sha256=zuFkJgKcswB2ftjnyEgs70dp61VMA7gK0h0uzR3gYFw,42563
224
224
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=UjAJFqz9DpOFXP04fVm5lB2eAyTtjOg3oKjfWei8yLU,14262
225
- modal_version/__init__.py,sha256=cu46zxNS94zYuVAj9ur17gaJXBOED7PP6rcTDTcUmDg,120
225
+ modal_version/__init__.py,sha256=zYJtwXbCQmLK_1TJrqGEPKIM37_3WzIufalqBc6zeKQ,120
226
226
  modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
227
- modal-1.5.5.dev0.dist-info/METADATA,sha256=GMEPtyua1CiIhCldmiXGcx5sGRNVQ_pFGsiQrpLHPGM,2512
228
- modal-1.5.5.dev0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
229
- modal-1.5.5.dev0.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
230
- modal-1.5.5.dev0.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
231
- modal-1.5.5.dev0.dist-info/RECORD,,
227
+ modal-1.5.5.dev2.dist-info/METADATA,sha256=6GWwJ7mcD2wQTmbP_wEDvwtMtQFUMbIWU1CSv87jOtg,2512
228
+ modal-1.5.5.dev2.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
229
+ modal-1.5.5.dev2.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
230
+ modal-1.5.5.dev2.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
231
+ modal-1.5.5.dev2.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2026
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.5.5.dev0"
4
+ __version__ = "1.5.5.dev2"