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.
- modal/_container_entrypoint.py +4 -1
- modal/_partial_function.py +28 -3
- modal/_utils/function_utils.py +4 -0
- modal/_utils/task_command_router_client.py +537 -0
- modal/app.py +93 -54
- modal/app.pyi +48 -18
- modal/cli/_download.py +19 -3
- modal/cli/cluster.py +4 -2
- modal/cli/container.py +4 -2
- modal/cli/entry_point.py +1 -0
- modal/cli/launch.py +1 -2
- modal/cli/run.py +6 -0
- modal/cli/volume.py +7 -1
- modal/client.pyi +2 -2
- modal/cls.py +5 -12
- modal/config.py +14 -0
- modal/container_process.py +283 -3
- modal/container_process.pyi +95 -32
- modal/exception.py +4 -0
- modal/experimental/flash.py +21 -47
- modal/experimental/flash.pyi +6 -20
- modal/functions.pyi +6 -6
- modal/io_streams.py +455 -122
- modal/io_streams.pyi +220 -95
- modal/partial_function.pyi +4 -1
- modal/runner.py +39 -36
- modal/runner.pyi +40 -24
- modal/sandbox.py +130 -11
- modal/sandbox.pyi +145 -9
- modal/volume.py +23 -3
- modal/volume.pyi +30 -0
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/METADATA +5 -5
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/RECORD +49 -48
- modal_proto/api.proto +2 -26
- modal_proto/api_grpc.py +0 -32
- modal_proto/api_pb2.py +327 -367
- modal_proto/api_pb2.pyi +6 -69
- modal_proto/api_pb2_grpc.py +0 -67
- modal_proto/api_pb2_grpc.pyi +0 -22
- modal_proto/modal_api_grpc.py +0 -2
- modal_proto/sandbox_router.proto +0 -4
- modal_proto/sandbox_router_pb2.pyi +0 -4
- modal_proto/task_command_router.proto +1 -1
- modal_proto/task_command_router_pb2.py +2 -2
- modal_version/__init__.py +1 -1
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/WHEEL +0 -0
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/entry_points.txt +0 -0
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
345
|
+
local_state = self._local_state
|
|
346
|
+
for obj in local_state.functions.values():
|
|
323
347
|
obj._unhydrate()
|
|
324
|
-
for obj in
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
524
|
+
local_state.functions[function.tag] = function
|
|
499
525
|
if is_web_endpoint:
|
|
500
|
-
|
|
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.
|
|
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
|
|
523
|
-
obj =
|
|
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
|
|
530
|
-
obj =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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={**
|
|
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
|
|
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=[*
|
|
1063
|
+
secrets=[*local_state.secrets_default, *secrets],
|
|
1033
1064
|
gpu=gpu,
|
|
1034
1065
|
network_file_systems=network_file_systems,
|
|
1035
|
-
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
|
|
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
|
-
|
|
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
|
|
1109
|
-
existing_cls =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
581
|
+
lifecycle_method_partials = _find_partial_methods_for_user_cls(
|
|
592
582
|
user_cls, ~_PartialFunctionFlags.interface_flags()
|
|
593
|
-
)
|
|
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
|