modal 1.2.1.dev22__py3-none-any.whl → 1.2.1.dev23__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

modal/client.pyi CHANGED
@@ -33,7 +33,7 @@ class _Client:
33
33
  server_url: str,
34
34
  client_type: int,
35
35
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.2.1.dev22",
36
+ version: str = "1.2.1.dev23",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -164,7 +164,7 @@ class Client:
164
164
  server_url: str,
165
165
  client_type: int,
166
166
  credentials: typing.Optional[tuple[str, str]],
167
- version: str = "1.2.1.dev22",
167
+ version: str = "1.2.1.dev23",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
modal/io_streams.py CHANGED
@@ -64,7 +64,6 @@ async def _container_process_logs_iterator(
64
64
  get_raw_bytes=True,
65
65
  last_batch_index=last_index,
66
66
  )
67
-
68
67
  stream = client.stub.ContainerExecGetOutput.unary_stream(req)
69
68
  while True:
70
69
  # Check deadline before attempting to receive the next batch
@@ -76,11 +75,13 @@ async def _container_process_logs_iterator(
76
75
  break
77
76
  except StopAsyncIteration:
78
77
  break
78
+
79
+ for item in batch.items:
80
+ yield item.message_bytes, batch.batch_index
81
+
79
82
  if batch.HasField("exit_code"):
80
83
  yield None, batch.batch_index
81
84
  break
82
- for item in batch.items:
83
- yield item.message_bytes, batch.batch_index
84
85
 
85
86
 
86
87
  T = TypeVar("T", str, bytes)
@@ -89,7 +90,7 @@ T = TypeVar("T", str, bytes)
89
90
  class _StreamReaderThroughServer(Generic[T]):
90
91
  """A StreamReader implementation that reads from the server."""
91
92
 
92
- _stream: Optional[AsyncGenerator[Optional[bytes], None]]
93
+ _stream: Optional[AsyncGenerator[T, None]]
93
94
 
94
95
  def __init__(
95
96
  self,
@@ -134,10 +135,9 @@ class _StreamReaderThroughServer(Generic[T]):
134
135
  self._stream_type = stream_type
135
136
 
136
137
  if self._object_type == "container_process":
137
- # Container process streams need to be consumed as they are produced,
138
- # otherwise the process will block. Use a buffer to store the stream
139
- # until the client consumes it.
140
- self._container_process_buffer: list[Optional[bytes]] = []
138
+ # TODO: we should not have this async code in constructors!
139
+ # it only works as long as all the construction happens inside of synchronicity code
140
+ self._container_process_buffer: list[Optional[bytes]] = [] # TODO: change this to an asyncio.Queue
141
141
  self._consume_container_process_task = asyncio.create_task(self._consume_container_process_stream())
142
142
 
143
143
  @property
@@ -147,21 +147,18 @@ class _StreamReaderThroughServer(Generic[T]):
147
147
 
148
148
  async def read(self) -> T:
149
149
  """Fetch the entire contents of the stream until EOF."""
150
- data_str = ""
151
- data_bytes = b""
152
150
  logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read starting")
153
- async for message in self._get_logs():
154
- if message is None:
155
- break
156
- if self._text:
157
- data_str += message.decode("utf-8")
158
- else:
159
- data_bytes += message
160
-
161
- logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
162
151
  if self._text:
152
+ data_str = ""
153
+ async for message in _decode_bytes_stream_to_str(self._get_logs()):
154
+ data_str += message
155
+ logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
163
156
  return cast(T, data_str)
164
157
  else:
158
+ data_bytes = b""
159
+ async for message in self._get_logs():
160
+ data_bytes += message
161
+ logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
165
162
  return cast(T, data_bytes)
166
163
 
167
164
  async def _consume_container_process_stream(self):
@@ -181,6 +178,7 @@ class _StreamReaderThroughServer(Generic[T]):
181
178
  )
182
179
  async for message, batch_index in iterator:
183
180
  if self._stream_type == StreamType.STDOUT and message:
181
+ # TODO: rearchitect this, since these bytes aren't necessarily decodable
184
182
  print(message.decode("utf-8"), end="")
185
183
  elif self._stream_type == StreamType.PIPE:
186
184
  self._container_process_buffer.append(message)
@@ -208,6 +206,9 @@ class _StreamReaderThroughServer(Generic[T]):
208
206
 
209
207
  async def _stream_container_process(self) -> AsyncGenerator[tuple[Optional[bytes], str], None]:
210
208
  """Streams the container process buffer to the reader."""
209
+ # Container process streams need to be consumed as they are produced,
210
+ # otherwise the process will block. Use a buffer to store the stream
211
+ # until the client consumes it.
211
212
  entry_id = 0
212
213
  if self._last_entry_id:
213
214
  entry_id = int(self._last_entry_id) + 1
@@ -225,7 +226,7 @@ class _StreamReaderThroughServer(Generic[T]):
225
226
 
226
227
  entry_id += 1
227
228
 
228
- async def _get_logs(self, skip_empty_messages: bool = True) -> AsyncGenerator[Optional[bytes], None]:
229
+ async def _get_logs(self, skip_empty_messages: bool = True) -> AsyncGenerator[bytes, None]:
229
230
  """Streams sandbox or process logs from the server to the reader.
230
231
 
231
232
  Logs returned by this method may contain partial or multiple lines at a time.
@@ -237,7 +238,6 @@ class _StreamReaderThroughServer(Generic[T]):
237
238
  raise InvalidError("Logs can only be retrieved using the PIPE stream type.")
238
239
 
239
240
  if self.eof:
240
- yield None
241
241
  return
242
242
 
243
243
  completed = False
@@ -262,6 +262,8 @@ class _StreamReaderThroughServer(Generic[T]):
262
262
  if message is None:
263
263
  completed = True
264
264
  self.eof = True
265
+ return
266
+
265
267
  yield message
266
268
 
267
269
  except (GRPCError, StreamTerminatedError) as exc:
@@ -275,43 +277,37 @@ class _StreamReaderThroughServer(Generic[T]):
275
277
  continue
276
278
  raise
277
279
 
278
- async def _get_logs_by_line(self) -> AsyncGenerator[Optional[bytes], None]:
280
+ async def _get_logs_by_line(self) -> AsyncGenerator[bytes, None]:
279
281
  """Process logs from the server and yield complete lines only."""
280
282
  async for message in self._get_logs():
281
- if message is None:
282
- if self._line_buffer:
283
- yield self._line_buffer
284
- self._line_buffer = b""
285
- yield None
286
- else:
287
- assert isinstance(message, bytes)
288
- self._line_buffer += message
289
- while b"\n" in self._line_buffer:
290
- line, self._line_buffer = self._line_buffer.split(b"\n", 1)
291
- yield line + b"\n"
283
+ assert isinstance(message, bytes)
284
+ self._line_buffer += message
285
+ while b"\n" in self._line_buffer:
286
+ line, self._line_buffer = self._line_buffer.split(b"\n", 1)
287
+ yield line + b"\n"
288
+
289
+ if self._line_buffer:
290
+ yield self._line_buffer
291
+ self._line_buffer = b""
292
292
 
293
- def _ensure_stream(self) -> AsyncGenerator[Optional[bytes], None]:
293
+ def _ensure_stream(self) -> AsyncGenerator[T, None]:
294
294
  if not self._stream:
295
295
  if self._by_line:
296
- self._stream = self._get_logs_by_line()
296
+ # TODO: This is quite odd - it does line buffering in binary mode
297
+ # but we then always add the buffered text decoding on top of that.
298
+ # feels a bit upside down...
299
+ stream = self._get_logs_by_line()
297
300
  else:
298
- self._stream = self._get_logs()
301
+ stream = self._get_logs()
302
+ if self._text:
303
+ stream = _decode_bytes_stream_to_str(stream)
304
+ self._stream = cast(AsyncGenerator[T, None], stream)
299
305
  return self._stream
300
306
 
301
307
  async def __anext__(self) -> T:
302
308
  """mdmd:hidden"""
303
309
  stream = self._ensure_stream()
304
-
305
- value = await stream.__anext__()
306
-
307
- # The stream yields None if it receives an EOF batch.
308
- if value is None:
309
- raise StopAsyncIteration
310
-
311
- if self._text:
312
- return cast(T, value.decode("utf-8"))
313
- else:
314
- return cast(T, value)
310
+ return cast(T, await stream.__anext__())
315
311
 
316
312
  async def aclose(self):
317
313
  """mdmd:hidden"""
@@ -330,6 +326,7 @@ async def _decode_bytes_stream_to_str(stream: AsyncGenerator[bytes, None]) -> As
330
326
  text = decoder.decode(item, final=False)
331
327
  if text:
332
328
  yield text
329
+
333
330
  # Flush any buffered partial character at end-of-stream
334
331
  tail = decoder.decode(b"", final=True)
335
332
  if tail:
modal/io_streams.pyi CHANGED
@@ -21,7 +21,7 @@ T = typing.TypeVar("T")
21
21
  class _StreamReaderThroughServer(typing.Generic[T]):
22
22
  """A StreamReader implementation that reads from the server."""
23
23
 
24
- _stream: typing.Optional[collections.abc.AsyncGenerator[typing.Optional[bytes], None]]
24
+ _stream: typing.Optional[collections.abc.AsyncGenerator[T, None]]
25
25
 
26
26
  def __init__(
27
27
  self,
@@ -54,9 +54,7 @@ class _StreamReaderThroughServer(typing.Generic[T]):
54
54
  """Streams the container process buffer to the reader."""
55
55
  ...
56
56
 
57
- def _get_logs(
58
- self, skip_empty_messages: bool = True
59
- ) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]:
57
+ def _get_logs(self, skip_empty_messages: bool = True) -> collections.abc.AsyncGenerator[bytes, None]:
60
58
  """Streams sandbox or process logs from the server to the reader.
61
59
 
62
60
  Logs returned by this method may contain partial or multiple lines at a time.
@@ -66,11 +64,11 @@ class _StreamReaderThroughServer(typing.Generic[T]):
66
64
  """
67
65
  ...
68
66
 
69
- def _get_logs_by_line(self) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]:
67
+ def _get_logs_by_line(self) -> collections.abc.AsyncGenerator[bytes, None]:
70
68
  """Process logs from the server and yield complete lines only."""
71
69
  ...
72
70
 
73
- def _ensure_stream(self) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]: ...
71
+ def _ensure_stream(self) -> collections.abc.AsyncGenerator[T, None]: ...
74
72
  async def __anext__(self) -> T:
75
73
  """mdmd:hidden"""
76
74
  ...
modal/sandbox.py CHANGED
@@ -516,10 +516,10 @@ class _Sandbox(_Object, type_prefix="sb"):
516
516
  return obj
517
517
 
518
518
  def _hydrate_metadata(self, handle_metadata: Optional[Message]):
519
- self._stdout: _StreamReader[str] = StreamReader[str](
519
+ self._stdout = StreamReader(
520
520
  api_pb2.FILE_DESCRIPTOR_STDOUT, self.object_id, "sandbox", self._client, by_line=True
521
521
  )
522
- self._stderr: _StreamReader[str] = StreamReader[str](
522
+ self._stderr = StreamReader(
523
523
  api_pb2.FILE_DESCRIPTOR_STDERR, self.object_id, "sandbox", self._client, by_line=True
524
524
  )
525
525
  self._stdin = StreamWriter(self.object_id, "sandbox", self._client)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.1.dev22
3
+ Version: 1.2.1.dev23
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -24,7 +24,7 @@ modal/app.pyi,sha256=AUV5Rp8qQrZJTP2waoKHFY7rYgsXNMYibMcCAQKuSeo,50544
24
24
  modal/billing.py,sha256=zmQ3bcCJlwa4KD1IA_QgdWpm1pn13c-7qfy79iEauYI,195
25
25
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
26
26
  modal/client.py,sha256=kyAIVB3Ay-XKJizQ_1ufUFB__EagV0MLmHJpyYyJ7J0,18636
27
- modal/client.pyi,sha256=x1ql_vmUpSl0DryzlHksUBC-kFtp6c6eH9uVA4hTwN8,15831
27
+ modal/client.pyi,sha256=TOxThnWDwbzrOv88NOmjHtveunQWiuMMrQBB3rJrAXQ,15831
28
28
  modal/cloud_bucket_mount.py,sha256=I2GRXYhOWLIz2kJZjXu75jAm9EJkBNcutGc6jR2ReUw,5928
29
29
  modal/cloud_bucket_mount.pyi,sha256=VuUOipMIHqFXMkD-3g2bsoqpSxV5qswlFHDOqPQzYAo,7405
30
30
  modal/cls.py,sha256=ZxzivE3fNci4-A5uyBYNAzXMXtdqDg3gnYvgbdy5fhg,40384
@@ -45,8 +45,8 @@ modal/functions.pyi,sha256=CMwApS396tdElFrjnV6RuL2DTCz4C3jYzYoq1y_LPUQ,37988
45
45
  modal/gpu.py,sha256=Fe5ORvVPDIstSq1xjmM6OoNgLYFWvogP9r5BgmD3hYg,6769
46
46
  modal/image.py,sha256=HDkOnhIAN8g63a8LTN4J5SjC9ciReFQQJIxTS2z5KFM,107216
47
47
  modal/image.pyi,sha256=dMvMwAuvWkNN2BRYJFijkEy2m_xtEXgCKK0T7FVldsc,77514
48
- modal/io_streams.py,sha256=6fTMyPt8wCdoWFH5EuEBoW1Ye0dHITaxxMmzDPA-sdM,29565
49
- modal/io_streams.pyi,sha256=h7qtAbj8LsN-eJKAGjBhnMBegvWprc_0AmwVFi6rj2Y,18084
48
+ modal/io_streams.py,sha256=HHRuwwWQAF1SKi0zBFugE2ldlHnyAPLyyJIpetOBGh0,29907
49
+ modal/io_streams.pyi,sha256=_Tu84oFE2VD6ueqGCqhUM0b18eJkHO7of3-YJRScwDw,17994
50
50
  modal/mount.py,sha256=G7_xhQMZqokgfsaFLMch0YR3fs-OUNqYUm3f4jHTSMQ,33161
51
51
  modal/mount.pyi,sha256=MD_zV2M7eCWxbOpQRjU60aHevN-bmbiywaCX82QoFlw,15380
52
52
  modal/network_file_system.py,sha256=ZdEIRgdcR-p_ILyw_AecEtPOhhrSWJeADYCtFnhtaHM,13509
@@ -67,7 +67,7 @@ modal/retries.py,sha256=IvNLDM0f_GLUDD5VgEDoN09C88yoxSrCquinAuxT1Sc,5205
67
67
  modal/runner.py,sha256=Ni54hwa42SEBxLPpqFwKMsUPYY8Dv-I-Kz3_jL1StCI,25220
68
68
  modal/runner.pyi,sha256=DV3Z7h0owgRyOu9W5KU5O3UbRftX99KGrZQId91fpsU,8671
69
69
  modal/running_app.py,sha256=v61mapYNV1-O-Uaho5EfJlryMLvIT9We0amUOSvSGx8,1188
70
- modal/sandbox.py,sha256=QHpnp7ifmlVSzJcJyRCEHYmhvu5SrLBVjIx6gaTnlXg,51071
70
+ modal/sandbox.py,sha256=JAC4__5bOVaIIfVn35_SDm9BEu0TVaFVubGZX9d9ycc,51021
71
71
  modal/sandbox.pyi,sha256=VqGO59NZX5fSU1tnA_g0pAd7eq6GvV6lNtC8TH9Xlo8,57478
72
72
  modal/schedule.py,sha256=ng0g0AqNY5GQI9KhkXZQ5Wam5G42glbkqVQsNpBtbDE,3078
73
73
  modal/scheduler_placement.py,sha256=BAREdOY5HzHpzSBqt6jDVR6YC_jYfHMVqOzkyqQfngU,1235
@@ -156,7 +156,7 @@ modal/experimental/__init__.py,sha256=9gkVuDmu3m4TlKoU3MzEtTOemUSs8EEOWba40s7Aa0
156
156
  modal/experimental/flash.py,sha256=-lSyFBbeT6UT-uB29L955SNh6L6ISg_uBDy5gF4ZpLo,26919
157
157
  modal/experimental/flash.pyi,sha256=uwinKAYxpunNNfBj58FP88DXb535Qik4F6tnJKPAIwQ,14696
158
158
  modal/experimental/ipython.py,sha256=TrCfmol9LGsRZMeDoeMPx3Hv3BFqQhYnmD_iH0pqdhk,2904
159
- modal-1.2.1.dev22.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
159
+ modal-1.2.1.dev23.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
160
160
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
161
161
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
162
162
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -184,10 +184,10 @@ modal_proto/task_command_router_pb2.py,sha256=_pD2ZpU0bNzhwBdzmLoLyLtAtftI_Agxwn
184
184
  modal_proto/task_command_router_pb2.pyi,sha256=EyDgXPLr7alqjXYERV8w_MPuO404x0uCppmSkrfE9IE,14589
185
185
  modal_proto/task_command_router_pb2_grpc.py,sha256=uEQ0HdrCp8v-9bB5yIic9muA8spCShLHY6Bz9cCgOUE,10114
186
186
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=s3Yxsrawdj4nr8vqQqsAxyX6ilWaGbdECy425KKbLIA,3301
187
- modal_version/__init__.py,sha256=PfenEFRXc4ydfsfvygP1gRIGi9C18rye2ygpxGAsme0,121
187
+ modal_version/__init__.py,sha256=BDwnkqA3KOmOzb1T7KMF0UbUZbk5R2ZY6EXWf_FjeLc,121
188
188
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
189
- modal-1.2.1.dev22.dist-info/METADATA,sha256=c_E5p4U4G6rGKGidIB2qWJp6anOeG27rIMDHXbhRq74,2484
190
- modal-1.2.1.dev22.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
191
- modal-1.2.1.dev22.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
192
- modal-1.2.1.dev22.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
193
- modal-1.2.1.dev22.dist-info/RECORD,,
189
+ modal-1.2.1.dev23.dist-info/METADATA,sha256=Tue222Y3MT_-09yxF2bZuqLxxm-kDzDPb7CtH3O7CSg,2484
190
+ modal-1.2.1.dev23.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
191
+ modal-1.2.1.dev23.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
192
+ modal-1.2.1.dev23.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
193
+ modal-1.2.1.dev23.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.2.1.dev22"
4
+ __version__ = "1.2.1.dev23"