modal 1.1.5.dev66__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.
- modal/__init__.py +4 -4
- modal/__main__.py +4 -29
- modal/_billing.py +84 -0
- modal/_clustered_functions.py +1 -3
- modal/_container_entrypoint.py +33 -208
- modal/_functions.py +171 -138
- modal/_grpc_client.py +191 -0
- modal/_ipython.py +16 -6
- modal/_load_context.py +106 -0
- modal/_object.py +72 -21
- modal/_output.py +12 -14
- modal/_partial_function.py +31 -4
- modal/_resolver.py +44 -57
- modal/_runtime/container_io_manager.py +30 -28
- modal/_runtime/container_io_manager.pyi +42 -44
- modal/_runtime/gpu_memory_snapshot.py +9 -7
- modal/_runtime/user_code_event_loop.py +80 -0
- modal/_runtime/user_code_imports.py +236 -10
- modal/_serialization.py +2 -1
- modal/_traceback.py +4 -13
- modal/_tunnel.py +16 -11
- modal/_tunnel.pyi +25 -3
- modal/_utils/async_utils.py +337 -10
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +29 -22
- modal/_utils/function_utils.py +20 -21
- modal/_utils/grpc_testing.py +6 -3
- modal/_utils/grpc_utils.py +223 -64
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +2 -3
- modal/_utils/package_utils.py +0 -1
- modal/_utils/rand_pb_testing.py +8 -1
- modal/_utils/task_command_router_client.py +524 -0
- modal/_vendor/cloudpickle.py +144 -48
- modal/app.py +285 -105
- modal/app.pyi +216 -53
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +6 -3
- modal/builder/PREVIEW.txt +2 -1
- modal/builder/base-images.json +4 -2
- modal/cli/_download.py +19 -3
- modal/cli/cluster.py +4 -2
- modal/cli/config.py +3 -1
- modal/cli/container.py +5 -4
- modal/cli/dict.py +5 -2
- modal/cli/entry_point.py +26 -2
- modal/cli/environment.py +2 -16
- modal/cli/launch.py +1 -76
- modal/cli/network_file_system.py +5 -20
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +5 -4
- modal/cli/run.py +24 -204
- modal/cli/secret.py +1 -2
- modal/cli/shell.py +375 -0
- modal/cli/utils.py +1 -13
- modal/cli/volume.py +11 -17
- modal/client.py +16 -125
- modal/client.pyi +94 -144
- modal/cloud_bucket_mount.py +3 -1
- modal/cloud_bucket_mount.pyi +4 -0
- modal/cls.py +101 -64
- modal/cls.pyi +9 -8
- modal/config.py +21 -1
- modal/container_process.py +288 -12
- modal/container_process.pyi +99 -38
- modal/dict.py +72 -33
- modal/dict.pyi +88 -57
- modal/environments.py +16 -8
- modal/environments.pyi +6 -2
- modal/exception.py +154 -16
- modal/experimental/__init__.py +24 -53
- modal/experimental/flash.py +161 -74
- modal/experimental/flash.pyi +97 -49
- modal/file_io.py +50 -92
- modal/file_io.pyi +117 -89
- modal/functions.pyi +70 -87
- modal/image.py +82 -47
- modal/image.pyi +51 -30
- modal/io_streams.py +500 -149
- modal/io_streams.pyi +279 -189
- modal/mount.py +60 -46
- modal/mount.pyi +41 -17
- modal/network_file_system.py +19 -11
- modal/network_file_system.pyi +72 -39
- modal/object.pyi +114 -22
- modal/parallel_map.py +42 -44
- modal/parallel_map.pyi +9 -17
- modal/partial_function.pyi +4 -2
- modal/proxy.py +14 -6
- modal/proxy.pyi +10 -2
- modal/queue.py +45 -38
- modal/queue.pyi +88 -52
- modal/runner.py +96 -96
- modal/runner.pyi +44 -27
- modal/sandbox.py +225 -107
- modal/sandbox.pyi +226 -60
- modal/secret.py +58 -56
- modal/secret.pyi +28 -13
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +29 -15
- modal/snapshot.pyi +18 -10
- modal/token_flow.py +1 -1
- modal/token_flow.pyi +4 -6
- modal/volume.py +102 -55
- modal/volume.pyi +125 -66
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
- modal-1.3.1.dev8.dist-info/RECORD +189 -0
- modal_proto/api.proto +141 -70
- modal_proto/api_grpc.py +42 -26
- modal_proto/api_pb2.py +1123 -1103
- modal_proto/api_pb2.pyi +331 -83
- modal_proto/api_pb2_grpc.py +80 -48
- modal_proto/api_pb2_grpc.pyi +26 -18
- modal_proto/modal_api_grpc.py +175 -174
- modal_proto/task_command_router.proto +164 -0
- modal_proto/task_command_router_grpc.py +138 -0
- modal_proto/task_command_router_pb2.py +180 -0
- modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
- modal_proto/task_command_router_pb2_grpc.py +272 -0
- modal_proto/task_command_router_pb2_grpc.pyi +100 -0
- modal_version/__init__.py +1 -1
- modal_version/__main__.py +1 -1
- modal/cli/programs/launch_instance_ssh.py +0 -94
- modal/cli/programs/run_marimo.py +0 -95
- modal-1.1.5.dev66.dist-info/RECORD +0 -191
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- modal_proto/sandbox_router.proto +0 -125
- modal_proto/sandbox_router_grpc.py +0 -89
- modal_proto/sandbox_router_pb2.py +0 -128
- modal_proto/sandbox_router_pb2_grpc.py +0 -169
- modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/cls.py
CHANGED
|
@@ -7,11 +7,11 @@ from pathlib import PurePosixPath
|
|
|
7
7
|
from typing import Any, Callable, Optional, Sequence, TypeVar, Union
|
|
8
8
|
|
|
9
9
|
from google.protobuf.message import Message
|
|
10
|
-
from grpclib import GRPCError, Status
|
|
11
10
|
|
|
12
11
|
from modal_proto import api_pb2
|
|
13
12
|
|
|
14
13
|
from ._functions import _Function, _parse_retries
|
|
14
|
+
from ._load_context import LoadContext
|
|
15
15
|
from ._object import _Object, live_method
|
|
16
16
|
from ._partial_function import (
|
|
17
17
|
_find_callables_for_obj,
|
|
@@ -30,14 +30,12 @@ from ._utils.deprecation import (
|
|
|
30
30
|
warn_if_passing_namespace,
|
|
31
31
|
warn_on_renamed_autoscaler_settings,
|
|
32
32
|
)
|
|
33
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
34
33
|
from ._utils.mount_utils import validate_volumes
|
|
34
|
+
from .client import _Client
|
|
35
35
|
from .cloud_bucket_mount import _CloudBucketMount
|
|
36
|
-
from .config import config
|
|
37
36
|
from .exception import ExecutionError, InvalidError, NotFoundError
|
|
38
37
|
from .gpu import GPU_T
|
|
39
38
|
from .retries import Retries
|
|
40
|
-
from .scheduler_placement import SchedulerPlacement
|
|
41
39
|
from .secret import _Secret
|
|
42
40
|
from .volume import _Volume
|
|
43
41
|
|
|
@@ -135,7 +133,7 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
|
|
|
135
133
|
method_metadata = cls._method_metadata[method_name]
|
|
136
134
|
new_function._hydrate(service_function.object_id, service_function.client, method_metadata)
|
|
137
135
|
|
|
138
|
-
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
|
136
|
+
async def _load(fun: "_Function", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
139
137
|
# there is currently no actual loading logic executed to create each method on
|
|
140
138
|
# the *parametrized* instance of a class - it uses the parameter-bound service-function
|
|
141
139
|
# for the instance. This load method just makes sure to set all attributes after the
|
|
@@ -154,11 +152,14 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
|
|
|
154
152
|
|
|
155
153
|
rep = f"Method({cls._name}.{method_name})"
|
|
156
154
|
|
|
155
|
+
# Bound methods should *reference* their parent Cls's LoadContext
|
|
156
|
+
# so that it can be modified in place on the parent and be reflected in the method
|
|
157
157
|
fun = _Function._from_loader(
|
|
158
158
|
_load,
|
|
159
159
|
rep,
|
|
160
160
|
deps=_deps,
|
|
161
161
|
hydrate_lazily=True,
|
|
162
|
+
load_context_overrides=cls._load_context_overrides,
|
|
162
163
|
)
|
|
163
164
|
if service_function.is_hydrated:
|
|
164
165
|
# Eager hydration (skip load) if the instance service function is already loaded
|
|
@@ -422,14 +423,13 @@ class _Obj:
|
|
|
422
423
|
|
|
423
424
|
# Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
|
|
424
425
|
# has not yet been loaded. So use a special loader that loads it lazily:
|
|
425
|
-
async def method_loader(fun, resolver: Resolver, existing_object_id):
|
|
426
|
-
await resolver.load(self._cls) # load class so we get info about methods
|
|
426
|
+
async def method_loader(fun, resolver: Resolver, load_context: LoadContext, existing_object_id):
|
|
427
427
|
method_function = _get_maybe_method()
|
|
428
428
|
if method_function is None:
|
|
429
429
|
raise NotFoundError(
|
|
430
430
|
f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
|
|
431
431
|
)
|
|
432
|
-
await resolver.load(method_function) # get the appropriate method handle (lazy)
|
|
432
|
+
await resolver.load(method_function, load_context) # get the appropriate method handle (lazy)
|
|
433
433
|
fun._hydrate_from_other(method_function)
|
|
434
434
|
|
|
435
435
|
# The reason we don't *always* use this lazy loader is because it precludes attribute access
|
|
@@ -437,8 +437,9 @@ class _Obj:
|
|
|
437
437
|
return _Function._from_loader(
|
|
438
438
|
method_loader,
|
|
439
439
|
rep=f"Method({self._cls._name}.{k})",
|
|
440
|
-
deps=lambda: [],
|
|
440
|
+
deps=lambda: [self._cls],
|
|
441
441
|
hydrate_lazily=True,
|
|
442
|
+
load_context_overrides=self._cls._load_context_overrides,
|
|
442
443
|
)
|
|
443
444
|
|
|
444
445
|
|
|
@@ -485,6 +486,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
|
485
486
|
self._callables = other._callables
|
|
486
487
|
self._name = other._name
|
|
487
488
|
self._method_metadata = other._method_metadata
|
|
489
|
+
self._load_context_overrides = other._load_context_overrides
|
|
488
490
|
|
|
489
491
|
def _get_partial_functions(self) -> dict[str, _PartialFunction]:
|
|
490
492
|
if not self._user_cls:
|
|
@@ -559,7 +561,7 @@ class {user_cls.__name__}:
|
|
|
559
561
|
More information on class parameterization can be found here: https://modal.com/docs/guide/parametrized-functions
|
|
560
562
|
""",
|
|
561
563
|
)
|
|
562
|
-
annotations =
|
|
564
|
+
annotations = inspect.get_annotations(user_cls)
|
|
563
565
|
missing_annotations = params.keys() - annotations.keys()
|
|
564
566
|
if missing_annotations:
|
|
565
567
|
raise InvalidError("All modal.parameter() specifications need to be type-annotated")
|
|
@@ -577,22 +579,15 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
577
579
|
# validate signature
|
|
578
580
|
_Cls.validate_construction_mechanism(user_cls)
|
|
579
581
|
|
|
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
582
|
# Disable the warning that lifecycle methods are not wrapped
|
|
591
|
-
|
|
583
|
+
lifecycle_method_partials = _find_partial_methods_for_user_cls(
|
|
592
584
|
user_cls, ~_PartialFunctionFlags.interface_flags()
|
|
593
|
-
)
|
|
585
|
+
)
|
|
586
|
+
for partial_function in lifecycle_method_partials.values():
|
|
594
587
|
partial_function.registered = True
|
|
595
588
|
|
|
589
|
+
method_partials = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.interface_flags())
|
|
590
|
+
|
|
596
591
|
# Get all callables
|
|
597
592
|
callables: dict[str, Callable] = {
|
|
598
593
|
k: pf.raw_f
|
|
@@ -603,15 +598,18 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
603
598
|
def _deps() -> list[_Function]:
|
|
604
599
|
return [class_service_function]
|
|
605
600
|
|
|
606
|
-
async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
|
|
601
|
+
async def _load(self: "_Cls", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
607
602
|
req = api_pb2.ClassCreateRequest(
|
|
608
|
-
app_id=
|
|
603
|
+
app_id=load_context.app_id, existing_class_id=existing_object_id, only_class_function=True
|
|
609
604
|
)
|
|
610
|
-
resp = await
|
|
611
|
-
self._hydrate(resp.class_id,
|
|
605
|
+
resp = await load_context.client.stub.ClassCreate(req)
|
|
606
|
+
self._hydrate(resp.class_id, load_context.client, resp.handle_metadata)
|
|
612
607
|
|
|
613
608
|
rep = f"Cls({user_cls.__name__})"
|
|
614
|
-
|
|
609
|
+
# Pass a *reference* to the App's LoadContext - this is important since the App is
|
|
610
|
+
# the only way to infer a LoadContext for an `@app.cls`, and the App doesn't
|
|
611
|
+
# get its client until *after* the Cls is created.
|
|
612
|
+
cls: _Cls = _Cls._from_loader(_load, rep, deps=_deps, load_context_overrides=app._root_load_context)
|
|
615
613
|
cls._app = app
|
|
616
614
|
cls._user_cls = user_cls
|
|
617
615
|
cls._class_service_function = class_service_function
|
|
@@ -628,6 +626,7 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
628
626
|
*,
|
|
629
627
|
namespace: Any = None, # mdmd:line-hidden
|
|
630
628
|
environment_name: Optional[str] = None,
|
|
629
|
+
client: Optional["_Client"] = None,
|
|
631
630
|
) -> "_Cls":
|
|
632
631
|
"""Reference a Cls from a deployed App by its name.
|
|
633
632
|
|
|
@@ -640,42 +639,47 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
640
639
|
```
|
|
641
640
|
"""
|
|
642
641
|
warn_if_passing_namespace(namespace, "modal.Cls.from_name")
|
|
643
|
-
_environment_name = environment_name or config.get("environment")
|
|
644
642
|
|
|
645
|
-
async def _load_remote(
|
|
643
|
+
async def _load_remote(
|
|
644
|
+
self: _Cls, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
645
|
+
):
|
|
646
646
|
request = api_pb2.ClassGetRequest(
|
|
647
647
|
app_name=app_name,
|
|
648
648
|
object_tag=name,
|
|
649
|
-
environment_name=
|
|
649
|
+
environment_name=load_context.environment_name,
|
|
650
650
|
only_class_function=True,
|
|
651
651
|
)
|
|
652
652
|
try:
|
|
653
|
-
response = await
|
|
653
|
+
response = await load_context.client.stub.ClassGet(request)
|
|
654
654
|
except NotFoundError as exc:
|
|
655
|
-
env_context =
|
|
655
|
+
env_context = (
|
|
656
|
+
f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
|
|
657
|
+
)
|
|
656
658
|
raise NotFoundError(
|
|
657
659
|
f"Lookup failed for Cls '{name}' from the '{app_name}' app{env_context}: {exc}."
|
|
658
660
|
) from None
|
|
659
|
-
except GRPCError as exc:
|
|
660
|
-
if exc.status == Status.FAILED_PRECONDITION:
|
|
661
|
-
raise InvalidError(exc.message) from None
|
|
662
|
-
else:
|
|
663
|
-
raise
|
|
664
661
|
|
|
665
662
|
print_server_warnings(response.server_warnings)
|
|
666
|
-
await resolver.load(self._class_service_function)
|
|
667
|
-
self._hydrate(response.class_id,
|
|
663
|
+
await resolver.load(self._class_service_function, load_context)
|
|
664
|
+
self._hydrate(response.class_id, load_context.client, response.handle_metadata)
|
|
668
665
|
|
|
669
666
|
environment_rep = f", environment_name={environment_name!r}" if environment_name else ""
|
|
670
667
|
rep = f"Cls.from_name({app_name!r}, {name!r}{environment_rep})"
|
|
671
|
-
|
|
668
|
+
|
|
669
|
+
load_context_overrides = LoadContext(client=client, environment_name=environment_name)
|
|
670
|
+
cls = cls._from_loader(
|
|
671
|
+
_load_remote,
|
|
672
|
+
rep,
|
|
673
|
+
is_another_app=True,
|
|
674
|
+
hydrate_lazily=True,
|
|
675
|
+
load_context_overrides=load_context_overrides,
|
|
676
|
+
)
|
|
672
677
|
|
|
673
678
|
class_service_name = f"{name}.*" # special name of the base service function for the class
|
|
674
679
|
cls._class_service_function = _Function._from_name(
|
|
675
680
|
app_name,
|
|
676
681
|
class_service_name,
|
|
677
|
-
|
|
678
|
-
environment_name=_environment_name,
|
|
682
|
+
load_context_overrides=load_context_overrides,
|
|
679
683
|
)
|
|
680
684
|
cls._name = name
|
|
681
685
|
return cls
|
|
@@ -735,8 +739,6 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
735
739
|
else:
|
|
736
740
|
resources = None
|
|
737
741
|
|
|
738
|
-
scheduler_placement = SchedulerPlacement(region=region).proto if region else None
|
|
739
|
-
|
|
740
742
|
if allow_concurrent_inputs is not None:
|
|
741
743
|
deprecation_warning(
|
|
742
744
|
(2025, 5, 9),
|
|
@@ -744,7 +746,7 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
744
746
|
" please use the `.with_concurrency` method instead.",
|
|
745
747
|
)
|
|
746
748
|
|
|
747
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
749
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
748
750
|
# this is a bit confusing, the cls will always have the same metadata
|
|
749
751
|
# since it has the same *class* service function (i.e. "template")
|
|
750
752
|
# But the (instance) service function for each Obj will be different
|
|
@@ -753,14 +755,21 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
753
755
|
if not self.is_hydrated:
|
|
754
756
|
# this should only happen for Cls.from_name instances
|
|
755
757
|
# other classes should already be hydrated!
|
|
756
|
-
await resolver.load(self)
|
|
758
|
+
await resolver.load(self, load_context)
|
|
757
759
|
|
|
758
760
|
new_cls._initialize_from_other(self)
|
|
759
761
|
|
|
760
762
|
def _deps():
|
|
761
763
|
return []
|
|
762
764
|
|
|
763
|
-
cls = _Cls._from_loader(
|
|
765
|
+
cls = _Cls._from_loader(
|
|
766
|
+
_load_from_base,
|
|
767
|
+
rep=f"{self._name}.with_options(...)",
|
|
768
|
+
is_another_app=True,
|
|
769
|
+
deps=_deps,
|
|
770
|
+
load_context_overrides=self._load_context_overrides,
|
|
771
|
+
hydrate_lazily=True,
|
|
772
|
+
)
|
|
764
773
|
cls._initialize_from_other(self)
|
|
765
774
|
|
|
766
775
|
# Validate volumes
|
|
@@ -772,6 +781,11 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
772
781
|
if env:
|
|
773
782
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
774
783
|
|
|
784
|
+
scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
|
|
785
|
+
if region:
|
|
786
|
+
regions = [region] if isinstance(region, str) else list(region)
|
|
787
|
+
scheduler_placement = api_pb2.SchedulerPlacement(regions=regions)
|
|
788
|
+
|
|
775
789
|
new_options = _ServiceOptions(
|
|
776
790
|
secrets=secrets,
|
|
777
791
|
validated_volumes=validated_volumes_no_cloud_buckets,
|
|
@@ -805,16 +819,21 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
805
819
|
```
|
|
806
820
|
"""
|
|
807
821
|
|
|
808
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
822
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
809
823
|
if not self.is_hydrated:
|
|
810
|
-
await resolver.load(self)
|
|
824
|
+
await resolver.load(self, load_context)
|
|
811
825
|
new_cls._initialize_from_other(self)
|
|
812
826
|
|
|
813
827
|
def _deps():
|
|
814
828
|
return []
|
|
815
829
|
|
|
816
830
|
cls = _Cls._from_loader(
|
|
817
|
-
_load_from_base,
|
|
831
|
+
_load_from_base,
|
|
832
|
+
rep=f"{self._name}.with_concurrency(...)",
|
|
833
|
+
is_another_app=True,
|
|
834
|
+
deps=_deps,
|
|
835
|
+
load_context_overrides=self._load_context_overrides,
|
|
836
|
+
hydrate_lazily=True,
|
|
818
837
|
)
|
|
819
838
|
cls._initialize_from_other(self)
|
|
820
839
|
|
|
@@ -834,16 +853,21 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
834
853
|
```
|
|
835
854
|
"""
|
|
836
855
|
|
|
837
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
856
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
838
857
|
if not self.is_hydrated:
|
|
839
|
-
await resolver.load(self)
|
|
858
|
+
await resolver.load(self, load_context)
|
|
840
859
|
new_cls._initialize_from_other(self)
|
|
841
860
|
|
|
842
861
|
def _deps():
|
|
843
862
|
return []
|
|
844
863
|
|
|
845
864
|
cls = _Cls._from_loader(
|
|
846
|
-
_load_from_base,
|
|
865
|
+
_load_from_base,
|
|
866
|
+
rep=f"{self._name}.with_batching(...)",
|
|
867
|
+
is_another_app=True,
|
|
868
|
+
deps=_deps,
|
|
869
|
+
load_context_overrides=self._load_context_overrides,
|
|
870
|
+
hydrate_lazily=True,
|
|
847
871
|
)
|
|
848
872
|
cls._initialize_from_other(self)
|
|
849
873
|
|
|
@@ -863,18 +887,31 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
863
887
|
)
|
|
864
888
|
|
|
865
889
|
def __getattr__(self, k):
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
# if
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
890
|
+
if self._user_cls is not None:
|
|
891
|
+
# local class, we can check if there are static attributes and let the user access them
|
|
892
|
+
# except if they are PartialFunction (i.e. methods)
|
|
893
|
+
v = getattr(self._user_cls, k)
|
|
894
|
+
if not isinstance(v, modal.partial_function.PartialFunction):
|
|
895
|
+
return v
|
|
896
|
+
|
|
897
|
+
# We create a synthetic dummy Function that is guaranteed to raise an AttributeError when
|
|
898
|
+
# a user tries to use any of its "live methods" - this lets us raise exceptions for users
|
|
899
|
+
# only if they try to access methods on a Cls as if they were methods on the instance.
|
|
900
|
+
async def error_loader(
|
|
901
|
+
fun: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
902
|
+
):
|
|
903
|
+
raise AttributeError(
|
|
904
|
+
"You can't access methods on a Cls directly - Did you forget to instantiate the class first?\n"
|
|
905
|
+
"e.g. instead of MyClass.method.remote(), do MyClass().method.remote()"
|
|
874
906
|
)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
907
|
+
|
|
908
|
+
return _Function._from_loader(
|
|
909
|
+
error_loader,
|
|
910
|
+
rep=f"UnboundMethod({self._name}.{k})",
|
|
911
|
+
deps=lambda: [],
|
|
912
|
+
hydrate_lazily=True,
|
|
913
|
+
load_context_overrides=self._load_context_overrides,
|
|
914
|
+
)
|
|
878
915
|
|
|
879
916
|
def _is_local(self) -> bool:
|
|
880
917
|
return self._user_cls is not None
|
modal/cls.pyi
CHANGED
|
@@ -5,6 +5,7 @@ import modal._functions
|
|
|
5
5
|
import modal._object
|
|
6
6
|
import modal._partial_function
|
|
7
7
|
import modal.app
|
|
8
|
+
import modal.client
|
|
8
9
|
import modal.cloud_bucket_mount
|
|
9
10
|
import modal.functions
|
|
10
11
|
import modal.gpu
|
|
@@ -178,8 +179,6 @@ class _Obj:
|
|
|
178
179
|
async def _aenter(self): ...
|
|
179
180
|
def __getattr__(self, k): ...
|
|
180
181
|
|
|
181
|
-
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
|
182
|
-
|
|
183
182
|
class Obj:
|
|
184
183
|
"""An instance of a `Cls`, i.e. `Cls("foo", 42)` returns an `Obj`.
|
|
185
184
|
|
|
@@ -202,7 +201,7 @@ class Obj:
|
|
|
202
201
|
def _get_parameter_values(self) -> dict[str, typing.Any]: ...
|
|
203
202
|
def _new_user_cls_instance(self): ...
|
|
204
203
|
|
|
205
|
-
class __update_autoscaler_spec(typing_extensions.Protocol
|
|
204
|
+
class __update_autoscaler_spec(typing_extensions.Protocol):
|
|
206
205
|
def __call__(
|
|
207
206
|
self,
|
|
208
207
|
/,
|
|
@@ -273,9 +272,9 @@ class Obj:
|
|
|
273
272
|
"""
|
|
274
273
|
...
|
|
275
274
|
|
|
276
|
-
update_autoscaler: __update_autoscaler_spec
|
|
275
|
+
update_autoscaler: __update_autoscaler_spec
|
|
277
276
|
|
|
278
|
-
class __keep_warm_spec(typing_extensions.Protocol
|
|
277
|
+
class __keep_warm_spec(typing_extensions.Protocol):
|
|
279
278
|
def __call__(self, /, warm_pool_size: int) -> None:
|
|
280
279
|
"""mdmd:hidden
|
|
281
280
|
Set the warm pool size for the class containers
|
|
@@ -314,7 +313,7 @@ class Obj:
|
|
|
314
313
|
"""
|
|
315
314
|
...
|
|
316
315
|
|
|
317
|
-
keep_warm: __keep_warm_spec
|
|
316
|
+
keep_warm: __keep_warm_spec
|
|
318
317
|
|
|
319
318
|
def _cached_user_cls_instance(self):
|
|
320
319
|
"""Get or construct the local object
|
|
@@ -379,6 +378,7 @@ class _Cls(modal._object._Object):
|
|
|
379
378
|
*,
|
|
380
379
|
namespace: typing.Any = None,
|
|
381
380
|
environment_name: typing.Optional[str] = None,
|
|
381
|
+
client: typing.Optional[modal.client._Client] = None,
|
|
382
382
|
) -> _Cls:
|
|
383
383
|
"""Reference a Cls from a deployed App by its name.
|
|
384
384
|
|
|
@@ -507,7 +507,7 @@ class Cls(modal.object.Object):
|
|
|
507
507
|
def _get_class_service_function(self) -> modal.functions.Function: ...
|
|
508
508
|
def _get_method_names(self) -> collections.abc.Collection[str]: ...
|
|
509
509
|
|
|
510
|
-
class ___experimental_get_flash_urls_spec(typing_extensions.Protocol
|
|
510
|
+
class ___experimental_get_flash_urls_spec(typing_extensions.Protocol):
|
|
511
511
|
def __call__(self, /) -> typing.Optional[list[str]]:
|
|
512
512
|
"""URL of the flash service for the class."""
|
|
513
513
|
...
|
|
@@ -516,7 +516,7 @@ class Cls(modal.object.Object):
|
|
|
516
516
|
"""URL of the flash service for the class."""
|
|
517
517
|
...
|
|
518
518
|
|
|
519
|
-
_experimental_get_flash_urls: ___experimental_get_flash_urls_spec
|
|
519
|
+
_experimental_get_flash_urls: ___experimental_get_flash_urls_spec
|
|
520
520
|
|
|
521
521
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
|
522
522
|
@staticmethod
|
|
@@ -537,6 +537,7 @@ class Cls(modal.object.Object):
|
|
|
537
537
|
*,
|
|
538
538
|
namespace: typing.Any = None,
|
|
539
539
|
environment_name: typing.Optional[str] = None,
|
|
540
|
+
client: typing.Optional[modal.client.Client] = None,
|
|
540
541
|
) -> Cls:
|
|
541
542
|
"""Reference a Cls from a deployed App by its name.
|
|
542
543
|
|
modal/config.py
CHANGED
|
@@ -51,6 +51,10 @@ Other possible configuration options are:
|
|
|
51
51
|
Defaults to 10.
|
|
52
52
|
Number of seconds to wait for logs to drain when closing the session,
|
|
53
53
|
before giving up.
|
|
54
|
+
* `max_throttle_wait` (in the .toml file) / `MODAL_MAX_THROTTLE_WAIT` (as an env var).
|
|
55
|
+
Defaults to None (no limit).
|
|
56
|
+
Maximum number of seconds to wait when requests are being throttled (i.e., due
|
|
57
|
+
to rate limiting or other cases that can normally be resolved through backoff).
|
|
54
58
|
* `force_build` (in the .toml file) / `MODAL_FORCE_BUILD` (as an env var).
|
|
55
59
|
Defaults to False.
|
|
56
60
|
When set, ignores the Image cache and builds all Image layers. Note that this
|
|
@@ -71,6 +75,10 @@ Other possible configuration options are:
|
|
|
71
75
|
The log formatting pattern that will be used by the modal client itself.
|
|
72
76
|
See https://docs.python.org/3/library/logging.html#logrecord-attributes for available
|
|
73
77
|
log attributes.
|
|
78
|
+
* `dev_suffix` (in the .toml file) / `MODAL_DEV_SUFFIX` (as an env var).
|
|
79
|
+
Overrides the default `-dev` suffix added to URLs generated for web endpoints
|
|
80
|
+
when the App is ephemeral (i.e., created via `modal serve`). Must be a short
|
|
81
|
+
alphanumeric string.
|
|
74
82
|
|
|
75
83
|
Meta-configuration
|
|
76
84
|
------------------
|
|
@@ -85,6 +93,7 @@ Some "meta-options" are set using environment variables only:
|
|
|
85
93
|
|
|
86
94
|
import logging
|
|
87
95
|
import os
|
|
96
|
+
import re
|
|
88
97
|
import typing
|
|
89
98
|
import warnings
|
|
90
99
|
from typing import Any, Callable, Optional
|
|
@@ -142,7 +151,7 @@ async def _lookup_workspace(server_url: str, token_id: str, token_secret: str) -
|
|
|
142
151
|
|
|
143
152
|
credentials = (token_id, token_secret)
|
|
144
153
|
async with _Client(server_url, api_pb2.CLIENT_TYPE_CLIENT, credentials) as client:
|
|
145
|
-
return await client.stub.WorkspaceNameLookup(Empty(), timeout=3)
|
|
154
|
+
return await client.stub.WorkspaceNameLookup(Empty(), retry=None, timeout=3)
|
|
146
155
|
|
|
147
156
|
|
|
148
157
|
def config_profiles():
|
|
@@ -206,6 +215,12 @@ def _check_value(options: list[str]) -> Callable[[str], str]:
|
|
|
206
215
|
return checker
|
|
207
216
|
|
|
208
217
|
|
|
218
|
+
def _enforce_suffix_rules(x: str) -> str:
|
|
219
|
+
if x and not re.match(r"^[a-zA-Z0-9]{1,8}$", x):
|
|
220
|
+
raise ValueError("Suffix must be an alphanumeric string of no more than 8 characters.")
|
|
221
|
+
return x
|
|
222
|
+
|
|
223
|
+
|
|
209
224
|
class _Setting(typing.NamedTuple):
|
|
210
225
|
default: typing.Any = None
|
|
211
226
|
transform: typing.Callable[[str], typing.Any] = lambda x: x # noqa: E731
|
|
@@ -236,6 +251,8 @@ _SETTINGS = {
|
|
|
236
251
|
"traceback": _Setting(False, transform=_to_boolean),
|
|
237
252
|
"image_builder_version": _Setting(),
|
|
238
253
|
"strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
|
|
254
|
+
# Allow insecure TLS for the task command router when running locally (testing/dev only)
|
|
255
|
+
"task_command_router_insecure": _Setting(False, transform=_to_boolean),
|
|
239
256
|
"snapshot_debug": _Setting(False, transform=_to_boolean),
|
|
240
257
|
"cuda_checkpoint_path": _Setting("/__modal/.bin/cuda-checkpoint"), # Used for snapshotting GPU memory.
|
|
241
258
|
"build_validation": _Setting("error", transform=_check_value(["error", "warn", "ignore"])),
|
|
@@ -244,6 +261,9 @@ _SETTINGS = {
|
|
|
244
261
|
"pickle",
|
|
245
262
|
transform=lambda s: _check_value(["pickle", "cbor"])(s.lower()),
|
|
246
263
|
),
|
|
264
|
+
"dev_suffix": _Setting("", transform=_enforce_suffix_rules),
|
|
265
|
+
"max_throttle_wait": _Setting(None, transform=lambda x: int(x) if x else None),
|
|
266
|
+
"async_warnings": _Setting(False, transform=_to_boolean), # Feature flag for async API warnings
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
|