modAL 1.2.7.dev7__py3-none-any.whl → 1.2.7.dev9__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.
@@ -152,12 +152,13 @@ async def perform_multipart_upload(
152
152
  part_etags = await TaskContext.gather(*upload_coros)
153
153
 
154
154
  # The body of the complete_multipart_upload command needs some data in xml format:
155
- completion_body = "<CompleteMultipartUpload>\n"
155
+ completion_parts = ["<CompleteMultipartUpload>"]
156
156
  for part_number, etag in enumerate(part_etags, 1):
157
- completion_body += f"""<Part>\n<PartNumber>{part_number}</PartNumber>\n<ETag>"{etag}"</ETag>\n</Part>\n"""
158
- completion_body += "</CompleteMultipartUpload>"
157
+ completion_parts.append(f"""<Part>\n<PartNumber>{part_number}</PartNumber>\n<ETag>"{etag}"</ETag>\n</Part>""")
158
+ completion_parts.append("</CompleteMultipartUpload>")
159
+ completion_body = "\n".join(completion_parts)
159
160
 
160
- # etag of combined object should be md5 hex of concatendated md5 *bytes* from parts + `-{num_parts}`
161
+ # etag of combined object should be md5 hex of concatenated md5 *bytes* from parts + `-{num_parts}`
161
162
  bin_hash_parts = [bytes.fromhex(etag) for etag in part_etags]
162
163
 
163
164
  expected_multipart_etag = hashlib.md5(b"".join(bin_hash_parts)).hexdigest() + f"-{len(part_etags)}"
modal/client.pyi CHANGED
@@ -32,7 +32,7 @@ class _Client:
32
32
  server_url: str,
33
33
  client_type: int,
34
34
  credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.7.dev7",
35
+ version: str = "1.2.7.dev9",
36
36
  ):
37
37
  """mdmd:hidden
38
38
  The Modal client object is not intended to be instantiated directly by users.
@@ -163,7 +163,7 @@ class Client:
163
163
  server_url: str,
164
164
  client_type: int,
165
165
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.2.7.dev7",
166
+ version: str = "1.2.7.dev9",
167
167
  ):
168
168
  """mdmd:hidden
169
169
  The Modal client object is not intended to be instantiated directly by users.
modal/file_io.py CHANGED
@@ -190,7 +190,7 @@ class _FileIO(Generic[T]):
190
190
 
191
191
  async def _wait(self, exec_id: str) -> bytes:
192
192
  # The logic here is similar to how output is read from `exec`
193
- output = b""
193
+ output_buffer = io.BytesIO()
194
194
  completed = False
195
195
  retries_remaining = 10
196
196
  while not completed:
@@ -201,7 +201,7 @@ class _FileIO(Generic[T]):
201
201
  break
202
202
  if isinstance(data, Exception):
203
203
  raise data
204
- output += data
204
+ output_buffer.write(data)
205
205
  except (GRPCError, StreamTerminatedError) as exc:
206
206
  if retries_remaining > 0:
207
207
  retries_remaining -= 1
@@ -212,7 +212,7 @@ class _FileIO(Generic[T]):
212
212
  elif isinstance(exc, StreamTerminatedError):
213
213
  continue
214
214
  raise
215
- return output
215
+ return output_buffer.getvalue()
216
216
 
217
217
  def _validate_type(self, data: Union[bytes, str]) -> None:
218
218
  if self._binary and isinstance(data, str):
@@ -421,10 +421,22 @@ class _FileIO(Generic[T]):
421
421
  task_id=self._task_id,
422
422
  ),
423
423
  )
424
+
425
+ def end_of_event(item: bytes, buffer: io.BytesIO, boundary_token: bytes) -> bool:
426
+ if not item.endswith(b"\n"):
427
+ return False
428
+ boundary_token_size = len(boundary_token)
429
+ if buffer.tell() < boundary_token_size:
430
+ return False
431
+ buffer.seek(-boundary_token_size, io.SEEK_END)
432
+ if buffer.read(boundary_token_size) == boundary_token:
433
+ return True
434
+ return False
435
+
424
436
  async with TaskContext() as tc:
425
437
  tc.create_task(self._consume_watch_output(resp.exec_id))
426
438
 
427
- buffer = b""
439
+ item_buffer = io.BytesIO()
428
440
  while True:
429
441
  if len(self._watch_output_buffer) > 0:
430
442
  item = self._watch_output_buffer.pop(0)
@@ -432,12 +444,12 @@ class _FileIO(Generic[T]):
432
444
  break
433
445
  if isinstance(item, Exception):
434
446
  raise item
435
- buffer += item
436
- # a single event may be split across multiple messages
437
- # the end of an event is marked by two newlines
438
- if buffer.endswith(b"\n\n"):
447
+ item_buffer.write(item)
448
+ assert isinstance(item, bytes)
449
+ # Single events may span multiple messages so we need to check for a special event boundary token
450
+ if end_of_event(item, item_buffer, boundary_token=b"\n\n"):
439
451
  try:
440
- event_json = json.loads(buffer.strip().decode())
452
+ event_json = json.loads(item_buffer.getvalue().strip().decode())
441
453
  event = FileWatchEvent(
442
454
  type=FileWatchEventType(event_json["event_type"]),
443
455
  paths=event_json["paths"],
@@ -447,7 +459,7 @@ class _FileIO(Generic[T]):
447
459
  except (json.JSONDecodeError, KeyError, ValueError):
448
460
  # skip invalid events
449
461
  pass
450
- buffer = b""
462
+ item_buffer = io.BytesIO()
451
463
  else:
452
464
  await asyncio.sleep(0.1)
453
465
 
modal/io_streams.py CHANGED
@@ -23,7 +23,7 @@ from grpclib.exceptions import GRPCError, StreamTerminatedError
23
23
  from modal.exception import ClientClosed, ExecTimeoutError, InvalidError
24
24
  from modal_proto import api_pb2
25
25
 
26
- from ._utils.async_utils import aclosing, synchronize_api
26
+ from ._utils.async_utils import aclosing, synchronize_api, synchronizer
27
27
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
28
28
  from ._utils.task_command_router_client import TaskCommandRouterClient
29
29
  from .client import _Client
@@ -153,17 +153,17 @@ class _StreamReaderThroughServer(Generic[T]):
153
153
  """Fetch the entire contents of the stream until EOF."""
154
154
  logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read starting")
155
155
  if self._text:
156
- data_str = ""
156
+ buffer = io.StringIO()
157
157
  async for message in _decode_bytes_stream_to_str(self._get_logs()):
158
- data_str += message
158
+ buffer.write(message)
159
159
  logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
160
- return cast(T, data_str)
160
+ return cast(T, buffer.getvalue())
161
161
  else:
162
- data_bytes = b""
162
+ buffer = io.BytesIO()
163
163
  async for message in self._get_logs():
164
- data_bytes += message
164
+ buffer.write(message)
165
165
  logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
166
- return cast(T, data_bytes)
166
+ return cast(T, buffer.getvalue())
167
167
 
168
168
  async def _consume_container_process_stream(self):
169
169
  """Consume the container process stream and store messages in the buffer."""
@@ -566,6 +566,9 @@ class _StreamReader(Generic[T]):
566
566
  task_id: Optional[str] = None,
567
567
  ) -> None:
568
568
  """mdmd:hidden"""
569
+ # we can remove this once we ensure no constructors use async code
570
+ assert asyncio.get_running_loop() == synchronizer._get_loop(start=False)
571
+
569
572
  if by_line and not text:
570
573
  raise ValueError("line-buffering is only supported when text=True")
571
574
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.7.dev7
3
+ Version: 1.2.7.dev9
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -26,7 +26,7 @@ modal/app.pyi,sha256=rL-UAzRkwoQp-PA96fQAS4YfmjWBDyfasziCMq6NrHI,51015
26
26
  modal/billing.py,sha256=zmQ3bcCJlwa4KD1IA_QgdWpm1pn13c-7qfy79iEauYI,195
27
27
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
28
28
  modal/client.py,sha256=k1CqQ_0t4-vHylkqYZgYCnClg-FT0QrTPAKivL01A_k,14354
29
- modal/client.pyi,sha256=iDYFbDtM2tc7Sx9WkenkYajql1W5XCmwYk5QETBzolU,13146
29
+ modal/client.pyi,sha256=vBmss5dgofpPZ-_mCYAZRaMenwB4uTsrogebAm7pQ0Y,13146
30
30
  modal/cloud_bucket_mount.py,sha256=cvVj5Rt4nDHoQbTneoJPTaAcnl_Ab_mrsjnnzd0v9OI,6013
31
31
  modal/cloud_bucket_mount.pyi,sha256=vXusGMOUk9HjCs4lhxteuCK4xLQclPfugGWKo7LECEA,7539
32
32
  modal/cls.py,sha256=PIGe4ED28jcxC2HeKtMD3odMIb4qNmlRt3tj1dZstRo,41738
@@ -39,7 +39,7 @@ modal/dict.pyi,sha256=yQHHZe98M6gB2ahzrmqlq8KUXvqFy6vamsijI2H0RdE,32002
39
39
  modal/environments.py,sha256=wMiMNpPkvIl_BdxRb81CY9Djzhczp_y4GEYKWp0JyBc,6135
40
40
  modal/environments.pyi,sha256=bMIxvwahckgaolGD3-hP2iovRHDXewvgbVs_nx4ze0o,3808
41
41
  modal/exception.py,sha256=8QeIE6XcuAc1moa1hSgQTxFFjfEyIdiuKiAyCS3RS4M,10210
42
- modal/file_io.py,sha256=lfcQAWeuujQol_e1mYlm331Uesh__yWsv-u19gQV7wc,19308
42
+ modal/file_io.py,sha256=hqK7ZXo7iEidXRriBAU2Es08HC-rGKmMjMbcT5FaASI,19939
43
43
  modal/file_io.pyi,sha256=MpO5-EA6KuBbOQV0zXV_nPDD5Aw5dfYVa8q9SGBsAVM,14666
44
44
  modal/file_pattern_matcher.py,sha256=A_Kdkej6q7YQyhM_2-BvpFmPqJ0oHb54B6yf9VqvPVE,8116
45
45
  modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
@@ -47,7 +47,7 @@ modal/functions.pyi,sha256=CeSVZcrjsjh8Gqo5PK7QkbpfSlrKvnFzm3HLYqGEePw,38107
47
47
  modal/gpu.py,sha256=Fe5ORvVPDIstSq1xjmM6OoNgLYFWvogP9r5BgmD3hYg,6769
48
48
  modal/image.py,sha256=Pm26DwuD7RqfrxscAHxhoA9LGJYTt_k0wcsHRqbwnzE,107945
49
49
  modal/image.pyi,sha256=grkqK9BfyKDXmJYNme-lGa0O3u7pOU5GifyLOAhG3sc,78202
50
- modal/io_streams.py,sha256=ziMQ1VNxxj0tJPYl9QpJZCWd72jyQ9BJ1mmanPXvxv0,30615
50
+ modal/io_streams.py,sha256=jhBdJH080kPkNdkYyvtxJumcJt2nEDozsKDF5I0o3hM,30818
51
51
  modal/io_streams.pyi,sha256=GGRPHumOaTwr11GzmZPf4_aSXGGKsMvB6NoSGHp_F7Q,18633
52
52
  modal/mount.py,sha256=NYWmSqK842qmYUQZqaacwCdMRRyfj8stA3DvsChjGRc,33838
53
53
  modal/mount.pyi,sha256=cJ4HPB7Oa7vl_AkBWQ0cKaty36ZcO8fNLE48DYUa9eQ,15807
@@ -98,7 +98,7 @@ modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
98
98
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
99
99
  modal/_utils/async_utils.py,sha256=PnwHfaHcqjLwMnlGC2kn8nqubxpXcSKcy4jHx6477dg,30625
100
100
  modal/_utils/auth_token_manager.py,sha256=QsDokl1c1dN9m6Z9N-Se-8cY3laphByjJtNdRXUTieI,5151
101
- modal/_utils/blob_utils.py,sha256=4z0qH5txEUvfmfIYnZnvIG6G4BwiTpn3fsfI24ppD6M,23316
101
+ modal/_utils/blob_utils.py,sha256=L4lEL-rvZwoxCxrGaa8zWVoaHjhq1eQfO4SLrvt4g8k,23376
102
102
  modal/_utils/bytes_io_segment_payload.py,sha256=vaXPq8b52-x6G2hwE7SrjS58pg_aRm7gV3bn3yjmTzQ,4261
103
103
  modal/_utils/deprecation.py,sha256=-Bgg7jZdcJU8lROy18YyVnQYbM8hue-hVmwJqlWAGH0,5504
104
104
  modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
@@ -158,7 +158,7 @@ modal/experimental/__init__.py,sha256=GOuo3t4-Le0ZVddiHIOw0MQxkdTJge-bVbhDvgnblm
158
158
  modal/experimental/flash.py,sha256=GJZwSr-4OHlP6gCIZdcbsSb8D8WX9xJwMphjwcfsRoU,31262
159
159
  modal/experimental/flash.pyi,sha256=q4a3mdgL0jzATPL2tS7LpuyulYzF1vUSE453S9xc8n8,16965
160
160
  modal/experimental/ipython.py,sha256=TrCfmol9LGsRZMeDoeMPx3Hv3BFqQhYnmD_iH0pqdhk,2904
161
- modal-1.2.7.dev7.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
161
+ modal-1.2.7.dev9.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
162
162
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
163
163
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
164
164
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -180,10 +180,10 @@ modal_proto/task_command_router_pb2.py,sha256=2Y8wRvnq4_7RiQ-5ZAPIgXaxrHv_MXzgS5
180
180
  modal_proto/task_command_router_pb2.pyi,sha256=yI4tFteErLlonFA2HyaWtONfhY_VGSe7KCWFIspvmkE,16323
181
181
  modal_proto/task_command_router_pb2_grpc.py,sha256=q2VWfK2-4uDcPaPLhAqPZkgxhuQRYnWmwUcnEbPbGuk,13895
182
182
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=e1MM10VGUgDkm041v53QeTglGQPzTpA0FUsFtezjp74,4536
183
- modal_version/__init__.py,sha256=EIU8WY6zGrQ5WjfZUZ42Hl3spbaZh3QHRY0P_2zdMb8,120
183
+ modal_version/__init__.py,sha256=Nj3_Nk88t3yPscVokW4lp7tcogFqwHgva5YAz4Zww3Y,120
184
184
  modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
185
- modal-1.2.7.dev7.dist-info/METADATA,sha256=FpCHbZMv2nPWKfnWIBQ7nXtlMtihMgF0eImXNNBNFK8,2491
186
- modal-1.2.7.dev7.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
187
- modal-1.2.7.dev7.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
188
- modal-1.2.7.dev7.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
189
- modal-1.2.7.dev7.dist-info/RECORD,,
185
+ modal-1.2.7.dev9.dist-info/METADATA,sha256=R8W0pUmCnIhbGcyymb9v_tlZiuQiUX--xbI-f39wpq0,2491
186
+ modal-1.2.7.dev9.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
187
+ modal-1.2.7.dev9.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
188
+ modal-1.2.7.dev9.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
189
+ modal-1.2.7.dev9.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.7.dev7"
4
+ __version__ = "1.2.7.dev9"