modal 0.71.6__py3-none-any.whl → 0.71.8__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.
@@ -116,7 +116,7 @@ class UserCodeEventLoop:
116
116
 
117
117
  def __enter__(self):
118
118
  self.loop = asyncio.new_event_loop()
119
- self.tasks = []
119
+ self.tasks = set()
120
120
  return self
121
121
 
122
122
  def __exit__(self, exc_type, exc_value, traceback):
@@ -130,7 +130,10 @@ class UserCodeEventLoop:
130
130
  self.loop.close()
131
131
 
132
132
  def create_task(self, coro):
133
- self.tasks.append(self.loop.create_task(coro))
133
+ task = self.loop.create_task(coro)
134
+ self.tasks.add(task)
135
+ task.add_done_callback(self.tasks.discard)
136
+ return task
134
137
 
135
138
  def run(self, coro):
136
139
  task = asyncio.ensure_future(coro, loop=self.loop)
@@ -531,10 +534,13 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
531
534
  with container_io_manager.handle_user_exception():
532
535
  finalized_functions = service.get_finalized_functions(function_def, container_io_manager)
533
536
  # Execute the function.
537
+ lifespan_background_tasks = []
534
538
  try:
535
539
  for finalized_function in finalized_functions.values():
536
540
  if finalized_function.lifespan_manager:
537
- event_loop.create_task(finalized_function.lifespan_manager.background_task())
541
+ lifespan_background_tasks.append(
542
+ event_loop.create_task(finalized_function.lifespan_manager.background_task())
543
+ )
538
544
  with container_io_manager.handle_user_exception():
539
545
  event_loop.run(finalized_function.lifespan_manager.lifespan_startup())
540
546
  call_function(
@@ -559,6 +565,10 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
559
565
  with container_io_manager.handle_user_exception():
560
566
  event_loop.run(finalized_function.lifespan_manager.lifespan_shutdown())
561
567
  finally:
568
+ # no need to keep the lifespan asgi call around - we send it no more messages
569
+ for lifespan_background_task in lifespan_background_tasks:
570
+ lifespan_background_task.cancel() # prevent dangling tasks
571
+
562
572
  # Identify "exit" methods and run them.
563
573
  # want to make sure this is called even if the lifespan manager fails
564
574
  if service.user_cls_instance is not None and not is_auto_snapshot:
modal/_runtime/asgi.py CHANGED
@@ -22,11 +22,11 @@ FIRST_MESSAGE_TIMEOUT_SECONDS = 5.0
22
22
 
23
23
 
24
24
  class LifespanManager:
25
- startup: asyncio.Future
26
- shutdown: asyncio.Future
27
- queue: asyncio.Queue
28
- has_run_init: bool = False
29
- lifespan_supported: bool = False
25
+ _startup: asyncio.Future
26
+ _shutdown: asyncio.Future
27
+ _queue: asyncio.Queue
28
+ _has_run_init: bool = False
29
+ _lifespan_supported: bool = False
30
30
 
31
31
  def __init__(self, asgi_app, state):
32
32
  self.asgi_app = asgi_app
@@ -37,59 +37,63 @@ class LifespanManager:
37
37
  # no async code since it has to run inside
38
38
  # the event loop to tie the
39
39
  # objects to the correct loop in python 3.9
40
- if not self.has_run_init:
41
- self.queue = asyncio.Queue()
42
- self.startup = asyncio.Future()
43
- self.shutdown = asyncio.Future()
44
- self.has_run_init = True
40
+ if not self._has_run_init:
41
+ self._queue = asyncio.Queue()
42
+ self._startup = asyncio.Future()
43
+ self._shutdown = asyncio.Future()
44
+ self._has_run_init = True
45
45
 
46
46
  async def background_task(self):
47
47
  await self.ensure_init()
48
48
 
49
49
  async def receive():
50
- self.lifespan_supported = True
51
- return await self.queue.get()
50
+ self._lifespan_supported = True
51
+ return await self._queue.get()
52
52
 
53
53
  async def send(message):
54
54
  if message["type"] == "lifespan.startup.complete":
55
- self.startup.set_result(None)
55
+ self._startup.set_result(None)
56
56
  elif message["type"] == "lifespan.startup.failed":
57
- self.startup.set_exception(ExecutionError("ASGI lifespan startup failed"))
57
+ self._startup.set_exception(ExecutionError("ASGI lifespan startup failed"))
58
58
  elif message["type"] == "lifespan.shutdown.complete":
59
- self.shutdown.set_result(None)
59
+ self._shutdown.set_result(None)
60
60
  elif message["type"] == "lifespan.shutdown.failed":
61
- self.shutdown.set_exception(ExecutionError("ASGI lifespan shutdown failed"))
61
+ self._shutdown.set_exception(ExecutionError("ASGI lifespan shutdown failed"))
62
62
  else:
63
63
  raise ExecutionError(f"Unexpected message type: {message['type']}")
64
64
 
65
65
  try:
66
66
  await self.asgi_app({"type": "lifespan", "state": self.state}, receive, send)
67
67
  except Exception as e:
68
- if not self.lifespan_supported:
68
+ if not self._lifespan_supported:
69
69
  logger.info(f"ASGI lifespan task exited before receiving any messages with exception:\n{e}")
70
- self.startup.set_result(None)
71
- self.shutdown.set_result(None)
70
+ if not self._startup.done():
71
+ self._startup.set_result(None)
72
+ if not self._shutdown.done():
73
+ self._shutdown.set_result(None)
72
74
  return
73
75
 
74
76
  logger.error(f"Error in ASGI lifespan task: {e}")
75
- if not self.startup.done():
76
- self.startup.set_exception(ExecutionError("ASGI lifespan task exited startup"))
77
- if not self.shutdown.done():
78
- self.shutdown.set_exception(ExecutionError("ASGI lifespan task exited shutdown"))
77
+ if not self._startup.done():
78
+ self._startup.set_exception(ExecutionError("ASGI lifespan task exited startup"))
79
+ if not self._shutdown.done():
80
+ self._shutdown.set_exception(ExecutionError("ASGI lifespan task exited shutdown"))
79
81
  else:
80
82
  logger.info("ASGI Lifespan protocol is probably not supported by this library")
81
- self.startup.set_result(None)
82
- self.shutdown.set_result(None)
83
+ if not self._startup.done():
84
+ self._startup.set_result(None)
85
+ if not self._shutdown.done():
86
+ self._shutdown.set_result(None)
83
87
 
84
88
  async def lifespan_startup(self):
85
89
  await self.ensure_init()
86
- self.queue.put_nowait({"type": "lifespan.startup"})
87
- await self.startup
90
+ self._queue.put_nowait({"type": "lifespan.startup"})
91
+ await self._startup
88
92
 
89
93
  async def lifespan_shutdown(self):
90
94
  await self.ensure_init()
91
- self.queue.put_nowait({"type": "lifespan.shutdown"})
92
- await self.shutdown
95
+ self._queue.put_nowait({"type": "lifespan.shutdown"})
96
+ await self._shutdown
93
97
 
94
98
 
95
99
  def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., AsyncGenerator], LifespanManager]:
modal/client.pyi CHANGED
@@ -26,7 +26,7 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
27
 
28
28
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.6"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.8"
30
30
  ): ...
31
31
  def is_closed(self) -> bool: ...
32
32
  @property
@@ -81,7 +81,7 @@ class Client:
81
81
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
82
82
 
83
83
  def __init__(
84
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.6"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.8"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/image.py CHANGED
@@ -832,6 +832,24 @@ class _Image(_Object, type_prefix="im"):
832
832
  ),
833
833
  )
834
834
 
835
+ @staticmethod
836
+ async def from_id(image_id: str, client: Optional[_Client] = None) -> "_Image":
837
+ """Construct an Image from an id and look up the Image result.
838
+
839
+ The ID of an Image object can be accessed using `.object_id`.
840
+ """
841
+ if client is None:
842
+ client = await _Client.from_env()
843
+
844
+ async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
845
+ resp = await retry_transient_errors(client.stub.ImageFromId, api_pb2.ImageFromIdRequest(image_id=image_id))
846
+ self._hydrate(resp.image_id, resolver.client, resp.metadata)
847
+
848
+ rep = "Image()"
849
+ obj = _Image._from_loader(_load, rep)
850
+
851
+ return obj
852
+
835
853
  def pip_install(
836
854
  self,
837
855
  *packages: Union[str, list[str]], # A list of Python packages, eg. ["numpy", "matplotlib>=3.5.0"]
modal/image.pyi CHANGED
@@ -126,6 +126,8 @@ class _Image(modal.object._Object):
126
126
  remote_path: typing.Union[str, pathlib.Path] = ".",
127
127
  ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = [],
128
128
  ) -> _Image: ...
129
+ @staticmethod
130
+ async def from_id(image_id: str, client: typing.Optional[modal.client._Client] = None) -> _Image: ...
129
131
  def pip_install(
130
132
  self,
131
133
  *packages: typing.Union[str, list[str]],
@@ -387,6 +389,13 @@ class Image(modal.object.Object):
387
389
  remote_path: typing.Union[str, pathlib.Path] = ".",
388
390
  ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = [],
389
391
  ) -> Image: ...
392
+
393
+ class __from_id_spec(typing_extensions.Protocol):
394
+ def __call__(self, image_id: str, client: typing.Optional[modal.client.Client] = None) -> Image: ...
395
+ async def aio(self, image_id: str, client: typing.Optional[modal.client.Client] = None) -> Image: ...
396
+
397
+ from_id: __from_id_spec
398
+
390
399
  def pip_install(
391
400
  self,
392
401
  *packages: typing.Union[str, list[str]],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.71.6
3
+ Version: 0.71.8
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -2,7 +2,7 @@ modal/__init__.py,sha256=3NJLLHb0TRc2tc68kf8NHzORx38GbtbZvPEWDWrQ6N4,2234
2
2
  modal/__main__.py,sha256=scYhGFqh8OJcVDo-VOxIT6CCwxOgzgflYWMnIZiMRqE,2871
3
3
  modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
4
4
  modal/_clustered_functions.pyi,sha256=vllkegc99A0jrUOWa8mdlSbdp6uz36TsHhGxysAOpaQ,771
5
- modal/_container_entrypoint.py,sha256=-zUa567FgOKmF0TtFWQ6DgehUD2CMfABDBQ8oLSpjyc,29171
5
+ modal/_container_entrypoint.py,sha256=U1ZU-Sa0LC646YPh0AWSGVBHmzfTxYUiGu2viKzW-Zs,29643
6
6
  modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
7
7
  modal/_location.py,sha256=S3lSxIU3h9HkWpkJ3Pwo0pqjIOSB1fjeSgUsY3x7eec,1202
8
8
  modal/_output.py,sha256=0fWX_KQwhER--U81ys16CL-pA5A-LN20C0EZjElKGJQ,25410
@@ -19,7 +19,7 @@ modal/app.py,sha256=vEE0cK5QPF6_cdW5AJvcuWxz5KmeprHwBEtlDkVRHgE,45582
19
19
  modal/app.pyi,sha256=Gx7gxjfQ70sxhbwfpx1VjvzEON-ZEMTJ_Vy8qt0oQvo,25302
20
20
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
21
21
  modal/client.py,sha256=JAnd4-GCN093BwkvOFAK5a6iy5ycxofjpUncMxlrIMw,15253
22
- modal/client.pyi,sha256=NfXng63F8_mjJjfXLCd0JrnZkIDk9U-XyL8_l52QLN4,7278
22
+ modal/client.pyi,sha256=TrQn13Pm1t7N2XRd5-jvxe5zzSL69I5EcZndQUyFmcM,7278
23
23
  modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
24
24
  modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
25
25
  modal/cls.py,sha256=3hjb0JcoPjxKZNeK22f5rR43bZRBjoRI7_EMZXY7YrE,31172
@@ -39,8 +39,8 @@ modal/file_pattern_matcher.py,sha256=LaI7Paxg0xR9D-D7Tgc60xR0w1KZee22LjGbFie1Vms
39
39
  modal/functions.py,sha256=3uJPbrEAWhpFfLfUnoRjGmvEUC-_wVh-8yNJBx8eVeM,68249
40
40
  modal/functions.pyi,sha256=LiSDgH-X7jcZ56pAoLMwo3x9Dzdp_3Sd7W5MVAJPoCg,25407
41
41
  modal/gpu.py,sha256=MTxj6ql8EpgfBg8YmZ5a1cLznyuZFssX1qXbEX4LKVM,7503
42
- modal/image.py,sha256=Krvcsclomp9YsqSNwFj2FoAyg10OvU47RDnsNCwjGbQ,84550
43
- modal/image.pyi,sha256=1fgGvsL5Rb0Sa-_2OCgIyJ_QgHcL0_9MWD_oY7cyFFM,24937
42
+ modal/image.py,sha256=oKqqLhc3Ap2XMG5MKVlERKkMTwJPkNMNcSzxoZh4zuw,85259
43
+ modal/image.pyi,sha256=Pa1_LVr3FyNsnu_MhBO08fBgCeLazTEe25phYdu0bzE,25365
44
44
  modal/io_streams.py,sha256=Xxc5grJiO94nBA48FFWH3S3g6SPR0xFVgZ_DZ1oFmvI,14428
45
45
  modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
46
46
  modal/io_streams_helper.py,sha256=B5Ui56ph7LkRpZX0tAF80Q-gOMsvPPLx5bpIPX0kgDc,1772
@@ -78,7 +78,7 @@ modal/token_flow.pyi,sha256=gOYtYujrWt_JFZeiI8EmfahXPx5GCR5Na-VaPQcWgEY,1937
78
78
  modal/volume.py,sha256=T-pLxCYqmqRO6OolpAXlPxomMu0RWjti2e4kUpaj2cQ,29229
79
79
  modal/volume.pyi,sha256=eekb2dnAAwFK_NO9ciAOOTthl8NP1iAmMFrCGgjDA2k,11100
80
80
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
81
- modal/_runtime/asgi.py,sha256=Mjs859pSgOmtZL-YmEsSKN557v1A2Ax_5-ERgPfj55E,21920
81
+ modal/_runtime/asgi.py,sha256=c4hmaMW1pLo-cm7ouriJjieuFm4ZF6D2LMy0638sfOs,22139
82
82
  modal/_runtime/container_io_manager.py,sha256=HgDLjE78yy1P7WZTmsEVDf89YnFFWG63Ddes8uYLVDY,43764
83
83
  modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7JpLq4Kg,2712
84
84
  modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
@@ -166,10 +166,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
166
166
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
167
167
  modal_version/__init__.py,sha256=BEBWj9tcbFUwzEjUrqly601rauw5cYsHdcmJHs3iu0s,470
168
168
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
169
- modal_version/_version_generated.py,sha256=jbZljLgZ-HocRNgrswTNVhAp8D4H4g32moUc0dud9-s,148
170
- modal-0.71.6.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
171
- modal-0.71.6.dist-info/METADATA,sha256=NoiwOq43-7EWLSJ8JnfeI7NyPHwFU_5wjNDsrojkIe4,2328
172
- modal-0.71.6.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
173
- modal-0.71.6.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
174
- modal-0.71.6.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
175
- modal-0.71.6.dist-info/RECORD,,
169
+ modal_version/_version_generated.py,sha256=Wsequ_fpOFVKI_31Vx8o7dmn9xo9m9A_dDR6wZzyDIU,148
170
+ modal-0.71.8.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
171
+ modal-0.71.8.dist-info/METADATA,sha256=KRAySrnrs0NscfurQ57XIYvXEEJ9FKumg47sf3ufFi8,2328
172
+ modal-0.71.8.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
173
+ modal-0.71.8.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
174
+ modal-0.71.8.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
175
+ modal-0.71.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 6 # git: 37bdeea
4
+ build_number = 8 # git: e068215
File without changes