modal 0.67.1__py3-none-any.whl → 0.67.33__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/_clustered_functions.py +2 -2
- modal/_clustered_functions.pyi +2 -2
- modal/_container_entrypoint.py +8 -5
- modal/_output.py +29 -28
- modal/_pty.py +2 -2
- modal/_resolver.py +6 -5
- modal/_resources.py +3 -3
- modal/_runtime/asgi.py +46 -6
- modal/_runtime/container_io_manager.py +22 -26
- modal/_runtime/execution_context.py +2 -2
- modal/_runtime/telemetry.py +1 -2
- modal/_runtime/user_code_imports.py +12 -14
- modal/_serialization.py +3 -7
- modal/_traceback.py +5 -5
- modal/_tunnel.py +5 -4
- modal/_tunnel.pyi +2 -2
- modal/_utils/async_utils.py +53 -17
- modal/_utils/blob_utils.py +22 -7
- modal/_utils/function_utils.py +20 -10
- modal/_utils/grpc_testing.py +7 -6
- modal/_utils/grpc_utils.py +2 -3
- modal/_utils/hash_utils.py +2 -2
- modal/_utils/mount_utils.py +5 -4
- modal/_utils/package_utils.py +2 -3
- modal/_utils/pattern_matcher.py +6 -6
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +2 -1
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +8 -7
- modal/app.py +68 -62
- modal/app.pyi +104 -99
- modal/call_graph.py +6 -6
- modal/cli/_download.py +3 -2
- modal/cli/_traceback.py +4 -4
- modal/cli/app.py +4 -4
- modal/cli/container.py +4 -4
- modal/cli/dict.py +1 -1
- modal/cli/environment.py +2 -3
- modal/cli/import_refs.py +1 -1
- modal/cli/launch.py +2 -2
- modal/cli/network_file_system.py +1 -1
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +2 -2
- modal/cli/programs/vscode.py +3 -3
- modal/cli/queues.py +1 -1
- modal/cli/run.py +6 -6
- modal/cli/secret.py +3 -3
- modal/cli/utils.py +2 -1
- modal/cli/volume.py +3 -3
- modal/client.py +6 -11
- modal/client.pyi +18 -27
- modal/cloud_bucket_mount.py +3 -3
- modal/cloud_bucket_mount.pyi +2 -2
- modal/cls.py +100 -47
- modal/cls.pyi +40 -40
- modal/config.py +3 -2
- modal/container_process.py +6 -2
- modal/dict.py +6 -3
- modal/dict.pyi +10 -9
- modal/environments.py +3 -3
- modal/environments.pyi +3 -3
- modal/exception.py +2 -3
- modal/functions.py +112 -104
- modal/functions.pyi +77 -58
- modal/image.py +59 -57
- modal/image.pyi +104 -103
- modal/io_streams.py +20 -12
- modal/io_streams.pyi +24 -14
- modal/mount.py +24 -24
- modal/mount.pyi +28 -29
- modal/network_file_system.py +14 -11
- modal/network_file_system.pyi +12 -11
- modal/object.py +9 -8
- modal/object.pyi +47 -34
- modal/output.py +2 -1
- modal/parallel_map.py +4 -4
- modal/partial_function.py +10 -14
- modal/partial_function.pyi +17 -18
- modal/queue.py +11 -8
- modal/queue.pyi +23 -22
- modal/retries.py +38 -0
- modal/runner.py +8 -7
- modal/runner.pyi +8 -14
- modal/running_app.py +3 -3
- modal/sandbox.py +20 -13
- modal/sandbox.pyi +73 -72
- modal/scheduler_placement.py +2 -1
- modal/secret.py +7 -7
- modal/secret.pyi +12 -12
- modal/serving.py +4 -3
- modal/serving.pyi +5 -4
- modal/token_flow.py +3 -2
- modal/token_flow.pyi +3 -3
- modal/volume.py +16 -23
- modal/volume.pyi +17 -16
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/METADATA +2 -2
- modal-0.67.33.dist-info/RECORD +168 -0
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/mounts/python_standalone.py +1 -1
- modal_proto/api.proto +15 -0
- modal_proto/api_grpc.py +32 -0
- modal_proto/api_pb2.py +674 -654
- modal_proto/api_pb2.pyi +45 -1
- modal_proto/api_pb2_grpc.py +66 -0
- modal_proto/api_pb2_grpc.pyi +20 -0
- modal_proto/modal_api_grpc.py +2 -0
- modal_version/_version_generated.py +1 -1
- modal-0.67.1.dist-info/RECORD +0 -168
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/LICENSE +0 -0
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/WHEEL +0 -0
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/entry_points.txt +0 -0
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/top_level.txt +0 -0
modal/cls.py
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
import inspect
|
3
3
|
import os
|
4
4
|
import typing
|
5
|
-
from
|
5
|
+
from collections.abc import Collection
|
6
|
+
from typing import Any, Callable, Optional, TypeVar, Union
|
6
7
|
|
7
8
|
from google.protobuf.message import Message
|
8
9
|
from grpclib import GRPCError, Status
|
@@ -71,15 +72,77 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
|
71
72
|
return inspect.Signature(constructor_parameters)
|
72
73
|
|
73
74
|
|
75
|
+
def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
|
76
|
+
"""mdmd:hidden
|
77
|
+
|
78
|
+
Binds an "instance service function" to a specific method.
|
79
|
+
This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
|
80
|
+
it does it forward invocations to the underlying instance_service_function with the specified method,
|
81
|
+
and we don't support web_config for parameterized methods at the moment.
|
82
|
+
"""
|
83
|
+
# TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
|
84
|
+
# underlying instance_service_function. It's currently used in order to take advantage
|
85
|
+
# of resolver logic and get "chained" resolution of lazy loads, even though this thin
|
86
|
+
# object itself doesn't need any "loading"
|
87
|
+
assert service_function._obj
|
88
|
+
method_name = class_bound_method._use_method_name
|
89
|
+
full_function_name = f"{class_bound_method._function_name}[parameterized]"
|
90
|
+
|
91
|
+
def hydrate_from_instance_service_function(method_placeholder_fun):
|
92
|
+
method_placeholder_fun._hydrate_from_other(service_function)
|
93
|
+
method_placeholder_fun._obj = service_function._obj
|
94
|
+
method_placeholder_fun._web_url = (
|
95
|
+
class_bound_method._web_url
|
96
|
+
) # TODO: this shouldn't be set when actual parameters are used
|
97
|
+
method_placeholder_fun._function_name = full_function_name
|
98
|
+
method_placeholder_fun._is_generator = class_bound_method._is_generator
|
99
|
+
method_placeholder_fun._cluster_size = class_bound_method._cluster_size
|
100
|
+
method_placeholder_fun._use_method_name = method_name
|
101
|
+
method_placeholder_fun._is_method = True
|
102
|
+
|
103
|
+
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
104
|
+
# there is currently no actual loading logic executed to create each method on
|
105
|
+
# the *parameterized* instance of a class - it uses the parameter-bound service-function
|
106
|
+
# for the instance. This load method just makes sure to set all attributes after the
|
107
|
+
# `service_function` has been loaded (it's in the `_deps`)
|
108
|
+
hydrate_from_instance_service_function(fun)
|
109
|
+
|
110
|
+
def _deps():
|
111
|
+
if service_function.is_hydrated:
|
112
|
+
# without this check, the common service_function will be reloaded by all methods
|
113
|
+
# TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
|
114
|
+
return []
|
115
|
+
return [service_function]
|
116
|
+
|
117
|
+
rep = f"Method({full_function_name})"
|
118
|
+
|
119
|
+
fun = _Function._from_loader(
|
120
|
+
_load,
|
121
|
+
rep,
|
122
|
+
deps=_deps,
|
123
|
+
hydrate_lazily=True,
|
124
|
+
)
|
125
|
+
if service_function.is_hydrated:
|
126
|
+
# Eager hydration (skip load) if the instance service function is already loaded
|
127
|
+
hydrate_from_instance_service_function(fun)
|
128
|
+
|
129
|
+
fun._info = class_bound_method._info
|
130
|
+
fun._obj = service_function._obj
|
131
|
+
fun._is_method = True
|
132
|
+
fun._app = class_bound_method._app
|
133
|
+
fun._spec = class_bound_method._spec
|
134
|
+
return fun
|
135
|
+
|
136
|
+
|
74
137
|
class _Obj:
|
75
138
|
"""An instance of a `Cls`, i.e. `Cls("foo", 42)` returns an `Obj`.
|
76
139
|
|
77
140
|
All this class does is to return `Function` objects."""
|
78
141
|
|
79
|
-
_functions:
|
80
|
-
|
142
|
+
_functions: dict[str, _Function]
|
143
|
+
_has_entered: bool
|
81
144
|
_user_cls_instance: Optional[Any] = None
|
82
|
-
_construction_args:
|
145
|
+
_construction_args: tuple[tuple, dict[str, Any]]
|
83
146
|
|
84
147
|
_instance_service_function: Optional[_Function]
|
85
148
|
|
@@ -89,10 +152,9 @@ class _Obj:
|
|
89
152
|
|
90
153
|
def __init__(
|
91
154
|
self,
|
92
|
-
user_cls: type,
|
155
|
+
user_cls: Optional[type], # this would be None in case of lookups
|
93
156
|
class_service_function: Optional[_Function], # only None for <v0.63 classes
|
94
|
-
classbound_methods:
|
95
|
-
from_other_workspace: bool,
|
157
|
+
classbound_methods: dict[str, _Function],
|
96
158
|
options: Optional[api_pb2.FunctionOptions],
|
97
159
|
args,
|
98
160
|
kwargs,
|
@@ -106,22 +168,19 @@ class _Obj:
|
|
106
168
|
if class_service_function:
|
107
169
|
# >= v0.63 classes
|
108
170
|
# first create the singular object function used by all methods on this parameterization
|
109
|
-
self._instance_service_function = class_service_function._bind_parameters(
|
110
|
-
self, from_other_workspace, options, args, kwargs
|
111
|
-
)
|
171
|
+
self._instance_service_function = class_service_function._bind_parameters(self, options, args, kwargs)
|
112
172
|
for method_name, class_bound_method in classbound_methods.items():
|
113
|
-
method = self._instance_service_function
|
173
|
+
method = _bind_instance_method(self._instance_service_function, class_bound_method)
|
114
174
|
self._method_functions[method_name] = method
|
115
175
|
else:
|
116
176
|
# looked up <v0.63 classes - bind each individual method to the new parameters
|
117
177
|
self._instance_service_function = None
|
118
178
|
for method_name, class_bound_method in classbound_methods.items():
|
119
|
-
method = class_bound_method._bind_parameters(self,
|
179
|
+
method = class_bound_method._bind_parameters(self, options, args, kwargs)
|
120
180
|
self._method_functions[method_name] = method
|
121
181
|
|
122
182
|
# Used for construction local object lazily
|
123
|
-
self.
|
124
|
-
self._local_user_cls_instance = None
|
183
|
+
self._has_entered = False
|
125
184
|
self._user_cls = user_cls
|
126
185
|
self._construction_args = (args, kwargs) # used for lazy construction in case of explicit constructors
|
127
186
|
|
@@ -154,7 +213,7 @@ class _Obj:
|
|
154
213
|
Note that all Modal methods and web endpoints of a class share the same set
|
155
214
|
of containers and the warm_pool_size affects that common container pool.
|
156
215
|
|
157
|
-
```python
|
216
|
+
```python notest
|
158
217
|
# Usage on a parametrized function.
|
159
218
|
Model = modal.Cls.lookup("my-app", "Model")
|
160
219
|
Model("fine-tuned-model").keep_warm(2)
|
@@ -175,8 +234,8 @@ class _Obj:
|
|
175
234
|
|
176
235
|
return self._user_cls_instance
|
177
236
|
|
178
|
-
def
|
179
|
-
if not self.
|
237
|
+
def _enter(self):
|
238
|
+
if not self._has_entered:
|
180
239
|
if hasattr(self._user_cls_instance, "__enter__"):
|
181
240
|
self._user_cls_instance.__enter__()
|
182
241
|
|
@@ -187,26 +246,26 @@ class _Obj:
|
|
187
246
|
for enter_method in _find_callables_for_obj(self._user_cls_instance, method_flag).values():
|
188
247
|
enter_method()
|
189
248
|
|
190
|
-
self.
|
249
|
+
self._has_entered = True
|
191
250
|
|
192
251
|
@property
|
193
|
-
def
|
194
|
-
# needed because
|
195
|
-
return self.
|
252
|
+
def _entered(self) -> bool:
|
253
|
+
# needed because _aenter is nowrap
|
254
|
+
return self._has_entered
|
196
255
|
|
197
|
-
@
|
198
|
-
def
|
199
|
-
self.
|
256
|
+
@_entered.setter
|
257
|
+
def _entered(self, val: bool):
|
258
|
+
self._has_entered = val
|
200
259
|
|
201
260
|
@synchronizer.nowrap
|
202
|
-
async def
|
203
|
-
if not self.
|
261
|
+
async def _aenter(self):
|
262
|
+
if not self._entered: # use the property to get at the impl class
|
204
263
|
user_cls_instance = self._cached_user_cls_instance()
|
205
264
|
if hasattr(user_cls_instance, "__aenter__"):
|
206
265
|
await user_cls_instance.__aenter__()
|
207
266
|
elif hasattr(user_cls_instance, "__enter__"):
|
208
267
|
user_cls_instance.__enter__()
|
209
|
-
self.
|
268
|
+
self._has_entered = True
|
210
269
|
|
211
270
|
def __getattr__(self, k):
|
212
271
|
if k in self._method_functions:
|
@@ -244,10 +303,9 @@ class _Cls(_Object, type_prefix="cs"):
|
|
244
303
|
_class_service_function: Optional[
|
245
304
|
_Function
|
246
305
|
] # The _Function serving *all* methods of the class, used for version >=v0.63
|
247
|
-
_method_functions: Optional[
|
306
|
+
_method_functions: Optional[dict[str, _Function]] = None # Placeholder _Functions for each method
|
248
307
|
_options: Optional[api_pb2.FunctionOptions]
|
249
|
-
_callables:
|
250
|
-
_from_other_workspace: Optional[bool] # Functions require FunctionBindParams before invocation.
|
308
|
+
_callables: dict[str, Callable[..., Any]]
|
251
309
|
_app: Optional["modal.app._App"] = None # not set for lookups
|
252
310
|
|
253
311
|
def _initialize_from_empty(self):
|
@@ -255,7 +313,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
255
313
|
self._class_service_function = None
|
256
314
|
self._options = None
|
257
315
|
self._callables = {}
|
258
|
-
self._from_other_workspace = None
|
259
316
|
|
260
317
|
def _initialize_from_other(self, other: "_Cls"):
|
261
318
|
self._user_cls = other._user_cls
|
@@ -263,9 +320,8 @@ class _Cls(_Object, type_prefix="cs"):
|
|
263
320
|
self._method_functions = other._method_functions
|
264
321
|
self._options = other._options
|
265
322
|
self._callables = other._callables
|
266
|
-
self._from_other_workspace = other._from_other_workspace
|
267
323
|
|
268
|
-
def _get_partial_functions(self) ->
|
324
|
+
def _get_partial_functions(self) -> dict[str, _PartialFunction]:
|
269
325
|
if not self._user_cls:
|
270
326
|
raise AttributeError("You can only get the partial functions of a local Cls instance")
|
271
327
|
return _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.all())
|
@@ -277,7 +333,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
277
333
|
and self._class_service_function._method_handle_metadata
|
278
334
|
and len(self._class_service_function._method_handle_metadata)
|
279
335
|
):
|
280
|
-
# The class only has a class service
|
336
|
+
# The class only has a class service function and no method placeholders (v0.67+)
|
281
337
|
if self._method_functions:
|
282
338
|
# We're here when the Cls is loaded locally (e.g. _Cls.from_local) so the _method_functions mapping is
|
283
339
|
# populated with (un-hydrated) _Function objects
|
@@ -298,7 +354,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
298
354
|
self._method_functions[method_name] = _Function._new_hydrated(
|
299
355
|
self._class_service_function.object_id, self._client, method_handle_metadata
|
300
356
|
)
|
301
|
-
elif self._class_service_function:
|
357
|
+
elif self._class_service_function and self._class_service_function.object_id:
|
302
358
|
# A class with a class service function and method placeholder functions
|
303
359
|
self._method_functions = {}
|
304
360
|
for method in metadata.methods:
|
@@ -344,8 +400,8 @@ class _Cls(_Object, type_prefix="cs"):
|
|
344
400
|
# validate signature
|
345
401
|
_Cls.validate_construction_mechanism(user_cls)
|
346
402
|
|
347
|
-
method_functions:
|
348
|
-
partial_functions:
|
403
|
+
method_functions: dict[str, _Function] = {}
|
404
|
+
partial_functions: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
|
349
405
|
user_cls, _PartialFunctionFlags.FUNCTION
|
350
406
|
)
|
351
407
|
|
@@ -361,11 +417,11 @@ class _Cls(_Object, type_prefix="cs"):
|
|
361
417
|
partial_function.wrapped = True
|
362
418
|
|
363
419
|
# Get all callables
|
364
|
-
callables:
|
420
|
+
callables: dict[str, Callable] = {
|
365
421
|
k: pf.raw_f for k, pf in _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.all()).items()
|
366
422
|
}
|
367
423
|
|
368
|
-
def _deps() ->
|
424
|
+
def _deps() -> list[_Function]:
|
369
425
|
return [class_service_function]
|
370
426
|
|
371
427
|
async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
|
@@ -382,7 +438,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
382
438
|
cls._class_service_function = class_service_function
|
383
439
|
cls._method_functions = method_functions
|
384
440
|
cls._callables = callables
|
385
|
-
cls._from_other_workspace = False
|
386
441
|
return cls
|
387
442
|
|
388
443
|
def _uses_common_service_function(self):
|
@@ -392,7 +447,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
392
447
|
|
393
448
|
@classmethod
|
394
449
|
def from_name(
|
395
|
-
cls:
|
450
|
+
cls: type["_Cls"],
|
396
451
|
app_name: str,
|
397
452
|
tag: str,
|
398
453
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
@@ -449,16 +504,15 @@ class _Cls(_Object, type_prefix="cs"):
|
|
449
504
|
|
450
505
|
rep = f"Ref({app_name})"
|
451
506
|
cls = cls._from_loader(_load_remote, rep, is_another_app=True)
|
452
|
-
cls._from_other_workspace = bool(workspace is not None)
|
453
507
|
return cls
|
454
508
|
|
455
509
|
def with_options(
|
456
510
|
self: "_Cls",
|
457
|
-
cpu: Optional[Union[float,
|
458
|
-
memory: Optional[Union[int,
|
511
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
512
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
459
513
|
gpu: GPU_T = None,
|
460
514
|
secrets: Collection[_Secret] = (),
|
461
|
-
volumes:
|
515
|
+
volumes: dict[Union[str, os.PathLike], _Volume] = {},
|
462
516
|
retries: Optional[Union[int, Retries]] = None,
|
463
517
|
timeout: Optional[int] = None,
|
464
518
|
concurrency_limit: Optional[int] = None,
|
@@ -524,7 +578,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
524
578
|
In contrast to `modal.Cls.from_name`, this is an eager method
|
525
579
|
that will hydrate the local object with metadata from Modal servers.
|
526
580
|
|
527
|
-
```python
|
581
|
+
```python notest
|
528
582
|
Class = modal.Cls.lookup("other-app", "Class")
|
529
583
|
obj = Class()
|
530
584
|
```
|
@@ -543,7 +597,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
543
597
|
self._user_cls,
|
544
598
|
self._class_service_function,
|
545
599
|
self._method_functions,
|
546
|
-
self._from_other_workspace,
|
547
600
|
self._options,
|
548
601
|
args,
|
549
602
|
kwargs,
|
modal/cls.pyi
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import collections.abc
|
1
2
|
import google.protobuf.message
|
2
3
|
import inspect
|
3
4
|
import modal.app
|
@@ -18,21 +19,23 @@ T = typing.TypeVar("T")
|
|
18
19
|
|
19
20
|
def _use_annotation_parameters(user_cls) -> bool: ...
|
20
21
|
def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
|
22
|
+
def _bind_instance_method(
|
23
|
+
service_function: modal.functions._Function, class_bound_method: modal.functions._Function
|
24
|
+
): ...
|
21
25
|
|
22
26
|
class _Obj:
|
23
|
-
_functions:
|
24
|
-
|
27
|
+
_functions: dict[str, modal.functions._Function]
|
28
|
+
_has_entered: bool
|
25
29
|
_user_cls_instance: typing.Optional[typing.Any]
|
26
|
-
_construction_args:
|
30
|
+
_construction_args: tuple[tuple, dict[str, typing.Any]]
|
27
31
|
_instance_service_function: typing.Optional[modal.functions._Function]
|
28
32
|
|
29
33
|
def _uses_common_service_function(self): ...
|
30
34
|
def __init__(
|
31
35
|
self,
|
32
|
-
user_cls: type,
|
36
|
+
user_cls: typing.Optional[type],
|
33
37
|
class_service_function: typing.Optional[modal.functions._Function],
|
34
|
-
classbound_methods:
|
35
|
-
from_other_workspace: bool,
|
38
|
+
classbound_methods: dict[str, modal.functions._Function],
|
36
39
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
37
40
|
args,
|
38
41
|
kwargs,
|
@@ -40,27 +43,26 @@ class _Obj:
|
|
40
43
|
def _new_user_cls_instance(self): ...
|
41
44
|
async def keep_warm(self, warm_pool_size: int) -> None: ...
|
42
45
|
def _cached_user_cls_instance(self): ...
|
43
|
-
def
|
46
|
+
def _enter(self): ...
|
44
47
|
@property
|
45
|
-
def
|
46
|
-
@
|
47
|
-
def
|
48
|
-
async def
|
48
|
+
def _entered(self) -> bool: ...
|
49
|
+
@_entered.setter
|
50
|
+
def _entered(self, val: bool): ...
|
51
|
+
async def _aenter(self): ...
|
49
52
|
def __getattr__(self, k): ...
|
50
53
|
|
51
54
|
class Obj:
|
52
|
-
_functions:
|
53
|
-
|
55
|
+
_functions: dict[str, modal.functions.Function]
|
56
|
+
_has_entered: bool
|
54
57
|
_user_cls_instance: typing.Optional[typing.Any]
|
55
|
-
_construction_args:
|
58
|
+
_construction_args: tuple[tuple, dict[str, typing.Any]]
|
56
59
|
_instance_service_function: typing.Optional[modal.functions.Function]
|
57
60
|
|
58
61
|
def __init__(
|
59
62
|
self,
|
60
|
-
user_cls: type,
|
63
|
+
user_cls: typing.Optional[type],
|
61
64
|
class_service_function: typing.Optional[modal.functions.Function],
|
62
|
-
classbound_methods:
|
63
|
-
from_other_workspace: bool,
|
65
|
+
classbound_methods: dict[str, modal.functions.Function],
|
64
66
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
65
67
|
args,
|
66
68
|
kwargs,
|
@@ -75,26 +77,25 @@ class Obj:
|
|
75
77
|
keep_warm: __keep_warm_spec
|
76
78
|
|
77
79
|
def _cached_user_cls_instance(self): ...
|
78
|
-
def
|
80
|
+
def _enter(self): ...
|
79
81
|
@property
|
80
|
-
def
|
81
|
-
@
|
82
|
-
def
|
83
|
-
async def
|
82
|
+
def _entered(self) -> bool: ...
|
83
|
+
@_entered.setter
|
84
|
+
def _entered(self, val: bool): ...
|
85
|
+
async def _aenter(self): ...
|
84
86
|
def __getattr__(self, k): ...
|
85
87
|
|
86
88
|
class _Cls(modal.object._Object):
|
87
89
|
_user_cls: typing.Optional[type]
|
88
90
|
_class_service_function: typing.Optional[modal.functions._Function]
|
89
|
-
_method_functions: typing.Optional[
|
91
|
+
_method_functions: typing.Optional[dict[str, modal.functions._Function]]
|
90
92
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
91
|
-
_callables:
|
92
|
-
_from_other_workspace: typing.Optional[bool]
|
93
|
+
_callables: dict[str, typing.Callable[..., typing.Any]]
|
93
94
|
_app: typing.Optional[modal.app._App]
|
94
95
|
|
95
96
|
def _initialize_from_empty(self): ...
|
96
97
|
def _initialize_from_other(self, other: _Cls): ...
|
97
|
-
def _get_partial_functions(self) ->
|
98
|
+
def _get_partial_functions(self) -> dict[str, modal.partial_function._PartialFunction]: ...
|
98
99
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
99
100
|
@staticmethod
|
100
101
|
def validate_construction_mechanism(user_cls): ...
|
@@ -103,7 +104,7 @@ class _Cls(modal.object._Object):
|
|
103
104
|
def _uses_common_service_function(self): ...
|
104
105
|
@classmethod
|
105
106
|
def from_name(
|
106
|
-
cls:
|
107
|
+
cls: type[_Cls],
|
107
108
|
app_name: str,
|
108
109
|
tag: str,
|
109
110
|
namespace=1,
|
@@ -112,11 +113,11 @@ class _Cls(modal.object._Object):
|
|
112
113
|
) -> _Cls: ...
|
113
114
|
def with_options(
|
114
115
|
self: _Cls,
|
115
|
-
cpu: typing.Union[float,
|
116
|
-
memory: typing.Union[int,
|
116
|
+
cpu: typing.Union[float, tuple[float, float], None] = None,
|
117
|
+
memory: typing.Union[int, tuple[int, int], None] = None,
|
117
118
|
gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
|
118
|
-
secrets:
|
119
|
-
volumes:
|
119
|
+
secrets: collections.abc.Collection[modal.secret._Secret] = (),
|
120
|
+
volumes: dict[typing.Union[str, os.PathLike], modal.volume._Volume] = {},
|
120
121
|
retries: typing.Union[int, modal.retries.Retries, None] = None,
|
121
122
|
timeout: typing.Optional[int] = None,
|
122
123
|
concurrency_limit: typing.Optional[int] = None,
|
@@ -138,16 +139,15 @@ class _Cls(modal.object._Object):
|
|
138
139
|
class Cls(modal.object.Object):
|
139
140
|
_user_cls: typing.Optional[type]
|
140
141
|
_class_service_function: typing.Optional[modal.functions.Function]
|
141
|
-
_method_functions: typing.Optional[
|
142
|
+
_method_functions: typing.Optional[dict[str, modal.functions.Function]]
|
142
143
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
143
|
-
_callables:
|
144
|
-
_from_other_workspace: typing.Optional[bool]
|
144
|
+
_callables: dict[str, typing.Callable[..., typing.Any]]
|
145
145
|
_app: typing.Optional[modal.app.App]
|
146
146
|
|
147
147
|
def __init__(self, *args, **kwargs): ...
|
148
148
|
def _initialize_from_empty(self): ...
|
149
149
|
def _initialize_from_other(self, other: Cls): ...
|
150
|
-
def _get_partial_functions(self) ->
|
150
|
+
def _get_partial_functions(self) -> dict[str, modal.partial_function.PartialFunction]: ...
|
151
151
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
152
152
|
@staticmethod
|
153
153
|
def validate_construction_mechanism(user_cls): ...
|
@@ -156,7 +156,7 @@ class Cls(modal.object.Object):
|
|
156
156
|
def _uses_common_service_function(self): ...
|
157
157
|
@classmethod
|
158
158
|
def from_name(
|
159
|
-
cls:
|
159
|
+
cls: type[Cls],
|
160
160
|
app_name: str,
|
161
161
|
tag: str,
|
162
162
|
namespace=1,
|
@@ -165,11 +165,11 @@ class Cls(modal.object.Object):
|
|
165
165
|
) -> Cls: ...
|
166
166
|
def with_options(
|
167
167
|
self: Cls,
|
168
|
-
cpu: typing.Union[float,
|
169
|
-
memory: typing.Union[int,
|
168
|
+
cpu: typing.Union[float, tuple[float, float], None] = None,
|
169
|
+
memory: typing.Union[int, tuple[int, int], None] = None,
|
170
170
|
gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
|
171
|
-
secrets:
|
172
|
-
volumes:
|
171
|
+
secrets: collections.abc.Collection[modal.secret.Secret] = (),
|
172
|
+
volumes: dict[typing.Union[str, os.PathLike], modal.volume.Volume] = {},
|
173
173
|
retries: typing.Union[int, modal.retries.Retries, None] = None,
|
174
174
|
timeout: typing.Optional[int] = None,
|
175
175
|
concurrency_limit: typing.Optional[int] = None,
|
modal/config.py
CHANGED
@@ -80,7 +80,7 @@ import os
|
|
80
80
|
import typing
|
81
81
|
import warnings
|
82
82
|
from textwrap import dedent
|
83
|
-
from typing import Any,
|
83
|
+
from typing import Any, Optional
|
84
84
|
|
85
85
|
from google.protobuf.empty_pb2 import Empty
|
86
86
|
|
@@ -221,6 +221,7 @@ _SETTINGS = {
|
|
221
221
|
"image_builder_version": _Setting(),
|
222
222
|
"strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
|
223
223
|
"snapshot_debug": _Setting(False, transform=_to_boolean),
|
224
|
+
"client_retries": _Setting(False, transform=_to_boolean), # For internal testing.
|
224
225
|
}
|
225
226
|
|
226
227
|
|
@@ -282,7 +283,7 @@ configure_logger(logger, config["loglevel"], config["log_format"])
|
|
282
283
|
|
283
284
|
|
284
285
|
def _store_user_config(
|
285
|
-
new_settings:
|
286
|
+
new_settings: dict[str, Any], profile: Optional[str] = None, active_profile: Optional[str] = None
|
286
287
|
):
|
287
288
|
"""Internal method, used by the CLI to set tokens."""
|
288
289
|
if profile is None:
|
modal/container_process.py
CHANGED
@@ -128,12 +128,16 @@ class _ContainerProcess(Generic[T]):
|
|
128
128
|
on_connect = asyncio.Event()
|
129
129
|
|
130
130
|
async def _write_to_fd_loop(stream: _StreamReader):
|
131
|
-
|
131
|
+
# Don't skip empty messages so we can detect when the process has booted.
|
132
|
+
async for chunk in stream._get_logs(skip_empty_messages=False):
|
133
|
+
if chunk is None:
|
134
|
+
break
|
135
|
+
|
132
136
|
if not on_connect.is_set():
|
133
137
|
connecting_status.stop()
|
134
138
|
on_connect.set()
|
135
139
|
|
136
|
-
await write_to_fd(stream.file_descriptor,
|
140
|
+
await write_to_fd(stream.file_descriptor, chunk)
|
137
141
|
|
138
142
|
async def _handle_input(data: bytes, message_index: int):
|
139
143
|
self.stdin.write(data)
|
modal/dict.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
-
from
|
2
|
+
from collections.abc import AsyncIterator
|
3
|
+
from typing import Any, Optional
|
3
4
|
|
4
5
|
from grpclib import GRPCError
|
5
6
|
from synchronicity.async_wrap import asynccontextmanager
|
@@ -74,7 +75,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
74
75
|
@classmethod
|
75
76
|
@asynccontextmanager
|
76
77
|
async def ephemeral(
|
77
|
-
cls:
|
78
|
+
cls: type["_Dict"],
|
78
79
|
data: Optional[dict] = None,
|
79
80
|
client: Optional[_Client] = None,
|
80
81
|
environment_name: Optional[str] = None,
|
@@ -88,7 +89,9 @@ class _Dict(_Object, type_prefix="di"):
|
|
88
89
|
|
89
90
|
with Dict.ephemeral() as d:
|
90
91
|
d["foo"] = "bar"
|
92
|
+
```
|
91
93
|
|
94
|
+
```python notest
|
92
95
|
async with Dict.ephemeral() as d:
|
93
96
|
await d.put.aio("foo", "bar")
|
94
97
|
```
|
@@ -314,7 +317,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
314
317
|
yield deserialize(resp.value, self._client)
|
315
318
|
|
316
319
|
@live_method_gen
|
317
|
-
async def items(self) -> AsyncIterator[
|
320
|
+
async def items(self) -> AsyncIterator[tuple[Any, Any]]:
|
318
321
|
"""Return an iterator over the (key, value) tuples in this dictionary.
|
319
322
|
|
320
323
|
Note that (unlike with Python dicts) the return value is a simple iterator,
|
modal/dict.pyi
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import collections.abc
|
1
2
|
import modal.client
|
2
3
|
import modal.object
|
3
4
|
import synchronicity.combined_types
|
@@ -12,7 +13,7 @@ class _Dict(modal.object._Object):
|
|
12
13
|
def __init__(self, data={}): ...
|
13
14
|
@classmethod
|
14
15
|
def ephemeral(
|
15
|
-
cls:
|
16
|
+
cls: type[_Dict],
|
16
17
|
data: typing.Optional[dict] = None,
|
17
18
|
client: typing.Optional[modal.client._Client] = None,
|
18
19
|
environment_name: typing.Optional[str] = None,
|
@@ -53,9 +54,9 @@ class _Dict(modal.object._Object):
|
|
53
54
|
async def pop(self, key: typing.Any) -> typing.Any: ...
|
54
55
|
async def __delitem__(self, key: typing.Any) -> typing.Any: ...
|
55
56
|
async def __contains__(self, key: typing.Any) -> bool: ...
|
56
|
-
def keys(self) ->
|
57
|
-
def values(self) ->
|
58
|
-
def items(self) ->
|
57
|
+
def keys(self) -> collections.abc.AsyncIterator[typing.Any]: ...
|
58
|
+
def values(self) -> collections.abc.AsyncIterator[typing.Any]: ...
|
59
|
+
def items(self) -> collections.abc.AsyncIterator[tuple[typing.Any, typing.Any]]: ...
|
59
60
|
|
60
61
|
class Dict(modal.object.Object):
|
61
62
|
def __init__(self, data={}): ...
|
@@ -63,7 +64,7 @@ class Dict(modal.object.Object):
|
|
63
64
|
def new(data: typing.Optional[dict] = None): ...
|
64
65
|
@classmethod
|
65
66
|
def ephemeral(
|
66
|
-
cls:
|
67
|
+
cls: type[Dict],
|
67
68
|
data: typing.Optional[dict] = None,
|
68
69
|
client: typing.Optional[modal.client.Client] = None,
|
69
70
|
environment_name: typing.Optional[str] = None,
|
@@ -186,18 +187,18 @@ class Dict(modal.object.Object):
|
|
186
187
|
|
187
188
|
class __keys_spec(typing_extensions.Protocol):
|
188
189
|
def __call__(self) -> typing.Iterator[typing.Any]: ...
|
189
|
-
def aio(self) ->
|
190
|
+
def aio(self) -> collections.abc.AsyncIterator[typing.Any]: ...
|
190
191
|
|
191
192
|
keys: __keys_spec
|
192
193
|
|
193
194
|
class __values_spec(typing_extensions.Protocol):
|
194
195
|
def __call__(self) -> typing.Iterator[typing.Any]: ...
|
195
|
-
def aio(self) ->
|
196
|
+
def aio(self) -> collections.abc.AsyncIterator[typing.Any]: ...
|
196
197
|
|
197
198
|
values: __values_spec
|
198
199
|
|
199
200
|
class __items_spec(typing_extensions.Protocol):
|
200
|
-
def __call__(self) -> typing.Iterator[
|
201
|
-
def aio(self) ->
|
201
|
+
def __call__(self) -> typing.Iterator[tuple[typing.Any, typing.Any]]: ...
|
202
|
+
def aio(self) -> collections.abc.AsyncIterator[tuple[typing.Any, typing.Any]]: ...
|
202
203
|
|
203
204
|
items: __items_spec
|
modal/environments.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Copyright Modal Labs 2023
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import
|
3
|
+
from typing import Optional
|
4
4
|
|
5
5
|
from google.protobuf.empty_pb2 import Empty
|
6
6
|
from google.protobuf.message import Message
|
@@ -98,7 +98,7 @@ Environment = synchronize_api(_Environment)
|
|
98
98
|
|
99
99
|
|
100
100
|
# Needs to be after definition; synchronicity interferes with forward references?
|
101
|
-
ENVIRONMENT_CACHE:
|
101
|
+
ENVIRONMENT_CACHE: dict[str, _Environment] = {}
|
102
102
|
|
103
103
|
|
104
104
|
async def _get_environment_cached(name: str, client: _Client) -> _Environment:
|
@@ -151,7 +151,7 @@ async def create_environment(name: str, client: Optional[_Client] = None):
|
|
151
151
|
|
152
152
|
|
153
153
|
@synchronizer.create_blocking
|
154
|
-
async def list_environments(client: Optional[_Client] = None) ->
|
154
|
+
async def list_environments(client: Optional[_Client] = None) -> list[api_pb2.EnvironmentListItem]:
|
155
155
|
if client is None:
|
156
156
|
client = await _Client.from_env()
|
157
157
|
resp = await client.stub.EnvironmentList(Empty())
|
modal/environments.pyi
CHANGED
@@ -87,13 +87,13 @@ create_environment: __create_environment_spec
|
|
87
87
|
class __list_environments_spec(typing_extensions.Protocol):
|
88
88
|
def __call__(
|
89
89
|
self, client: typing.Optional[modal.client.Client] = None
|
90
|
-
) ->
|
90
|
+
) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
|
91
91
|
async def aio(
|
92
92
|
self, client: typing.Optional[modal.client.Client] = None
|
93
|
-
) ->
|
93
|
+
) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
|
94
94
|
|
95
95
|
list_environments: __list_environments_spec
|
96
96
|
|
97
97
|
def ensure_env(environment_name: typing.Optional[str] = None) -> str: ...
|
98
98
|
|
99
|
-
ENVIRONMENT_CACHE:
|
99
|
+
ENVIRONMENT_CACHE: dict[str, _Environment]
|