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/app.pyi CHANGED
@@ -1,5 +1,6 @@
1
1
  import collections.abc
2
2
  import modal._functions
3
+ import modal._load_context
3
4
  import modal._partial_function
4
5
  import modal._utils.function_utils
5
6
  import modal.client
@@ -74,6 +75,42 @@ class _FunctionDecoratorType:
74
75
  self, func: collections.abc.Callable[P, ReturnType]
75
76
  ) -> modal.functions.Function[P, ReturnType, ReturnType]: ...
76
77
 
78
+ class _LocalAppState:
79
+ """All state for apps that's part of the local/definition state"""
80
+
81
+ functions: dict[str, modal._functions._Function]
82
+ classes: dict[str, modal.cls._Cls]
83
+ image_default: typing.Optional[modal.image._Image]
84
+ web_endpoints: list[str]
85
+ local_entrypoints: dict[str, _LocalEntrypoint]
86
+ tags: dict[str, str]
87
+ include_source_default: bool
88
+ secrets_default: collections.abc.Sequence[modal.secret._Secret]
89
+ volumes_default: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume]
90
+
91
+ def __init__(
92
+ self,
93
+ functions: dict[str, modal._functions._Function],
94
+ classes: dict[str, modal.cls._Cls],
95
+ image_default: typing.Optional[modal.image._Image],
96
+ web_endpoints: list[str],
97
+ local_entrypoints: dict[str, _LocalEntrypoint],
98
+ tags: dict[str, str],
99
+ include_source_default: bool,
100
+ secrets_default: collections.abc.Sequence[modal.secret._Secret],
101
+ volumes_default: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume],
102
+ ) -> None:
103
+ """Initialize self. See help(type(self)) for accurate signature."""
104
+ ...
105
+
106
+ def __repr__(self):
107
+ """Return repr(self)."""
108
+ ...
109
+
110
+ def __eq__(self, other):
111
+ """Return self==value."""
112
+ ...
113
+
77
114
  class _App:
78
115
  """A Modal App is a group of functions and classes that are deployed together.
79
116
 
@@ -110,18 +147,16 @@ class _App:
110
147
  _container_app: typing.ClassVar[typing.Optional[_App]]
111
148
  _name: typing.Optional[str]
112
149
  _description: typing.Optional[str]
113
- _tags: dict[str, str]
114
- _functions: dict[str, modal._functions._Function]
115
- _classes: dict[str, modal.cls._Cls]
116
- _image: typing.Optional[modal.image._Image]
117
- _secrets: collections.abc.Sequence[modal.secret._Secret]
118
- _volumes: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume]
119
- _web_endpoints: list[str]
120
- _local_entrypoints: dict[str, _LocalEntrypoint]
150
+ _local_state_attr: typing.Optional[_LocalAppState]
121
151
  _app_id: typing.Optional[str]
122
152
  _running_app: typing.Optional[modal.running_app.RunningApp]
123
153
  _client: typing.Optional[modal.client._Client]
124
- _include_source_default: typing.Optional[bool]
154
+ _root_load_context: modal._load_context.LoadContext
155
+
156
+ @property
157
+ def _local_state(self) -> _LocalAppState:
158
+ """For internal use only. Do not use this property directly."""
159
+ ...
125
160
 
126
161
  def __init__(
127
162
  self,
@@ -452,14 +487,14 @@ class _App:
452
487
  is_generator: typing.Optional[bool] = None,
453
488
  cloud: typing.Optional[str] = None,
454
489
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
490
+ nonpreemptible: bool = False,
455
491
  enable_memory_snapshot: bool = False,
456
492
  block_network: bool = False,
457
493
  restrict_modal_access: bool = False,
458
- max_inputs: typing.Optional[int] = None,
494
+ single_use_containers: bool = False,
459
495
  i6pn: typing.Optional[bool] = None,
460
496
  include_source: typing.Optional[bool] = None,
461
497
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
462
- _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
463
498
  _experimental_proxy_ip: typing.Optional[str] = None,
464
499
  _experimental_custom_scaling_factor: typing.Optional[float] = None,
465
500
  _experimental_restrict_output: bool = False,
@@ -467,7 +502,9 @@ class _App:
467
502
  concurrency_limit: typing.Optional[int] = None,
468
503
  container_idle_timeout: typing.Optional[int] = None,
469
504
  allow_concurrent_inputs: typing.Optional[int] = None,
505
+ max_inputs: typing.Optional[int] = None,
470
506
  _experimental_buffer_containers: typing.Optional[int] = None,
507
+ _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
471
508
  ) -> _FunctionDecoratorType:
472
509
  """Decorator to register a new Modal Function with this App."""
473
510
  ...
@@ -505,14 +542,14 @@ class _App:
505
542
  startup_timeout: typing.Optional[int] = None,
506
543
  cloud: typing.Optional[str] = None,
507
544
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
545
+ nonpreemptible: bool = False,
508
546
  enable_memory_snapshot: bool = False,
509
547
  block_network: bool = False,
510
548
  restrict_modal_access: bool = False,
511
- max_inputs: typing.Optional[int] = None,
549
+ single_use_containers: bool = False,
512
550
  i6pn: typing.Optional[bool] = None,
513
551
  include_source: typing.Optional[bool] = None,
514
552
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
515
- _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
516
553
  _experimental_proxy_ip: typing.Optional[str] = None,
517
554
  _experimental_custom_scaling_factor: typing.Optional[float] = None,
518
555
  _experimental_restrict_output: bool = False,
@@ -520,7 +557,9 @@ class _App:
520
557
  concurrency_limit: typing.Optional[int] = None,
521
558
  container_idle_timeout: typing.Optional[int] = None,
522
559
  allow_concurrent_inputs: typing.Optional[int] = None,
560
+ max_inputs: typing.Optional[int] = None,
523
561
  _experimental_buffer_containers: typing.Optional[int] = None,
562
+ _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
524
563
  ) -> collections.abc.Callable[[typing.Union[CLS_T, modal._partial_function._PartialFunction]], CLS_T]:
525
564
  """Decorator to register a new Modal [Cls](https://modal.com/docs/reference/modal.Cls) with this App."""
526
565
  ...
@@ -630,18 +669,11 @@ class App:
630
669
  _container_app: typing.ClassVar[typing.Optional[App]]
631
670
  _name: typing.Optional[str]
632
671
  _description: typing.Optional[str]
633
- _tags: dict[str, str]
634
- _functions: dict[str, modal.functions.Function]
635
- _classes: dict[str, modal.cls.Cls]
636
- _image: typing.Optional[modal.image.Image]
637
- _secrets: collections.abc.Sequence[modal.secret.Secret]
638
- _volumes: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume.Volume]
639
- _web_endpoints: list[str]
640
- _local_entrypoints: dict[str, LocalEntrypoint]
672
+ _local_state_attr: typing.Optional[_LocalAppState]
641
673
  _app_id: typing.Optional[str]
642
674
  _running_app: typing.Optional[modal.running_app.RunningApp]
643
675
  _client: typing.Optional[modal.client.Client]
644
- _include_source_default: typing.Optional[bool]
676
+ _root_load_context: modal._load_context.LoadContext
645
677
 
646
678
  def __init__(
647
679
  self,
@@ -664,6 +696,11 @@ class App:
664
696
  """
665
697
  ...
666
698
 
699
+ @property
700
+ def _local_state(self) -> _LocalAppState:
701
+ """For internal use only. Do not use this property directly."""
702
+ ...
703
+
667
704
  @property
668
705
  def name(self) -> typing.Optional[str]:
669
706
  """The user-provided name of the App."""
@@ -733,7 +770,7 @@ class App:
733
770
  """
734
771
  ...
735
772
 
736
- lookup: __lookup_spec
773
+ lookup: typing.ClassVar[__lookup_spec]
737
774
 
738
775
  def set_description(self, description: str):
739
776
  """mdmd:hidden
@@ -761,7 +798,7 @@ class App:
761
798
 
762
799
  def _uncreate_all_objects(self): ...
763
800
 
764
- class ___set_local_app_spec(typing_extensions.Protocol[SUPERSELF]):
801
+ class ___set_local_app_spec(typing_extensions.Protocol):
765
802
  def __call__(
766
803
  self, /, client: modal.client.Client, running_app: modal.running_app.RunningApp
767
804
  ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[None]: ...
@@ -769,9 +806,9 @@ class App:
769
806
  self, /, client: modal.client.Client, running_app: modal.running_app.RunningApp
770
807
  ) -> typing.AsyncContextManager[None]: ...
771
808
 
772
- _set_local_app: ___set_local_app_spec[typing_extensions.Self]
809
+ _set_local_app: ___set_local_app_spec
773
810
 
774
- class __run_spec(typing_extensions.Protocol[SUPERSELF]):
811
+ class __run_spec(typing_extensions.Protocol):
775
812
  def __call__(
776
813
  self,
777
814
  /,
@@ -870,7 +907,7 @@ class App:
870
907
  """
871
908
  ...
872
909
 
873
- run: __run_spec[typing_extensions.Self]
910
+ run: __run_spec
874
911
 
875
912
  class __deploy_spec(typing_extensions.Protocol[SUPERSELF]):
876
913
  def __call__(
@@ -1115,14 +1152,14 @@ class App:
1115
1152
  is_generator: typing.Optional[bool] = None,
1116
1153
  cloud: typing.Optional[str] = None,
1117
1154
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
1155
+ nonpreemptible: bool = False,
1118
1156
  enable_memory_snapshot: bool = False,
1119
1157
  block_network: bool = False,
1120
1158
  restrict_modal_access: bool = False,
1121
- max_inputs: typing.Optional[int] = None,
1159
+ single_use_containers: bool = False,
1122
1160
  i6pn: typing.Optional[bool] = None,
1123
1161
  include_source: typing.Optional[bool] = None,
1124
1162
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
1125
- _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
1126
1163
  _experimental_proxy_ip: typing.Optional[str] = None,
1127
1164
  _experimental_custom_scaling_factor: typing.Optional[float] = None,
1128
1165
  _experimental_restrict_output: bool = False,
@@ -1130,7 +1167,9 @@ class App:
1130
1167
  concurrency_limit: typing.Optional[int] = None,
1131
1168
  container_idle_timeout: typing.Optional[int] = None,
1132
1169
  allow_concurrent_inputs: typing.Optional[int] = None,
1170
+ max_inputs: typing.Optional[int] = None,
1133
1171
  _experimental_buffer_containers: typing.Optional[int] = None,
1172
+ _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
1134
1173
  ) -> _FunctionDecoratorType:
1135
1174
  """Decorator to register a new Modal Function with this App."""
1136
1175
  ...
@@ -1168,14 +1207,14 @@ class App:
1168
1207
  startup_timeout: typing.Optional[int] = None,
1169
1208
  cloud: typing.Optional[str] = None,
1170
1209
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
1210
+ nonpreemptible: bool = False,
1171
1211
  enable_memory_snapshot: bool = False,
1172
1212
  block_network: bool = False,
1173
1213
  restrict_modal_access: bool = False,
1174
- max_inputs: typing.Optional[int] = None,
1214
+ single_use_containers: bool = False,
1175
1215
  i6pn: typing.Optional[bool] = None,
1176
1216
  include_source: typing.Optional[bool] = None,
1177
1217
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
1178
- _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
1179
1218
  _experimental_proxy_ip: typing.Optional[str] = None,
1180
1219
  _experimental_custom_scaling_factor: typing.Optional[float] = None,
1181
1220
  _experimental_restrict_output: bool = False,
@@ -1183,7 +1222,9 @@ class App:
1183
1222
  concurrency_limit: typing.Optional[int] = None,
1184
1223
  container_idle_timeout: typing.Optional[int] = None,
1185
1224
  allow_concurrent_inputs: typing.Optional[int] = None,
1225
+ max_inputs: typing.Optional[int] = None,
1186
1226
  _experimental_buffer_containers: typing.Optional[int] = None,
1227
+ _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
1187
1228
  ) -> collections.abc.Callable[[typing.Union[CLS_T, modal.partial_function.PartialFunction]], CLS_T]:
1188
1229
  """Decorator to register a new Modal [Cls](https://modal.com/docs/reference/modal.Cls) with this App."""
1189
1230
  ...
@@ -1217,7 +1258,7 @@ class App:
1217
1258
  """
1218
1259
  ...
1219
1260
 
1220
- class __set_tags_spec(typing_extensions.Protocol[SUPERSELF]):
1261
+ class __set_tags_spec(typing_extensions.Protocol):
1221
1262
  def __call__(
1222
1263
  self, /, tags: collections.abc.Mapping[str, str], *, client: typing.Optional[modal.client.Client] = None
1223
1264
  ) -> None:
@@ -1246,9 +1287,9 @@ class App:
1246
1287
  """
1247
1288
  ...
1248
1289
 
1249
- set_tags: __set_tags_spec[typing_extensions.Self]
1290
+ set_tags: __set_tags_spec
1250
1291
 
1251
- class __get_tags_spec(typing_extensions.Protocol[SUPERSELF]):
1292
+ class __get_tags_spec(typing_extensions.Protocol):
1252
1293
  def __call__(self, /, *, client: typing.Optional[modal.client.Client] = None) -> dict[str, str]:
1253
1294
  """Get the tags that are currently attached to the App."""
1254
1295
  ...
@@ -1257,9 +1298,9 @@ class App:
1257
1298
  """Get the tags that are currently attached to the App."""
1258
1299
  ...
1259
1300
 
1260
- get_tags: __get_tags_spec[typing_extensions.Self]
1301
+ get_tags: __get_tags_spec
1261
1302
 
1262
- class ___logs_spec(typing_extensions.Protocol[SUPERSELF]):
1303
+ class ___logs_spec(typing_extensions.Protocol):
1263
1304
  def __call__(self, /, client: typing.Optional[modal.client.Client] = None) -> typing.Generator[str, None, None]:
1264
1305
  """Stream logs from the app.
1265
1306
 
@@ -1276,7 +1317,7 @@ class App:
1276
1317
  """
1277
1318
  ...
1278
1319
 
1279
- _logs: ___logs_spec[typing_extensions.Self]
1320
+ _logs: ___logs_spec
1280
1321
 
1281
1322
  @classmethod
1282
1323
  def _get_container_app(cls) -> typing.Optional[App]:
modal/billing.py ADDED
@@ -0,0 +1,5 @@
1
+ # Copyright Modal Labs 2025
2
+ from ._billing import _workspace_billing_report
3
+ from ._utils.async_utils import synchronize_api
4
+
5
+ workspace_billing_report = synchronize_api(_workspace_billing_report)
modal/builder/2025.06.txt CHANGED
@@ -1,12 +1,15 @@
1
1
  aiohappyeyeballs==2.6.1
2
- aiohttp==3.12.7
3
- aiosignal==1.3.2
2
+ aiohttp==3.12.7 ; python_version < "3.14"
3
+ aiohttp==3.13.2 ; python_version >= "3.14"
4
+ aiosignal==1.3.2 ; python_version < "3.14"
5
+ aiosignal==1.4.0 ; python_version >= "3.14"
4
6
  async-timeout==5.0.1 ; python_version < "3.11"
5
7
  attrs==25.3.0
6
8
  cbor2==5.7.0
7
9
  certifi==2025.4.26
8
10
  frozenlist==1.6.0
9
- grpclib==0.4.8
11
+ grpclib==0.4.8 ; python_version < "3.14"
12
+ grpclib==0.4.9 ; python_version >= "3.14"
10
13
  h2==4.2.0
11
14
  hpack==4.1.0
12
15
  hyperframe==6.1.0
modal/builder/PREVIEW.txt CHANGED
@@ -6,7 +6,8 @@ attrs==25.3.0
6
6
  cbor2==5.7.0
7
7
  certifi==2025.4.26
8
8
  frozenlist==1.6.0
9
- grpclib==0.4.8
9
+ grpclib==0.4.8 ; python_version < "3.14"
10
+ grpclib==0.4.9 ; python_version >= "3.14"
10
11
  h2==4.2.0
11
12
  hpack==4.1.0
12
13
  hyperframe==6.1.0
@@ -12,14 +12,16 @@
12
12
  "3.10.17",
13
13
  "3.11.12",
14
14
  "3.12.10",
15
- "3.13.3"
15
+ "3.13.3",
16
+ "3.14.2"
16
17
  ],
17
18
  "2025.06": [
18
19
  "3.9.22",
19
20
  "3.10.17",
20
21
  "3.11.12",
21
22
  "3.12.10",
22
- "3.13.3"
23
+ "3.13.3",
24
+ "3.14.2"
23
25
  ],
24
26
  "2024.10": [
25
27
  "3.9.20",
modal/cli/_download.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2023
2
2
  import asyncio
3
3
  import functools
4
+ import multiprocessing
4
5
  import os
5
6
  import shutil
6
7
  import sys
@@ -23,12 +24,22 @@ async def _volume_download(
23
24
  remote_path: str,
24
25
  local_destination: Path,
25
26
  overwrite: bool,
26
- progress_cb: Callable,
27
+ concurrency: Optional[int] = None,
28
+ progress_cb: Optional[Callable] = None,
27
29
  ):
30
+ if progress_cb is None:
31
+
32
+ def progress_cb(*_, **__):
33
+ pass
34
+
35
+ if concurrency is None:
36
+ concurrency = max(128, 2 * multiprocessing.cpu_count())
37
+
28
38
  is_pipe = local_destination == PIPE_PATH
29
39
 
30
40
  q: asyncio.Queue[tuple[Optional[Path], Optional[FileEntry]]] = asyncio.Queue()
31
- num_consumers = 1 if is_pipe else 10 # concurrency limit for downloading files
41
+ num_consumers = 1 if is_pipe else concurrency # concurrency limit for downloading files
42
+ download_semaphore = asyncio.Semaphore(concurrency)
32
43
 
33
44
  async def producer():
34
45
  iterator: AsyncIterator[FileEntry]
@@ -86,7 +97,12 @@ async def _volume_download(
86
97
 
87
98
  with output_path.open("wb") as fp:
88
99
  if isinstance(volume, _Volume):
89
- b = await volume.read_file_into_fileobj(entry.path, fp, file_progress_cb)
100
+ b = await volume._read_file_into_fileobj(
101
+ path=entry.path,
102
+ fileobj=fp,
103
+ download_semaphore=download_semaphore,
104
+ progress_cb=file_progress_cb,
105
+ )
90
106
  else:
91
107
  b = 0
92
108
  async for chunk in volume.read_file(entry.path):
modal/cli/cluster.py CHANGED
@@ -83,7 +83,9 @@ async def shell(
83
83
  )
84
84
  exec_res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
85
85
  if pty:
86
- await _ContainerProcess(exec_res.exec_id, client).attach()
86
+ await _ContainerProcess(exec_res.exec_id, task_id, client).attach()
87
87
  else:
88
88
  # TODO: redirect stderr to its own stream?
89
- await _ContainerProcess(exec_res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
89
+ await _ContainerProcess(
90
+ exec_res.exec_id, task_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
91
+ ).wait()
modal/cli/config.py CHANGED
@@ -1,4 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
+ import json
3
+
2
4
  import typer
3
5
 
4
6
  from modal._output import make_console
@@ -25,7 +27,7 @@ def show(redact: bool = typer.Option(True, help="Redact the `token_secret` value
25
27
  config_dict["token_secret"] = "***"
26
28
 
27
29
  console = make_console()
28
- console.print(config_dict)
30
+ console.print_json(json.dumps(config_dict))
29
31
 
30
32
 
31
33
  SET_DEFAULT_ENV_HELP = """Set the default Modal environment for the active profile
modal/cli/container.py CHANGED
@@ -7,7 +7,6 @@ from rich.text import Text
7
7
  from modal._object import _get_environment_name
8
8
  from modal._pty import get_pty_info
9
9
  from modal._utils.async_utils import synchronizer
10
- from modal._utils.grpc_utils import retry_transient_errors
11
10
  from modal._utils.time_utils import timestamp_to_localized_str
12
11
  from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs
13
12
  from modal.client import _Client
@@ -80,10 +79,12 @@ async def exec(
80
79
  res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
81
80
 
82
81
  if pty:
83
- await _ContainerProcess(res.exec_id, client).attach()
82
+ await _ContainerProcess(res.exec_id, container_id, client).attach()
84
83
  else:
85
84
  # TODO: redirect stderr to its own stream?
86
- await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
85
+ await _ContainerProcess(
86
+ res.exec_id, container_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
87
+ ).wait()
87
88
 
88
89
 
89
90
  @container_cli.command("stop")
@@ -95,4 +96,4 @@ async def stop(container_id: str = typer.Argument(help="Container ID")):
95
96
  """
96
97
  client = await _Client.from_env()
97
98
  request = api_pb2.ContainerStopRequest(task_id=container_id)
98
- await retry_transient_errors(client.stub.ContainerStop, request)
99
+ await client.stub.ContainerStop(request)
modal/cli/dict.py CHANGED
@@ -4,6 +4,7 @@ from typing import Optional
4
4
  import typer
5
5
  from typer import Argument, Option, Typer
6
6
 
7
+ from modal._load_context import LoadContext
7
8
  from modal._output import make_console
8
9
  from modal._resolver import Resolver
9
10
  from modal._utils.async_utils import synchronizer
@@ -29,8 +30,10 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
29
30
  """
30
31
  d = _Dict.from_name(name, environment_name=env, create_if_missing=True)
31
32
  client = await _Client.from_env()
32
- resolver = Resolver(client=client)
33
- await resolver.load(d)
33
+ resolver = Resolver()
34
+
35
+ load_context = LoadContext(client=client, environment_name=env)
36
+ await resolver.load(d, load_context)
34
37
 
35
38
 
36
39
  @dict_cli.command(name="list", rich_help_panel="Management")
modal/cli/entry_point.py CHANGED
@@ -8,7 +8,7 @@ from rich.rule import Rule
8
8
  from modal._output import make_console
9
9
  from modal._utils.async_utils import synchronizer
10
10
 
11
- from . import run
11
+ from . import run, shell as shell_module
12
12
  from .app import app_cli
13
13
  from .cluster import cluster_cli
14
14
  from .config import config_cli
@@ -36,6 +36,7 @@ entrypoint_cli_typer = typer.Typer(
36
36
  no_args_is_help=False,
37
37
  add_completion=False,
38
38
  rich_markup_mode="markdown",
39
+ context_settings={"help_option_names": ["-h", "--help"]},
39
40
  help="""
40
41
  Modal is the fastest way to run code in the cloud.
41
42
 
@@ -86,6 +87,29 @@ def check_path():
86
87
  async def setup(profile: Optional[str] = None):
87
88
  check_path()
88
89
 
90
+ art = """
91
+ ############# #############
92
+ #### ## #### ##
93
+ ## ## ## ## ## ##
94
+ ## ## ## ## ## ##
95
+ ## ## #### ## ##
96
+ ## ############# ## ##
97
+ ## ## #### ## ##
98
+ ## ## ## ## ## ##
99
+ ## ## ## ## ## ##
100
+ ## ## ## ## ## ##
101
+ ## ## ## ## ## ##
102
+ ## ## ## ## #############
103
+ ## ## ## ## ## ##
104
+ ## ## ## ## ## ##
105
+ ## ## ## ## ## ##
106
+ #### ## #### ##
107
+ ############# #############
108
+ """
109
+
110
+ console = make_console()
111
+ console.print(art, style="green")
112
+
89
113
  # Fetch a new token (same as `modal token new` but redirect to /home once finishes)
90
114
  await _new_token(profile=profile, next_url="/home")
91
115
 
@@ -93,7 +117,7 @@ async def setup(profile: Optional[str] = None):
93
117
  # Commands
94
118
  entrypoint_cli_typer.command("deploy", no_args_is_help=True)(run.deploy)
95
119
  entrypoint_cli_typer.command("serve", no_args_is_help=True)(run.serve)
96
- entrypoint_cli_typer.command("shell")(run.shell)
120
+ entrypoint_cli_typer.command("shell")(shell_module.shell)
97
121
  entrypoint_cli_typer.add_typer(launch_cli)
98
122
 
99
123
  # Deployments
modal/cli/environment.py CHANGED
@@ -3,14 +3,12 @@ from typing import Annotated, Optional, Union
3
3
 
4
4
  import typer
5
5
  from click import UsageError
6
- from grpclib import GRPCError, Status
7
6
  from rich.text import Text
8
7
 
9
8
  from modal import environments
10
9
  from modal._utils.name_utils import check_environment_name
11
10
  from modal.cli.utils import YES_OPTION, display_table
12
11
  from modal.config import config
13
- from modal.exception import InvalidError
14
12
 
15
13
  ENVIRONMENT_HELP_TEXT = """Create and interact with Environments
16
14
 
@@ -61,13 +59,7 @@ ENVIRONMENT_CREATE_HELP = """Create a new environment in the current workspace""
61
59
  @environment_cli.command(name="create", help=ENVIRONMENT_CREATE_HELP)
62
60
  def create(name: Annotated[str, typer.Argument(help="Name of the new environment. Must be unique. Case sensitive")]):
63
61
  check_environment_name(name)
64
-
65
- try:
66
- environments.create_environment(name)
67
- except GRPCError as exc:
68
- if exc.status == Status.INVALID_ARGUMENT:
69
- raise InvalidError(exc.message)
70
- raise
62
+ environments.create_environment(name)
71
63
  typer.echo(f"Environment created: {name}")
72
64
 
73
65
 
@@ -114,11 +106,5 @@ def update(
114
106
  if set_name:
115
107
  check_environment_name(set_name)
116
108
 
117
- try:
118
- environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
119
- except GRPCError as exc:
120
- if exc.status == Status.INVALID_ARGUMENT:
121
- raise InvalidError(exc.message)
122
- raise
123
-
109
+ environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
124
110
  typer.echo("Environment updated")
modal/cli/launch.py CHANGED
@@ -3,8 +3,6 @@ import asyncio
3
3
  import inspect
4
4
  import json
5
5
  import os
6
- import subprocess
7
- import tempfile
8
6
  from pathlib import Path
9
7
  from typing import Any, Optional
10
8
 
@@ -23,8 +21,7 @@ launch_cli = Typer(
23
21
  no_args_is_help=True,
24
22
  rich_markup_mode="markdown",
25
23
  help="""
26
- Open a serverless app instance on Modal.
27
- >⚠️ `modal launch` is **experimental** and may change in the future.
24
+ [Experimental] Open a serverless app instance on Modal.
28
25
  """,
29
26
  )
30
27
 
@@ -121,75 +118,3 @@ def vscode(
121
118
  "volume": volume,
122
119
  }
123
120
  _launch_program("vscode", "vscode.py", detach, args)
124
-
125
-
126
- @launch_cli.command(name="machine", help="Start an instance on Modal, with direct SSH access.", hidden=True)
127
- def machine(
128
- name: str, # Name of the machine App.
129
- cpu: int = 8, # Reservation of CPU cores (can burst above this value).
130
- memory: int = 32768, # Reservation of memory in MiB (can burst above this value).
131
- gpu: Optional[str] = None, # GPU type and count, e.g. "t4" or "h100:2".
132
- image: Optional[str] = None, # Image tag to use from registry. Defaults to the notebook base image.
133
- timeout: int = 3600 * 24, # Timeout in seconds for the instance.
134
- volume: str = "machine-vol", # Attach a persisted `modal.Volume` at /workspace (created if missing).
135
- ):
136
- tempdir = Path(tempfile.gettempdir())
137
- key_path = tempdir / "modal-machine-keyfile.pem"
138
- # Generate a new SSH key pair for this machine instance.
139
- if not key_path.exists():
140
- subprocess.run(
141
- ["ssh-keygen", "-t", "ed25519", "-f", str(key_path), "-N", ""],
142
- check=True,
143
- stdout=subprocess.DEVNULL,
144
- )
145
- # Add the key with expiry 1d to ssh agent.
146
- subprocess.run(
147
- ["ssh-add", "-t", "1d", str(key_path)],
148
- check=True,
149
- stdout=subprocess.DEVNULL,
150
- stderr=subprocess.DEVNULL,
151
- )
152
-
153
- os.environ["SSH_PUBLIC_KEY"] = Path(str(key_path) + ".pub").read_text()
154
- os.environ["MODAL_LOGS_TIMEOUT"] = "0" # hack to work with --detach
155
-
156
- args = {
157
- "cpu": cpu,
158
- "memory": memory,
159
- "gpu": gpu,
160
- "image": image,
161
- "timeout": timeout,
162
- "volume": volume,
163
- }
164
- _launch_program(
165
- "machine",
166
- "launch_instance_ssh.py",
167
- True,
168
- args,
169
- description=name,
170
- )
171
-
172
-
173
- @launch_cli.command(name="marimo", help="Start a remote Marimo notebook on Modal.", hidden=True)
174
- def marimo(
175
- cpu: int = 8,
176
- memory: int = 32768,
177
- gpu: Optional[str] = None,
178
- image: str = "debian:12",
179
- timeout: int = 3600,
180
- add_python: Optional[str] = "3.12",
181
- mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
182
- volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
183
- detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
184
- ):
185
- args = {
186
- "cpu": cpu,
187
- "memory": memory,
188
- "gpu": gpu,
189
- "timeout": timeout,
190
- "image": image,
191
- "add_python": add_python,
192
- "mount": mount,
193
- "volume": volume,
194
- }
195
- _launch_program("marimo", "run_marimo.py", detach, args)