modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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 (160) hide show
  1. modal/__init__.py +0 -2
  2. modal/__main__.py +3 -4
  3. modal/_billing.py +80 -0
  4. modal/_clustered_functions.py +7 -3
  5. modal/_clustered_functions.pyi +15 -3
  6. modal/_container_entrypoint.py +51 -69
  7. modal/_functions.py +508 -240
  8. modal/_grpc_client.py +171 -0
  9. modal/_load_context.py +105 -0
  10. modal/_object.py +81 -21
  11. modal/_output.py +58 -45
  12. modal/_partial_function.py +48 -73
  13. modal/_pty.py +7 -3
  14. modal/_resolver.py +26 -46
  15. modal/_runtime/asgi.py +4 -3
  16. modal/_runtime/container_io_manager.py +358 -220
  17. modal/_runtime/container_io_manager.pyi +296 -101
  18. modal/_runtime/execution_context.py +18 -2
  19. modal/_runtime/execution_context.pyi +64 -7
  20. modal/_runtime/gpu_memory_snapshot.py +262 -57
  21. modal/_runtime/user_code_imports.py +28 -58
  22. modal/_serialization.py +90 -6
  23. modal/_traceback.py +42 -1
  24. modal/_tunnel.pyi +380 -12
  25. modal/_utils/async_utils.py +84 -29
  26. modal/_utils/auth_token_manager.py +111 -0
  27. modal/_utils/blob_utils.py +181 -58
  28. modal/_utils/deprecation.py +19 -0
  29. modal/_utils/function_utils.py +91 -47
  30. modal/_utils/grpc_utils.py +89 -66
  31. modal/_utils/mount_utils.py +26 -1
  32. modal/_utils/name_utils.py +17 -3
  33. modal/_utils/task_command_router_client.py +536 -0
  34. modal/_utils/time_utils.py +34 -6
  35. modal/app.py +256 -88
  36. modal/app.pyi +909 -92
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +18 -0
  39. modal/builder/PREVIEW.txt +18 -0
  40. modal/builder/base-images.json +58 -0
  41. modal/cli/_download.py +19 -3
  42. modal/cli/_traceback.py +3 -2
  43. modal/cli/app.py +4 -4
  44. modal/cli/cluster.py +15 -7
  45. modal/cli/config.py +5 -3
  46. modal/cli/container.py +7 -6
  47. modal/cli/dict.py +22 -16
  48. modal/cli/entry_point.py +12 -5
  49. modal/cli/environment.py +5 -4
  50. modal/cli/import_refs.py +3 -3
  51. modal/cli/launch.py +102 -5
  52. modal/cli/network_file_system.py +11 -12
  53. modal/cli/profile.py +3 -2
  54. modal/cli/programs/launch_instance_ssh.py +94 -0
  55. modal/cli/programs/run_jupyter.py +1 -1
  56. modal/cli/programs/run_marimo.py +95 -0
  57. modal/cli/programs/vscode.py +1 -1
  58. modal/cli/queues.py +57 -26
  59. modal/cli/run.py +91 -23
  60. modal/cli/secret.py +48 -22
  61. modal/cli/token.py +7 -8
  62. modal/cli/utils.py +4 -7
  63. modal/cli/volume.py +31 -25
  64. modal/client.py +15 -85
  65. modal/client.pyi +183 -62
  66. modal/cloud_bucket_mount.py +5 -3
  67. modal/cloud_bucket_mount.pyi +197 -5
  68. modal/cls.py +200 -126
  69. modal/cls.pyi +446 -68
  70. modal/config.py +29 -11
  71. modal/container_process.py +319 -19
  72. modal/container_process.pyi +190 -20
  73. modal/dict.py +290 -71
  74. modal/dict.pyi +835 -83
  75. modal/environments.py +15 -27
  76. modal/environments.pyi +46 -24
  77. modal/exception.py +14 -2
  78. modal/experimental/__init__.py +194 -40
  79. modal/experimental/flash.py +618 -0
  80. modal/experimental/flash.pyi +380 -0
  81. modal/experimental/ipython.py +11 -7
  82. modal/file_io.py +29 -36
  83. modal/file_io.pyi +251 -53
  84. modal/file_pattern_matcher.py +56 -16
  85. modal/functions.pyi +673 -92
  86. modal/gpu.py +1 -1
  87. modal/image.py +528 -176
  88. modal/image.pyi +1572 -145
  89. modal/io_streams.py +458 -128
  90. modal/io_streams.pyi +433 -52
  91. modal/mount.py +216 -151
  92. modal/mount.pyi +225 -78
  93. modal/network_file_system.py +45 -62
  94. modal/network_file_system.pyi +277 -56
  95. modal/object.pyi +93 -17
  96. modal/parallel_map.py +942 -129
  97. modal/parallel_map.pyi +294 -15
  98. modal/partial_function.py +0 -2
  99. modal/partial_function.pyi +234 -19
  100. modal/proxy.py +17 -8
  101. modal/proxy.pyi +36 -3
  102. modal/queue.py +270 -65
  103. modal/queue.pyi +817 -57
  104. modal/runner.py +115 -101
  105. modal/runner.pyi +205 -49
  106. modal/sandbox.py +512 -136
  107. modal/sandbox.pyi +845 -111
  108. modal/schedule.py +1 -1
  109. modal/secret.py +300 -70
  110. modal/secret.pyi +589 -34
  111. modal/serving.py +7 -11
  112. modal/serving.pyi +7 -8
  113. modal/snapshot.py +11 -8
  114. modal/snapshot.pyi +25 -4
  115. modal/token_flow.py +4 -4
  116. modal/token_flow.pyi +28 -8
  117. modal/volume.py +416 -158
  118. modal/volume.pyi +1117 -121
  119. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
  120. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  121. modal_docs/mdmd/mdmd.py +17 -4
  122. modal_proto/api.proto +534 -79
  123. modal_proto/api_grpc.py +337 -1
  124. modal_proto/api_pb2.py +1522 -968
  125. modal_proto/api_pb2.pyi +1619 -134
  126. modal_proto/api_pb2_grpc.py +699 -4
  127. modal_proto/api_pb2_grpc.pyi +226 -14
  128. modal_proto/modal_api_grpc.py +175 -154
  129. modal_proto/sandbox_router.proto +145 -0
  130. modal_proto/sandbox_router_grpc.py +105 -0
  131. modal_proto/sandbox_router_pb2.py +149 -0
  132. modal_proto/sandbox_router_pb2.pyi +333 -0
  133. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  134. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  135. modal_proto/task_command_router.proto +144 -0
  136. modal_proto/task_command_router_grpc.py +105 -0
  137. modal_proto/task_command_router_pb2.py +149 -0
  138. modal_proto/task_command_router_pb2.pyi +333 -0
  139. modal_proto/task_command_router_pb2_grpc.py +203 -0
  140. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  141. modal_version/__init__.py +1 -1
  142. modal/requirements/PREVIEW.txt +0 -16
  143. modal/requirements/base-images.json +0 -26
  144. modal-1.0.3.dev10.dist-info/RECORD +0 -179
  145. modal_proto/modal_options_grpc.py +0 -3
  146. modal_proto/options.proto +0 -19
  147. modal_proto/options_grpc.py +0 -3
  148. modal_proto/options_pb2.py +0 -35
  149. modal_proto/options_pb2.pyi +0 -20
  150. modal_proto/options_pb2_grpc.py +0 -4
  151. modal_proto/options_pb2_grpc.pyi +0 -7
  152. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  153. /modal/{requirements → builder}/2023.12.txt +0 -0
  154. /modal/{requirements → builder}/2024.04.txt +0 -0
  155. /modal/{requirements → builder}/2024.10.txt +0 -0
  156. /modal/{requirements → builder}/README.md +0 -0
  157. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  158. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  159. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  160. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
@@ -19,7 +19,7 @@ from ._functions import _Function
19
19
  from ._utils.async_utils import synchronizer
20
20
  from ._utils.deprecation import deprecation_warning
21
21
  from ._utils.function_utils import callable_has_non_self_params
22
- from .config import logger
22
+ from .config import config, logger
23
23
  from .exception import InvalidError
24
24
 
25
25
  MAX_MAX_BATCH_SIZE = 1000
@@ -31,7 +31,6 @@ if typing.TYPE_CHECKING:
31
31
 
32
32
  class _PartialFunctionFlags(enum.IntFlag):
33
33
  # Lifecycle method flags
34
- BUILD = 1 # Deprecated, will be removed
35
34
  ENTER_PRE_SNAPSHOT = 2
36
35
  ENTER_POST_SNAPSHOT = 4
37
36
  EXIT = 8
@@ -55,8 +54,7 @@ class _PartialFunctionFlags(enum.IntFlag):
55
54
  @staticmethod
56
55
  def lifecycle_flags() -> int:
57
56
  return (
58
- _PartialFunctionFlags.BUILD # Deprecated, will be removed
59
- | _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
57
+ _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
60
58
  | _PartialFunctionFlags.ENTER_POST_SNAPSHOT
61
59
  | _PartialFunctionFlags.EXIT
62
60
  )
@@ -95,6 +93,26 @@ NullaryFuncOrMethod = Union[Callable[[], Any], Callable[[Any], Any]]
95
93
  NullaryMethod = Callable[[Any], Any]
96
94
 
97
95
 
96
+ def verify_concurrent_params(params: _PartialFunctionParams, is_flash: bool = False) -> None:
97
+ def _verify_concurrent_params_with_flash_settings(params: _PartialFunctionParams) -> None:
98
+ if params.max_concurrent_inputs is not None:
99
+ raise TypeError(
100
+ "@modal.concurrent(max_inputs=...) is not yet supported for Flash functions. "
101
+ "Use `@modal.concurrent(target_inputs=...)` instead."
102
+ )
103
+ if params.target_concurrent_inputs is None:
104
+ raise TypeError("`@modal.concurrent()` missing required argument: `target_inputs`.")
105
+
106
+ def _verify_concurrent_params(params: _PartialFunctionParams) -> None:
107
+ if params.max_concurrent_inputs is None:
108
+ raise TypeError("`@modal.concurrent()` missing required argument: `max_inputs`.")
109
+
110
+ if is_flash:
111
+ _verify_concurrent_params_with_flash_settings(params)
112
+ else:
113
+ _verify_concurrent_params(params)
114
+
115
+
98
116
  class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
99
117
  """Object produced by a decorator in the `modal` namespace
100
118
 
@@ -284,7 +302,7 @@ class _MethodDecoratorType:
284
302
 
285
303
  # TODO(elias): fix support for coroutine type unwrapping for methods (static typing)
286
304
  def _method(
287
- _warn_parentheses_missing=None,
305
+ _warn_parentheses_missing=None, # mdmd:line-hidden
288
306
  *,
289
307
  # Set this to True if it's a non-generator function returning
290
308
  # a [sync/async] generator object
@@ -339,7 +357,7 @@ def _parse_custom_domains(custom_domains: Optional[Iterable[str]] = None) -> lis
339
357
 
340
358
 
341
359
  def _fastapi_endpoint(
342
- _warn_parentheses_missing=None,
360
+ _warn_parentheses_missing=None, # mdmd:line-hidden
343
361
  *,
344
362
  method: str = "GET", # REST method for the created endpoint.
345
363
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
@@ -380,6 +398,7 @@ def _fastapi_endpoint(
380
398
  method=method,
381
399
  web_endpoint_docs=docs,
382
400
  requested_suffix=label or "",
401
+ ephemeral_suffix=config.get("dev_suffix"),
383
402
  async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
384
403
  custom_domains=_parse_custom_domains(custom_domains),
385
404
  requires_proxy_auth=requires_proxy_auth,
@@ -402,7 +421,7 @@ def _fastapi_endpoint(
402
421
 
403
422
 
404
423
  def _web_endpoint(
405
- _warn_parentheses_missing=None,
424
+ _warn_parentheses_missing=None, # mdmd:line-hidden
406
425
  *,
407
426
  method: str = "GET", # REST method for the created endpoint.
408
427
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
@@ -448,6 +467,7 @@ def _web_endpoint(
448
467
  method=method,
449
468
  web_endpoint_docs=docs,
450
469
  requested_suffix=label or "",
470
+ ephemeral_suffix=config.get("dev_suffix"),
451
471
  async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
452
472
  custom_domains=_parse_custom_domains(custom_domains),
453
473
  requires_proxy_auth=requires_proxy_auth,
@@ -470,7 +490,7 @@ def _web_endpoint(
470
490
 
471
491
 
472
492
  def _asgi_app(
473
- _warn_parentheses_missing=None,
493
+ _warn_parentheses_missing=None, # mdmd:line-hidden
474
494
  *,
475
495
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
476
496
  custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
@@ -507,6 +527,7 @@ def _asgi_app(
507
527
  webhook_config = api_pb2.WebhookConfig(
508
528
  type=api_pb2.WEBHOOK_TYPE_ASGI_APP,
509
529
  requested_suffix=label or "",
530
+ ephemeral_suffix=config.get("dev_suffix"),
510
531
  async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
511
532
  custom_domains=_parse_custom_domains(custom_domains),
512
533
  requires_proxy_auth=requires_proxy_auth,
@@ -527,7 +548,7 @@ def _asgi_app(
527
548
 
528
549
 
529
550
  def _wsgi_app(
530
- _warn_parentheses_missing=None,
551
+ _warn_parentheses_missing=None, # mdmd:line-hidden
531
552
  *,
532
553
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
533
554
  custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
@@ -538,7 +559,7 @@ def _wsgi_app(
538
559
  Web Server Gateway Interface (WSGI) is a standard for synchronous Python web apps.
539
560
  It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
540
561
  which is compatible with ASGI and supports additional functionality such as web sockets.
541
- Modal supports ASGI via [`asgi_app`](/docs/reference/modal.asgi_app).
562
+ Modal supports ASGI via [`asgi_app`](https://modal.com/docs/reference/modal.asgi_app).
542
563
 
543
564
  **Usage:**
544
565
 
@@ -564,6 +585,7 @@ def _wsgi_app(
564
585
  webhook_config = api_pb2.WebhookConfig(
565
586
  type=api_pb2.WEBHOOK_TYPE_WSGI_APP,
566
587
  requested_suffix=label or "",
588
+ ephemeral_suffix=config.get("dev_suffix"),
567
589
  async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
568
590
  custom_domains=_parse_custom_domains(custom_domains),
569
591
  requires_proxy_auth=requires_proxy_auth,
@@ -625,6 +647,7 @@ def _web_server(
625
647
  webhook_config = api_pb2.WebhookConfig(
626
648
  type=api_pb2.WEBHOOK_TYPE_WEB_SERVER,
627
649
  requested_suffix=label or "",
650
+ ephemeral_suffix=config.get("dev_suffix"),
628
651
  async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
629
652
  custom_domains=_parse_custom_domains(custom_domains),
630
653
  web_server_port=port,
@@ -646,61 +669,8 @@ def _web_server(
646
669
  return wrapper
647
670
 
648
671
 
649
- def _build(
650
- _warn_parentheses_missing=None, *, force: bool = False, timeout: int = 86400
651
- ) -> Callable[[Union[_PartialFunction, NullaryMethod]], _PartialFunction]:
652
- """mdmd:hidden
653
- Decorator for methods that execute at _build time_ to create a new Image layer.
654
-
655
- **Deprecated**: This function is deprecated. We recommend using `modal.Volume`
656
- to store large assets (such as model weights) instead of writing them to the
657
- Image during the build process. For other use cases, you can replace this
658
- decorator with the `Image.run_function` method.
659
-
660
- **Usage**
661
-
662
- ```python notest
663
- @app.cls(gpu="A10G")
664
- class AlpacaLoRAModel:
665
- @build()
666
- def download_models(self):
667
- model = LlamaForCausalLM.from_pretrained(
668
- base_model,
669
- )
670
- PeftModel.from_pretrained(model, lora_weights)
671
- LlamaTokenizer.from_pretrained(base_model)
672
- ```
673
- """
674
- if _warn_parentheses_missing is not None:
675
- raise InvalidError(
676
- "Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@modal.build()`."
677
- )
678
-
679
- deprecation_warning(
680
- (2025, 1, 15),
681
- "The `@modal.build` decorator is deprecated and will be removed in a future release."
682
- "\n\nWe now recommend storing large assets (such as model weights) using a `modal.Volume`"
683
- " instead of writing them directly into the `modal.Image` filesystem."
684
- " For other use cases we recommend using `Image.run_function` instead."
685
- "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
686
- )
687
-
688
- flags = _PartialFunctionFlags.BUILD
689
- params = _PartialFunctionParams(force_build=force, build_timeout=timeout)
690
-
691
- def wrapper(obj: Union[_PartialFunction, NullaryMethod]) -> _PartialFunction:
692
- if isinstance(obj, _PartialFunction):
693
- pf = obj.stack(flags, params)
694
- else:
695
- pf = _PartialFunction(obj, flags, params)
696
- pf.validate_obj_compatibility("build")
697
- return pf
698
-
699
- return wrapper
700
-
701
-
702
672
  def _enter(
703
- _warn_parentheses_missing=None,
673
+ _warn_parentheses_missing=None, # mdmd:line-hidden
704
674
  *,
705
675
  snap: bool = False,
706
676
  ) -> Callable[[Union[_PartialFunction, NullaryMethod]], _PartialFunction]:
@@ -751,7 +721,7 @@ def _exit(_warn_parentheses_missing=None) -> Callable[[NullaryMethod], _PartialF
751
721
 
752
722
 
753
723
  def _batched(
754
- _warn_parentheses_missing=None,
724
+ _warn_parentheses_missing=None, # mdmd:line-hidden
755
725
  *,
756
726
  max_batch_size: int,
757
727
  wait_ms: int,
@@ -789,12 +759,12 @@ def _batched(
789
759
  )
790
760
  if max_batch_size < 1:
791
761
  raise InvalidError("max_batch_size must be a positive integer.")
792
- if max_batch_size >= MAX_MAX_BATCH_SIZE:
793
- raise InvalidError(f"max_batch_size must be less than {MAX_MAX_BATCH_SIZE}.")
762
+ if max_batch_size > MAX_MAX_BATCH_SIZE:
763
+ raise InvalidError(f"max_batch_size cannot be greater than {MAX_MAX_BATCH_SIZE}.")
794
764
  if wait_ms < 0:
795
765
  raise InvalidError("wait_ms must be a non-negative integer.")
796
- if wait_ms >= MAX_BATCH_WAIT_MS:
797
- raise InvalidError(f"wait_ms must be less than {MAX_BATCH_WAIT_MS}.")
766
+ if wait_ms > MAX_BATCH_WAIT_MS:
767
+ raise InvalidError(f"wait_ms cannot be greater than {MAX_BATCH_WAIT_MS}.")
798
768
 
799
769
  flags = _PartialFunctionFlags.CALLABLE_INTERFACE | _PartialFunctionFlags.BATCHED
800
770
  params = _PartialFunctionParams(batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
@@ -813,9 +783,9 @@ def _batched(
813
783
 
814
784
 
815
785
  def _concurrent(
816
- _warn_parentheses_missing=None,
786
+ _warn_parentheses_missing=None, # mdmd:line-hidden
817
787
  *,
818
- max_inputs: int, # Hard limit on each container's input concurrency
788
+ max_inputs: Optional[int] = None, # Hard limit on each container's input concurrency
819
789
  target_inputs: Optional[int] = None, # Input concurrency that Modal's autoscaler should target
820
790
  ) -> Callable[
821
791
  [Union[Callable[P, ReturnType], _PartialFunction[P, ReturnType, ReturnType]]],
@@ -867,7 +837,7 @@ def _concurrent(
867
837
  "Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@modal.concurrent()`."
868
838
  )
869
839
 
870
- if target_inputs and target_inputs > max_inputs:
840
+ if max_inputs is not None and target_inputs is not None and target_inputs > max_inputs:
871
841
  raise InvalidError("`target_inputs` parameter cannot be greater than `max_inputs`.")
872
842
 
873
843
  flags = _PartialFunctionFlags.CONCURRENT
@@ -891,7 +861,12 @@ def _concurrent(
891
861
 
892
862
 
893
863
  # NOTE: clustered is currently exposed through modal.experimental, not the top-level namespace
894
- def _clustered(size: int, broadcast: bool = True, rdma: bool = False):
864
+ def _clustered(
865
+ size: int, broadcast: bool = True, rdma: bool = False
866
+ ) -> Callable[
867
+ [Union[Callable[P, ReturnType], _PartialFunction[P, ReturnType, ReturnType]]],
868
+ _PartialFunction[P, ReturnType, ReturnType],
869
+ ]:
895
870
  """Provision clusters of colocated and networked containers for the Function.
896
871
 
897
872
  Parameters:
modal/_pty.py CHANGED
@@ -7,8 +7,11 @@ from typing import Optional
7
7
  from modal_proto import api_pb2
8
8
 
9
9
 
10
- def get_winsz(fd) -> tuple[Optional[int], Optional[int]]:
10
+ def get_winsz(fd=None) -> tuple[Optional[int], Optional[int]]:
11
11
  try:
12
+ if fd is None:
13
+ fd = sys.stdin.fileno()
14
+
12
15
  import fcntl
13
16
  import struct
14
17
  import termios
@@ -40,8 +43,8 @@ def raw_terminal():
40
43
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
41
44
 
42
45
 
43
- def get_pty_info(shell: bool) -> api_pb2.PTYInfo:
44
- rows, cols = get_winsz(sys.stdin.fileno())
46
+ def get_pty_info(shell: bool, no_terminate_on_idle_stdin: bool = False) -> api_pb2.PTYInfo:
47
+ rows, cols = get_winsz()
45
48
  return api_pb2.PTYInfo(
46
49
  enabled=True, # TODO(erikbern): deprecated
47
50
  winsz_rows=rows,
@@ -50,4 +53,5 @@ def get_pty_info(shell: bool) -> api_pb2.PTYInfo:
50
53
  env_colorterm=os.environ.get("COLORTERM"),
51
54
  env_term_program=os.environ.get("TERM_PROGRAM"),
52
55
  pty_type=api_pb2.PTYInfo.PTY_TYPE_SHELL if shell else api_pb2.PTYInfo.PTY_TYPE_FUNCTION,
56
+ no_terminate_on_idle_stdin=no_terminate_on_idle_stdin,
53
57
  )
modal/_resolver.py CHANGED
@@ -8,19 +8,16 @@ from asyncio import Future
8
8
  from collections.abc import Hashable
9
9
  from typing import TYPE_CHECKING, Optional
10
10
 
11
- from grpclib import GRPCError, Status
12
-
11
+ import modal._object
12
+ from modal._traceback import suppress_tb_frames
13
13
  from modal_proto import api_pb2
14
14
 
15
+ from ._load_context import LoadContext
15
16
  from ._utils.async_utils import TaskContext
16
- from .client import _Client
17
- from .exception import NotFoundError
18
17
 
19
18
  if TYPE_CHECKING:
20
19
  from rich.tree import Tree
21
20
 
22
- import modal._object
23
-
24
21
 
25
22
  class StatusRow:
26
23
  def __init__(self, progress: "typing.Optional[Tree]"):
@@ -50,19 +47,10 @@ class StatusRow:
50
47
 
51
48
  class Resolver:
52
49
  _local_uuid_to_future: dict[str, Future]
53
- _environment_name: Optional[str]
54
- _app_id: Optional[str]
55
50
  _deduplication_cache: dict[Hashable, Future]
56
- _client: _Client
57
51
  _build_start: float
58
52
 
59
- def __init__(
60
- self,
61
- client: _Client,
62
- *,
63
- environment_name: Optional[str] = None,
64
- app_id: Optional[str] = None,
65
- ):
53
+ def __init__(self):
66
54
  try:
67
55
  # TODO(michael) If we don't clean this up more thoroughly, it would probably
68
56
  # be good to have a single source of truth for "rich is installed" rather than
@@ -77,9 +65,6 @@ class Resolver:
77
65
 
78
66
  self._local_uuid_to_future = {}
79
67
  self._tree = tree
80
- self._client = client
81
- self._app_id = app_id
82
- self._environment_name = environment_name
83
68
  self._deduplication_cache = {}
84
69
 
85
70
  with tempfile.TemporaryFile() as temp_file:
@@ -87,27 +72,24 @@ class Resolver:
87
72
  # to the mtime on mounted files, and want those measurements to have the same resolution.
88
73
  self._build_start = os.fstat(temp_file.fileno()).st_mtime
89
74
 
90
- @property
91
- def app_id(self) -> Optional[str]:
92
- return self._app_id
93
-
94
- @property
95
- def client(self):
96
- return self._client
97
-
98
- @property
99
- def environment_name(self):
100
- return self._environment_name
101
-
102
75
  @property
103
76
  def build_start(self) -> float:
104
77
  return self._build_start
105
78
 
106
- async def preload(self, obj, existing_object_id: Optional[str]):
79
+ async def preload(
80
+ self, obj: "modal._object._Object", parent_load_context: "LoadContext", existing_object_id: Optional[str]
81
+ ):
107
82
  if obj._preload is not None:
108
- await obj._preload(obj, self, existing_object_id)
83
+ load_context = obj._load_context_overrides.merged_with(parent_load_context)
84
+ await obj._preload(obj, self, load_context, existing_object_id)
109
85
 
110
- async def load(self, obj: "modal._object._Object", existing_object_id: Optional[str] = None):
86
+ async def load(
87
+ self,
88
+ obj: "modal._object._Object",
89
+ parent_load_context: "LoadContext",
90
+ *,
91
+ existing_object_id: Optional[str] = None,
92
+ ):
111
93
  if obj._is_hydrated and obj._is_another_app:
112
94
  # No need to reload this, it won't typically change
113
95
  if obj.local_uuid not in self._local_uuid_to_future:
@@ -131,25 +113,23 @@ class Resolver:
131
113
  cached_future = self._deduplication_cache.get(deduplication_key)
132
114
  if cached_future:
133
115
  hydrated_object = await cached_future
134
- obj._hydrate(hydrated_object.object_id, self._client, hydrated_object._get_metadata())
116
+ # Use the client from the already-hydrated object
117
+ obj._hydrate(hydrated_object.object_id, hydrated_object.client, hydrated_object._get_metadata())
135
118
  return obj
136
119
 
137
120
  if not cached_future:
138
121
  # don't run any awaits within this if-block to prevent race conditions
139
122
  async def loader():
140
- # Wait for all its dependencies
123
+ load_context = await obj._load_context_overrides.merged_with(parent_load_context).apply_defaults()
124
+
141
125
  # TODO(erikbern): do we need existing_object_id for those?
142
- await TaskContext.gather(*[self.load(dep) for dep in obj.deps()])
126
+ await TaskContext.gather(*[self.load(dep, load_context) for dep in obj.deps()])
143
127
 
144
128
  # Load the object itself
145
129
  if not obj._load:
146
130
  raise Exception(f"Object {obj} has no loader function")
147
- try:
148
- await obj._load(obj, self, existing_object_id)
149
- except GRPCError as exc:
150
- if exc.status == Status.NOT_FOUND:
151
- raise NotFoundError(exc.message)
152
- raise
131
+
132
+ await obj._load(obj, self, load_context, existing_object_id)
153
133
 
154
134
  # Check that the id of functions didn't change
155
135
  # Persisted refs are ignored because their life cycle is managed independently.
@@ -169,9 +149,9 @@ class Resolver:
169
149
  self._local_uuid_to_future[obj.local_uuid] = cached_future
170
150
  if deduplication_key is not None:
171
151
  self._deduplication_cache[deduplication_key] = cached_future
172
-
173
- # TODO(elias): print original exception/trace rather than the Resolver-internal trace
174
- return await cached_future
152
+ with suppress_tb_frames(2):
153
+ # skip current frame + `loader()` closure frame from above
154
+ return await cached_future
175
155
 
176
156
  def objects(self) -> list["modal._object._Object"]:
177
157
  unique_objects: dict[str, "modal._object._Object"] = {}
modal/_runtime/asgi.py CHANGED
@@ -16,7 +16,7 @@ from modal.config import logger
16
16
  from modal.exception import ExecutionError, InvalidError
17
17
  from modal.experimental import stop_fetching_inputs
18
18
 
19
- from .execution_context import current_function_call_id
19
+ from .execution_context import current_attempt_token, current_function_call_id
20
20
 
21
21
  FIRST_MESSAGE_TIMEOUT_SECONDS = 5.0
22
22
 
@@ -106,6 +106,7 @@ def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., Asyn
106
106
  raise ExecutionError("Unpexected state in ASGI scope")
107
107
  scope["state"] = state
108
108
  function_call_id = current_function_call_id()
109
+ attempt_token = current_attempt_token()
109
110
  assert function_call_id, "internal error: function_call_id not set in asgi_app() scope"
110
111
 
111
112
  messages_from_app: asyncio.Queue[dict[str, Any]] = asyncio.Queue(1)
@@ -119,7 +120,7 @@ def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., Asyn
119
120
 
120
121
  async def handle_first_input_timeout():
121
122
  if scope["type"] == "http":
122
- await messages_from_app.put({"type": "http.response.start", "status": 502})
123
+ await messages_from_app.put({"type": "http.response.start", "status": 408})
123
124
  await messages_from_app.put(
124
125
  {
125
126
  "type": "http.response.body",
@@ -142,7 +143,7 @@ def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., Asyn
142
143
  # This initial message, "http.request" or "websocket.connect", should be sent
143
144
  # immediately after starting the ASGI app's function call. If it is not received, that
144
145
  # indicates a request cancellation or other abnormal circumstance.
145
- message_gen = container_io_manager.get_data_in.aio(function_call_id)
146
+ message_gen = container_io_manager.get_data_in.aio(function_call_id, attempt_token)
146
147
  first_message_task = asyncio.create_task(message_gen.__anext__())
147
148
 
148
149
  try: