modal 0.66.50__py3-none-any.whl → 0.66.52__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.
modal/client.pyi CHANGED
@@ -31,7 +31,7 @@ class _Client:
31
31
  server_url: str,
32
32
  client_type: int,
33
33
  credentials: typing.Optional[typing.Tuple[str, str]],
34
- version: str = "0.66.50",
34
+ version: str = "0.66.52",
35
35
  ): ...
36
36
  def is_closed(self) -> bool: ...
37
37
  @property
@@ -90,7 +90,7 @@ class Client:
90
90
  server_url: str,
91
91
  client_type: int,
92
92
  credentials: typing.Optional[typing.Tuple[str, str]],
93
- version: str = "0.66.50",
93
+ version: str = "0.66.52",
94
94
  ): ...
95
95
  def is_closed(self) -> bool: ...
96
96
  @property
modal/functions.pyi CHANGED
@@ -446,11 +446,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
446
446
 
447
447
  _call_generator_nowait: ___call_generator_nowait_spec
448
448
 
449
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
449
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
450
450
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
451
451
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
452
452
 
453
- remote: __remote_spec[P, ReturnType]
453
+ remote: __remote_spec[ReturnType, P]
454
454
 
455
455
  class __remote_gen_spec(typing_extensions.Protocol):
456
456
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -462,17 +462,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
462
462
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
463
463
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
464
464
 
465
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
465
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
466
466
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
467
467
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
468
468
 
469
- _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
469
+ _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
470
470
 
471
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
471
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
472
472
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
473
473
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
474
474
 
475
- spawn: __spawn_spec[P, ReturnType]
475
+ spawn: __spawn_spec[ReturnType, P]
476
476
 
477
477
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
478
478
 
modal/image.py CHANGED
@@ -19,7 +19,6 @@ from typing import (
19
19
  Optional,
20
20
  Sequence,
21
21
  Set,
22
- Tuple,
23
22
  Union,
24
23
  cast,
25
24
  get_args,
@@ -36,6 +35,7 @@ from ._utils.async_utils import synchronize_api
36
35
  from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
37
36
  from ._utils.function_utils import FunctionInfo
38
37
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
38
+ from .client import _Client
39
39
  from .cloud_bucket_mount import _CloudBucketMount
40
40
  from .config import config, logger, user_config_path
41
41
  from .environments import _get_environment_cached
@@ -52,7 +52,6 @@ from .volume import _Volume
52
52
  if typing.TYPE_CHECKING:
53
53
  import modal.functions
54
54
 
55
-
56
55
  # This is used for both type checking and runtime validation
57
56
  ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10"]
58
57
 
@@ -147,8 +146,8 @@ def _get_modal_requirements_command(version: ImageBuilderVersion) -> str:
147
146
  return f"{prefix} -r {CONTAINER_REQUIREMENTS_PATH}"
148
147
 
149
148
 
150
- def _flatten_str_args(function_name: str, arg_name: str, args: Tuple[Union[str, List[str]], ...]) -> List[str]:
151
- """Takes a tuple of strings, or string lists, and flattens it.
149
+ def _flatten_str_args(function_name: str, arg_name: str, args: Sequence[Union[str, List[str]]]) -> List[str]:
150
+ """Takes a sequence of strings, or string lists, and flattens it.
152
151
 
153
152
  Raises an error if any of the elements are not strings or string lists.
154
153
  """
@@ -244,7 +243,7 @@ class _ImageRegistryConfig:
244
243
  def __init__(
245
244
  self,
246
245
  # TODO: change to _PUBLIC after worker starts handling it.
247
- registry_auth_type: int = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
246
+ registry_auth_type: "api_pb2.RegistryAuthType.ValueType" = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
248
247
  secret: Optional[_Secret] = None,
249
248
  ):
250
249
  self.registry_auth_type = registry_auth_type
@@ -253,7 +252,7 @@ class _ImageRegistryConfig:
253
252
  def get_proto(self) -> api_pb2.ImageRegistryConfig:
254
253
  return api_pb2.ImageRegistryConfig(
255
254
  registry_auth_type=self.registry_auth_type,
256
- secret_id=(self.secret.object_id if self.secret else None),
255
+ secret_id=(self.secret.object_id if self.secret else ""),
257
256
  )
258
257
 
259
258
 
@@ -264,6 +263,45 @@ class DockerfileSpec:
264
263
  context_files: Dict[str, str]
265
264
 
266
265
 
266
+ async def _image_await_build_result(image_id: str, client: _Client) -> api_pb2.ImageJoinStreamingResponse:
267
+ last_entry_id: str = ""
268
+ result_response: Optional[api_pb2.ImageJoinStreamingResponse] = None
269
+
270
+ async def join():
271
+ nonlocal last_entry_id, result_response
272
+
273
+ request = api_pb2.ImageJoinStreamingRequest(image_id=image_id, timeout=55, last_entry_id=last_entry_id)
274
+ async for response in client.stub.ImageJoinStreaming.unary_stream(request):
275
+ if response.entry_id:
276
+ last_entry_id = response.entry_id
277
+ if response.result.status:
278
+ result_response = response
279
+ # can't return yet, since there may still be logs streaming back in subsequent responses
280
+ for task_log in response.task_logs:
281
+ if task_log.task_progress.pos or task_log.task_progress.len:
282
+ assert task_log.task_progress.progress_type == api_pb2.IMAGE_SNAPSHOT_UPLOAD
283
+ if output_mgr := _get_output_manager():
284
+ output_mgr.update_snapshot_progress(image_id, task_log.task_progress)
285
+ elif task_log.data:
286
+ if output_mgr := _get_output_manager():
287
+ await output_mgr.put_log_content(task_log)
288
+ if output_mgr := _get_output_manager():
289
+ output_mgr.flush_lines()
290
+
291
+ # Handle up to n exceptions while fetching logs
292
+ retry_count = 0
293
+ while result_response is None:
294
+ try:
295
+ await join()
296
+ except (StreamTerminatedError, GRPCError) as exc:
297
+ if isinstance(exc, GRPCError) and exc.status not in RETRYABLE_GRPC_STATUS_CODES:
298
+ raise exc
299
+ retry_count += 1
300
+ if retry_count >= 3:
301
+ raise exc
302
+ return result_response
303
+
304
+
267
305
  class _Image(_Object, type_prefix="im"):
268
306
  """Base class for container images to run functions in.
269
307
 
@@ -292,7 +330,7 @@ class _Image(_Object, type_prefix="im"):
292
330
  self._serve_mounts = other._serve_mounts
293
331
  self._deferred_mounts = other._deferred_mounts
294
332
 
295
- def _hydrate_metadata(self, message: Optional[Message]):
333
+ def _hydrate_metadata(self, metadata: Optional[Message]):
296
334
  env_image_id = config.get("image_id") # set as an env var in containers
297
335
  if env_image_id == self.object_id:
298
336
  for exc in self.inside_exceptions:
@@ -300,9 +338,9 @@ class _Image(_Object, type_prefix="im"):
300
338
  # if the hydrated image is the one used by the container
301
339
  raise exc
302
340
 
303
- if message:
304
- assert isinstance(message, api_pb2.ImageMetadata)
305
- self._metadata = message
341
+ if metadata:
342
+ assert isinstance(metadata, api_pb2.ImageMetadata)
343
+ self._metadata = metadata
306
344
 
307
345
  def _add_mount_layer_or_copy(self, mount: _Mount, copy: bool = False):
308
346
  if copy:
@@ -318,7 +356,7 @@ class _Image(_Object, type_prefix="im"):
318
356
  return _Image._from_loader(_load, "Image(local files)", deps=lambda: [base_image, mount])
319
357
 
320
358
  @property
321
- def _mount_layers(self) -> typing.Tuple[_Mount]:
359
+ def _mount_layers(self) -> typing.Sequence[_Mount]:
322
360
  """Non-evaluated mount layers on the image
323
361
 
324
362
  When the image is used by a Modal container, these mounts need to be attached as well to
@@ -362,7 +400,7 @@ class _Image(_Object, type_prefix="im"):
362
400
  context_mount: Optional[_Mount] = None,
363
401
  force_build: bool = False,
364
402
  # For internal use only.
365
- _namespace: int = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
403
+ _namespace: "api_pb2.DeploymentNamespace.ValueType" = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
366
404
  _do_assert_no_mount_layers: bool = True,
367
405
  ):
368
406
  if base_images is None:
@@ -382,14 +420,14 @@ class _Image(_Object, type_prefix="im"):
382
420
  if build_function and len(base_images) != 1:
383
421
  raise InvalidError("Cannot run a build function with multiple base images!")
384
422
 
385
- def _deps() -> List[_Object]:
386
- deps: List[_Object] = list(base_images.values()) + list(secrets)
423
+ def _deps() -> Sequence[_Object]:
424
+ deps = tuple(base_images.values()) + tuple(secrets)
387
425
  if build_function:
388
- deps.append(build_function)
426
+ deps += (build_function,)
389
427
  if context_mount:
390
- deps.append(context_mount)
391
- if image_registry_config.secret:
392
- deps.append(image_registry_config.secret)
428
+ deps += (context_mount,)
429
+ if image_registry_config and image_registry_config.secret:
430
+ deps += (image_registry_config.secret,)
393
431
  return deps
394
432
 
395
433
  async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
@@ -398,6 +436,7 @@ class _Image(_Object, type_prefix="im"):
398
436
  # base images can't have
399
437
  image._assert_no_mount_layers()
400
438
 
439
+ assert resolver.app_id # type narrowing
401
440
  environment = await _get_environment_cached(resolver.environment_name or "", resolver.client)
402
441
  # A bit hacky,but assume that the environment provides a valid builder version
403
442
  image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
@@ -432,7 +471,6 @@ class _Image(_Object, type_prefix="im"):
432
471
 
433
472
  if build_function:
434
473
  build_function_id = build_function.object_id
435
-
436
474
  globals = build_function._get_info().get_globals()
437
475
  attrs = build_function._get_info().get_cls_var_attrs()
438
476
  globals = {**globals, **attrs}
@@ -454,14 +492,14 @@ class _Image(_Object, type_prefix="im"):
454
492
 
455
493
  # Cloudpickle function serialization produces unstable values.
456
494
  # TODO: better way to filter out types that don't have a stable hash?
457
- build_function_globals = serialize(filtered_globals) if filtered_globals else None
495
+ build_function_globals = serialize(filtered_globals) if filtered_globals else b""
458
496
  _build_function = api_pb2.BuildFunction(
459
497
  definition=build_function.get_build_def(),
460
498
  globals=build_function_globals,
461
499
  input=build_function_input,
462
500
  )
463
501
  else:
464
- build_function_id = None
502
+ build_function_id = ""
465
503
  _build_function = None
466
504
 
467
505
  image_definition = api_pb2.Image(
@@ -470,7 +508,7 @@ class _Image(_Object, type_prefix="im"):
470
508
  context_files=context_file_pb2s,
471
509
  secret_ids=[secret.object_id for secret in secrets],
472
510
  gpu=bool(gpu_config.type), # Note: as of 2023-01-27, server still uses this
473
- context_mount_id=(context_mount.object_id if context_mount else None),
511
+ context_mount_id=(context_mount.object_id if context_mount else ""),
474
512
  gpu_config=gpu_config, # Note: as of 2023-01-27, server ignores this
475
513
  image_registry_config=image_registry_config.get_proto(),
476
514
  runtime=config.get("function_runtime"),
@@ -481,7 +519,7 @@ class _Image(_Object, type_prefix="im"):
481
519
  req = api_pb2.ImageGetOrCreateRequest(
482
520
  app_id=resolver.app_id,
483
521
  image=image_definition,
484
- existing_image_id=existing_object_id, # TODO: ignored
522
+ existing_image_id=existing_object_id or "", # TODO: ignored
485
523
  build_function_id=build_function_id,
486
524
  force_build=config.get("force_build") or force_build,
487
525
  namespace=_namespace,
@@ -492,46 +530,22 @@ class _Image(_Object, type_prefix="im"):
492
530
  )
493
531
  resp = await retry_transient_errors(resolver.client.stub.ImageGetOrCreate, req)
494
532
  image_id = resp.image_id
533
+ result: api_pb2.GenericResult
534
+ metadata: Optional[api_pb2.ImageMetadata] = None
535
+
536
+ if resp.result.status:
537
+ # image already built
538
+ result = resp.result
539
+ if resp.HasField("metadata"):
540
+ metadata = resp.metadata
541
+ else:
542
+ # not built or in the process of building - wait for build
543
+ logger.debug("Waiting for image %s" % image_id)
544
+ resp = await _image_await_build_result(image_id, resolver.client)
545
+ result = resp.result
546
+ if resp.HasField("metadata"):
547
+ metadata = resp.metadata
495
548
 
496
- logger.debug("Waiting for image %s" % image_id)
497
- last_entry_id: Optional[str] = None
498
- result_response: Optional[api_pb2.ImageJoinStreamingResponse] = None
499
-
500
- async def join():
501
- nonlocal last_entry_id, result_response
502
-
503
- request = api_pb2.ImageJoinStreamingRequest(image_id=image_id, timeout=55, last_entry_id=last_entry_id)
504
-
505
- async for response in resolver.client.stub.ImageJoinStreaming.unary_stream(request):
506
- if response.entry_id:
507
- last_entry_id = response.entry_id
508
- if response.result.status:
509
- result_response = response
510
- # can't return yet, since there may still be logs streaming back in subsequent responses
511
- for task_log in response.task_logs:
512
- if task_log.task_progress.pos or task_log.task_progress.len:
513
- assert task_log.task_progress.progress_type == api_pb2.IMAGE_SNAPSHOT_UPLOAD
514
- if output_mgr := _get_output_manager():
515
- output_mgr.update_snapshot_progress(image_id, task_log.task_progress)
516
- elif task_log.data:
517
- if output_mgr := _get_output_manager():
518
- await output_mgr.put_log_content(task_log)
519
- if output_mgr := _get_output_manager():
520
- output_mgr.flush_lines()
521
-
522
- # Handle up to n exceptions while fetching logs
523
- retry_count = 0
524
- while result_response is None:
525
- try:
526
- await join()
527
- except (StreamTerminatedError, GRPCError) as exc:
528
- if isinstance(exc, GRPCError) and exc.status not in RETRYABLE_GRPC_STATUS_CODES:
529
- raise exc
530
- retry_count += 1
531
- if retry_count >= 3:
532
- raise exc
533
-
534
- result = result_response.result
535
549
  if result.status == api_pb2.GenericResult.GENERIC_STATUS_FAILURE:
536
550
  raise RemoteError(f"Image build for {image_id} failed with the exception:\n{result.exception}")
537
551
  elif result.status == api_pb2.GenericResult.GENERIC_STATUS_TERMINATED:
@@ -545,7 +559,7 @@ class _Image(_Object, type_prefix="im"):
545
559
  else:
546
560
  raise RemoteError("Unknown status %s!" % result.status)
547
561
 
548
- self._hydrate(image_id, resolver.client, result_response.metadata)
562
+ self._hydrate(image_id, resolver.client, metadata)
549
563
  local_mounts = set()
550
564
  for base in base_images.values():
551
565
  local_mounts |= base._serve_mounts
@@ -666,7 +680,7 @@ class _Image(_Object, type_prefix="im"):
666
680
  context_mount=mount,
667
681
  )
668
682
 
669
- def _add_local_python_packages(self, *packages: Union[str, Path], copy: bool = False) -> "_Image":
683
+ def _add_local_python_packages(self, *packages: str, copy: bool = False) -> "_Image":
670
684
  """Adds Python package files to containers
671
685
 
672
686
  Adds all files from the specified Python packages to containers running the Image.
@@ -1632,7 +1646,7 @@ class _Image(_Object, type_prefix="im"):
1632
1646
  function = _Function.from_args(
1633
1647
  info,
1634
1648
  app=None,
1635
- image=self,
1649
+ image=self, # type: ignore[reportArgumentType] # TODO: probably conflict with type stub?
1636
1650
  secrets=secrets,
1637
1651
  gpu=gpu,
1638
1652
  mounts=mounts,
@@ -1700,7 +1714,7 @@ class _Image(_Object, type_prefix="im"):
1700
1714
  """
1701
1715
 
1702
1716
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
1703
- commands = ["FROM base", f"WORKDIR {shlex.quote(path)}"]
1717
+ commands = ["FROM base", f"WORKDIR {shlex.quote(str(path))}"]
1704
1718
  return DockerfileSpec(commands=commands, context_files={})
1705
1719
 
1706
1720
  return _Image._from_args(
@@ -1744,7 +1758,7 @@ class _Image(_Object, type_prefix="im"):
1744
1758
 
1745
1759
  This method is considered private since its interface may change - use it at your own risk!
1746
1760
  """
1747
- last_entry_id: Optional[str] = None
1761
+ last_entry_id: str = ""
1748
1762
 
1749
1763
  request = api_pb2.ImageJoinStreamingRequest(
1750
1764
  image_id=self._object_id, timeout=55, last_entry_id=last_entry_id, include_logs_for_finished=True
modal/image.pyi CHANGED
@@ -1,4 +1,5 @@
1
1
  import google.protobuf.message
2
+ import modal.client
2
3
  import modal.cloud_bucket_mount
3
4
  import modal.functions
4
5
  import modal.gpu
@@ -28,7 +29,7 @@ def _get_modal_requirements_path(
28
29
  ) -> str: ...
29
30
  def _get_modal_requirements_command(version: typing.Literal["2023.12", "2024.04", "2024.10"]) -> str: ...
30
31
  def _flatten_str_args(
31
- function_name: str, arg_name: str, args: typing.Tuple[typing.Union[str, typing.List[str]], ...]
32
+ function_name: str, arg_name: str, args: typing.Sequence[typing.Union[str, typing.List[str]]]
32
33
  ) -> typing.List[str]: ...
33
34
  def _validate_packages(packages: typing.List[str]) -> bool: ...
34
35
  def _warn_invalid_packages(old_command: str) -> None: ...
@@ -55,6 +56,10 @@ class DockerfileSpec:
55
56
  def __repr__(self): ...
56
57
  def __eq__(self, other): ...
57
58
 
59
+ async def _image_await_build_result(
60
+ image_id: str, client: modal.client._Client
61
+ ) -> modal_proto.api_pb2.ImageJoinStreamingResponse: ...
62
+
58
63
  class _Image(modal.object._Object):
59
64
  force_build: bool
60
65
  inside_exceptions: typing.List[Exception]
@@ -64,10 +69,10 @@ class _Image(modal.object._Object):
64
69
 
65
70
  def _initialize_from_empty(self): ...
66
71
  def _initialize_from_other(self, other: _Image): ...
67
- def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
72
+ def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
68
73
  def _add_mount_layer_or_copy(self, mount: modal.mount._Mount, copy: bool = False): ...
69
74
  @property
70
- def _mount_layers(self) -> typing.Tuple[modal.mount._Mount]: ...
75
+ def _mount_layers(self) -> typing.Sequence[modal.mount._Mount]: ...
71
76
  def _assert_no_mount_layers(self): ...
72
77
  @staticmethod
73
78
  def _from_args(
@@ -109,7 +114,7 @@ class _Image(modal.object._Object):
109
114
  def copy_local_file(
110
115
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
111
116
  ) -> _Image: ...
112
- def _add_local_python_packages(self, *module_names, copy: bool = False) -> _Image: ...
117
+ def _add_local_python_packages(self, *module_names: str, copy: bool = False) -> _Image: ...
113
118
  def copy_local_dir(
114
119
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
115
120
  ) -> _Image: ...
@@ -321,10 +326,10 @@ class Image(modal.object.Object):
321
326
  def __init__(self, *args, **kwargs): ...
322
327
  def _initialize_from_empty(self): ...
323
328
  def _initialize_from_other(self, other: Image): ...
324
- def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
329
+ def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
325
330
  def _add_mount_layer_or_copy(self, mount: modal.mount.Mount, copy: bool = False): ...
326
331
  @property
327
- def _mount_layers(self) -> typing.Tuple[modal.mount.Mount]: ...
332
+ def _mount_layers(self) -> typing.Sequence[modal.mount.Mount]: ...
328
333
  def _assert_no_mount_layers(self): ...
329
334
  @staticmethod
330
335
  def _from_args(
@@ -366,7 +371,7 @@ class Image(modal.object.Object):
366
371
  def copy_local_file(
367
372
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
368
373
  ) -> Image: ...
369
- def _add_local_python_packages(self, *module_names, copy: bool = False) -> Image: ...
374
+ def _add_local_python_packages(self, *module_names: str, copy: bool = False) -> Image: ...
370
375
  def copy_local_dir(
371
376
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
372
377
  ) -> Image: ...
modal/object.py CHANGED
@@ -17,7 +17,7 @@ O = TypeVar("O", bound="_Object")
17
17
 
18
18
  _BLOCKING_O = synchronize_api(O)
19
19
 
20
- EPHEMERAL_OBJECT_HEARTBEAT_SLEEP = 300
20
+ EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int = 300
21
21
 
22
22
 
23
23
  def _get_environment_name(environment_name: Optional[str] = None, resolver: Optional[Resolver] = None) -> Optional[str]:
@@ -205,7 +205,7 @@ class _Object:
205
205
  return self._local_uuid
206
206
 
207
207
  @property
208
- def object_id(self):
208
+ def object_id(self) -> str:
209
209
  """mdmd:hidden"""
210
210
  return self._object_id
211
211
 
modal/object.pyi CHANGED
@@ -86,7 +86,7 @@ class _Object:
86
86
  @property
87
87
  def local_uuid(self): ...
88
88
  @property
89
- def object_id(self): ...
89
+ def object_id(self) -> str: ...
90
90
  @property
91
91
  def is_hydrated(self) -> bool: ...
92
92
  @property
@@ -188,7 +188,7 @@ class Object:
188
188
  @property
189
189
  def local_uuid(self): ...
190
190
  @property
191
- def object_id(self): ...
191
+ def object_id(self) -> str: ...
192
192
  @property
193
193
  def is_hydrated(self) -> bool: ...
194
194
  @property
@@ -202,3 +202,5 @@ class Object:
202
202
 
203
203
  def live_method(method): ...
204
204
  def live_method_gen(method): ...
205
+
206
+ EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int
modal/volume.py CHANGED
@@ -122,14 +122,21 @@ class _Volume(_Object, type_prefix="vo"):
122
122
  ```
123
123
  """
124
124
 
125
- _lock: asyncio.Lock
125
+ _lock: Optional[asyncio.Lock] = None
126
126
 
127
- def _initialize_from_empty(self):
127
+ async def _get_lock(self):
128
128
  # To (mostly*) prevent multiple concurrent operations on the same volume, which can cause problems under
129
129
  # some unlikely circumstances.
130
130
  # *: You can bypass this by creating multiple handles to the same volume, e.g. via lookup. But this
131
131
  # covers the typical case = good enough.
132
- self._lock = asyncio.Lock()
132
+
133
+ # Note: this function runs no async code but is marked as async to ensure it's
134
+ # being run inside the synchronicity event loop and binds the lock to the
135
+ # correct event loop on Python 3.9 which eagerly assigns event loops on
136
+ # constructions of locks
137
+ if self._lock is None:
138
+ self._lock = asyncio.Lock()
139
+ return self._lock
133
140
 
134
141
  @staticmethod
135
142
  def new():
@@ -188,7 +195,7 @@ class _Volume(_Object, type_prefix="vo"):
188
195
  environment_name: Optional[str] = None,
189
196
  version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
190
197
  _heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
191
- ) -> AsyncIterator["_Volume"]:
198
+ ) -> AsyncGenerator["_Volume", None]:
192
199
  """Creates a new ephemeral volume within a context manager:
193
200
 
194
201
  Usage:
@@ -269,7 +276,7 @@ class _Volume(_Object, type_prefix="vo"):
269
276
 
270
277
  @live_method
271
278
  async def _do_reload(self, lock=True):
272
- async with self._lock if lock else asyncnullcontext():
279
+ async with (await self._get_lock()) if lock else asyncnullcontext():
273
280
  req = api_pb2.VolumeReloadRequest(volume_id=self.object_id)
274
281
  _ = await retry_transient_errors(self._client.stub.VolumeReload, req)
275
282
 
@@ -280,7 +287,7 @@ class _Volume(_Object, type_prefix="vo"):
280
287
  If successful, the changes made are now persisted in durable storage and available to other containers accessing
281
288
  the volume.
282
289
  """
283
- async with self._lock:
290
+ async with await self._get_lock():
284
291
  req = api_pb2.VolumeCommitRequest(volume_id=self.object_id)
285
292
  try:
286
293
  # TODO(gongy): only apply indefinite retries on 504 status.
modal/volume.pyi CHANGED
@@ -33,9 +33,9 @@ class FileEntry:
33
33
  def __hash__(self): ...
34
34
 
35
35
  class _Volume(modal.object._Object):
36
- _lock: asyncio.locks.Lock
36
+ _lock: typing.Optional[asyncio.locks.Lock]
37
37
 
38
- def _initialize_from_empty(self): ...
38
+ async def _get_lock(self): ...
39
39
  @staticmethod
40
40
  def new(): ...
41
41
  @staticmethod
@@ -122,10 +122,16 @@ class _VolumeUploadContextManager:
122
122
  ) -> modal_proto.api_pb2.MountFile: ...
123
123
 
124
124
  class Volume(modal.object.Object):
125
- _lock: asyncio.locks.Lock
125
+ _lock: typing.Optional[asyncio.locks.Lock]
126
126
 
127
127
  def __init__(self, *args, **kwargs): ...
128
- def _initialize_from_empty(self): ...
128
+
129
+ class ___get_lock_spec(typing_extensions.Protocol):
130
+ def __call__(self): ...
131
+ async def aio(self): ...
132
+
133
+ _get_lock: ___get_lock_spec
134
+
129
135
  @staticmethod
130
136
  def new(): ...
131
137
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.66.50
3
+ Version: 0.66.52
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -19,7 +19,7 @@ modal/app.py,sha256=ZQux8ZGLblIWbKHn7s15mucx97EwbjJso9WKRTYYOf0,45208
19
19
  modal/app.pyi,sha256=sX2BXX_178lp8O_GvwZqsxDdxQi1j3DjNfthMvlMlJU,25273
20
20
  modal/call_graph.py,sha256=l-Wi6vM8aosCdHTWegcCyGeVJGFdZ_fzlCmbRVPBXFI,2593
21
21
  modal/client.py,sha256=4SpWb4n0nolITR36kADZl1tYLOg6avukmzZU56UQjCo,16385
22
- modal/client.pyi,sha256=WsQ9bDrSHJSCa_xKRMoYw6PEOP_ECRJoi3bI8fdUgdQ,7372
22
+ modal/client.pyi,sha256=fZ4BnAY7mf5yMxkRhmEJsqP6ioP6e0IZZA42KcCxluk,7372
23
23
  modal/cloud_bucket_mount.py,sha256=eWQhCtMIczpokjfTZEgNBCGO_s5ft46PqTSLfKBykq4,5748
24
24
  modal/cloud_bucket_mount.pyi,sha256=tTF7M4FR9bTA30cFkz8qq3ZTlFL19NHU_36e_5GgAGA,1424
25
25
  modal/cls.py,sha256=apKnBOHKYEpBiMC8mRvHtCDJl1g0vP0tG1r8mUZ1yH0,24684
@@ -34,18 +34,18 @@ modal/environments.pyi,sha256=oScvFAclF55-tL9UioLIL_SPBwgy_9O-BBvJ-PLbRgY,3542
34
34
  modal/exception.py,sha256=K-czk1oK8wFvK8snWrytXSByo2WNb9SJAlgBVPGWZBs,6417
35
35
  modal/experimental.py,sha256=jFuNbwrNHos47viMB9q-cHJSvf2RDxDdoEcss9plaZE,2302
36
36
  modal/functions.py,sha256=BxccB-3a1migZQ6JA6iiHZJQ7WQ-jYpmg9DEZoTxzcc,71639
37
- modal/functions.pyi,sha256=0JsvWf9vvj2fMSNIHoXmL9FS7sWoRU4hU6911PyqEls,24800
37
+ modal/functions.pyi,sha256=5JGM4Mhpm674Ia7h3OTsPBmZA32goyOs2oBCCUG8A3I,24800
38
38
  modal/gpu.py,sha256=r4rL6uH3UJIQthzYvfWauXNyh01WqCPtKZCmmSX1fd4,6881
39
- modal/image.py,sha256=j-NH8pLWk4jd5UOGD4y6W7DHWoeb3rG_VR7zPLSqj-Q,78927
40
- modal/image.pyi,sha256=QEjjnl4ZSmqt7toHww5ZbhL2Re5qaFGgH7qADcJS_vA,24493
39
+ modal/image.py,sha256=f4OB4gfyaSz3mQjumzEMeZT4Uq0SzsGBMN5NkPQQSec,79550
40
+ modal/image.pyi,sha256=3rfae_E0KuNHqdi5j33nHXp_7P3tTkt7QKH5cXYczUc,24672
41
41
  modal/io_streams.py,sha256=XUsNsxRzDrhkjyb2Hx0hugCoOEz266SHQF8wP-VgsfY,14582
42
42
  modal/io_streams.pyi,sha256=WJmSI1WvZITUNBO7mnIuJgYdSKdbLaHk10V4GbttAVw,4452
43
43
  modal/mount.py,sha256=QZ4nabpbNU9tjLIPCq86rlHor9CXzADMkhJWBYfKKgg,27750
44
44
  modal/mount.pyi,sha256=nywUmeUELLY2OEnAc1NNBHmSxuEylTWBzkh6nuXkkuc,9965
45
45
  modal/network_file_system.py,sha256=P_LsILecyda1SRHU76Hk4Lq3M1HSx9shFJbaLThzw0U,14071
46
46
  modal/network_file_system.pyi,sha256=XLyUnDx55ExbJcF_xlKxRax_r06XTvSsQh-a-_EyCOU,7239
47
- modal/object.py,sha256=zbRFZIt-Z3NQtgZPzlcEdy7u36ug4tKAuntYQBR3sDM,9625
48
- modal/object.pyi,sha256=cwWg93H4rBk9evt1itLZAZXH5wUMyTJBZ_ADazgfjGg,8465
47
+ modal/object.py,sha256=Qgee_lQJY_583YsGIVrSNuDF_gJA_qmTAeVTVI1tf-g,9637
48
+ modal/object.pyi,sha256=uGGD5A2B_mj8jxLfFiHama5wzCcBS_GNvPSKsIfsCO0,8518
49
49
  modal/output.py,sha256=FtPR7yvjZMgdSKD_KYkIcwYgCOiV9EKYjaj7K55Hjvg,1940
50
50
  modal/parallel_map.py,sha256=lf8Wer6FAf8-dYqPqoL45cz7FYDU66-TF-h5CO2Kf5Q,16052
51
51
  modal/parallel_map.pyi,sha256=pOhT0P3DDYlwLx0fR3PTsecA7DI8uOdXC1N8i-ZkyOY,2328
@@ -71,8 +71,8 @@ modal/serving.pyi,sha256=0KWUH5rdYnihSv1XB8bK9GokzpfzrCq8Sf6nYlUvQI8,1689
71
71
  modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
72
72
  modal/token_flow.py,sha256=lsVpJACut76AeJLw44vJKMSlpcqp8wcvxdUOoX6CIOc,6754
73
73
  modal/token_flow.pyi,sha256=qEYP7grgqSA440w7kBREU9Ezeo_NxCT67OciIPgDzcc,1958
74
- modal/volume.py,sha256=PfwXajTBuZdxwQv2lHRqzfchn39I77pRiC60Ga1EJo4,28914
75
- modal/volume.pyi,sha256=JbeGYBda2mctzyK2psAen4nnfFB2v3jEB7S7Oyv_Vm0,10986
74
+ modal/volume.py,sha256=5IdcerxXjP9MpAZm9QXPTWRDYZD5UJSFebWGglCha8k,29301
75
+ modal/volume.pyi,sha256=3lB6wiC75u3o44cwJVqDsmvR4wsP2JXSxJrVXi9KrK4,11127
76
76
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
77
77
  modal/_runtime/asgi.py,sha256=WoAwIiGKpk089MOca3_iA73h36v0uBuoPx0-87ajIDY,19843
78
78
  modal/_runtime/container_io_manager.py,sha256=_MEhwyCSYeCaPQnztPxkm0anRXa3CPcwIKi403N53uo,44120
@@ -159,10 +159,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
159
159
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
160
  modal_version/__init__.py,sha256=UnAuHBPuPSstqgdCOx0SBVdfhpeJnMlY_oxEbu44Izg,470
161
161
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
162
- modal_version/_version_generated.py,sha256=ebE9iJTKZYTHQ3sX0Co4kO9qZg6vc814hd9KvMgaFvk,149
163
- modal-0.66.50.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
- modal-0.66.50.dist-info/METADATA,sha256=VEm-0bnVheLxgWXX5oiYnLb2HXzro3kK9gvF_cnU3JA,2329
165
- modal-0.66.50.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
- modal-0.66.50.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
- modal-0.66.50.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
- modal-0.66.50.dist-info/RECORD,,
162
+ modal_version/_version_generated.py,sha256=ejUtktb37whw_RO7gJu2mzTcbyyje5SrnzvpI_2mGrs,149
163
+ modal-0.66.52.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
+ modal-0.66.52.dist-info/METADATA,sha256=KVM51Rvw6DWszOpsfhoC4drFw8BsskaqrjACjmrlF_c,2329
165
+ modal-0.66.52.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
+ modal-0.66.52.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
+ modal-0.66.52.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
+ modal-0.66.52.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2024
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 50 # git: 2903bbe
4
+ build_number = 52 # git: a1760e2