modal 0.68.9__py3-none-any.whl → 0.68.14__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.
- modal/_ipython.py +3 -13
- modal/_runtime/user_code_imports.py +4 -2
- modal/_utils/blob_utils.py +19 -6
- modal/_utils/function_utils.py +5 -1
- modal/_utils/grpc_testing.py +6 -2
- modal/_utils/hash_utils.py +38 -9
- modal/client.pyi +2 -2
- modal/cls.py +128 -58
- modal/cls.pyi +13 -7
- modal/functions.py +26 -11
- modal/functions.pyi +5 -3
- modal/mount.py +3 -1
- modal/network_file_system.py +4 -1
- modal/object.py +4 -2
- modal/volume.py +5 -1
- {modal-0.68.9.dist-info → modal-0.68.14.dist-info}/METADATA +1 -1
- {modal-0.68.9.dist-info → modal-0.68.14.dist-info}/RECORD +22 -22
- modal_version/_version_generated.py +1 -1
- {modal-0.68.9.dist-info → modal-0.68.14.dist-info}/LICENSE +0 -0
- {modal-0.68.9.dist-info → modal-0.68.14.dist-info}/WHEEL +0 -0
- {modal-0.68.9.dist-info → modal-0.68.14.dist-info}/entry_points.txt +0 -0
- {modal-0.68.9.dist-info → modal-0.68.14.dist-info}/top_level.txt +0 -0
modal/_ipython.py
CHANGED
@@ -1,21 +1,11 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
import sys
|
3
|
-
import warnings
|
4
|
-
|
5
|
-
ipy_outstream = None
|
6
|
-
try:
|
7
|
-
with warnings.catch_warnings():
|
8
|
-
warnings.simplefilter("ignore")
|
9
|
-
import ipykernel.iostream
|
10
|
-
|
11
|
-
ipy_outstream = ipykernel.iostream.OutStream
|
12
|
-
except ImportError:
|
13
|
-
pass
|
14
3
|
|
15
4
|
|
16
5
|
def is_notebook(stdout=None):
|
17
|
-
|
6
|
+
ipykernel_iostream = sys.modules.get("ipykernel.iostream")
|
7
|
+
if ipykernel_iostream is None:
|
18
8
|
return False
|
19
9
|
if stdout is None:
|
20
10
|
stdout = sys.stdout
|
21
|
-
return isinstance(stdout,
|
11
|
+
return isinstance(stdout, ipykernel_iostream.OutStream)
|
@@ -269,10 +269,12 @@ def import_single_function_service(
|
|
269
269
|
# The cls decorator is in global scope
|
270
270
|
_cls = synchronizer._translate_in(cls)
|
271
271
|
user_defined_callable = _cls._callables[fun_name]
|
272
|
-
function = _cls._method_functions.get(
|
272
|
+
function = _cls._method_functions.get(
|
273
|
+
fun_name
|
274
|
+
) # bound to the class service function - there is no instance
|
273
275
|
active_app = _cls._app
|
274
276
|
else:
|
275
|
-
# This is
|
277
|
+
# This is non-decorated class
|
276
278
|
user_defined_callable = getattr(cls, fun_name)
|
277
279
|
else:
|
278
280
|
raise InvalidError(f"Invalid function qualname {qual_name}")
|
modal/_utils/blob_utils.py
CHANGED
@@ -21,7 +21,7 @@ from modal_proto.modal_api_grpc import ModalClientModal
|
|
21
21
|
from ..exception import ExecutionError
|
22
22
|
from .async_utils import TaskContext, retry
|
23
23
|
from .grpc_utils import retry_transient_errors
|
24
|
-
from .hash_utils import UploadHashes,
|
24
|
+
from .hash_utils import UploadHashes, get_upload_hashes
|
25
25
|
from .http_utils import ClientSessionRegistry
|
26
26
|
from .logger import logger
|
27
27
|
|
@@ -38,6 +38,11 @@ BLOB_MAX_PARALLELISM = 10
|
|
38
38
|
# read ~16MiB chunks by default
|
39
39
|
DEFAULT_SEGMENT_CHUNK_SIZE = 2**24
|
40
40
|
|
41
|
+
# Files larger than this will be multipart uploaded. The server might request multipart upload for smaller files as
|
42
|
+
# well, but the limit will never be raised.
|
43
|
+
# TODO(dano): remove this once we stop requiring md5 for blobs
|
44
|
+
MULTIPART_UPLOAD_THRESHOLD = 1024**3
|
45
|
+
|
41
46
|
|
42
47
|
class BytesIOSegmentPayload(BytesIOPayload):
|
43
48
|
"""Modified bytes payload for concurrent sends of chunks from the same file.
|
@@ -305,9 +310,13 @@ async def blob_upload(payload: bytes, stub: ModalClientModal) -> str:
|
|
305
310
|
|
306
311
|
|
307
312
|
async def blob_upload_file(
|
308
|
-
file_obj: BinaryIO,
|
313
|
+
file_obj: BinaryIO,
|
314
|
+
stub: ModalClientModal,
|
315
|
+
progress_report_cb: Optional[Callable] = None,
|
316
|
+
sha256_hex: Optional[str] = None,
|
317
|
+
md5_hex: Optional[str] = None,
|
309
318
|
) -> str:
|
310
|
-
upload_hashes = get_upload_hashes(file_obj)
|
319
|
+
upload_hashes = get_upload_hashes(file_obj, sha256_hex=sha256_hex, md5_hex=md5_hex)
|
311
320
|
return await _blob_upload(upload_hashes, file_obj, stub, progress_report_cb)
|
312
321
|
|
313
322
|
|
@@ -366,6 +375,7 @@ class FileUploadSpec:
|
|
366
375
|
use_blob: bool
|
367
376
|
content: Optional[bytes] # typically None if using blob, required otherwise
|
368
377
|
sha256_hex: str
|
378
|
+
md5_hex: str
|
369
379
|
mode: int # file permission bits (last 12 bits of st_mode)
|
370
380
|
size: int
|
371
381
|
|
@@ -383,13 +393,15 @@ def _get_file_upload_spec(
|
|
383
393
|
fp.seek(0)
|
384
394
|
|
385
395
|
if size >= LARGE_FILE_LIMIT:
|
396
|
+
# TODO(dano): remove the placeholder md5 once we stop requiring md5 for blobs
|
397
|
+
md5_hex = "baadbaadbaadbaadbaadbaadbaadbaad" if size > MULTIPART_UPLOAD_THRESHOLD else None
|
386
398
|
use_blob = True
|
387
399
|
content = None
|
388
|
-
|
400
|
+
hashes = get_upload_hashes(fp, md5_hex=md5_hex)
|
389
401
|
else:
|
390
402
|
use_blob = False
|
391
403
|
content = fp.read()
|
392
|
-
|
404
|
+
hashes = get_upload_hashes(content)
|
393
405
|
|
394
406
|
return FileUploadSpec(
|
395
407
|
source=source,
|
@@ -397,7 +409,8 @@ def _get_file_upload_spec(
|
|
397
409
|
mount_filename=mount_filename.as_posix(),
|
398
410
|
use_blob=use_blob,
|
399
411
|
content=content,
|
400
|
-
sha256_hex=sha256_hex,
|
412
|
+
sha256_hex=hashes.sha256_hex(),
|
413
|
+
md5_hex=hashes.md5_hex(),
|
401
414
|
mode=mode & 0o7777,
|
402
415
|
size=size,
|
403
416
|
)
|
modal/_utils/function_utils.py
CHANGED
@@ -99,7 +99,11 @@ def get_function_type(is_generator: Optional[bool]) -> "api_pb2.Function.Functio
|
|
99
99
|
|
100
100
|
|
101
101
|
class FunctionInfo:
|
102
|
-
"""Class that helps us extract a bunch of information about a function.
|
102
|
+
"""Class that helps us extract a bunch of information about a locally defined function.
|
103
|
+
|
104
|
+
Used for populating the definition of a remote function, and for making .local() calls
|
105
|
+
on a host with the local definition available.
|
106
|
+
"""
|
103
107
|
|
104
108
|
raw_f: Optional[Callable[..., Any]] # if None - this is a "class service function"
|
105
109
|
function_name: str
|
modal/_utils/grpc_testing.py
CHANGED
@@ -50,7 +50,7 @@ def patch_mock_servicer(cls):
|
|
50
50
|
|
51
51
|
@contextlib.contextmanager
|
52
52
|
def intercept(servicer):
|
53
|
-
ctx = InterceptionContext()
|
53
|
+
ctx = InterceptionContext(servicer)
|
54
54
|
servicer.interception_context = ctx
|
55
55
|
yield ctx
|
56
56
|
ctx._assert_responses_consumed()
|
@@ -101,7 +101,8 @@ class ResponseNotConsumed(Exception):
|
|
101
101
|
|
102
102
|
|
103
103
|
class InterceptionContext:
|
104
|
-
def __init__(self):
|
104
|
+
def __init__(self, servicer):
|
105
|
+
self._servicer = servicer
|
105
106
|
self.calls: list[tuple[str, Any]] = [] # List[Tuple[method_name, message]]
|
106
107
|
self.custom_responses: dict[str, list[tuple[Callable[[Any], bool], list[Any]]]] = defaultdict(list)
|
107
108
|
self.custom_defaults: dict[str, Callable[["MockClientServicer", grpclib.server.Stream], Awaitable[None]]] = {}
|
@@ -149,6 +150,9 @@ class InterceptionContext:
|
|
149
150
|
raise KeyError(f"No message of that type in call list: {self.calls}")
|
150
151
|
|
151
152
|
def get_requests(self, method_name: str) -> list[Any]:
|
153
|
+
if not hasattr(self._servicer, method_name):
|
154
|
+
# we check this to prevent things like `assert ctx.get_requests("ASdfFunctionCreate") == 0` passing
|
155
|
+
raise ValueError(f"{method_name} not in MockServicer - did you spell it right?")
|
152
156
|
return [msg for _method_name, msg in self.calls if _method_name == method_name]
|
153
157
|
|
154
158
|
def _add_recv(self, method_name: str, msg):
|
modal/_utils/hash_utils.py
CHANGED
@@ -3,14 +3,14 @@ import base64
|
|
3
3
|
import dataclasses
|
4
4
|
import hashlib
|
5
5
|
import time
|
6
|
-
from typing import BinaryIO, Callable, Union
|
6
|
+
from typing import BinaryIO, Callable, Optional, Sequence, Union
|
7
7
|
|
8
8
|
from modal.config import logger
|
9
9
|
|
10
10
|
HASH_CHUNK_SIZE = 65536
|
11
11
|
|
12
12
|
|
13
|
-
def _update(hashers:
|
13
|
+
def _update(hashers: Sequence[Callable[[bytes], None]], data: Union[bytes, BinaryIO]) -> None:
|
14
14
|
if isinstance(data, bytes):
|
15
15
|
for hasher in hashers:
|
16
16
|
hasher(data)
|
@@ -57,15 +57,44 @@ class UploadHashes:
|
|
57
57
|
md5_base64: str
|
58
58
|
sha256_base64: str
|
59
59
|
|
60
|
+
def md5_hex(self) -> str:
|
61
|
+
return base64.b64decode(self.md5_base64).hex()
|
60
62
|
|
61
|
-
def
|
63
|
+
def sha256_hex(self) -> str:
|
64
|
+
return base64.b64decode(self.sha256_base64).hex()
|
65
|
+
|
66
|
+
|
67
|
+
def get_upload_hashes(
|
68
|
+
data: Union[bytes, BinaryIO], sha256_hex: Optional[str] = None, md5_hex: Optional[str] = None
|
69
|
+
) -> UploadHashes:
|
62
70
|
t0 = time.monotonic()
|
63
|
-
|
64
|
-
|
65
|
-
|
71
|
+
hashers = {}
|
72
|
+
|
73
|
+
if not sha256_hex:
|
74
|
+
sha256 = hashlib.sha256()
|
75
|
+
hashers["sha256"] = sha256
|
76
|
+
if not md5_hex:
|
77
|
+
md5 = hashlib.md5()
|
78
|
+
hashers["md5"] = md5
|
79
|
+
|
80
|
+
if hashers:
|
81
|
+
updaters = [h.update for h in hashers.values()]
|
82
|
+
_update(updaters, data)
|
83
|
+
|
84
|
+
if sha256_hex:
|
85
|
+
sha256_base64 = base64.b64encode(bytes.fromhex(sha256_hex)).decode("ascii")
|
86
|
+
else:
|
87
|
+
sha256_base64 = base64.b64encode(hashers["sha256"].digest()).decode("ascii")
|
88
|
+
|
89
|
+
if md5_hex:
|
90
|
+
md5_base64 = base64.b64encode(bytes.fromhex(md5_hex)).decode("ascii")
|
91
|
+
else:
|
92
|
+
md5_base64 = base64.b64encode(hashers["md5"].digest()).decode("ascii")
|
93
|
+
|
66
94
|
hashes = UploadHashes(
|
67
|
-
md5_base64=
|
68
|
-
sha256_base64=
|
95
|
+
md5_base64=md5_base64,
|
96
|
+
sha256_base64=sha256_base64,
|
69
97
|
)
|
70
|
-
|
98
|
+
|
99
|
+
logger.debug("get_upload_hashes took %.3fs (%s)", time.monotonic() - t0, hashers.keys())
|
71
100
|
return hashes
|
modal/client.pyi
CHANGED
@@ -26,7 +26,7 @@ class _Client:
|
|
26
26
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
27
27
|
|
28
28
|
def __init__(
|
29
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.
|
29
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.14"
|
30
30
|
): ...
|
31
31
|
def is_closed(self) -> bool: ...
|
32
32
|
@property
|
@@ -81,7 +81,7 @@ class Client:
|
|
81
81
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
82
82
|
|
83
83
|
def __init__(
|
84
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.
|
84
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.14"
|
85
85
|
): ...
|
86
86
|
def is_closed(self) -> bool: ...
|
87
87
|
@property
|
modal/cls.py
CHANGED
@@ -19,7 +19,7 @@ from ._utils.async_utils import synchronize_api, synchronizer
|
|
19
19
|
from ._utils.grpc_utils import retry_transient_errors
|
20
20
|
from ._utils.mount_utils import validate_volumes
|
21
21
|
from .client import _Client
|
22
|
-
from .exception import InvalidError, NotFoundError, VersionError
|
22
|
+
from .exception import ExecutionError, InvalidError, NotFoundError, VersionError
|
23
23
|
from .functions import _Function, _parse_retries
|
24
24
|
from .gpu import GPU_T
|
25
25
|
from .object import _get_environment_name, _Object
|
@@ -40,7 +40,7 @@ if typing.TYPE_CHECKING:
|
|
40
40
|
import modal.app
|
41
41
|
|
42
42
|
|
43
|
-
def _use_annotation_parameters(user_cls) -> bool:
|
43
|
+
def _use_annotation_parameters(user_cls: type) -> bool:
|
44
44
|
has_parameters = any(is_parameter(cls_member) for cls_member in user_cls.__dict__.values())
|
45
45
|
has_explicit_constructor = user_cls.__init__ != object.__init__
|
46
46
|
return has_parameters and not has_explicit_constructor
|
@@ -73,7 +73,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
|
73
73
|
def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
|
74
74
|
"""mdmd:hidden
|
75
75
|
|
76
|
-
Binds an "instance service function" to a specific method.
|
76
|
+
Binds an "instance service function" to a specific method name.
|
77
77
|
This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
|
78
78
|
it does it forward invocations to the underlying instance_service_function with the specified method,
|
79
79
|
and we don't support web_config for parameterized methods at the moment.
|
@@ -84,7 +84,6 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
|
|
84
84
|
# object itself doesn't need any "loading"
|
85
85
|
assert service_function._obj
|
86
86
|
method_name = class_bound_method._use_method_name
|
87
|
-
full_function_name = f"{class_bound_method._function_name}[parameterized]"
|
88
87
|
|
89
88
|
def hydrate_from_instance_service_function(method_placeholder_fun):
|
90
89
|
method_placeholder_fun._hydrate_from_other(service_function)
|
@@ -92,7 +91,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
|
|
92
91
|
method_placeholder_fun._web_url = (
|
93
92
|
class_bound_method._web_url
|
94
93
|
) # TODO: this shouldn't be set when actual parameters are used
|
95
|
-
method_placeholder_fun._function_name =
|
94
|
+
method_placeholder_fun._function_name = f"{class_bound_method._function_name}[parameterized]"
|
96
95
|
method_placeholder_fun._is_generator = class_bound_method._is_generator
|
97
96
|
method_placeholder_fun._cluster_size = class_bound_method._cluster_size
|
98
97
|
method_placeholder_fun._use_method_name = method_name
|
@@ -112,7 +111,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
|
|
112
111
|
return []
|
113
112
|
return [service_function]
|
114
113
|
|
115
|
-
rep = f"Method({
|
114
|
+
rep = f"Method({method_name})"
|
116
115
|
|
117
116
|
fun = _Function._from_loader(
|
118
117
|
_load,
|
@@ -137,22 +136,23 @@ class _Obj:
|
|
137
136
|
|
138
137
|
All this class does is to return `Function` objects."""
|
139
138
|
|
139
|
+
_cls: "_Cls" # parent
|
140
140
|
_functions: dict[str, _Function]
|
141
141
|
_has_entered: bool
|
142
142
|
_user_cls_instance: Optional[Any] = None
|
143
|
-
|
143
|
+
_args: tuple[Any, ...]
|
144
|
+
_kwargs: dict[str, Any]
|
144
145
|
|
145
|
-
_instance_service_function: Optional[_Function]
|
146
|
+
_instance_service_function: Optional[_Function] = None # this gets set lazily
|
146
147
|
|
147
148
|
def _uses_common_service_function(self):
|
148
149
|
# Used for backwards compatibility checks with pre v0.63 classes
|
149
|
-
return self.
|
150
|
+
return self._cls._class_service_function is not None
|
150
151
|
|
151
152
|
def __init__(
|
152
153
|
self,
|
154
|
+
cls: "_Cls",
|
153
155
|
user_cls: Optional[type], # this would be None in case of lookups
|
154
|
-
class_service_function: Optional[_Function], # only None for <v0.63 classes
|
155
|
-
classbound_methods: dict[str, _Function],
|
156
156
|
options: Optional[api_pb2.FunctionOptions],
|
157
157
|
args,
|
158
158
|
kwargs,
|
@@ -161,45 +161,60 @@ class _Obj:
|
|
161
161
|
check_valid_cls_constructor_arg(i + 1, arg)
|
162
162
|
for key, kwarg in kwargs.items():
|
163
163
|
check_valid_cls_constructor_arg(key, kwarg)
|
164
|
-
|
165
|
-
self._method_functions = {}
|
166
|
-
if class_service_function:
|
167
|
-
# >= v0.63 classes
|
168
|
-
# first create the singular object function used by all methods on this parameterization
|
169
|
-
self._instance_service_function = class_service_function._bind_parameters(self, options, args, kwargs)
|
170
|
-
for method_name, class_bound_method in classbound_methods.items():
|
171
|
-
method = _bind_instance_method(self._instance_service_function, class_bound_method)
|
172
|
-
self._method_functions[method_name] = method
|
173
|
-
else:
|
174
|
-
# looked up <v0.63 classes - bind each individual method to the new parameters
|
175
|
-
self._instance_service_function = None
|
176
|
-
for method_name, class_bound_method in classbound_methods.items():
|
177
|
-
method = class_bound_method._bind_parameters(self, options, args, kwargs)
|
178
|
-
self._method_functions[method_name] = method
|
164
|
+
self._cls = cls
|
179
165
|
|
180
166
|
# Used for construction local object lazily
|
181
167
|
self._has_entered = False
|
182
168
|
self._user_cls = user_cls
|
183
|
-
|
169
|
+
|
170
|
+
# used for lazy construction in case of explicit constructors
|
171
|
+
self._args = args
|
172
|
+
self._kwargs = kwargs
|
173
|
+
self._options = options
|
174
|
+
|
175
|
+
def _cached_service_function(self) -> "modal.functions._Function":
|
176
|
+
# Returns a service function for this _Obj, serving all its methods
|
177
|
+
# In case of methods without parameters or options, this is simply proxying to the class service function
|
178
|
+
|
179
|
+
# only safe to call for 0.63+ classes (before then, all methods had their own services)
|
180
|
+
if not self._instance_service_function:
|
181
|
+
assert self._cls._class_service_function
|
182
|
+
self._instance_service_function = self._cls._class_service_function._bind_parameters(
|
183
|
+
self, self._options, self._args, self._kwargs
|
184
|
+
)
|
185
|
+
return self._instance_service_function
|
186
|
+
|
187
|
+
def _get_parameter_values(self) -> dict[str, Any]:
|
188
|
+
# binds args and kwargs according to the class constructor signature
|
189
|
+
# (implicit by parameters or explicit)
|
190
|
+
sig = _get_class_constructor_signature(self._user_cls)
|
191
|
+
bound_vars = sig.bind(*self._args, **self._kwargs)
|
192
|
+
bound_vars.apply_defaults()
|
193
|
+
return bound_vars.arguments
|
184
194
|
|
185
195
|
def _new_user_cls_instance(self):
|
186
|
-
args, kwargs = self._construction_args
|
187
196
|
if not _use_annotation_parameters(self._user_cls):
|
188
197
|
# TODO(elias): deprecate this code path eventually
|
189
|
-
user_cls_instance = self._user_cls(*
|
198
|
+
user_cls_instance = self._user_cls(*self._args, **self._kwargs)
|
190
199
|
else:
|
191
200
|
# ignore constructor (assumes there is no custom constructor,
|
192
201
|
# which is guaranteed by _use_annotation_parameters)
|
193
202
|
# set the attributes on the class corresponding to annotations
|
194
203
|
# with = parameter() specifications
|
195
|
-
|
196
|
-
bound_vars = sig.bind(*args, **kwargs)
|
197
|
-
bound_vars.apply_defaults()
|
204
|
+
param_values = self._get_parameter_values()
|
198
205
|
user_cls_instance = self._user_cls.__new__(self._user_cls) # new instance without running __init__
|
199
|
-
user_cls_instance.__dict__.update(
|
206
|
+
user_cls_instance.__dict__.update(param_values)
|
200
207
|
|
201
208
|
# TODO: always use Obj instances instead of making modifications to user cls
|
202
|
-
|
209
|
+
# TODO: OR (if simpler for now) replace all the PartialFunctions on the user cls
|
210
|
+
# with getattr(self, method_name)
|
211
|
+
|
212
|
+
# user cls instances are only created locally, so we have all partial functions available
|
213
|
+
instance_methods = {}
|
214
|
+
for method_name in _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.FUNCTION):
|
215
|
+
instance_methods[method_name] = getattr(self, method_name)
|
216
|
+
|
217
|
+
user_cls_instance._modal_functions = instance_methods
|
203
218
|
return user_cls_instance
|
204
219
|
|
205
220
|
async def keep_warm(self, warm_pool_size: int) -> None:
|
@@ -221,7 +236,7 @@ class _Obj:
|
|
221
236
|
raise VersionError(
|
222
237
|
"Class instance `.keep_warm(...)` can't be used on classes deployed using client version <v0.63"
|
223
238
|
)
|
224
|
-
await self.
|
239
|
+
await self._cached_service_function().keep_warm(warm_pool_size)
|
225
240
|
|
226
241
|
def _cached_user_cls_instance(self):
|
227
242
|
"""Get or construct the local object
|
@@ -233,18 +248,20 @@ class _Obj:
|
|
233
248
|
return self._user_cls_instance
|
234
249
|
|
235
250
|
def _enter(self):
|
251
|
+
assert self._user_cls
|
236
252
|
if not self._has_entered:
|
237
|
-
|
238
|
-
|
253
|
+
user_cls_instance = self._cached_user_cls_instance()
|
254
|
+
if hasattr(user_cls_instance, "__enter__"):
|
255
|
+
user_cls_instance.__enter__()
|
239
256
|
|
240
257
|
for method_flag in (
|
241
258
|
_PartialFunctionFlags.ENTER_PRE_SNAPSHOT,
|
242
259
|
_PartialFunctionFlags.ENTER_POST_SNAPSHOT,
|
243
260
|
):
|
244
|
-
for enter_method in _find_callables_for_obj(
|
261
|
+
for enter_method in _find_callables_for_obj(user_cls_instance, method_flag).values():
|
245
262
|
enter_method()
|
246
263
|
|
247
|
-
|
264
|
+
self._has_entered = True
|
248
265
|
|
249
266
|
@property
|
250
267
|
def _entered(self) -> bool:
|
@@ -266,23 +283,74 @@ class _Obj:
|
|
266
283
|
self._has_entered = True
|
267
284
|
|
268
285
|
def __getattr__(self, k):
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
286
|
+
# This is a bit messy and branchy because:
|
287
|
+
# * Support for pre-0.63 lookups *and* newer classes
|
288
|
+
# * Support .remote() on both hydrated (local or remote classes) or unhydrated classes (remote classes only)
|
289
|
+
# * Support .local() on both hydrated and unhydrated classes (assuming local access to code)
|
290
|
+
# * Support attribute access (when local cls is available)
|
291
|
+
|
292
|
+
def _get_method_bound_function() -> Optional["_Function"]:
|
293
|
+
"""Gets _Function object for method - either for a local or a hydrated remote class
|
294
|
+
|
295
|
+
* If class is neither local or hydrated - raise exception (should never happen)
|
296
|
+
* If attribute isn't a method - return None
|
297
|
+
"""
|
298
|
+
if self._cls._method_functions is None:
|
299
|
+
raise ExecutionError("Method is not local and not hydrated")
|
300
|
+
|
301
|
+
if class_bound_method := self._cls._method_functions.get(k, None):
|
302
|
+
# If we know the user is accessing a *method* and not another attribute,
|
303
|
+
# we don't have to create an instance of the user class yet.
|
304
|
+
# This is because it might just be a call to `.remote()` on it which
|
305
|
+
# doesn't require a local instance.
|
306
|
+
# As long as we have the service function or params, we can do remote calls
|
307
|
+
# without calling the constructor of the class in the calling context.
|
308
|
+
if self._cls._class_service_function is None:
|
309
|
+
# a <v0.63 lookup
|
310
|
+
return class_bound_method._bind_parameters(self, self._options, self._args, self._kwargs)
|
311
|
+
else:
|
312
|
+
return _bind_instance_method(self._cached_service_function(), class_bound_method)
|
313
|
+
|
314
|
+
return None # The attribute isn't a method
|
315
|
+
|
316
|
+
if self._cls._method_functions is not None:
|
317
|
+
# We get here with either a hydrated Cls or an unhydrated one with local definition
|
318
|
+
if method := _get_method_bound_function():
|
319
|
+
return method
|
320
|
+
elif self._user_cls:
|
321
|
+
# We have the local definition, and the attribute isn't a method
|
322
|
+
# so we instantiate if we don't have an instance, and try to get the attribute
|
323
|
+
user_cls_instance = self._cached_user_cls_instance()
|
324
|
+
return getattr(user_cls_instance, k)
|
325
|
+
else:
|
326
|
+
# This is the case for a *hydrated* class without the local definition, i.e. a lookup
|
327
|
+
# where the attribute isn't a registered method of the class
|
328
|
+
raise NotFoundError(
|
329
|
+
f"Class has no method `{k}` and attributes (or undecorated methods) can't be accessed for"
|
330
|
+
f" remote classes (`Cls.from_name` instances)"
|
331
|
+
)
|
332
|
+
|
333
|
+
# Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
|
334
|
+
# has not yet been loaded. So use a special loader that loads it lazily:
|
277
335
|
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
336
|
+
async def method_loader(fun, resolver: Resolver, existing_object_id):
|
337
|
+
await resolver.load(self._cls) # load class so we get info about methods
|
338
|
+
method_function = _get_method_bound_function()
|
339
|
+
if method_function is None:
|
340
|
+
raise NotFoundError(
|
341
|
+
f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
|
342
|
+
)
|
343
|
+
await resolver.load(method_function) # get the appropriate method handle (lazy)
|
344
|
+
fun._hydrate_from_other(method_function)
|
345
|
+
|
346
|
+
# The reason we don't *always* use this lazy loader is because it precludes attribute access
|
347
|
+
# on local classes.
|
348
|
+
return _Function._from_loader(
|
349
|
+
method_loader,
|
350
|
+
repr,
|
351
|
+
deps=lambda: [], # TODO: use cls as dep instead of loading inside method_loader?
|
352
|
+
hydrate_lazily=True,
|
353
|
+
)
|
286
354
|
|
287
355
|
|
288
356
|
Obj = synchronize_api(_Obj)
|
@@ -313,6 +381,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
313
381
|
self._callables = {}
|
314
382
|
|
315
383
|
def _initialize_from_other(self, other: "_Cls"):
|
384
|
+
super()._initialize_from_other(other)
|
316
385
|
self._user_cls = other._user_cls
|
317
386
|
self._class_service_function = other._class_service_function
|
318
387
|
self._method_functions = other._method_functions
|
@@ -503,7 +572,8 @@ class _Cls(_Object, type_prefix="cs"):
|
|
503
572
|
obj._hydrate(response.class_id, resolver.client, response.handle_metadata)
|
504
573
|
|
505
574
|
rep = f"Ref({app_name})"
|
506
|
-
cls = cls._from_loader(_load_remote, rep, is_another_app=True)
|
575
|
+
cls = cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
|
576
|
+
# TODO: when pre 0.63 is phased out, we can set class_service_function here instead
|
507
577
|
return cls
|
508
578
|
|
509
579
|
def with_options(
|
@@ -594,9 +664,8 @@ class _Cls(_Object, type_prefix="cs"):
|
|
594
664
|
def __call__(self, *args, **kwargs) -> _Obj:
|
595
665
|
"""This acts as the class constructor."""
|
596
666
|
return _Obj(
|
667
|
+
self,
|
597
668
|
self._user_cls,
|
598
|
-
self._class_service_function,
|
599
|
-
self._method_functions,
|
600
669
|
self._options,
|
601
670
|
args,
|
602
671
|
kwargs,
|
@@ -604,6 +673,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
604
673
|
|
605
674
|
def __getattr__(self, k):
|
606
675
|
# Used by CLI and container entrypoint
|
676
|
+
# TODO: remove this method - access to attributes on classes should be discouraged
|
607
677
|
if k in self._method_functions:
|
608
678
|
return self._method_functions[k]
|
609
679
|
return getattr(self._user_cls, k)
|
modal/cls.pyi
CHANGED
@@ -17,29 +17,32 @@ import typing_extensions
|
|
17
17
|
|
18
18
|
T = typing.TypeVar("T")
|
19
19
|
|
20
|
-
def _use_annotation_parameters(user_cls) -> bool: ...
|
20
|
+
def _use_annotation_parameters(user_cls: type) -> bool: ...
|
21
21
|
def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
|
22
22
|
def _bind_instance_method(
|
23
23
|
service_function: modal.functions._Function, class_bound_method: modal.functions._Function
|
24
24
|
): ...
|
25
25
|
|
26
26
|
class _Obj:
|
27
|
+
_cls: _Cls
|
27
28
|
_functions: dict[str, modal.functions._Function]
|
28
29
|
_has_entered: bool
|
29
30
|
_user_cls_instance: typing.Optional[typing.Any]
|
30
|
-
|
31
|
+
_args: tuple[typing.Any, ...]
|
32
|
+
_kwargs: dict[str, typing.Any]
|
31
33
|
_instance_service_function: typing.Optional[modal.functions._Function]
|
32
34
|
|
33
35
|
def _uses_common_service_function(self): ...
|
34
36
|
def __init__(
|
35
37
|
self,
|
38
|
+
cls: _Cls,
|
36
39
|
user_cls: typing.Optional[type],
|
37
|
-
class_service_function: typing.Optional[modal.functions._Function],
|
38
|
-
classbound_methods: dict[str, modal.functions._Function],
|
39
40
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
40
41
|
args,
|
41
42
|
kwargs,
|
42
43
|
): ...
|
44
|
+
def _cached_service_function(self) -> modal.functions._Function: ...
|
45
|
+
def _get_parameter_values(self) -> dict[str, typing.Any]: ...
|
43
46
|
def _new_user_cls_instance(self): ...
|
44
47
|
async def keep_warm(self, warm_pool_size: int) -> None: ...
|
45
48
|
def _cached_user_cls_instance(self): ...
|
@@ -52,22 +55,25 @@ class _Obj:
|
|
52
55
|
def __getattr__(self, k): ...
|
53
56
|
|
54
57
|
class Obj:
|
58
|
+
_cls: Cls
|
55
59
|
_functions: dict[str, modal.functions.Function]
|
56
60
|
_has_entered: bool
|
57
61
|
_user_cls_instance: typing.Optional[typing.Any]
|
58
|
-
|
62
|
+
_args: tuple[typing.Any, ...]
|
63
|
+
_kwargs: dict[str, typing.Any]
|
59
64
|
_instance_service_function: typing.Optional[modal.functions.Function]
|
60
65
|
|
61
66
|
def __init__(
|
62
67
|
self,
|
68
|
+
cls: Cls,
|
63
69
|
user_cls: typing.Optional[type],
|
64
|
-
class_service_function: typing.Optional[modal.functions.Function],
|
65
|
-
classbound_methods: dict[str, modal.functions.Function],
|
66
70
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
67
71
|
args,
|
68
72
|
kwargs,
|
69
73
|
): ...
|
70
74
|
def _uses_common_service_function(self): ...
|
75
|
+
def _cached_service_function(self) -> modal.functions.Function: ...
|
76
|
+
def _get_parameter_values(self) -> dict[str, typing.Any]: ...
|
71
77
|
def _new_user_cls_instance(self): ...
|
72
78
|
|
73
79
|
class __keep_warm_spec(typing_extensions.Protocol):
|
modal/functions.py
CHANGED
@@ -432,7 +432,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
432
432
|
image: _Image,
|
433
433
|
secrets: Sequence[_Secret] = (),
|
434
434
|
schedule: Optional[Schedule] = None,
|
435
|
-
is_generator=False,
|
435
|
+
is_generator: bool = False,
|
436
436
|
gpu: Union[GPU_T, list[GPU_T]] = None,
|
437
437
|
# TODO: maybe break this out into a separate decorator for notebooks.
|
438
438
|
mounts: Collection[_Mount] = (),
|
@@ -628,7 +628,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
628
628
|
raise InvalidError(f"Expected modal.Image object. Got {type(image)}.")
|
629
629
|
|
630
630
|
method_definitions: Optional[dict[str, api_pb2.MethodDefinition]] = None
|
631
|
-
|
631
|
+
|
632
632
|
if info.user_cls:
|
633
633
|
method_definitions = {}
|
634
634
|
partial_functions = _find_partial_methods_for_user_cls(info.user_cls, _PartialFunctionFlags.FUNCTION)
|
@@ -1191,9 +1191,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1191
1191
|
return self._web_url
|
1192
1192
|
|
1193
1193
|
@property
|
1194
|
-
def is_generator(self) -> bool:
|
1194
|
+
async def is_generator(self) -> bool:
|
1195
1195
|
"""mdmd:hidden"""
|
1196
|
-
|
1196
|
+
# hacky: kind of like @live_method, but not hydrating if we have the value already from local source
|
1197
|
+
if self._is_generator is not None:
|
1198
|
+
# this is set if the function or class is local
|
1199
|
+
return self._is_generator
|
1200
|
+
|
1201
|
+
# not set - this is a from_name lookup - hydrate
|
1202
|
+
await self.resolve()
|
1203
|
+
assert self._is_generator is not None # should be set now
|
1197
1204
|
return self._is_generator
|
1198
1205
|
|
1199
1206
|
@property
|
@@ -1318,6 +1325,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1318
1325
|
async for item in self._call_generator(args, kwargs): # type: ignore
|
1319
1326
|
yield item
|
1320
1327
|
|
1328
|
+
def _is_local(self):
|
1329
|
+
return self._info is not None
|
1330
|
+
|
1321
1331
|
def _get_info(self) -> FunctionInfo:
|
1322
1332
|
if not self._info:
|
1323
1333
|
raise ExecutionError("Can't get info for a function that isn't locally defined")
|
@@ -1342,19 +1352,24 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1342
1352
|
"""
|
1343
1353
|
# TODO(erikbern): it would be nice to remove the nowrap thing, but right now that would cause
|
1344
1354
|
# "user code" to run on the synchronicity thread, which seems bad
|
1355
|
+
if not self._is_local():
|
1356
|
+
msg = (
|
1357
|
+
"The definition for this function is missing here so it is not possible to invoke it locally. "
|
1358
|
+
"If this function was retrieved via `Function.lookup` you need to use `.remote()`."
|
1359
|
+
)
|
1360
|
+
raise ExecutionError(msg)
|
1361
|
+
|
1345
1362
|
info = self._get_info()
|
1363
|
+
if not info.raw_f:
|
1364
|
+
# Here if calling .local on a service function itself which should never happen
|
1365
|
+
# TODO: check if we end up here in a container for a serialized function?
|
1366
|
+
raise ExecutionError("Can't call .local on service function")
|
1346
1367
|
|
1347
1368
|
if is_local() and self.spec.volumes or self.spec.network_file_systems:
|
1348
1369
|
warnings.warn(
|
1349
1370
|
f"The {info.function_name} function is executing locally "
|
1350
1371
|
+ "and will not have access to the mounted Volume or NetworkFileSystem data"
|
1351
1372
|
)
|
1352
|
-
if not info or not info.raw_f:
|
1353
|
-
msg = (
|
1354
|
-
"The definition for this function is missing so it is not possible to invoke it locally. "
|
1355
|
-
"If this function was retrieved via `Function.lookup` you need to use `.remote()`."
|
1356
|
-
)
|
1357
|
-
raise ExecutionError(msg)
|
1358
1373
|
|
1359
1374
|
obj: Optional["modal.cls._Obj"] = self._get_obj()
|
1360
1375
|
|
@@ -1364,9 +1379,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1364
1379
|
else:
|
1365
1380
|
# This is a method on a class, so bind the self to the function
|
1366
1381
|
user_cls_instance = obj._cached_user_cls_instance()
|
1367
|
-
|
1368
1382
|
fun = info.raw_f.__get__(user_cls_instance)
|
1369
1383
|
|
1384
|
+
# TODO: replace implicit local enter/exit with a context manager
|
1370
1385
|
if is_async(info.raw_f):
|
1371
1386
|
# We want to run __aenter__ and fun in the same coroutine
|
1372
1387
|
async def coro():
|
modal/functions.pyi
CHANGED
@@ -157,7 +157,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
157
157
|
image: modal.image._Image,
|
158
158
|
secrets: collections.abc.Sequence[modal.secret._Secret] = (),
|
159
159
|
schedule: typing.Optional[modal.schedule.Schedule] = None,
|
160
|
-
is_generator=False,
|
160
|
+
is_generator: bool = False,
|
161
161
|
gpu: typing.Union[
|
162
162
|
None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]
|
163
163
|
] = None,
|
@@ -234,7 +234,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
234
234
|
@property
|
235
235
|
async def web_url(self) -> str: ...
|
236
236
|
@property
|
237
|
-
def is_generator(self) -> bool: ...
|
237
|
+
async def is_generator(self) -> bool: ...
|
238
238
|
@property
|
239
239
|
def cluster_size(self) -> int: ...
|
240
240
|
def _map(
|
@@ -246,6 +246,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
246
246
|
async def _call_generator_nowait(self, args, kwargs): ...
|
247
247
|
async def remote(self, *args: P.args, **kwargs: P.kwargs) -> ReturnType: ...
|
248
248
|
def remote_gen(self, *args, **kwargs) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
|
249
|
+
def _is_local(self): ...
|
249
250
|
def _get_info(self) -> modal._utils.function_utils.FunctionInfo: ...
|
250
251
|
def _get_obj(self) -> typing.Optional[modal.cls._Obj]: ...
|
251
252
|
def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
|
@@ -325,7 +326,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
325
326
|
image: modal.image.Image,
|
326
327
|
secrets: collections.abc.Sequence[modal.secret.Secret] = (),
|
327
328
|
schedule: typing.Optional[modal.schedule.Schedule] = None,
|
328
|
-
is_generator=False,
|
329
|
+
is_generator: bool = False,
|
329
330
|
gpu: typing.Union[
|
330
331
|
None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]
|
331
332
|
] = None,
|
@@ -467,6 +468,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
467
468
|
|
468
469
|
remote_gen: __remote_gen_spec
|
469
470
|
|
471
|
+
def _is_local(self): ...
|
470
472
|
def _get_info(self) -> modal._utils.function_utils.FunctionInfo: ...
|
471
473
|
def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
|
472
474
|
def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
|
modal/mount.py
CHANGED
@@ -483,7 +483,9 @@ class _Mount(_Object, type_prefix="mo"):
|
|
483
483
|
logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
|
484
484
|
async with blob_upload_concurrency:
|
485
485
|
with file_spec.source() as fp:
|
486
|
-
blob_id = await blob_upload_file(
|
486
|
+
blob_id = await blob_upload_file(
|
487
|
+
fp, resolver.client.stub, sha256_hex=file_spec.sha256_hex, md5_hex=file_spec.md5_hex
|
488
|
+
)
|
487
489
|
logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
|
488
490
|
request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
|
489
491
|
else:
|
modal/network_file_system.py
CHANGED
@@ -245,7 +245,10 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
245
245
|
if data_size > LARGE_FILE_LIMIT:
|
246
246
|
progress_task_id = progress_cb(name=remote_path, size=data_size)
|
247
247
|
blob_id = await blob_upload_file(
|
248
|
-
fp,
|
248
|
+
fp,
|
249
|
+
self._client.stub,
|
250
|
+
progress_report_cb=functools.partial(progress_cb, progress_task_id),
|
251
|
+
sha256_hex=sha_hash,
|
249
252
|
)
|
250
253
|
req = api_pb2.SharedVolumePutFileRequest(
|
251
254
|
shared_volume_id=self.object_id,
|
modal/object.py
CHANGED
@@ -96,7 +96,9 @@ class _Object:
|
|
96
96
|
|
97
97
|
def _initialize_from_other(self, other):
|
98
98
|
# default implementation, can be overriden in subclasses
|
99
|
-
|
99
|
+
self._object_id = other._object_id
|
100
|
+
self._is_hydrated = other._is_hydrated
|
101
|
+
self._client = other._client
|
100
102
|
|
101
103
|
def _hydrate(self, object_id: str, client: _Client, metadata: Optional[Message]):
|
102
104
|
assert isinstance(object_id, str)
|
@@ -139,7 +141,7 @@ class _Object:
|
|
139
141
|
|
140
142
|
# Object to clone must already be hydrated, otherwise from_loader is more suitable.
|
141
143
|
self._validate_is_hydrated()
|
142
|
-
obj =
|
144
|
+
obj = type(self).__new__(type(self))
|
143
145
|
obj._initialize_from_other(self)
|
144
146
|
return obj
|
145
147
|
|
modal/volume.py
CHANGED
@@ -632,7 +632,11 @@ class _VolumeUploadContextManager:
|
|
632
632
|
logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
|
633
633
|
with file_spec.source() as fp:
|
634
634
|
blob_id = await blob_upload_file(
|
635
|
-
fp,
|
635
|
+
fp,
|
636
|
+
self._client.stub,
|
637
|
+
functools.partial(self._progress_cb, progress_task_id),
|
638
|
+
sha256_hex=file_spec.sha256_hex,
|
639
|
+
md5_hex=file_spec.md5_hex,
|
636
640
|
)
|
637
641
|
logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
|
638
642
|
request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
|
@@ -3,7 +3,7 @@ modal/__main__.py,sha256=scYhGFqh8OJcVDo-VOxIT6CCwxOgzgflYWMnIZiMRqE,2871
|
|
3
3
|
modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
|
4
4
|
modal/_clustered_functions.pyi,sha256=vllkegc99A0jrUOWa8mdlSbdp6uz36TsHhGxysAOpaQ,771
|
5
5
|
modal/_container_entrypoint.py,sha256=wk10vA5vRZZsVwQ0yINOLd0i-NwH7x6XbhTslumvGjo,28910
|
6
|
-
modal/_ipython.py,sha256=
|
6
|
+
modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
|
7
7
|
modal/_location.py,sha256=S3lSxIU3h9HkWpkJ3Pwo0pqjIOSB1fjeSgUsY3x7eec,1202
|
8
8
|
modal/_output.py,sha256=0fWX_KQwhER--U81ys16CL-pA5A-LN20C0EZjElKGJQ,25410
|
9
9
|
modal/_proxy_tunnel.py,sha256=gnKyCfmVB7x2d1A6c-JDysNIP3kEFxmXzhcXhPrzPn0,1906
|
@@ -19,11 +19,11 @@ modal/app.py,sha256=EJ7FUN6rWnSwLJoYJh8nmKg_t-8hdN8_rt0OrkP7JvQ,46084
|
|
19
19
|
modal/app.pyi,sha256=BE5SlR5tRECuc6-e2lUuOknDdov3zxgZ4N0AsLb5ZVQ,25270
|
20
20
|
modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
|
21
21
|
modal/client.py,sha256=nyPjfromWBeOyurexpFP2QLQNk822RPggMCLyX9j1jA,15247
|
22
|
-
modal/client.pyi,sha256=
|
22
|
+
modal/client.pyi,sha256=4x5j0yBlE_sZf8cS2eChs-pYuxr-_SsRiKrf1fSJmfA,7280
|
23
23
|
modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
|
24
24
|
modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
|
25
|
-
modal/cls.py,sha256=
|
26
|
-
modal/cls.pyi,sha256=
|
25
|
+
modal/cls.py,sha256=ONnrfZ2vPcaY2JuKypPiBA9eTiyg8Qfg-Ull40nn9zs,30956
|
26
|
+
modal/cls.pyi,sha256=uoOEANXgCFT9Au3e-_bU98M6ZfAgQWF5ngj8f4c6qpY,8225
|
27
27
|
modal/config.py,sha256=1KhNJkjYsJkX1V8RPPdRYPlM2HE-ZZs0JVSxbiXjmrw,11010
|
28
28
|
modal/container_process.py,sha256=zDxCLk6KfJT1G9FfNtjom6gekBQ46op3TWepT7-Hkbg,6077
|
29
29
|
modal/container_process.pyi,sha256=dqtqBmyRpXXpRrDooESL6WBVU_1Rh6OG-66P2Hk9E5U,2666
|
@@ -35,18 +35,18 @@ modal/exception.py,sha256=dRK789TD1HaB63kHhu1yZuvS2vP_Vua3iLMBtA6dgqk,7128
|
|
35
35
|
modal/experimental.py,sha256=jFuNbwrNHos47viMB9q-cHJSvf2RDxDdoEcss9plaZE,2302
|
36
36
|
modal/file_io.py,sha256=q8s872qf6Ntdw7dPogDlpYbixxGkwCA0BlQn2UUoVhY,14637
|
37
37
|
modal/file_io.pyi,sha256=pfkmJiaBpMCZReE6-KCjYOzB1dVtyYDYokJoYX8ARK4,6932
|
38
|
-
modal/functions.py,sha256=
|
39
|
-
modal/functions.pyi,sha256=
|
38
|
+
modal/functions.py,sha256=IIdHw0FNOdoMksG1b2zvkn8f-xskhJu07ZvHMey9iq4,67667
|
39
|
+
modal/functions.pyi,sha256=EYH4w4VgQtdbEWLGarnU5QtYVfuM2_tnovKFEbYyg2c,25068
|
40
40
|
modal/gpu.py,sha256=r4rL6uH3UJIQthzYvfWauXNyh01WqCPtKZCmmSX1fd4,6881
|
41
41
|
modal/image.py,sha256=cQ6WP1xHXZT_nY8z3aEFiGwKzrTV0yxi3Ab8JzF91eo,79653
|
42
42
|
modal/image.pyi,sha256=PIKH6JBA4L5TfdJrQu3pm2ykyIITmiP920TpP8cdyQA,24585
|
43
43
|
modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
|
44
44
|
modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
|
45
|
-
modal/mount.py,sha256=
|
45
|
+
modal/mount.py,sha256=7FJrS-QkRJGndKuvRnMz452wfUcbLpd_UEnmFgQCKQQ,27770
|
46
46
|
modal/mount.pyi,sha256=3e4nkXUeeVmUmOyK8Tiyk_EQlHeWruN3yGJVnmDUVrI,9761
|
47
|
-
modal/network_file_system.py,sha256=
|
47
|
+
modal/network_file_system.py,sha256=kwwQLCJVO086FTiAWSF_jz9BkqijZLpSbEYXpFvS0Ik,14600
|
48
48
|
modal/network_file_system.pyi,sha256=8mHKXuRkxHPazF6ljIW7g4M5aVqLSl6eKUPLgDCug5c,7901
|
49
|
-
modal/object.py,sha256=
|
49
|
+
modal/object.py,sha256=HZs3N59C6JxlMuPQWJYvrWV1FEEkH9txUovVDorVUbs,9763
|
50
50
|
modal/object.pyi,sha256=MO78H9yFSE5i1gExPEwyyQzLdlshkcGHN1aQ0ylyvq0,8802
|
51
51
|
modal/output.py,sha256=N0xf4qeudEaYrslzdAl35VKV8rapstgIM2e9wO8_iy0,1967
|
52
52
|
modal/parallel_map.py,sha256=4aoMXIrlG3wl5Ifk2YDNOQkXsGRsm6Xbfm6WtJ2t3WY,16002
|
@@ -73,22 +73,22 @@ modal/serving.pyi,sha256=ncV-9jY_vZYFnGs5ZnMb3ffrX8LmcLdIMHBC56xRbtE,1711
|
|
73
73
|
modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
|
74
74
|
modal/token_flow.py,sha256=LcgSce_MSQ2p7j55DPwpVRpiAtCDe8GRSEwzO7muNR8,6774
|
75
75
|
modal/token_flow.pyi,sha256=gOYtYujrWt_JFZeiI8EmfahXPx5GCR5Na-VaPQcWgEY,1937
|
76
|
-
modal/volume.py,sha256=
|
76
|
+
modal/volume.py,sha256=PGzbninvRU-IhSwJgM2jZKzD8llRhZhadsOxZ-YNwaM,29316
|
77
77
|
modal/volume.pyi,sha256=St0mDiaojfep6Bs4sBbkRJmeacYHF6lh6FKOWGmheHA,11182
|
78
78
|
modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
|
79
79
|
modal/_runtime/asgi.py,sha256=GvuxZqWnIHMIR-Bx5f7toCQlkERaJO8CHjTPNM9IFIw,21537
|
80
80
|
modal/_runtime/container_io_manager.py,sha256=ctgyNFiHjq1brCrabXmlurkAXjnrCeWPRvTVa735vRw,44215
|
81
81
|
modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7JpLq4Kg,2712
|
82
82
|
modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
|
83
|
-
modal/_runtime/user_code_imports.py,sha256=
|
83
|
+
modal/_runtime/user_code_imports.py,sha256=4fI0F9OIaNOcO_S4Tx2JcnYwZwZq6JdhMAkUzYN6je4,14629
|
84
84
|
modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
|
85
85
|
modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
|
86
86
|
modal/_utils/async_utils.py,sha256=9ubwMkwiDB4gzOYG2jL9j7Fs-5dxHjcifZe3r7JRg-k,25091
|
87
|
-
modal/_utils/blob_utils.py,sha256=
|
88
|
-
modal/_utils/function_utils.py,sha256=
|
89
|
-
modal/_utils/grpc_testing.py,sha256=
|
87
|
+
modal/_utils/blob_utils.py,sha256=1D_dXspFdsVxkW3gsYH-wJUUHiCpYvlwhmCgYZZgN9k,17237
|
88
|
+
modal/_utils/function_utils.py,sha256=LgcveUUb4XU_dWxtqgK_3ujZBvS3cGVzcDOkljyFZ2w,25066
|
89
|
+
modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
|
90
90
|
modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
|
91
|
-
modal/_utils/hash_utils.py,sha256=
|
91
|
+
modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
|
92
92
|
modal/_utils/http_utils.py,sha256=VKXYNPJtrSwZ1ttcXVGQUWmn8cLAXiOTv05g2ac3GbU,2179
|
93
93
|
modal/_utils/logger.py,sha256=ePzdudrtx9jJCjuO6-bcL_kwUJfi4AwloUmIiNtqkY0,1330
|
94
94
|
modal/_utils/mount_utils.py,sha256=J-FRZbPQv1i__Tob-FIpbB1oXWpFLAwZiB4OCiJpFG0,3206
|
@@ -161,10 +161,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
|
|
161
161
|
modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
162
162
|
modal_version/__init__.py,sha256=RT6zPoOdFO99u5Wcxxaoir4ZCuPTbQ22cvzFAXl3vUY,470
|
163
163
|
modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
|
164
|
-
modal_version/_version_generated.py,sha256=
|
165
|
-
modal-0.68.
|
166
|
-
modal-0.68.
|
167
|
-
modal-0.68.
|
168
|
-
modal-0.68.
|
169
|
-
modal-0.68.
|
170
|
-
modal-0.68.
|
164
|
+
modal_version/_version_generated.py,sha256=SWqJPFBaGp3gDChTb6Zx02oI_iQyIRtKpGgcw69Yegs,149
|
165
|
+
modal-0.68.14.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
166
|
+
modal-0.68.14.dist-info/METADATA,sha256=gHSGyvKb5gQiL1YbO3mRis3T8En0t19ug9mfOY4HYos,2329
|
167
|
+
modal-0.68.14.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
168
|
+
modal-0.68.14.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
|
169
|
+
modal-0.68.14.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
|
170
|
+
modal-0.68.14.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|