modal 1.1.5.dev83__py3-none-any.whl → 1.3.1.dev8__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.

Files changed (139) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +146 -121
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +26 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/package_utils.py +0 -1
  31. modal/_utils/rand_pb_testing.py +8 -1
  32. modal/_utils/task_command_router_client.py +524 -0
  33. modal/_vendor/cloudpickle.py +144 -48
  34. modal/app.py +215 -96
  35. modal/app.pyi +78 -37
  36. modal/billing.py +5 -0
  37. modal/builder/2025.06.txt +6 -3
  38. modal/builder/PREVIEW.txt +2 -1
  39. modal/builder/base-images.json +4 -2
  40. modal/cli/_download.py +19 -3
  41. modal/cli/cluster.py +4 -2
  42. modal/cli/config.py +3 -1
  43. modal/cli/container.py +5 -4
  44. modal/cli/dict.py +5 -2
  45. modal/cli/entry_point.py +26 -2
  46. modal/cli/environment.py +2 -16
  47. modal/cli/launch.py +1 -76
  48. modal/cli/network_file_system.py +5 -20
  49. modal/cli/queues.py +5 -4
  50. modal/cli/run.py +24 -204
  51. modal/cli/secret.py +1 -2
  52. modal/cli/shell.py +375 -0
  53. modal/cli/utils.py +1 -13
  54. modal/cli/volume.py +11 -17
  55. modal/client.py +16 -125
  56. modal/client.pyi +94 -144
  57. modal/cloud_bucket_mount.py +3 -1
  58. modal/cloud_bucket_mount.pyi +4 -0
  59. modal/cls.py +101 -64
  60. modal/cls.pyi +9 -8
  61. modal/config.py +21 -1
  62. modal/container_process.py +288 -12
  63. modal/container_process.pyi +99 -38
  64. modal/dict.py +72 -33
  65. modal/dict.pyi +88 -57
  66. modal/environments.py +16 -8
  67. modal/environments.pyi +6 -2
  68. modal/exception.py +154 -16
  69. modal/experimental/__init__.py +23 -5
  70. modal/experimental/flash.py +161 -74
  71. modal/experimental/flash.pyi +97 -49
  72. modal/file_io.py +50 -92
  73. modal/file_io.pyi +117 -89
  74. modal/functions.pyi +70 -87
  75. modal/image.py +73 -47
  76. modal/image.pyi +33 -30
  77. modal/io_streams.py +500 -149
  78. modal/io_streams.pyi +279 -189
  79. modal/mount.py +60 -45
  80. modal/mount.pyi +41 -17
  81. modal/network_file_system.py +19 -11
  82. modal/network_file_system.pyi +72 -39
  83. modal/object.pyi +114 -22
  84. modal/parallel_map.py +42 -44
  85. modal/parallel_map.pyi +9 -17
  86. modal/partial_function.pyi +4 -2
  87. modal/proxy.py +14 -6
  88. modal/proxy.pyi +10 -2
  89. modal/queue.py +45 -38
  90. modal/queue.pyi +88 -52
  91. modal/runner.py +96 -96
  92. modal/runner.pyi +44 -27
  93. modal/sandbox.py +225 -108
  94. modal/sandbox.pyi +226 -63
  95. modal/secret.py +58 -56
  96. modal/secret.pyi +28 -13
  97. modal/serving.py +7 -11
  98. modal/serving.pyi +7 -8
  99. modal/snapshot.py +29 -15
  100. modal/snapshot.pyi +18 -10
  101. modal/token_flow.py +1 -1
  102. modal/token_flow.pyi +4 -6
  103. modal/volume.py +102 -55
  104. modal/volume.pyi +125 -66
  105. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  106. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  107. modal_proto/api.proto +86 -30
  108. modal_proto/api_grpc.py +10 -25
  109. modal_proto/api_pb2.py +1080 -1047
  110. modal_proto/api_pb2.pyi +253 -79
  111. modal_proto/api_pb2_grpc.py +14 -48
  112. modal_proto/api_pb2_grpc.pyi +6 -18
  113. modal_proto/modal_api_grpc.py +175 -176
  114. modal_proto/{sandbox_router.proto → task_command_router.proto} +62 -45
  115. modal_proto/task_command_router_grpc.py +138 -0
  116. modal_proto/task_command_router_pb2.py +180 -0
  117. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +110 -63
  118. modal_proto/task_command_router_pb2_grpc.py +272 -0
  119. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  120. modal_version/__init__.py +1 -1
  121. modal_version/__main__.py +1 -1
  122. modal/cli/programs/launch_instance_ssh.py +0 -94
  123. modal/cli/programs/run_marimo.py +0 -95
  124. modal-1.1.5.dev83.dist-info/RECORD +0 -191
  125. modal_proto/modal_options_grpc.py +0 -3
  126. modal_proto/options.proto +0 -19
  127. modal_proto/options_grpc.py +0 -3
  128. modal_proto/options_pb2.py +0 -35
  129. modal_proto/options_pb2.pyi +0 -20
  130. modal_proto/options_pb2_grpc.py +0 -4
  131. modal_proto/options_pb2_grpc.pyi +0 -7
  132. modal_proto/sandbox_router_grpc.py +0 -105
  133. modal_proto/sandbox_router_pb2.py +0 -148
  134. modal_proto/sandbox_router_pb2_grpc.py +0 -203
  135. modal_proto/sandbox_router_pb2_grpc.pyi +0 -75
  136. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  137. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  138. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  139. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/secret.py CHANGED
@@ -1,21 +1,21 @@
1
1
  # Copyright Modal Labs 2022
2
+ import builtins
2
3
  import os
3
4
  from dataclasses import dataclass
4
5
  from datetime import datetime
5
6
  from typing import Optional, Union
6
7
 
7
8
  from google.protobuf.message import Message
8
- from grpclib import GRPCError, Status
9
9
  from synchronicity import classproperty
10
10
 
11
11
  from modal_proto import api_pb2
12
12
 
13
+ from ._load_context import LoadContext
13
14
  from ._object import _get_environment_name, _Object, live_method
14
15
  from ._resolver import Resolver
15
16
  from ._runtime.execution_context import is_local
16
17
  from ._utils.async_utils import synchronize_api
17
18
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
18
- from ._utils.grpc_utils import retry_transient_errors
19
19
  from ._utils.name_utils import check_object_name
20
20
  from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
21
21
  from .client import _Client
@@ -91,11 +91,9 @@ class _SecretManager:
91
91
  env_dict=env_dict,
92
92
  )
93
93
  try:
94
- await retry_transient_errors(client.stub.SecretGetOrCreate, req)
95
- except GRPCError as exc:
96
- if exc.status == Status.ALREADY_EXISTS and not allow_existing:
97
- raise AlreadyExistsError(exc.message)
98
- else:
94
+ await client.stub.SecretGetOrCreate(req)
95
+ except AlreadyExistsError:
96
+ if not allow_existing:
99
97
  raise
100
98
 
101
99
  @staticmethod
@@ -105,7 +103,7 @@ class _SecretManager:
105
103
  created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
106
104
  environment_name: str = "", # Uses active environment if not specified
107
105
  client: Optional[_Client] = None, # Optional client with Modal credentials
108
- ) -> list["_Secret"]:
106
+ ) -> builtins.list["_Secret"]:
109
107
  """Return a list of hydrated Secret objects.
110
108
 
111
109
  **Examples:**
@@ -143,7 +141,7 @@ class _SecretManager:
143
141
  req = api_pb2.SecretListRequest(
144
142
  environment_name=_get_environment_name(environment_name), pagination=pagination
145
143
  )
146
- resp = await retry_transient_errors(client.stub.SecretList, req)
144
+ resp = await client.stub.SecretList(req)
147
145
  items.extend(resp.items)
148
146
  finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
149
147
  return finished
@@ -200,12 +198,32 @@ class _SecretManager:
200
198
  raise
201
199
  else:
202
200
  req = api_pb2.SecretDeleteRequest(secret_id=obj.object_id)
203
- await retry_transient_errors(obj._client.stub.SecretDelete, req)
201
+ await obj._client.stub.SecretDelete(req)
204
202
 
205
203
 
206
204
  SecretManager = synchronize_api(_SecretManager)
207
205
 
208
206
 
207
+ async def _load_from_env_dict(instance: "_Secret", load_context: LoadContext, env_dict: dict[str, str]):
208
+ """helper method for loaders .from_dict and .from_dotenv etc."""
209
+ if load_context.app_id is not None:
210
+ req = api_pb2.SecretGetOrCreateRequest(
211
+ object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
212
+ env_dict=env_dict,
213
+ app_id=load_context.app_id,
214
+ environment_name=load_context.environment_name,
215
+ )
216
+ else:
217
+ req = api_pb2.SecretGetOrCreateRequest(
218
+ object_creation_type=api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL,
219
+ env_dict=env_dict,
220
+ environment_name=load_context.environment_name,
221
+ )
222
+
223
+ resp = await load_context.client.stub.SecretGetOrCreate(req)
224
+ instance._hydrate(resp.secret_id, load_context.client, resp.metadata)
225
+
226
+
209
227
  class _Secret(_Object, type_prefix="st"):
210
228
  """Secrets provide a dictionary of environment variables for images.
211
229
 
@@ -260,30 +278,14 @@ class _Secret(_Object, type_prefix="st"):
260
278
  if not all(isinstance(v, str) for v in env_dict_filtered.values()):
261
279
  raise InvalidError(ENV_DICT_WRONG_TYPE_ERR)
262
280
 
263
- async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
264
- if resolver.app_id is not None:
265
- object_creation_type = api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP
266
- else:
267
- object_creation_type = api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL
268
-
269
- req = api_pb2.SecretGetOrCreateRequest(
270
- object_creation_type=object_creation_type,
271
- env_dict=env_dict_filtered,
272
- app_id=resolver.app_id,
273
- environment_name=resolver.environment_name,
274
- )
275
- try:
276
- resp = await resolver.client.stub.SecretGetOrCreate(req)
277
- except GRPCError as exc:
278
- if exc.status == Status.INVALID_ARGUMENT:
279
- raise InvalidError(exc.message)
280
- if exc.status == Status.FAILED_PRECONDITION:
281
- raise InvalidError(exc.message)
282
- raise
283
- self._hydrate(resp.secret_id, resolver.client, resp.metadata)
281
+ async def _load(
282
+ self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
283
+ ):
284
+ await _load_from_env_dict(self, load_context, env_dict_filtered)
284
285
 
285
286
  rep = f"Secret.from_dict([{', '.join(env_dict.keys())}])"
286
- return _Secret._from_loader(_load, rep, hydrate_lazily=True)
287
+ # TODO: scoping - these should probably not be lazily hydrated without having an app and/or sandbox association
288
+ return _Secret._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
287
289
 
288
290
  @staticmethod
289
291
  def from_local_environ(
@@ -303,7 +305,7 @@ class _Secret(_Object, type_prefix="st"):
303
305
  return _Secret.from_dict({})
304
306
 
305
307
  @staticmethod
306
- def from_dotenv(path=None, *, filename=".env") -> "_Secret":
308
+ def from_dotenv(path=None, *, filename=".env", client: Optional[_Client] = None) -> "_Secret":
307
309
  """Create secrets from a .env file automatically.
308
310
 
309
311
  If no argument is provided, it will use the current working directory as the starting
@@ -331,7 +333,9 @@ class _Secret(_Object, type_prefix="st"):
331
333
  ```
332
334
  """
333
335
 
334
- async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
336
+ async def _load(
337
+ self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
338
+ ):
335
339
  try:
336
340
  from dotenv import dotenv_values, find_dotenv
337
341
  from dotenv.main import _walk_to_root
@@ -355,18 +359,13 @@ class _Secret(_Object, type_prefix="st"):
355
359
  # To simplify this, we just support the cwd and don't do any automatic path inference.
356
360
  dotenv_path = find_dotenv(filename, usecwd=True)
357
361
 
358
- env_dict = dotenv_values(dotenv_path)
359
-
360
- req = api_pb2.SecretGetOrCreateRequest(
361
- object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
362
- env_dict=env_dict,
363
- app_id=resolver.app_id,
364
- )
365
- resp = await resolver.client.stub.SecretGetOrCreate(req)
362
+ env_dict = {k: v or "" for k, v in dotenv_values(dotenv_path).items()}
366
363
 
367
- self._hydrate(resp.secret_id, resolver.client, resp.metadata)
364
+ await _load_from_env_dict(self, load_context, env_dict)
368
365
 
369
- return _Secret._from_loader(_load, "Secret.from_dotenv()", hydrate_lazily=True)
366
+ return _Secret._from_loader(
367
+ _load, "Secret.from_dotenv()", hydrate_lazily=True, load_context_overrides=LoadContext(client=client)
368
+ )
370
369
 
371
370
  @staticmethod
372
371
  def from_name(
@@ -377,6 +376,7 @@ class _Secret(_Object, type_prefix="st"):
377
376
  required_keys: list[
378
377
  str
379
378
  ] = [], # Optionally, a list of required environment variables (will be asserted server-side)
379
+ client: Optional[_Client] = None,
380
380
  ) -> "_Secret":
381
381
  """Reference a Secret by its name.
382
382
 
@@ -394,23 +394,25 @@ class _Secret(_Object, type_prefix="st"):
394
394
  """
395
395
  warn_if_passing_namespace(namespace, "modal.Secret.from_name")
396
396
 
397
- async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
397
+ async def _load(
398
+ self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
399
+ ):
398
400
  req = api_pb2.SecretGetOrCreateRequest(
399
401
  deployment_name=name,
400
- environment_name=_get_environment_name(environment_name, resolver),
402
+ environment_name=load_context.environment_name,
401
403
  required_keys=required_keys,
402
404
  )
403
- try:
404
- response = await resolver.client.stub.SecretGetOrCreate(req)
405
- except GRPCError as exc:
406
- if exc.status == Status.NOT_FOUND:
407
- raise NotFoundError(exc.message)
408
- else:
409
- raise
410
- self._hydrate(response.secret_id, resolver.client, response.metadata)
405
+ response = await load_context.client.stub.SecretGetOrCreate(req)
406
+ self._hydrate(response.secret_id, load_context.client, response.metadata)
411
407
 
412
408
  rep = _Secret._repr(name, environment_name)
413
- return _Secret._from_loader(_load, rep, hydrate_lazily=True, name=name)
409
+ return _Secret._from_loader(
410
+ _load,
411
+ rep,
412
+ hydrate_lazily=True,
413
+ name=name,
414
+ load_context_overrides=LoadContext(environment_name=environment_name, client=client),
415
+ )
414
416
 
415
417
  @staticmethod
416
418
  async def create_deployed(
@@ -454,7 +456,7 @@ class _Secret(_Object, type_prefix="st"):
454
456
  object_creation_type=object_creation_type,
455
457
  env_dict=env_dict,
456
458
  )
457
- resp = await retry_transient_errors(client.stub.SecretGetOrCreate, request)
459
+ resp = await client.stub.SecretGetOrCreate(request)
458
460
  return resp.secret_id
459
461
 
460
462
  @live_method
modal/secret.pyi CHANGED
@@ -1,5 +1,6 @@
1
1
  import datetime
2
2
  import google.protobuf.message
3
+ import modal._load_context
3
4
  import modal._object
4
5
  import modal.client
5
6
  import modal.object
@@ -219,7 +220,7 @@ class SecretManager:
219
220
  """
220
221
  ...
221
222
 
222
- create: __create_spec
223
+ create: typing.ClassVar[__create_spec]
223
224
 
224
225
  class __list_spec(typing_extensions.Protocol):
225
226
  def __call__(
@@ -292,7 +293,7 @@ class SecretManager:
292
293
  """
293
294
  ...
294
295
 
295
- list: __list_spec
296
+ list: typing.ClassVar[__list_spec]
296
297
 
297
298
  class __delete_spec(typing_extensions.Protocol):
298
299
  def __call__(
@@ -353,7 +354,13 @@ class SecretManager:
353
354
  """
354
355
  ...
355
356
 
356
- delete: __delete_spec
357
+ delete: typing.ClassVar[__delete_spec]
358
+
359
+ async def _load_from_env_dict(
360
+ instance: _Secret, load_context: modal._load_context.LoadContext, env_dict: dict[str, str]
361
+ ):
362
+ """helper method for loaders .from_dict and .from_dotenv etc."""
363
+ ...
357
364
 
358
365
  class _Secret(modal._object._Object):
359
366
  """Secrets provide a dictionary of environment variables for images.
@@ -392,7 +399,7 @@ class _Secret(modal._object._Object):
392
399
  ...
393
400
 
394
401
  @staticmethod
395
- def from_dotenv(path=None, *, filename=".env") -> _Secret:
402
+ def from_dotenv(path=None, *, filename=".env", client: typing.Optional[modal.client._Client] = None) -> _Secret:
396
403
  """Create secrets from a .env file automatically.
397
404
 
398
405
  If no argument is provided, it will use the current working directory as the starting
@@ -423,7 +430,12 @@ class _Secret(modal._object._Object):
423
430
 
424
431
  @staticmethod
425
432
  def from_name(
426
- name: str, *, namespace=None, environment_name: typing.Optional[str] = None, required_keys: list[str] = []
433
+ name: str,
434
+ *,
435
+ namespace=None,
436
+ environment_name: typing.Optional[str] = None,
437
+ required_keys: list[str] = [],
438
+ client: typing.Optional[modal.client._Client] = None,
427
439
  ) -> _Secret:
428
440
  """Reference a Secret by its name.
429
441
 
@@ -469,8 +481,6 @@ class _Secret(modal._object._Object):
469
481
  """Return information about the Secret object."""
470
482
  ...
471
483
 
472
- SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
473
-
474
484
  class Secret(modal.object.Object):
475
485
  """Secrets provide a dictionary of environment variables for images.
476
486
 
@@ -512,7 +522,7 @@ class Secret(modal.object.Object):
512
522
  ...
513
523
 
514
524
  @staticmethod
515
- def from_dotenv(path=None, *, filename=".env") -> Secret:
525
+ def from_dotenv(path=None, *, filename=".env", client: typing.Optional[modal.client.Client] = None) -> Secret:
516
526
  """Create secrets from a .env file automatically.
517
527
 
518
528
  If no argument is provided, it will use the current working directory as the starting
@@ -543,7 +553,12 @@ class Secret(modal.object.Object):
543
553
 
544
554
  @staticmethod
545
555
  def from_name(
546
- name: str, *, namespace=None, environment_name: typing.Optional[str] = None, required_keys: list[str] = []
556
+ name: str,
557
+ *,
558
+ namespace=None,
559
+ environment_name: typing.Optional[str] = None,
560
+ required_keys: list[str] = [],
561
+ client: typing.Optional[modal.client.Client] = None,
547
562
  ) -> Secret:
548
563
  """Reference a Secret by its name.
549
564
 
@@ -588,7 +603,7 @@ class Secret(modal.object.Object):
588
603
  """mdmd:hidden"""
589
604
  ...
590
605
 
591
- create_deployed: __create_deployed_spec
606
+ create_deployed: typing.ClassVar[__create_deployed_spec]
592
607
 
593
608
  class ___create_deployed_spec(typing_extensions.Protocol):
594
609
  def __call__(
@@ -617,9 +632,9 @@ class Secret(modal.object.Object):
617
632
  """mdmd:hidden"""
618
633
  ...
619
634
 
620
- _create_deployed: ___create_deployed_spec
635
+ _create_deployed: typing.ClassVar[___create_deployed_spec]
621
636
 
622
- class __info_spec(typing_extensions.Protocol[SUPERSELF]):
637
+ class __info_spec(typing_extensions.Protocol):
623
638
  def __call__(self, /) -> SecretInfo:
624
639
  """Return information about the Secret object."""
625
640
  ...
@@ -628,4 +643,4 @@ class Secret(modal.object.Object):
628
643
  """Return information about the Secret object."""
629
644
  ...
630
645
 
631
- info: __info_spec[typing_extensions.Self]
646
+ info: __info_spec
modal/serving.py CHANGED
@@ -4,13 +4,13 @@ import platform
4
4
  from collections.abc import AsyncGenerator
5
5
  from multiprocessing.context import SpawnProcess
6
6
  from multiprocessing.synchronize import Event
7
- from typing import TYPE_CHECKING, Optional, TypeVar
7
+ from typing import TYPE_CHECKING, Optional
8
8
 
9
9
  from synchronicity.async_wrap import asynccontextmanager
10
10
 
11
11
  from modal._output import OutputManager
12
12
 
13
- from ._utils.async_utils import TaskContext, asyncify, synchronize_api, synchronizer
13
+ from ._utils.async_utils import TaskContext, asyncify, synchronize_api
14
14
  from ._utils.logger import logger
15
15
  from ._watcher import watch
16
16
  from .cli.import_refs import ImportRef, import_app_from_ref
@@ -20,20 +20,16 @@ from .output import _get_output_manager, enable_output
20
20
  from .runner import _run_app, serve_update
21
21
 
22
22
  if TYPE_CHECKING:
23
- from .app import _App
24
- else:
25
- _App = TypeVar("_App")
23
+ import modal.app
26
24
 
27
25
 
28
26
  def _run_serve(
29
27
  import_ref: ImportRef, existing_app_id: str, is_ready: Event, environment_name: str, show_progress: bool
30
28
  ):
31
- # subprocess entrypoint
32
- _app = import_app_from_ref(import_ref, base_cmd="modal serve")
33
- blocking_app = synchronizer._translate_out(_app)
29
+ app = import_app_from_ref(import_ref, base_cmd="modal serve")
34
30
 
35
31
  with enable_output(show_progress=show_progress):
36
- serve_update(blocking_app, existing_app_id, is_ready, environment_name)
32
+ serve_update(app, existing_app_id, is_ready, environment_name)
37
33
 
38
34
 
39
35
  async def _restart_serve(
@@ -97,12 +93,12 @@ async def _run_watch_loop(
97
93
 
98
94
  @asynccontextmanager
99
95
  async def _serve_app(
100
- app: "_App",
96
+ app: "modal.app._App",
101
97
  import_ref: ImportRef,
102
98
  *,
103
99
  _watcher: Optional[AsyncGenerator[set[str], None]] = None, # for testing
104
100
  environment_name: Optional[str] = None,
105
- ) -> AsyncGenerator["_App", None]:
101
+ ) -> AsyncGenerator["modal.app._App", None]:
106
102
  if environment_name is None:
107
103
  environment_name = config.get("environment")
108
104
 
modal/serving.pyi CHANGED
@@ -1,4 +1,5 @@
1
1
  import collections.abc
2
+ import modal.app
2
3
  import modal.cli.import_refs
3
4
  import multiprocessing.context
4
5
  import multiprocessing.synchronize
@@ -6,8 +7,6 @@ import synchronicity.combined_types
6
7
  import typing
7
8
  import typing_extensions
8
9
 
9
- _App = typing.TypeVar("_App")
10
-
11
10
  def _run_serve(
12
11
  import_ref: modal.cli.import_refs.ImportRef,
13
12
  existing_app_id: str,
@@ -27,31 +26,31 @@ async def _run_watch_loop(
27
26
  environment_name: str,
28
27
  ): ...
29
28
  def _serve_app(
30
- app: _App,
29
+ app: modal.app._App,
31
30
  import_ref: modal.cli.import_refs.ImportRef,
32
31
  *,
33
32
  _watcher: typing.Optional[collections.abc.AsyncGenerator[set[str], None]] = None,
34
33
  environment_name: typing.Optional[str] = None,
35
- ) -> typing.AsyncContextManager[_App]: ...
34
+ ) -> typing.AsyncContextManager[modal.app._App]: ...
36
35
 
37
36
  class __serve_app_spec(typing_extensions.Protocol):
38
37
  def __call__(
39
38
  self,
40
39
  /,
41
- app: _App,
40
+ app: modal.app.App,
42
41
  import_ref: modal.cli.import_refs.ImportRef,
43
42
  *,
44
43
  _watcher: typing.Optional[typing.Generator[set[str], None, None]] = None,
45
44
  environment_name: typing.Optional[str] = None,
46
- ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[_App]: ...
45
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[modal.app.App]: ...
47
46
  def aio(
48
47
  self,
49
48
  /,
50
- app: _App,
49
+ app: modal.app.App,
51
50
  import_ref: modal.cli.import_refs.ImportRef,
52
51
  *,
53
52
  _watcher: typing.Optional[collections.abc.AsyncGenerator[set[str], None]] = None,
54
53
  environment_name: typing.Optional[str] = None,
55
- ) -> typing.AsyncContextManager[_App]: ...
54
+ ) -> typing.AsyncContextManager[modal.app.App]: ...
56
55
 
57
56
  serve_app: __serve_app_spec
modal/snapshot.py CHANGED
@@ -1,12 +1,15 @@
1
1
  # Copyright Modal Labs 2024
2
- from typing import Optional
2
+ from typing import Optional, cast
3
3
 
4
+ import typing_extensions
5
+
6
+ import modal.client
4
7
  from modal_proto import api_pb2
5
8
 
9
+ from ._load_context import LoadContext
6
10
  from ._object import _Object
7
11
  from ._resolver import Resolver
8
- from ._utils.async_utils import synchronize_api
9
- from ._utils.grpc_utils import retry_transient_errors
12
+ from ._utils.async_utils import deprecate_aio_usage, synchronize_api, synchronizer
10
13
  from .client import _Client
11
14
 
12
15
 
@@ -19,24 +22,35 @@ class _SandboxSnapshot(_Object, type_prefix="sn"):
19
22
  the original Sandbox at the time the snapshot was taken.
20
23
  """
21
24
 
22
- @staticmethod
23
- async def from_id(sandbox_snapshot_id: str, client: Optional[_Client] = None):
25
+ @deprecate_aio_usage((2025, 12, 5), "SandboxSnapshot.from_id")
26
+ @classmethod
27
+ def from_id(
28
+ cls, sandbox_snapshot_id: str, client: Optional["modal.client.Client"] = None
29
+ ) -> typing_extensions.Self:
24
30
  """
25
31
  Construct a `SandboxSnapshot` object from a sandbox snapshot ID.
26
32
  """
27
- if client is None:
28
- client = await _Client.from_env()
29
-
30
- async def _load(self: _SandboxSnapshot, resolver: Resolver, existing_object_id: Optional[str]):
31
- await retry_transient_errors(
32
- client.stub.SandboxSnapshotGet, api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id)
33
+ _client = cast(_Client, synchronizer._translate_in(client))
34
+
35
+ async def _load(
36
+ self: _SandboxSnapshot, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
37
+ ):
38
+ # hydration doesn't actually do much apart from validating the existance of the id
39
+ # which is implicitly done by trying to start a sandbox from the snapshot as well
40
+ resp: api_pb2.SandboxSnapshotGetResponse = await load_context.client.stub.SandboxSnapshotGet(
41
+ api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id)
33
42
  )
43
+ self._hydrate(resp.snapshot_id, load_context.client, None)
34
44
 
35
45
  rep = "SandboxSnapshot()"
36
- obj = _SandboxSnapshot._from_loader(_load, rep)
37
- obj._hydrate(sandbox_snapshot_id, client, None)
38
-
39
- return obj
46
+ obj = _SandboxSnapshot._from_loader(
47
+ _load, rep, load_context_overrides=LoadContext(client=_client), hydrate_lazily=True
48
+ )
49
+ # Setting the object id directly is a bit hacky, but
50
+ # it avoids hydrating the object fully if it's going
51
+ # to be used only for its object id anyway
52
+ obj._object_id = sandbox_snapshot_id
53
+ return cast(typing_extensions.Self, synchronizer._translate_out(obj))
40
54
 
41
55
 
42
56
  SandboxSnapshot = synchronize_api(_SandboxSnapshot)
modal/snapshot.pyi CHANGED
@@ -4,6 +4,8 @@ import modal.object
4
4
  import typing
5
5
  import typing_extensions
6
6
 
7
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
8
+
7
9
  class _SandboxSnapshot(modal._object._Object):
8
10
  """> Sandbox memory snapshots are in **early preview**.
9
11
 
@@ -11,10 +13,16 @@ class _SandboxSnapshot(modal._object._Object):
11
13
  `._experimental_snapshot()` on a Sandbox instance. This includes both the filesystem and memory state of
12
14
  the original Sandbox at the time the snapshot was taken.
13
15
  """
14
- @staticmethod
15
- async def from_id(sandbox_snapshot_id: str, client: typing.Optional[modal.client._Client] = None):
16
- """Construct a `SandboxSnapshot` object from a sandbox snapshot ID."""
17
- ...
16
+ class __from_id_spec(typing_extensions.Protocol[SUPERSELF]):
17
+ def __call__(
18
+ self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None
19
+ ) -> SUPERSELF:
20
+ """Construct a `SandboxSnapshot` object from a sandbox snapshot ID."""
21
+ ...
22
+
23
+ async def aio(self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None): ...
24
+
25
+ from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
18
26
 
19
27
  class SandboxSnapshot(modal.object.Object):
20
28
  """> Sandbox memory snapshots are in **early preview**.
@@ -27,13 +35,13 @@ class SandboxSnapshot(modal.object.Object):
27
35
  """mdmd:hidden"""
28
36
  ...
29
37
 
30
- class __from_id_spec(typing_extensions.Protocol):
31
- def __call__(self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None):
38
+ class __from_id_spec(typing_extensions.Protocol[SUPERSELF]):
39
+ def __call__(
40
+ self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None
41
+ ) -> SUPERSELF:
32
42
  """Construct a `SandboxSnapshot` object from a sandbox snapshot ID."""
33
43
  ...
34
44
 
35
- async def aio(self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None):
36
- """Construct a `SandboxSnapshot` object from a sandbox snapshot ID."""
37
- ...
45
+ async def aio(self, /, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None): ...
38
46
 
39
- from_id: __from_id_spec
47
+ from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
modal/token_flow.py CHANGED
@@ -56,7 +56,7 @@ class _TokenFlow:
56
56
  req = api_pb2.TokenFlowWaitRequest(
57
57
  token_flow_id=self.token_flow_id, timeout=timeout, wait_secret=self.wait_secret
58
58
  )
59
- resp = await self.stub.TokenFlowWait(req, timeout=(timeout + grpc_extra_timeout))
59
+ resp = await self.stub.TokenFlowWait(req, retry=None, timeout=timeout + grpc_extra_timeout)
60
60
  if not resp.timeout:
61
61
  return resp
62
62
  else:
modal/token_flow.pyi CHANGED
@@ -21,12 +21,10 @@ class _TokenFlow:
21
21
  """mdmd:hidden"""
22
22
  ...
23
23
 
24
- SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
25
-
26
24
  class TokenFlow:
27
25
  def __init__(self, client: modal.client.Client): ...
28
26
 
29
- class __start_spec(typing_extensions.Protocol[SUPERSELF]):
27
+ class __start_spec(typing_extensions.Protocol):
30
28
  def __call__(
31
29
  self, /, utm_source: typing.Optional[str] = None, next_url: typing.Optional[str] = None
32
30
  ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[tuple[str, str, str]]:
@@ -39,9 +37,9 @@ class TokenFlow:
39
37
  """mdmd:hidden"""
40
38
  ...
41
39
 
42
- start: __start_spec[typing_extensions.Self]
40
+ start: __start_spec
43
41
 
44
- class __finish_spec(typing_extensions.Protocol[SUPERSELF]):
42
+ class __finish_spec(typing_extensions.Protocol):
45
43
  def __call__(
46
44
  self, /, timeout: float = 40.0, grpc_extra_timeout: float = 5.0
47
45
  ) -> typing.Optional[modal_proto.api_pb2.TokenFlowWaitResponse]:
@@ -54,7 +52,7 @@ class TokenFlow:
54
52
  """mdmd:hidden"""
55
53
  ...
56
54
 
57
- finish: __finish_spec[typing_extensions.Self]
55
+ finish: __finish_spec
58
56
 
59
57
  async def _new_token(
60
58
  *,