modal 1.0.6.dev58__py3-none-any.whl → 1.2.3.dev7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +4 -2
- modal/_container_entrypoint.py +41 -49
- modal/_functions.py +424 -195
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +68 -20
- modal/_output.py +58 -45
- modal/_partial_function.py +36 -11
- modal/_pty.py +7 -3
- modal/_resolver.py +21 -35
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +301 -186
- modal/_runtime/container_io_manager.pyi +70 -61
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +4 -1
- modal/_runtime/gpu_memory_snapshot.py +170 -63
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +57 -1
- modal/_utils/async_utils.py +33 -12
- modal/_utils/auth_token_manager.py +2 -5
- modal/_utils/blob_utils.py +110 -53
- modal/_utils/function_utils.py +49 -42
- modal/_utils/grpc_utils.py +80 -50
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +219 -83
- modal/app.pyi +229 -56
- modal/billing.py +5 -0
- modal/{requirements → builder}/2025.06.txt +1 -0
- modal/{requirements → builder}/PREVIEW.txt +1 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +9 -13
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +58 -16
- modal/cli/secret.py +48 -22
- modal/cli/utils.py +3 -4
- modal/cli/volume.py +28 -25
- modal/client.py +13 -116
- modal/client.pyi +9 -91
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +5 -1
- modal/cls.py +130 -102
- modal/cls.pyi +45 -85
- modal/config.py +29 -10
- modal/container_process.py +291 -13
- modal/container_process.pyi +95 -32
- modal/dict.py +282 -63
- modal/dict.pyi +423 -73
- modal/environments.py +15 -27
- modal/environments.pyi +5 -15
- modal/exception.py +8 -0
- modal/experimental/__init__.py +143 -38
- modal/experimental/flash.py +247 -78
- modal/experimental/flash.pyi +137 -9
- modal/file_io.py +14 -28
- modal/file_io.pyi +2 -2
- modal/file_pattern_matcher.py +25 -16
- modal/functions.pyi +134 -61
- modal/image.py +255 -86
- modal/image.pyi +300 -62
- modal/io_streams.py +436 -126
- modal/io_streams.pyi +236 -171
- modal/mount.py +62 -157
- modal/mount.pyi +45 -172
- modal/network_file_system.py +30 -53
- modal/network_file_system.pyi +16 -76
- modal/object.pyi +42 -8
- modal/parallel_map.py +821 -113
- modal/parallel_map.pyi +134 -0
- modal/partial_function.pyi +4 -1
- modal/proxy.py +16 -7
- modal/proxy.pyi +10 -2
- modal/queue.py +263 -61
- modal/queue.pyi +409 -66
- modal/runner.py +112 -92
- modal/runner.pyi +45 -27
- modal/sandbox.py +451 -124
- modal/sandbox.pyi +513 -67
- modal/secret.py +291 -67
- modal/secret.pyi +425 -19
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/token_flow.py +4 -4
- modal/volume.py +344 -98
- modal/volume.pyi +464 -68
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +11 -1
- modal_proto/api.proto +399 -67
- modal_proto/api_grpc.py +241 -1
- modal_proto/api_pb2.py +1395 -1000
- modal_proto/api_pb2.pyi +1239 -79
- modal_proto/api_pb2_grpc.py +499 -4
- modal_proto/api_pb2_grpc.pyi +162 -14
- modal_proto/modal_api_grpc.py +175 -160
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal-1.0.6.dev58.dist-info/RECORD +0 -183
- 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/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- /modal/{requirements → builder}/base-images.json +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/cls.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import dataclasses
|
|
3
3
|
import inspect
|
|
4
|
-
import os
|
|
5
4
|
import typing
|
|
6
5
|
from collections.abc import Collection
|
|
7
|
-
from
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
from typing import Any, Callable, Optional, Sequence, TypeVar, Union
|
|
8
8
|
|
|
9
9
|
from google.protobuf.message import Message
|
|
10
10
|
from grpclib import GRPCError, Status
|
|
@@ -12,7 +12,8 @@ from grpclib import GRPCError, Status
|
|
|
12
12
|
from modal_proto import api_pb2
|
|
13
13
|
|
|
14
14
|
from ._functions import _Function, _parse_retries
|
|
15
|
-
from .
|
|
15
|
+
from ._load_context import LoadContext
|
|
16
|
+
from ._object import _Object, live_method
|
|
16
17
|
from ._partial_function import (
|
|
17
18
|
_find_callables_for_obj,
|
|
18
19
|
_find_partial_methods_for_user_cls,
|
|
@@ -30,13 +31,13 @@ from ._utils.deprecation import (
|
|
|
30
31
|
warn_if_passing_namespace,
|
|
31
32
|
warn_on_renamed_autoscaler_settings,
|
|
32
33
|
)
|
|
33
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
34
34
|
from ._utils.mount_utils import validate_volumes
|
|
35
35
|
from .client import _Client
|
|
36
|
-
from .
|
|
36
|
+
from .cloud_bucket_mount import _CloudBucketMount
|
|
37
37
|
from .exception import ExecutionError, InvalidError, NotFoundError
|
|
38
38
|
from .gpu import GPU_T
|
|
39
39
|
from .retries import Retries
|
|
40
|
+
from .scheduler_placement import SchedulerPlacement
|
|
40
41
|
from .secret import _Secret
|
|
41
42
|
from .volume import _Volume
|
|
42
43
|
|
|
@@ -80,7 +81,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
|
|
80
81
|
@dataclasses.dataclass()
|
|
81
82
|
class _ServiceOptions:
|
|
82
83
|
# Note that default values should always be "untruthy" so we can detect when they are not set
|
|
83
|
-
secrets:
|
|
84
|
+
secrets: Collection[_Secret] = ()
|
|
84
85
|
validated_volumes: typing.Sequence[tuple[str, _Volume]] = ()
|
|
85
86
|
resources: Optional[api_pb2.Resources] = None
|
|
86
87
|
retry_policy: Optional[api_pb2.FunctionRetryPolicy] = None
|
|
@@ -92,6 +93,9 @@ class _ServiceOptions:
|
|
|
92
93
|
target_concurrent_inputs: Optional[int] = None
|
|
93
94
|
batch_max_size: Optional[int] = None
|
|
94
95
|
batch_wait_ms: Optional[int] = None
|
|
96
|
+
scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
|
|
97
|
+
cloud: Optional[str] = None
|
|
98
|
+
cloud_bucket_mounts: typing.Sequence[tuple[str, _CloudBucketMount]] = ()
|
|
95
99
|
|
|
96
100
|
def merge_options(self, new_options: "_ServiceOptions") -> "_ServiceOptions":
|
|
97
101
|
"""Implement protobuf-like MergeFrom semantics for this dataclass.
|
|
@@ -131,7 +135,7 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
|
|
|
131
135
|
method_metadata = cls._method_metadata[method_name]
|
|
132
136
|
new_function._hydrate(service_function.object_id, service_function.client, method_metadata)
|
|
133
137
|
|
|
134
|
-
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
|
138
|
+
async def _load(fun: "_Function", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
135
139
|
# there is currently no actual loading logic executed to create each method on
|
|
136
140
|
# the *parametrized* instance of a class - it uses the parameter-bound service-function
|
|
137
141
|
# for the instance. This load method just makes sure to set all attributes after the
|
|
@@ -150,11 +154,14 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
|
|
|
150
154
|
|
|
151
155
|
rep = f"Method({cls._name}.{method_name})"
|
|
152
156
|
|
|
157
|
+
# Bound methods should *reference* their parent Cls's LoadContext
|
|
158
|
+
# so that it can be modified in place on the parent and be reflected in the method
|
|
153
159
|
fun = _Function._from_loader(
|
|
154
160
|
_load,
|
|
155
161
|
rep,
|
|
156
162
|
deps=_deps,
|
|
157
163
|
hydrate_lazily=True,
|
|
164
|
+
load_context_overrides=cls._load_context_overrides,
|
|
158
165
|
)
|
|
159
166
|
if service_function.is_hydrated:
|
|
160
167
|
# Eager hydration (skip load) if the instance service function is already loaded
|
|
@@ -418,14 +425,13 @@ class _Obj:
|
|
|
418
425
|
|
|
419
426
|
# Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
|
|
420
427
|
# has not yet been loaded. So use a special loader that loads it lazily:
|
|
421
|
-
async def method_loader(fun, resolver: Resolver, existing_object_id):
|
|
422
|
-
await resolver.load(self._cls) # load class so we get info about methods
|
|
428
|
+
async def method_loader(fun, resolver: Resolver, load_context: LoadContext, existing_object_id):
|
|
423
429
|
method_function = _get_maybe_method()
|
|
424
430
|
if method_function is None:
|
|
425
431
|
raise NotFoundError(
|
|
426
432
|
f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
|
|
427
433
|
)
|
|
428
|
-
await resolver.load(method_function) # get the appropriate method handle (lazy)
|
|
434
|
+
await resolver.load(method_function, load_context) # get the appropriate method handle (lazy)
|
|
429
435
|
fun._hydrate_from_other(method_function)
|
|
430
436
|
|
|
431
437
|
# The reason we don't *always* use this lazy loader is because it precludes attribute access
|
|
@@ -433,8 +439,9 @@ class _Obj:
|
|
|
433
439
|
return _Function._from_loader(
|
|
434
440
|
method_loader,
|
|
435
441
|
rep=f"Method({self._cls._name}.{k})",
|
|
436
|
-
deps=lambda: [],
|
|
442
|
+
deps=lambda: [self._cls],
|
|
437
443
|
hydrate_lazily=True,
|
|
444
|
+
load_context_overrides=self._cls._load_context_overrides,
|
|
438
445
|
)
|
|
439
446
|
|
|
440
447
|
|
|
@@ -481,6 +488,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
|
481
488
|
self._callables = other._callables
|
|
482
489
|
self._name = other._name
|
|
483
490
|
self._method_metadata = other._method_metadata
|
|
491
|
+
self._load_context_overrides = other._load_context_overrides
|
|
484
492
|
|
|
485
493
|
def _get_partial_functions(self) -> dict[str, _PartialFunction]:
|
|
486
494
|
if not self._user_cls:
|
|
@@ -507,6 +515,11 @@ class _Cls(_Object, type_prefix="cs"):
|
|
|
507
515
|
# returns method names for a *local* class only for now (used by cli)
|
|
508
516
|
return self._method_partials.keys()
|
|
509
517
|
|
|
518
|
+
@live_method
|
|
519
|
+
async def _experimental_get_flash_urls(self) -> Optional[list[str]]:
|
|
520
|
+
"""URL of the flash service for the class."""
|
|
521
|
+
return await self._get_class_service_function()._experimental_get_flash_urls()
|
|
522
|
+
|
|
510
523
|
def _hydrate_metadata(self, metadata: Message):
|
|
511
524
|
assert isinstance(metadata, api_pb2.ClassHandleMetadata)
|
|
512
525
|
class_service_function = self._get_class_service_function()
|
|
@@ -568,22 +581,15 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
568
581
|
# validate signature
|
|
569
582
|
_Cls.validate_construction_mechanism(user_cls)
|
|
570
583
|
|
|
571
|
-
method_partials: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
|
|
572
|
-
user_cls, _PartialFunctionFlags.interface_flags()
|
|
573
|
-
)
|
|
574
|
-
|
|
575
|
-
for method_name, partial_function in method_partials.items():
|
|
576
|
-
if partial_function.params.webhook_config is not None:
|
|
577
|
-
full_name = f"{user_cls.__name__}.{method_name}"
|
|
578
|
-
app._web_endpoints.append(full_name)
|
|
579
|
-
partial_function.registered = True
|
|
580
|
-
|
|
581
584
|
# Disable the warning that lifecycle methods are not wrapped
|
|
582
|
-
|
|
585
|
+
lifecycle_method_partials = _find_partial_methods_for_user_cls(
|
|
583
586
|
user_cls, ~_PartialFunctionFlags.interface_flags()
|
|
584
|
-
)
|
|
587
|
+
)
|
|
588
|
+
for partial_function in lifecycle_method_partials.values():
|
|
585
589
|
partial_function.registered = True
|
|
586
590
|
|
|
591
|
+
method_partials = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.interface_flags())
|
|
592
|
+
|
|
587
593
|
# Get all callables
|
|
588
594
|
callables: dict[str, Callable] = {
|
|
589
595
|
k: pf.raw_f
|
|
@@ -594,15 +600,18 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
594
600
|
def _deps() -> list[_Function]:
|
|
595
601
|
return [class_service_function]
|
|
596
602
|
|
|
597
|
-
async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
|
|
603
|
+
async def _load(self: "_Cls", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
598
604
|
req = api_pb2.ClassCreateRequest(
|
|
599
|
-
app_id=
|
|
605
|
+
app_id=load_context.app_id, existing_class_id=existing_object_id, only_class_function=True
|
|
600
606
|
)
|
|
601
|
-
resp = await
|
|
602
|
-
self._hydrate(resp.class_id,
|
|
607
|
+
resp = await load_context.client.stub.ClassCreate(req)
|
|
608
|
+
self._hydrate(resp.class_id, load_context.client, resp.handle_metadata)
|
|
603
609
|
|
|
604
610
|
rep = f"Cls({user_cls.__name__})"
|
|
605
|
-
|
|
611
|
+
# Pass a *reference* to the App's LoadContext - this is important since the App is
|
|
612
|
+
# the only way to infer a LoadContext for an `@app.cls`, and the App doesn't
|
|
613
|
+
# get its client until *after* the Cls is created.
|
|
614
|
+
cls: _Cls = _Cls._from_loader(_load, rep, deps=_deps, load_context_overrides=app._root_load_context)
|
|
606
615
|
cls._app = app
|
|
607
616
|
cls._user_cls = user_cls
|
|
608
617
|
cls._class_service_function = class_service_function
|
|
@@ -619,6 +628,7 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
619
628
|
*,
|
|
620
629
|
namespace: Any = None, # mdmd:line-hidden
|
|
621
630
|
environment_name: Optional[str] = None,
|
|
631
|
+
client: Optional["_Client"] = None,
|
|
622
632
|
) -> "_Cls":
|
|
623
633
|
"""Reference a Cls from a deployed App by its name.
|
|
624
634
|
|
|
@@ -631,19 +641,22 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
631
641
|
```
|
|
632
642
|
"""
|
|
633
643
|
warn_if_passing_namespace(namespace, "modal.Cls.from_name")
|
|
634
|
-
_environment_name = environment_name or config.get("environment")
|
|
635
644
|
|
|
636
|
-
async def _load_remote(
|
|
645
|
+
async def _load_remote(
|
|
646
|
+
self: _Cls, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
647
|
+
):
|
|
637
648
|
request = api_pb2.ClassGetRequest(
|
|
638
649
|
app_name=app_name,
|
|
639
650
|
object_tag=name,
|
|
640
|
-
environment_name=
|
|
651
|
+
environment_name=load_context.environment_name,
|
|
641
652
|
only_class_function=True,
|
|
642
653
|
)
|
|
643
654
|
try:
|
|
644
|
-
response = await
|
|
655
|
+
response = await load_context.client.stub.ClassGet(request)
|
|
645
656
|
except NotFoundError as exc:
|
|
646
|
-
env_context =
|
|
657
|
+
env_context = (
|
|
658
|
+
f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
|
|
659
|
+
)
|
|
647
660
|
raise NotFoundError(
|
|
648
661
|
f"Lookup failed for Cls '{name}' from the '{app_name}' app{env_context}: {exc}."
|
|
649
662
|
) from None
|
|
@@ -654,18 +667,26 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
654
667
|
raise
|
|
655
668
|
|
|
656
669
|
print_server_warnings(response.server_warnings)
|
|
657
|
-
await resolver.load(self._class_service_function)
|
|
658
|
-
self._hydrate(response.class_id,
|
|
670
|
+
await resolver.load(self._class_service_function, load_context)
|
|
671
|
+
self._hydrate(response.class_id, load_context.client, response.handle_metadata)
|
|
672
|
+
|
|
673
|
+
environment_rep = f", environment_name={environment_name!r}" if environment_name else ""
|
|
674
|
+
rep = f"Cls.from_name({app_name!r}, {name!r}{environment_rep})"
|
|
659
675
|
|
|
660
|
-
|
|
661
|
-
cls = cls._from_loader(
|
|
676
|
+
load_context_overrides = LoadContext(client=client, environment_name=environment_name)
|
|
677
|
+
cls = cls._from_loader(
|
|
678
|
+
_load_remote,
|
|
679
|
+
rep,
|
|
680
|
+
is_another_app=True,
|
|
681
|
+
hydrate_lazily=True,
|
|
682
|
+
load_context_overrides=load_context_overrides,
|
|
683
|
+
)
|
|
662
684
|
|
|
663
685
|
class_service_name = f"{name}.*" # special name of the base service function for the class
|
|
664
686
|
cls._class_service_function = _Function._from_name(
|
|
665
687
|
app_name,
|
|
666
688
|
class_service_name,
|
|
667
|
-
|
|
668
|
-
environment_name=_environment_name,
|
|
689
|
+
load_context_overrides=load_context_overrides,
|
|
669
690
|
)
|
|
670
691
|
cls._name = name
|
|
671
692
|
return cls
|
|
@@ -677,13 +698,16 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
677
698
|
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
|
678
699
|
memory: Optional[Union[int, tuple[int, int]]] = None,
|
|
679
700
|
gpu: GPU_T = None,
|
|
680
|
-
|
|
681
|
-
|
|
701
|
+
env: Optional[dict[str, Optional[str]]] = None,
|
|
702
|
+
secrets: Optional[Collection[_Secret]] = None,
|
|
703
|
+
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
|
|
682
704
|
retries: Optional[Union[int, Retries]] = None,
|
|
683
705
|
max_containers: Optional[int] = None, # Limit on the number of containers that can be concurrently running.
|
|
684
706
|
buffer_containers: Optional[int] = None, # Additional containers to scale up while Function is active.
|
|
685
707
|
scaledown_window: Optional[int] = None, # Max amount of time a container can remain idle before scaling down.
|
|
686
708
|
timeout: Optional[int] = None,
|
|
709
|
+
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
710
|
+
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
687
711
|
# The following parameters are deprecated
|
|
688
712
|
concurrency_limit: Optional[int] = None, # Now called `max_containers`
|
|
689
713
|
container_idle_timeout: Optional[int] = None, # Now called `scaledown_window`
|
|
@@ -722,6 +746,8 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
722
746
|
else:
|
|
723
747
|
resources = None
|
|
724
748
|
|
|
749
|
+
scheduler_placement = SchedulerPlacement(region=region).proto if region else None
|
|
750
|
+
|
|
725
751
|
if allow_concurrent_inputs is not None:
|
|
726
752
|
deprecation_warning(
|
|
727
753
|
(2025, 5, 9),
|
|
@@ -729,7 +755,7 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
729
755
|
" please use the `.with_concurrency` method instead.",
|
|
730
756
|
)
|
|
731
757
|
|
|
732
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
758
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
733
759
|
# this is a bit confusing, the cls will always have the same metadata
|
|
734
760
|
# since it has the same *class* service function (i.e. "template")
|
|
735
761
|
# But the (instance) service function for each Obj will be different
|
|
@@ -738,25 +764,44 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
738
764
|
if not self.is_hydrated:
|
|
739
765
|
# this should only happen for Cls.from_name instances
|
|
740
766
|
# other classes should already be hydrated!
|
|
741
|
-
await resolver.load(self)
|
|
767
|
+
await resolver.load(self, load_context)
|
|
742
768
|
|
|
743
769
|
new_cls._initialize_from_other(self)
|
|
744
770
|
|
|
745
771
|
def _deps():
|
|
746
772
|
return []
|
|
747
773
|
|
|
748
|
-
cls = _Cls._from_loader(
|
|
774
|
+
cls = _Cls._from_loader(
|
|
775
|
+
_load_from_base,
|
|
776
|
+
rep=f"{self._name}.with_options(...)",
|
|
777
|
+
is_another_app=True,
|
|
778
|
+
deps=_deps,
|
|
779
|
+
load_context_overrides=self._load_context_overrides,
|
|
780
|
+
hydrate_lazily=True,
|
|
781
|
+
)
|
|
749
782
|
cls._initialize_from_other(self)
|
|
750
783
|
|
|
784
|
+
# Validate volumes
|
|
785
|
+
validated_volumes = validate_volumes(volumes)
|
|
786
|
+
cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
|
|
787
|
+
validated_volumes_no_cloud_buckets = [(k, v) for k, v in validated_volumes if isinstance(v, _Volume)]
|
|
788
|
+
|
|
789
|
+
secrets = secrets or []
|
|
790
|
+
if env:
|
|
791
|
+
secrets = [*secrets, _Secret.from_dict(env)]
|
|
792
|
+
|
|
751
793
|
new_options = _ServiceOptions(
|
|
752
794
|
secrets=secrets,
|
|
753
|
-
validated_volumes=
|
|
795
|
+
validated_volumes=validated_volumes_no_cloud_buckets,
|
|
796
|
+
cloud_bucket_mounts=cloud_bucket_mounts,
|
|
754
797
|
resources=resources,
|
|
755
798
|
retry_policy=retry_policy,
|
|
756
799
|
max_containers=max_containers,
|
|
757
800
|
buffer_containers=buffer_containers,
|
|
758
801
|
scaledown_window=scaledown_window,
|
|
759
802
|
timeout_secs=timeout,
|
|
803
|
+
scheduler_placement=scheduler_placement,
|
|
804
|
+
cloud=cloud,
|
|
760
805
|
# Note: set both for backwards / forwards compatibility
|
|
761
806
|
# But going forward `.with_concurrency` is the preferred method with distinct parameterization
|
|
762
807
|
max_concurrent_inputs=allow_concurrent_inputs,
|
|
@@ -778,16 +823,21 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
778
823
|
```
|
|
779
824
|
"""
|
|
780
825
|
|
|
781
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
826
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
782
827
|
if not self.is_hydrated:
|
|
783
|
-
await resolver.load(self)
|
|
828
|
+
await resolver.load(self, load_context)
|
|
784
829
|
new_cls._initialize_from_other(self)
|
|
785
830
|
|
|
786
831
|
def _deps():
|
|
787
832
|
return []
|
|
788
833
|
|
|
789
834
|
cls = _Cls._from_loader(
|
|
790
|
-
_load_from_base,
|
|
835
|
+
_load_from_base,
|
|
836
|
+
rep=f"{self._name}.with_concurrency(...)",
|
|
837
|
+
is_another_app=True,
|
|
838
|
+
deps=_deps,
|
|
839
|
+
load_context_overrides=self._load_context_overrides,
|
|
840
|
+
hydrate_lazily=True,
|
|
791
841
|
)
|
|
792
842
|
cls._initialize_from_other(self)
|
|
793
843
|
|
|
@@ -807,16 +857,21 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
807
857
|
```
|
|
808
858
|
"""
|
|
809
859
|
|
|
810
|
-
async def _load_from_base(new_cls, resolver, existing_object_id):
|
|
860
|
+
async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
|
|
811
861
|
if not self.is_hydrated:
|
|
812
|
-
await resolver.load(self)
|
|
862
|
+
await resolver.load(self, load_context)
|
|
813
863
|
new_cls._initialize_from_other(self)
|
|
814
864
|
|
|
815
865
|
def _deps():
|
|
816
866
|
return []
|
|
817
867
|
|
|
818
868
|
cls = _Cls._from_loader(
|
|
819
|
-
_load_from_base,
|
|
869
|
+
_load_from_base,
|
|
870
|
+
rep=f"{self._name}.with_concurrency(...)",
|
|
871
|
+
is_another_app=True,
|
|
872
|
+
deps=_deps,
|
|
873
|
+
load_context_overrides=self._load_context_overrides,
|
|
874
|
+
hydrate_lazily=True,
|
|
820
875
|
)
|
|
821
876
|
cls._initialize_from_other(self)
|
|
822
877
|
|
|
@@ -824,46 +879,6 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
824
879
|
cls._options.merge_options(batching_options)
|
|
825
880
|
return cls
|
|
826
881
|
|
|
827
|
-
@staticmethod
|
|
828
|
-
async def lookup(
|
|
829
|
-
app_name: str,
|
|
830
|
-
name: str,
|
|
831
|
-
namespace=None, # mdmd:line-hidden
|
|
832
|
-
client: Optional[_Client] = None,
|
|
833
|
-
environment_name: Optional[str] = None,
|
|
834
|
-
) -> "_Cls":
|
|
835
|
-
"""mdmd:hidden
|
|
836
|
-
Lookup a Cls from a deployed App by its name.
|
|
837
|
-
|
|
838
|
-
DEPRECATED: This method is deprecated in favor of `modal.Cls.from_name`.
|
|
839
|
-
|
|
840
|
-
In contrast to `modal.Cls.from_name`, this is an eager method
|
|
841
|
-
that will hydrate the local object with metadata from Modal servers.
|
|
842
|
-
|
|
843
|
-
```python notest
|
|
844
|
-
Model = modal.Cls.from_name("other-app", "Model")
|
|
845
|
-
model = Model()
|
|
846
|
-
model.inference(...)
|
|
847
|
-
```
|
|
848
|
-
"""
|
|
849
|
-
deprecation_warning(
|
|
850
|
-
(2025, 1, 27),
|
|
851
|
-
"`modal.Cls.lookup` is deprecated and will be removed in a future release."
|
|
852
|
-
" It can be replaced with `modal.Cls.from_name`."
|
|
853
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
854
|
-
)
|
|
855
|
-
warn_if_passing_namespace(namespace, "modal.Cls.lookup")
|
|
856
|
-
obj = _Cls.from_name(
|
|
857
|
-
app_name,
|
|
858
|
-
name,
|
|
859
|
-
environment_name=environment_name,
|
|
860
|
-
)
|
|
861
|
-
if client is None:
|
|
862
|
-
client = await _Client.from_env()
|
|
863
|
-
resolver = Resolver(client=client)
|
|
864
|
-
await resolver.load(obj)
|
|
865
|
-
return obj
|
|
866
|
-
|
|
867
882
|
@synchronizer.no_input_translation
|
|
868
883
|
def __call__(self, *args, **kwargs) -> _Obj:
|
|
869
884
|
"""This acts as the class constructor."""
|
|
@@ -876,18 +891,31 @@ More information on class parameterization can be found here: https://modal.com/
|
|
|
876
891
|
)
|
|
877
892
|
|
|
878
893
|
def __getattr__(self, k):
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
# if
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
894
|
+
if self._user_cls is not None:
|
|
895
|
+
# local class, we can check if there are static attributes and let the user access them
|
|
896
|
+
# except if they are PartialFunction (i.e. methods)
|
|
897
|
+
v = getattr(self._user_cls, k)
|
|
898
|
+
if not isinstance(v, modal.partial_function.PartialFunction):
|
|
899
|
+
return v
|
|
900
|
+
|
|
901
|
+
# We create a synthetic dummy Function that is guaranteed to raise an AttributeError when
|
|
902
|
+
# a user tries to use any of its "live methods" - this lets us raise exceptions for users
|
|
903
|
+
# only if they try to access methods on a Cls as if they were methods on the instance.
|
|
904
|
+
async def error_loader(
|
|
905
|
+
fun: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
906
|
+
):
|
|
907
|
+
raise AttributeError(
|
|
908
|
+
"You can't access methods on a Cls directly - Did you forget to instantiate the class first?\n"
|
|
909
|
+
"e.g. instead of MyClass.method.remote(), do MyClass().method.remote()"
|
|
887
910
|
)
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
911
|
+
|
|
912
|
+
return _Function._from_loader(
|
|
913
|
+
error_loader,
|
|
914
|
+
rep=f"UnboundMethod({self._name}.{k})",
|
|
915
|
+
deps=lambda: [],
|
|
916
|
+
hydrate_lazily=True,
|
|
917
|
+
load_context_overrides=self._load_context_overrides,
|
|
918
|
+
)
|
|
891
919
|
|
|
892
920
|
def _is_local(self) -> bool:
|
|
893
921
|
return self._user_cls is not None
|