modal 1.2.0__py3-none-any.whl → 1.2.1__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 (49) hide show
  1. modal/_container_entrypoint.py +4 -1
  2. modal/_partial_function.py +28 -3
  3. modal/_utils/function_utils.py +4 -0
  4. modal/_utils/task_command_router_client.py +537 -0
  5. modal/app.py +93 -54
  6. modal/app.pyi +48 -18
  7. modal/cli/_download.py +19 -3
  8. modal/cli/cluster.py +4 -2
  9. modal/cli/container.py +4 -2
  10. modal/cli/entry_point.py +1 -0
  11. modal/cli/launch.py +1 -2
  12. modal/cli/run.py +6 -0
  13. modal/cli/volume.py +7 -1
  14. modal/client.pyi +2 -2
  15. modal/cls.py +5 -12
  16. modal/config.py +14 -0
  17. modal/container_process.py +283 -3
  18. modal/container_process.pyi +95 -32
  19. modal/exception.py +4 -0
  20. modal/experimental/flash.py +21 -47
  21. modal/experimental/flash.pyi +6 -20
  22. modal/functions.pyi +6 -6
  23. modal/io_streams.py +455 -122
  24. modal/io_streams.pyi +220 -95
  25. modal/partial_function.pyi +4 -1
  26. modal/runner.py +39 -36
  27. modal/runner.pyi +40 -24
  28. modal/sandbox.py +130 -11
  29. modal/sandbox.pyi +145 -9
  30. modal/volume.py +23 -3
  31. modal/volume.pyi +30 -0
  32. {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/METADATA +5 -5
  33. {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/RECORD +49 -48
  34. modal_proto/api.proto +2 -26
  35. modal_proto/api_grpc.py +0 -32
  36. modal_proto/api_pb2.py +327 -367
  37. modal_proto/api_pb2.pyi +6 -69
  38. modal_proto/api_pb2_grpc.py +0 -67
  39. modal_proto/api_pb2_grpc.pyi +0 -22
  40. modal_proto/modal_api_grpc.py +0 -2
  41. modal_proto/sandbox_router.proto +0 -4
  42. modal_proto/sandbox_router_pb2.pyi +0 -4
  43. modal_proto/task_command_router.proto +1 -1
  44. modal_proto/task_command_router_pb2.py +2 -2
  45. modal_version/__init__.py +1 -1
  46. {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/WHEEL +0 -0
  47. {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/entry_points.txt +0 -0
  48. {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/licenses/LICENSE +0 -0
  49. {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/top_level.txt +0 -0
modal/app.py CHANGED
@@ -2,6 +2,7 @@
2
2
  import inspect
3
3
  import typing
4
4
  from collections.abc import AsyncGenerator, Collection, Coroutine, Mapping, Sequence
5
+ from dataclasses import dataclass
5
6
  from pathlib import PurePosixPath
6
7
  from textwrap import dedent
7
8
  from typing import (
@@ -26,13 +27,14 @@ from ._partial_function import (
26
27
  _find_partial_methods_for_user_cls,
27
28
  _PartialFunction,
28
29
  _PartialFunctionFlags,
30
+ verify_concurrent_params,
29
31
  )
30
32
  from ._utils.async_utils import synchronize_api
31
33
  from ._utils.deprecation import (
32
34
  deprecation_warning,
33
35
  warn_on_renamed_autoscaler_settings,
34
36
  )
35
- from ._utils.function_utils import FunctionInfo, is_global_object, is_method_fn
37
+ from ._utils.function_utils import FunctionInfo, is_flash_object, is_global_object, is_method_fn
36
38
  from ._utils.grpc_utils import retry_transient_errors
37
39
  from ._utils.mount_utils import validate_volumes
38
40
  from ._utils.name_utils import check_object_name, check_tag_dict
@@ -114,6 +116,22 @@ class _FunctionDecoratorType:
114
116
  def __call__(self, func): ...
115
117
 
116
118
 
119
+ @dataclass()
120
+ class _LocalAppState:
121
+ """All state for apps that's part of the local/definition state"""
122
+
123
+ functions: dict[str, _Function]
124
+ classes: dict[str, _Cls]
125
+ image_default: Optional[_Image]
126
+ web_endpoints: list[str] # Used by the CLI
127
+ local_entrypoints: dict[str, _LocalEntrypoint]
128
+ tags: dict[str, str]
129
+
130
+ include_source_default: bool
131
+ secrets_default: Sequence[_Secret]
132
+ volumes_default: dict[Union[str, PurePosixPath], _Volume]
133
+
134
+
117
135
  class _App:
118
136
  """A Modal App is a group of functions and classes that are deployed together.
119
137
 
@@ -151,23 +169,21 @@ class _App:
151
169
 
152
170
  _name: Optional[str]
153
171
  _description: Optional[str]
154
- _tags: dict[str, str]
155
-
156
- _functions: dict[str, _Function]
157
- _classes: dict[str, _Cls]
158
172
 
159
- _image: Optional[_Image]
160
- _secrets: Sequence[_Secret]
161
- _volumes: dict[Union[str, PurePosixPath], _Volume]
162
- _web_endpoints: list[str] # Used by the CLI
163
- _local_entrypoints: dict[str, _LocalEntrypoint]
173
+ _local_state_attr: Optional[_LocalAppState] = None
164
174
 
165
175
  # Running apps only (container apps or running local)
166
176
  _app_id: Optional[str] # Kept after app finishes
167
177
  _running_app: Optional[RunningApp] # Various app info
168
178
  _client: Optional[_Client]
169
179
 
170
- _include_source_default: Optional[bool] = None
180
+ @property
181
+ def _local_state(self) -> _LocalAppState:
182
+ """For internal use only. Do not use this property directly."""
183
+
184
+ if self._local_state_attr is None:
185
+ raise AttributeError("Local state is not initialized - app is not locally available")
186
+ return self._local_state_attr
171
187
 
172
188
  def __init__(
173
189
  self,
@@ -196,8 +212,6 @@ class _App:
196
212
 
197
213
  self._name = name
198
214
  self._description = name
199
- self._tags = tags or {}
200
- self._include_source_default = include_source
201
215
 
202
216
  check_sequence(secrets, _Secret, "`secrets=` has to be a list or tuple of `modal.Secret` objects")
203
217
  validate_volumes(volumes)
@@ -205,16 +219,24 @@ class _App:
205
219
  if image is not None and not isinstance(image, _Image):
206
220
  raise InvalidError("`image=` has to be a `modal.Image` object")
207
221
 
208
- self._functions = {}
209
- self._classes = {}
210
- self._image = image
211
- self._secrets = secrets
212
- self._volumes = volumes
213
- self._local_entrypoints = {}
214
- self._web_endpoints = []
222
+ self._local_state_attr = _LocalAppState(
223
+ functions={},
224
+ classes={},
225
+ image_default=image,
226
+ secrets_default=secrets,
227
+ volumes_default=volumes,
228
+ include_source_default=include_source,
229
+ web_endpoints=[],
230
+ local_entrypoints={},
231
+ tags=tags or {},
232
+ )
215
233
 
234
+ # Running apps only
216
235
  self._app_id = None
217
236
  self._running_app = None # Set inside container, OR during the time an app is running locally
237
+
238
+ # Client is special - needed to be set just before the app is "hydrated" or running at the latest
239
+ # Guaranteed to be set for running apps, but also needed to actually *hydrate* the app and make it running
218
240
  self._client = None
219
241
 
220
242
  # Register this app. This is used to look up the app in the container, when we can't get it from the function
@@ -283,7 +305,8 @@ class _App:
283
305
 
284
306
  response = await retry_transient_errors(client.stub.AppGetOrCreate, request)
285
307
 
286
- app = _App(name)
308
+ app = _App(name) # TODO: this should probably be a distinct constructor, possibly even a distinct type
309
+ app._local_state_attr = None # this is not a locally defined App, so no local state
287
310
  app._app_id = response.app_id
288
311
  app._client = client
289
312
  app._running_app = RunningApp(response.app_id, interactive=False)
@@ -310,18 +333,19 @@ class _App:
310
333
  App that is retrieved via `modal.App.lookup`. It is likely to be deprecated in the future.
311
334
 
312
335
  """
313
- return self._image
336
+ return self._local_state.image_default
314
337
 
315
338
  @image.setter
316
339
  def image(self, value):
317
340
  """mdmd:hidden"""
318
- self._image = value
341
+ self._local_state.image_default = value
319
342
 
320
343
  def _uncreate_all_objects(self):
321
344
  # TODO(erikbern): this doesn't unhydrate objects that aren't tagged
322
- for obj in self._functions.values():
345
+ local_state = self._local_state
346
+ for obj in local_state.functions.values():
323
347
  obj._unhydrate()
324
- for obj in self._classes.values():
348
+ for obj in local_state.classes.values():
325
349
  obj._unhydrate()
326
350
 
327
351
  @asynccontextmanager
@@ -457,8 +481,9 @@ class _App:
457
481
  return self
458
482
 
459
483
  def _get_default_image(self):
460
- if self._image:
461
- return self._image
484
+ local_state = self._local_state
485
+ if local_state.image_default:
486
+ return local_state.image_default
462
487
  else:
463
488
  return _default_image
464
489
 
@@ -473,7 +498,8 @@ class _App:
473
498
  return [m for m in all_mounts if m.is_local()]
474
499
 
475
500
  def _add_function(self, function: _Function, is_web_endpoint: bool):
476
- if old_function := self._functions.get(function.tag, None):
501
+ local_state = self._local_state
502
+ if old_function := local_state.functions.get(function.tag, None):
477
503
  if old_function is function:
478
504
  return # already added the same exact instance, ignore
479
505
 
@@ -484,7 +510,7 @@ class _App:
484
510
  f"[{old_function._info.module_name}].{old_function._info.function_name}"
485
511
  f" with new function [{function._info.module_name}].{function._info.function_name}"
486
512
  )
487
- if function.tag in self._classes:
513
+ if function.tag in local_state.classes:
488
514
  logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
489
515
 
490
516
  if self._running_app:
@@ -495,9 +521,9 @@ class _App:
495
521
  metadata: Message = self._running_app.object_handle_metadata[object_id]
496
522
  function._hydrate(object_id, self._client, metadata)
497
523
 
498
- self._functions[function.tag] = function
524
+ local_state.functions[function.tag] = function
499
525
  if is_web_endpoint:
500
- self._web_endpoints.append(function.tag)
526
+ local_state.web_endpoints.append(function.tag)
501
527
 
502
528
  def _add_class(self, tag: str, cls: _Cls):
503
529
  if self._running_app:
@@ -508,7 +534,7 @@ class _App:
508
534
  metadata: Message = self._running_app.object_handle_metadata[object_id]
509
535
  cls._hydrate(object_id, self._client, metadata)
510
536
 
511
- self._classes[tag] = cls
537
+ self._local_state.classes[tag] = cls
512
538
 
513
539
  def _init_container(self, client: _Client, running_app: RunningApp):
514
540
  self._app_id = running_app.app_id
@@ -516,18 +542,18 @@ class _App:
516
542
  self._client = client
517
543
 
518
544
  _App._container_app = self
519
-
545
+ local_state = self._local_state
520
546
  # Hydrate function objects
521
547
  for tag, object_id in running_app.function_ids.items():
522
- if tag in self._functions:
523
- obj = self._functions[tag]
548
+ if tag in local_state.functions:
549
+ obj = local_state.functions[tag]
524
550
  handle_metadata = running_app.object_handle_metadata[object_id]
525
551
  obj._hydrate(object_id, client, handle_metadata)
526
552
 
527
553
  # Hydrate class objects
528
554
  for tag, object_id in running_app.class_ids.items():
529
- if tag in self._classes:
530
- obj = self._classes[tag]
555
+ if tag in local_state.classes:
556
+ obj = local_state.classes[tag]
531
557
  handle_metadata = running_app.object_handle_metadata[object_id]
532
558
  obj._hydrate(object_id, client, handle_metadata)
533
559
 
@@ -541,7 +567,7 @@ class _App:
541
567
  This method is likely to be deprecated in the future in favor of a different
542
568
  approach for retrieving the layout of a deployed App.
543
569
  """
544
- return self._functions
570
+ return self._local_state.functions
545
571
 
546
572
  @property
547
573
  def registered_classes(self) -> dict[str, _Cls]:
@@ -553,7 +579,7 @@ class _App:
553
579
  This method is likely to be deprecated in the future in favor of a different
554
580
  approach for retrieving the layout of a deployed App.
555
581
  """
556
- return self._classes
582
+ return self._local_state.classes
557
583
 
558
584
  @property
559
585
  def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
@@ -564,7 +590,7 @@ class _App:
564
590
  expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
565
591
  This method is likely to be deprecated in the future.
566
592
  """
567
- return self._local_entrypoints
593
+ return self._local_state.local_entrypoints
568
594
 
569
595
  @property
570
596
  def registered_web_endpoints(self) -> list[str]:
@@ -576,7 +602,7 @@ class _App:
576
602
  This method is likely to be deprecated in the future in favor of a different
577
603
  approach for retrieving the layout of a deployed App.
578
604
  """
579
- return self._web_endpoints
605
+ return self._local_state.web_endpoints
580
606
 
581
607
  def local_entrypoint(
582
608
  self, _warn_parentheses_missing: Any = None, *, name: Optional[str] = None
@@ -637,10 +663,11 @@ class _App:
637
663
  def wrapped(raw_f: Callable[..., Any]) -> _LocalEntrypoint:
638
664
  info = FunctionInfo(raw_f)
639
665
  tag = name if name is not None else raw_f.__qualname__
640
- if tag in self._local_entrypoints:
666
+ local_state = self._local_state
667
+ if tag in local_state.local_entrypoints:
641
668
  # TODO: get rid of this limitation.
642
669
  raise InvalidError(f"Duplicate local entrypoint name: {tag}. Local entrypoint names must be unique.")
643
- entrypoint = self._local_entrypoints[tag] = _LocalEntrypoint(info, self)
670
+ entrypoint = local_state.local_entrypoints[tag] = _LocalEntrypoint(info, self)
644
671
  return entrypoint
645
672
 
646
673
  return wrapped
@@ -732,7 +759,8 @@ class _App:
732
759
  secrets = secrets or []
733
760
  if env:
734
761
  secrets = [*secrets, _Secret.from_dict(env)]
735
- secrets = [*self._secrets, *secrets]
762
+ local_state = self._local_state
763
+ secrets = [*local_state.secrets_default, *secrets]
736
764
 
737
765
  def wrapped(
738
766
  f: Union[_PartialFunction, Callable[..., Any], None],
@@ -775,6 +803,7 @@ class _App:
775
803
  batch_max_size = f.params.batch_max_size
776
804
  batch_wait_ms = f.params.batch_wait_ms
777
805
  if f.flags & _PartialFunctionFlags.CONCURRENT:
806
+ verify_concurrent_params(params=f.params, is_flash=is_flash_object(experimental_options))
778
807
  max_concurrent_inputs = f.params.max_concurrent_inputs
779
808
  target_concurrent_inputs = f.params.target_concurrent_inputs
780
809
  else:
@@ -840,7 +869,7 @@ class _App:
840
869
  is_generator=is_generator,
841
870
  gpu=gpu,
842
871
  network_file_systems=network_file_systems,
843
- volumes={**self._volumes, **volumes},
872
+ volumes={**local_state.volumes_default, **volumes},
844
873
  cpu=cpu,
845
874
  memory=memory,
846
875
  ephemeral_disk=ephemeral_disk,
@@ -866,7 +895,7 @@ class _App:
866
895
  i6pn_enabled=i6pn_enabled,
867
896
  cluster_size=cluster_size, # Experimental: Clustered functions
868
897
  rdma=rdma,
869
- include_source=include_source if include_source is not None else self._include_source_default,
898
+ include_source=include_source if include_source is not None else local_state.include_source_default,
870
899
  experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
871
900
  _experimental_proxy_ip=_experimental_proxy_ip,
872
901
  restrict_output=_experimental_restrict_output,
@@ -963,11 +992,13 @@ class _App:
963
992
  secrets = [*secrets, _Secret.from_dict(env)]
964
993
 
965
994
  def wrapper(wrapped_cls: Union[CLS_T, _PartialFunction]) -> CLS_T:
995
+ local_state = self._local_state
966
996
  # Check if the decorated object is a class
967
997
  if isinstance(wrapped_cls, _PartialFunction):
968
998
  wrapped_cls.registered = True
969
999
  user_cls = wrapped_cls.user_cls
970
1000
  if wrapped_cls.flags & _PartialFunctionFlags.CONCURRENT:
1001
+ verify_concurrent_params(params=wrapped_cls.params, is_flash=is_flash_object(experimental_options))
971
1002
  max_concurrent_inputs = wrapped_cls.params.max_concurrent_inputs
972
1003
  target_concurrent_inputs = wrapped_cls.params.target_concurrent_inputs
973
1004
  else:
@@ -1029,10 +1060,10 @@ class _App:
1029
1060
  info,
1030
1061
  app=self,
1031
1062
  image=image or self._get_default_image(),
1032
- secrets=[*self._secrets, *secrets],
1063
+ secrets=[*local_state.secrets_default, *secrets],
1033
1064
  gpu=gpu,
1034
1065
  network_file_systems=network_file_systems,
1035
- volumes={**self._volumes, **volumes},
1066
+ volumes={**local_state.volumes_default, **volumes},
1036
1067
  cpu=cpu,
1037
1068
  memory=memory,
1038
1069
  ephemeral_disk=ephemeral_disk,
@@ -1057,7 +1088,7 @@ class _App:
1057
1088
  i6pn_enabled=i6pn_enabled,
1058
1089
  cluster_size=cluster_size,
1059
1090
  rdma=rdma,
1060
- include_source=include_source if include_source is not None else self._include_source_default,
1091
+ include_source=include_source if include_source is not None else local_state.include_source_default,
1061
1092
  experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
1062
1093
  _experimental_proxy_ip=_experimental_proxy_ip,
1063
1094
  _experimental_custom_scaling_factor=_experimental_custom_scaling_factor,
@@ -1067,6 +1098,11 @@ class _App:
1067
1098
  self._add_function(cls_func, is_web_endpoint=False)
1068
1099
 
1069
1100
  cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
1101
+ for method_name, partial_function in cls._method_partials.items():
1102
+ if partial_function.params.webhook_config is not None:
1103
+ full_name = f"{user_cls.__name__}.{method_name}"
1104
+ local_state.web_endpoints.append(full_name)
1105
+ partial_function.registered = True
1070
1106
 
1071
1107
  tag: str = user_cls.__name__
1072
1108
  self._add_class(tag, cls)
@@ -1102,11 +1138,14 @@ class _App:
1102
1138
  (with this App's tags taking precedence in the case of conflicts).
1103
1139
 
1104
1140
  """
1105
- for tag, function in other_app._functions.items():
1141
+ other_app_local_state = other_app._local_state
1142
+ this_local_state = self._local_state
1143
+
1144
+ for tag, function in other_app_local_state.functions.items():
1106
1145
  self._add_function(function, False) # TODO(erikbern): webhook config?
1107
1146
 
1108
- for tag, cls in other_app._classes.items():
1109
- existing_cls = self._classes.get(tag)
1147
+ for tag, cls in other_app_local_state.classes.items():
1148
+ existing_cls = this_local_state.classes.get(tag)
1110
1149
  if existing_cls and existing_cls != cls:
1111
1150
  logger.warning(
1112
1151
  f"Named app class {tag} with existing value {existing_cls} is being "
@@ -1116,7 +1155,7 @@ class _App:
1116
1155
  self._add_class(tag, cls)
1117
1156
 
1118
1157
  if inherit_tags:
1119
- self._tags = {**other_app._tags, **self._tags}
1158
+ this_local_state.tags = {**other_app_local_state.tags, **this_local_state.tags}
1120
1159
 
1121
1160
  return self
1122
1161
 
@@ -1132,7 +1171,7 @@ class _App:
1132
1171
 
1133
1172
  """
1134
1173
  # Note that we are requiring the App to be "running" before we set the tags.
1135
- # Alternatively, we could hold onto the tags (i.e. in `self._tags`) and then pass
1174
+ # Alternatively, we could hold onto the tags (i.e. in `self._local_state.tags`) and then pass
1136
1175
  # then up when AppPublish gets called. I'm not certain we want to support it, though.
1137
1176
  # It might not be obvious to users that `.set_tags()` is eager and has immediate effect
1138
1177
  # when the App is running, but lazy (and potentially ignored) otherwise. There would be
modal/app.pyi CHANGED
@@ -74,6 +74,42 @@ class _FunctionDecoratorType:
74
74
  self, func: collections.abc.Callable[P, ReturnType]
75
75
  ) -> modal.functions.Function[P, ReturnType, ReturnType]: ...
76
76
 
77
+ class _LocalAppState:
78
+ """All state for apps that's part of the local/definition state"""
79
+
80
+ functions: dict[str, modal._functions._Function]
81
+ classes: dict[str, modal.cls._Cls]
82
+ image_default: typing.Optional[modal.image._Image]
83
+ web_endpoints: list[str]
84
+ local_entrypoints: dict[str, _LocalEntrypoint]
85
+ tags: dict[str, str]
86
+ include_source_default: bool
87
+ secrets_default: collections.abc.Sequence[modal.secret._Secret]
88
+ volumes_default: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume]
89
+
90
+ def __init__(
91
+ self,
92
+ functions: dict[str, modal._functions._Function],
93
+ classes: dict[str, modal.cls._Cls],
94
+ image_default: typing.Optional[modal.image._Image],
95
+ web_endpoints: list[str],
96
+ local_entrypoints: dict[str, _LocalEntrypoint],
97
+ tags: dict[str, str],
98
+ include_source_default: bool,
99
+ secrets_default: collections.abc.Sequence[modal.secret._Secret],
100
+ volumes_default: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume],
101
+ ) -> None:
102
+ """Initialize self. See help(type(self)) for accurate signature."""
103
+ ...
104
+
105
+ def __repr__(self):
106
+ """Return repr(self)."""
107
+ ...
108
+
109
+ def __eq__(self, other):
110
+ """Return self==value."""
111
+ ...
112
+
77
113
  class _App:
78
114
  """A Modal App is a group of functions and classes that are deployed together.
79
115
 
@@ -110,18 +146,15 @@ class _App:
110
146
  _container_app: typing.ClassVar[typing.Optional[_App]]
111
147
  _name: typing.Optional[str]
112
148
  _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]
149
+ _local_state_attr: typing.Optional[_LocalAppState]
121
150
  _app_id: typing.Optional[str]
122
151
  _running_app: typing.Optional[modal.running_app.RunningApp]
123
152
  _client: typing.Optional[modal.client._Client]
124
- _include_source_default: typing.Optional[bool]
153
+
154
+ @property
155
+ def _local_state(self) -> _LocalAppState:
156
+ """For internal use only. Do not use this property directly."""
157
+ ...
125
158
 
126
159
  def __init__(
127
160
  self,
@@ -630,18 +663,10 @@ class App:
630
663
  _container_app: typing.ClassVar[typing.Optional[App]]
631
664
  _name: typing.Optional[str]
632
665
  _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]
666
+ _local_state_attr: typing.Optional[_LocalAppState]
641
667
  _app_id: typing.Optional[str]
642
668
  _running_app: typing.Optional[modal.running_app.RunningApp]
643
669
  _client: typing.Optional[modal.client.Client]
644
- _include_source_default: typing.Optional[bool]
645
670
 
646
671
  def __init__(
647
672
  self,
@@ -664,6 +689,11 @@ class App:
664
689
  """
665
690
  ...
666
691
 
692
+ @property
693
+ def _local_state(self) -> _LocalAppState:
694
+ """For internal use only. Do not use this property directly."""
695
+ ...
696
+
667
697
  @property
668
698
  def name(self) -> typing.Optional[str]:
669
699
  """The user-provided name of the App."""
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/container.py CHANGED
@@ -80,10 +80,12 @@ async def exec(
80
80
  res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
81
81
 
82
82
  if pty:
83
- await _ContainerProcess(res.exec_id, client).attach()
83
+ await _ContainerProcess(res.exec_id, container_id, client).attach()
84
84
  else:
85
85
  # TODO: redirect stderr to its own stream?
86
- await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
86
+ await _ContainerProcess(
87
+ res.exec_id, container_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
88
+ ).wait()
87
89
 
88
90
 
89
91
  @container_cli.command("stop")
modal/cli/entry_point.py CHANGED
@@ -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
 
modal/cli/launch.py CHANGED
@@ -23,8 +23,7 @@ launch_cli = Typer(
23
23
  no_args_is_help=True,
24
24
  rich_markup_mode="markdown",
25
25
  help="""
26
- Open a serverless app instance on Modal.
27
- >⚠️ `modal launch` is **experimental** and may change in the future.
26
+ [Experimental] Open a serverless app instance on Modal.
28
27
  """,
29
28
  )
30
29
 
modal/cli/run.py CHANGED
@@ -496,6 +496,12 @@ def serve(
496
496
  ```
497
497
  modal serve hello_world.py
498
498
  ```
499
+
500
+ Modal-generated URLs will have a `-dev` suffix appended to them when running with `modal serve`.
501
+ To customize this suffix (i.e., to avoid collisions with other users in your workspace who are
502
+ concurrently serving the App), you can set the `dev_suffix` in your `.modal.toml` file or the
503
+ `MODAL_DEV_SUFFIX` environment variable.
504
+
499
505
  """
500
506
  env = ensure_env(env)
501
507
  import_ref = parse_import_ref(app_ref, use_module_mode=use_module_mode)
modal/cli/volume.py CHANGED
@@ -96,7 +96,13 @@ async def get(
96
96
  console = make_console()
97
97
  progress_handler = ProgressHandler(type="download", console=console)
98
98
  with progress_handler.live:
99
- await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
99
+ await _volume_download(
100
+ volume=volume,
101
+ remote_path=remote_path,
102
+ local_destination=destination,
103
+ overwrite=force,
104
+ progress_cb=progress_handler.progress,
105
+ )
100
106
  console.print(OutputManager.step_completed("Finished downloading files to local!"))
101
107
 
102
108
 
modal/client.pyi CHANGED
@@ -29,7 +29,7 @@ class _Client:
29
29
  _snapshotted: bool
30
30
 
31
31
  def __init__(
32
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.2.0"
32
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.2.1"
33
33
  ):
34
34
  """mdmd:hidden
35
35
  The Modal client object is not intended to be instantiated directly by users.
@@ -156,7 +156,7 @@ class Client:
156
156
  _snapshotted: bool
157
157
 
158
158
  def __init__(
159
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.2.0"
159
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.2.1"
160
160
  ):
161
161
  """mdmd:hidden
162
162
  The Modal client object is not intended to be instantiated directly by users.
modal/cls.py CHANGED
@@ -577,22 +577,15 @@ More information on class parameterization can be found here: https://modal.com/
577
577
  # validate signature
578
578
  _Cls.validate_construction_mechanism(user_cls)
579
579
 
580
- method_partials: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
581
- user_cls, _PartialFunctionFlags.interface_flags()
582
- )
583
-
584
- for method_name, partial_function in method_partials.items():
585
- if partial_function.params.webhook_config is not None:
586
- full_name = f"{user_cls.__name__}.{method_name}"
587
- app._web_endpoints.append(full_name)
588
- partial_function.registered = True
589
-
590
580
  # Disable the warning that lifecycle methods are not wrapped
591
- for partial_function in _find_partial_methods_for_user_cls(
581
+ lifecycle_method_partials = _find_partial_methods_for_user_cls(
592
582
  user_cls, ~_PartialFunctionFlags.interface_flags()
593
- ).values():
583
+ )
584
+ for partial_function in lifecycle_method_partials.values():
594
585
  partial_function.registered = True
595
586
 
587
+ method_partials = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.interface_flags())
588
+
596
589
  # Get all callables
597
590
  callables: dict[str, Callable] = {
598
591
  k: pf.raw_f