modal 0.66.44__py3-none-any.whl → 0.67.0__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/_resolver.py +7 -2
- modal/_runtime/container_io_manager.py +1 -1
- modal/app.py +39 -24
- modal/app.pyi +4 -2
- modal/cli/config.py +3 -0
- modal/cli/import_refs.py +2 -2
- modal/cli/launch.py +6 -4
- modal/cli/run.py +18 -5
- modal/client.pyi +2 -2
- modal/cls.py +49 -44
- modal/cls.pyi +2 -4
- modal/config.py +1 -1
- modal/functions.py +4 -98
- modal/functions.pyi +0 -10
- modal/image.py +81 -67
- modal/image.pyi +12 -7
- modal/object.py +2 -2
- modal/object.pyi +4 -2
- modal/parallel_map.py +2 -2
- modal/runner.py +13 -7
- modal/volume.py +13 -6
- modal/volume.pyi +10 -4
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/METADATA +1 -1
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/RECORD +32 -32
- modal_proto/api.proto +3 -3
- modal_proto/api_pb2.pyi +3 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/LICENSE +0 -0
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/WHEEL +0 -0
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/entry_points.txt +0 -0
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/top_level.txt +0 -0
modal/functions.py
CHANGED
@@ -118,7 +118,7 @@ class _Invocation:
|
|
118
118
|
function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType",
|
119
119
|
) -> "_Invocation":
|
120
120
|
assert client.stub
|
121
|
-
function_id = function.
|
121
|
+
function_id = function.object_id
|
122
122
|
item = await _create_input(args, kwargs, client, method_name=function._use_method_name)
|
123
123
|
|
124
124
|
request = api_pb2.FunctionMapRequest(
|
@@ -319,8 +319,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
319
319
|
_cluster_size: Optional[int] = None
|
320
320
|
|
321
321
|
# when this is the method of a class/object function, invocation of this function
|
322
|
-
# should
|
323
|
-
_use_function_id: str # The function to invoke
|
322
|
+
# should supply the method name in the FunctionInput:
|
324
323
|
_use_method_name: str = ""
|
325
324
|
|
326
325
|
_class_parameter_info: Optional["api_pb2.ClassParameterInfo"] = None
|
@@ -360,94 +359,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
360
359
|
fun._is_method = True
|
361
360
|
return fun
|
362
361
|
|
363
|
-
def _bind_method_old(
|
364
|
-
self,
|
365
|
-
user_cls,
|
366
|
-
method_name: str,
|
367
|
-
partial_function: "modal.partial_function._PartialFunction",
|
368
|
-
):
|
369
|
-
"""mdmd:hidden
|
370
|
-
|
371
|
-
Creates a function placeholder function that binds a specific method name to
|
372
|
-
this function for use when invoking the function.
|
373
|
-
|
374
|
-
Should only be used on "class service functions". For "instance service functions",
|
375
|
-
we don't create an actual backend function, and instead do client-side "fake-hydration"
|
376
|
-
only, see _bind_instance_method.
|
377
|
-
|
378
|
-
"""
|
379
|
-
class_service_function = self
|
380
|
-
assert class_service_function._info # has to be a local function to be able to "bind" it
|
381
|
-
assert not class_service_function._is_method # should not be used on an already bound method placeholder
|
382
|
-
assert not class_service_function._obj # should only be used on base function / class service function
|
383
|
-
full_name = f"{user_cls.__name__}.{method_name}"
|
384
|
-
function_type = get_function_type(partial_function.is_generator)
|
385
|
-
|
386
|
-
async def _load(method_bound_function: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
387
|
-
function_definition = api_pb2.Function(
|
388
|
-
function_name=full_name,
|
389
|
-
webhook_config=partial_function.webhook_config,
|
390
|
-
function_type=function_type,
|
391
|
-
is_method=True,
|
392
|
-
use_function_id=class_service_function.object_id,
|
393
|
-
use_method_name=method_name,
|
394
|
-
batch_max_size=partial_function.batch_max_size or 0,
|
395
|
-
batch_linger_ms=partial_function.batch_wait_ms or 0,
|
396
|
-
)
|
397
|
-
assert resolver.app_id
|
398
|
-
request = api_pb2.FunctionCreateRequest(
|
399
|
-
app_id=resolver.app_id,
|
400
|
-
function=function_definition,
|
401
|
-
# method_bound_function.object_id usually gets set by preload
|
402
|
-
existing_function_id=existing_object_id or method_bound_function.object_id or "",
|
403
|
-
defer_updates=True,
|
404
|
-
)
|
405
|
-
assert resolver.client.stub is not None # client should be connected when load is called
|
406
|
-
with FunctionCreationStatus(resolver, full_name) as function_creation_status:
|
407
|
-
response = await resolver.client.stub.FunctionCreate(request)
|
408
|
-
method_bound_function._hydrate(
|
409
|
-
response.function_id,
|
410
|
-
resolver.client,
|
411
|
-
response.handle_metadata,
|
412
|
-
)
|
413
|
-
function_creation_status.set_response(response)
|
414
|
-
|
415
|
-
async def _preload(method_bound_function: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
416
|
-
if class_service_function._use_method_name:
|
417
|
-
raise ExecutionError(f"Can't bind method to already bound {class_service_function}")
|
418
|
-
assert resolver.app_id
|
419
|
-
req = api_pb2.FunctionPrecreateRequest(
|
420
|
-
app_id=resolver.app_id,
|
421
|
-
function_name=full_name,
|
422
|
-
function_type=function_type,
|
423
|
-
webhook_config=partial_function.webhook_config,
|
424
|
-
use_function_id=class_service_function.object_id,
|
425
|
-
use_method_name=method_name,
|
426
|
-
existing_function_id=existing_object_id or "",
|
427
|
-
)
|
428
|
-
assert resolver.client.stub # client should be connected at this point
|
429
|
-
response = await retry_transient_errors(resolver.client.stub.FunctionPrecreate, req)
|
430
|
-
method_bound_function._hydrate(response.function_id, resolver.client, response.handle_metadata)
|
431
|
-
|
432
|
-
def _deps():
|
433
|
-
return [class_service_function]
|
434
|
-
|
435
|
-
rep = f"Method({full_name})"
|
436
|
-
|
437
|
-
fun = _Function._from_loader(_load, rep, preload=_preload, deps=_deps)
|
438
|
-
fun._tag = full_name
|
439
|
-
fun._raw_f = partial_function.raw_f
|
440
|
-
fun._info = FunctionInfo(
|
441
|
-
partial_function.raw_f, user_cls=user_cls, serialized=class_service_function.info.is_serialized()
|
442
|
-
) # needed for .local()
|
443
|
-
fun._use_method_name = method_name
|
444
|
-
fun._app = class_service_function._app
|
445
|
-
fun._is_generator = partial_function.is_generator
|
446
|
-
fun._cluster_size = partial_function.cluster_size
|
447
|
-
fun._spec = class_service_function._spec
|
448
|
-
fun._is_method = True
|
449
|
-
return fun
|
450
|
-
|
451
362
|
def _bind_instance_method(self, class_bound_method: "_Function"):
|
452
363
|
"""mdmd:hidden
|
453
364
|
|
@@ -475,7 +386,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
475
386
|
method_placeholder_fun._is_generator = class_bound_method._is_generator
|
476
387
|
method_placeholder_fun._cluster_size = class_bound_method._cluster_size
|
477
388
|
method_placeholder_fun._use_method_name = method_name
|
478
|
-
method_placeholder_fun._use_function_id = instance_service_function.object_id
|
479
389
|
method_placeholder_fun._is_method = True
|
480
390
|
|
481
391
|
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
@@ -848,6 +758,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
848
758
|
class_serialized=class_serialized or b"",
|
849
759
|
function_type=function_type,
|
850
760
|
webhook_config=webhook_config,
|
761
|
+
method_definitions=method_definitions,
|
762
|
+
method_definitions_set=True,
|
851
763
|
shared_volume_mounts=network_file_system_mount_protos(
|
852
764
|
validated_network_file_systems, allow_cross_region_volumes
|
853
765
|
),
|
@@ -1224,7 +1136,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1224
1136
|
self._web_url = None
|
1225
1137
|
self._function_name = None
|
1226
1138
|
self._info = None
|
1227
|
-
self._use_function_id = ""
|
1228
1139
|
self._serve_mounts = frozenset()
|
1229
1140
|
|
1230
1141
|
def _hydrate_metadata(self, metadata: Optional[Message]):
|
@@ -1234,15 +1145,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1234
1145
|
self._web_url = metadata.web_url
|
1235
1146
|
self._function_name = metadata.function_name
|
1236
1147
|
self._is_method = metadata.is_method
|
1237
|
-
self._use_function_id = metadata.use_function_id
|
1238
1148
|
self._use_method_name = metadata.use_method_name
|
1239
1149
|
self._class_parameter_info = metadata.class_parameter_info
|
1240
1150
|
self._method_handle_metadata = dict(metadata.method_handle_metadata)
|
1241
1151
|
self._definition_id = metadata.definition_id
|
1242
1152
|
|
1243
|
-
def _invocation_function_id(self) -> str:
|
1244
|
-
return self._use_function_id or self.object_id
|
1245
|
-
|
1246
1153
|
def _get_metadata(self):
|
1247
1154
|
# Overridden concrete implementation of base class method
|
1248
1155
|
assert self._function_name, f"Function name must be set before metadata can be retrieved for {self}"
|
@@ -1251,7 +1158,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1251
1158
|
function_type=get_function_type(self._is_generator),
|
1252
1159
|
web_url=self._web_url or "",
|
1253
1160
|
use_method_name=self._use_method_name,
|
1254
|
-
use_function_id=self._use_function_id,
|
1255
1161
|
is_method=self._is_method,
|
1256
1162
|
class_parameter_info=self._class_parameter_info,
|
1257
1163
|
definition_id=self._definition_id,
|
modal/functions.pyi
CHANGED
@@ -122,15 +122,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
122
122
|
_build_args: dict
|
123
123
|
_is_generator: typing.Optional[bool]
|
124
124
|
_cluster_size: typing.Optional[int]
|
125
|
-
_use_function_id: str
|
126
125
|
_use_method_name: str
|
127
126
|
_class_parameter_info: typing.Optional[modal_proto.api_pb2.ClassParameterInfo]
|
128
127
|
_method_handle_metadata: typing.Optional[typing.Dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
129
128
|
|
130
129
|
def _bind_method(self, user_cls, method_name: str, partial_function: modal.partial_function._PartialFunction): ...
|
131
|
-
def _bind_method_old(
|
132
|
-
self, user_cls, method_name: str, partial_function: modal.partial_function._PartialFunction
|
133
|
-
): ...
|
134
130
|
def _bind_instance_method(self, class_bound_method: _Function): ...
|
135
131
|
@staticmethod
|
136
132
|
def from_args(
|
@@ -212,7 +208,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
212
208
|
def get_build_def(self) -> str: ...
|
213
209
|
def _initialize_from_empty(self): ...
|
214
210
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
215
|
-
def _invocation_function_id(self) -> str: ...
|
216
211
|
def _get_metadata(self): ...
|
217
212
|
def _check_no_web_url(self, fn_name: str): ...
|
218
213
|
@property
|
@@ -296,16 +291,12 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
296
291
|
_build_args: dict
|
297
292
|
_is_generator: typing.Optional[bool]
|
298
293
|
_cluster_size: typing.Optional[int]
|
299
|
-
_use_function_id: str
|
300
294
|
_use_method_name: str
|
301
295
|
_class_parameter_info: typing.Optional[modal_proto.api_pb2.ClassParameterInfo]
|
302
296
|
_method_handle_metadata: typing.Optional[typing.Dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
303
297
|
|
304
298
|
def __init__(self, *args, **kwargs): ...
|
305
299
|
def _bind_method(self, user_cls, method_name: str, partial_function: modal.partial_function.PartialFunction): ...
|
306
|
-
def _bind_method_old(
|
307
|
-
self, user_cls, method_name: str, partial_function: modal.partial_function.PartialFunction
|
308
|
-
): ...
|
309
300
|
def _bind_instance_method(self, class_bound_method: Function): ...
|
310
301
|
@staticmethod
|
311
302
|
def from_args(
|
@@ -406,7 +397,6 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
406
397
|
def get_build_def(self) -> str: ...
|
407
398
|
def _initialize_from_empty(self): ...
|
408
399
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
409
|
-
def _invocation_function_id(self) -> str: ...
|
410
400
|
def _get_metadata(self): ...
|
411
401
|
def _check_no_web_url(self, fn_name: str): ...
|
412
402
|
@property
|
modal/image.py
CHANGED
@@ -19,7 +19,6 @@ from typing import (
|
|
19
19
|
Optional,
|
20
20
|
Sequence,
|
21
21
|
Set,
|
22
|
-
Tuple,
|
23
22
|
Union,
|
24
23
|
cast,
|
25
24
|
get_args,
|
@@ -36,6 +35,7 @@ from ._utils.async_utils import synchronize_api
|
|
36
35
|
from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
37
36
|
from ._utils.function_utils import FunctionInfo
|
38
37
|
from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
|
38
|
+
from .client import _Client
|
39
39
|
from .cloud_bucket_mount import _CloudBucketMount
|
40
40
|
from .config import config, logger, user_config_path
|
41
41
|
from .environments import _get_environment_cached
|
@@ -52,7 +52,6 @@ from .volume import _Volume
|
|
52
52
|
if typing.TYPE_CHECKING:
|
53
53
|
import modal.functions
|
54
54
|
|
55
|
-
|
56
55
|
# This is used for both type checking and runtime validation
|
57
56
|
ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10"]
|
58
57
|
|
@@ -147,8 +146,8 @@ def _get_modal_requirements_command(version: ImageBuilderVersion) -> str:
|
|
147
146
|
return f"{prefix} -r {CONTAINER_REQUIREMENTS_PATH}"
|
148
147
|
|
149
148
|
|
150
|
-
def _flatten_str_args(function_name: str, arg_name: str, args:
|
151
|
-
"""Takes a
|
149
|
+
def _flatten_str_args(function_name: str, arg_name: str, args: Sequence[Union[str, List[str]]]) -> List[str]:
|
150
|
+
"""Takes a sequence of strings, or string lists, and flattens it.
|
152
151
|
|
153
152
|
Raises an error if any of the elements are not strings or string lists.
|
154
153
|
"""
|
@@ -244,7 +243,7 @@ class _ImageRegistryConfig:
|
|
244
243
|
def __init__(
|
245
244
|
self,
|
246
245
|
# TODO: change to _PUBLIC after worker starts handling it.
|
247
|
-
registry_auth_type:
|
246
|
+
registry_auth_type: "api_pb2.RegistryAuthType.ValueType" = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
|
248
247
|
secret: Optional[_Secret] = None,
|
249
248
|
):
|
250
249
|
self.registry_auth_type = registry_auth_type
|
@@ -253,7 +252,7 @@ class _ImageRegistryConfig:
|
|
253
252
|
def get_proto(self) -> api_pb2.ImageRegistryConfig:
|
254
253
|
return api_pb2.ImageRegistryConfig(
|
255
254
|
registry_auth_type=self.registry_auth_type,
|
256
|
-
secret_id=(self.secret.object_id if self.secret else
|
255
|
+
secret_id=(self.secret.object_id if self.secret else ""),
|
257
256
|
)
|
258
257
|
|
259
258
|
|
@@ -264,6 +263,45 @@ class DockerfileSpec:
|
|
264
263
|
context_files: Dict[str, str]
|
265
264
|
|
266
265
|
|
266
|
+
async def _image_await_build_result(image_id: str, client: _Client) -> api_pb2.ImageJoinStreamingResponse:
|
267
|
+
last_entry_id: str = ""
|
268
|
+
result_response: Optional[api_pb2.ImageJoinStreamingResponse] = None
|
269
|
+
|
270
|
+
async def join():
|
271
|
+
nonlocal last_entry_id, result_response
|
272
|
+
|
273
|
+
request = api_pb2.ImageJoinStreamingRequest(image_id=image_id, timeout=55, last_entry_id=last_entry_id)
|
274
|
+
async for response in client.stub.ImageJoinStreaming.unary_stream(request):
|
275
|
+
if response.entry_id:
|
276
|
+
last_entry_id = response.entry_id
|
277
|
+
if response.result.status:
|
278
|
+
result_response = response
|
279
|
+
# can't return yet, since there may still be logs streaming back in subsequent responses
|
280
|
+
for task_log in response.task_logs:
|
281
|
+
if task_log.task_progress.pos or task_log.task_progress.len:
|
282
|
+
assert task_log.task_progress.progress_type == api_pb2.IMAGE_SNAPSHOT_UPLOAD
|
283
|
+
if output_mgr := _get_output_manager():
|
284
|
+
output_mgr.update_snapshot_progress(image_id, task_log.task_progress)
|
285
|
+
elif task_log.data:
|
286
|
+
if output_mgr := _get_output_manager():
|
287
|
+
await output_mgr.put_log_content(task_log)
|
288
|
+
if output_mgr := _get_output_manager():
|
289
|
+
output_mgr.flush_lines()
|
290
|
+
|
291
|
+
# Handle up to n exceptions while fetching logs
|
292
|
+
retry_count = 0
|
293
|
+
while result_response is None:
|
294
|
+
try:
|
295
|
+
await join()
|
296
|
+
except (StreamTerminatedError, GRPCError) as exc:
|
297
|
+
if isinstance(exc, GRPCError) and exc.status not in RETRYABLE_GRPC_STATUS_CODES:
|
298
|
+
raise exc
|
299
|
+
retry_count += 1
|
300
|
+
if retry_count >= 3:
|
301
|
+
raise exc
|
302
|
+
return result_response
|
303
|
+
|
304
|
+
|
267
305
|
class _Image(_Object, type_prefix="im"):
|
268
306
|
"""Base class for container images to run functions in.
|
269
307
|
|
@@ -292,7 +330,7 @@ class _Image(_Object, type_prefix="im"):
|
|
292
330
|
self._serve_mounts = other._serve_mounts
|
293
331
|
self._deferred_mounts = other._deferred_mounts
|
294
332
|
|
295
|
-
def _hydrate_metadata(self,
|
333
|
+
def _hydrate_metadata(self, metadata: Optional[Message]):
|
296
334
|
env_image_id = config.get("image_id") # set as an env var in containers
|
297
335
|
if env_image_id == self.object_id:
|
298
336
|
for exc in self.inside_exceptions:
|
@@ -300,9 +338,9 @@ class _Image(_Object, type_prefix="im"):
|
|
300
338
|
# if the hydrated image is the one used by the container
|
301
339
|
raise exc
|
302
340
|
|
303
|
-
if
|
304
|
-
assert isinstance(
|
305
|
-
self._metadata =
|
341
|
+
if metadata:
|
342
|
+
assert isinstance(metadata, api_pb2.ImageMetadata)
|
343
|
+
self._metadata = metadata
|
306
344
|
|
307
345
|
def _add_mount_layer_or_copy(self, mount: _Mount, copy: bool = False):
|
308
346
|
if copy:
|
@@ -318,7 +356,7 @@ class _Image(_Object, type_prefix="im"):
|
|
318
356
|
return _Image._from_loader(_load, "Image(local files)", deps=lambda: [base_image, mount])
|
319
357
|
|
320
358
|
@property
|
321
|
-
def _mount_layers(self) -> typing.
|
359
|
+
def _mount_layers(self) -> typing.Sequence[_Mount]:
|
322
360
|
"""Non-evaluated mount layers on the image
|
323
361
|
|
324
362
|
When the image is used by a Modal container, these mounts need to be attached as well to
|
@@ -362,7 +400,7 @@ class _Image(_Object, type_prefix="im"):
|
|
362
400
|
context_mount: Optional[_Mount] = None,
|
363
401
|
force_build: bool = False,
|
364
402
|
# For internal use only.
|
365
|
-
_namespace:
|
403
|
+
_namespace: "api_pb2.DeploymentNamespace.ValueType" = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
366
404
|
_do_assert_no_mount_layers: bool = True,
|
367
405
|
):
|
368
406
|
if base_images is None:
|
@@ -382,14 +420,14 @@ class _Image(_Object, type_prefix="im"):
|
|
382
420
|
if build_function and len(base_images) != 1:
|
383
421
|
raise InvalidError("Cannot run a build function with multiple base images!")
|
384
422
|
|
385
|
-
def _deps() ->
|
386
|
-
deps
|
423
|
+
def _deps() -> Sequence[_Object]:
|
424
|
+
deps = tuple(base_images.values()) + tuple(secrets)
|
387
425
|
if build_function:
|
388
|
-
deps
|
426
|
+
deps += (build_function,)
|
389
427
|
if context_mount:
|
390
|
-
deps
|
391
|
-
if image_registry_config.secret:
|
392
|
-
deps
|
428
|
+
deps += (context_mount,)
|
429
|
+
if image_registry_config and image_registry_config.secret:
|
430
|
+
deps += (image_registry_config.secret,)
|
393
431
|
return deps
|
394
432
|
|
395
433
|
async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
|
@@ -398,6 +436,7 @@ class _Image(_Object, type_prefix="im"):
|
|
398
436
|
# base images can't have
|
399
437
|
image._assert_no_mount_layers()
|
400
438
|
|
439
|
+
assert resolver.app_id # type narrowing
|
401
440
|
environment = await _get_environment_cached(resolver.environment_name or "", resolver.client)
|
402
441
|
# A bit hacky,but assume that the environment provides a valid builder version
|
403
442
|
image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
|
@@ -432,7 +471,6 @@ class _Image(_Object, type_prefix="im"):
|
|
432
471
|
|
433
472
|
if build_function:
|
434
473
|
build_function_id = build_function.object_id
|
435
|
-
|
436
474
|
globals = build_function._get_info().get_globals()
|
437
475
|
attrs = build_function._get_info().get_cls_var_attrs()
|
438
476
|
globals = {**globals, **attrs}
|
@@ -454,14 +492,14 @@ class _Image(_Object, type_prefix="im"):
|
|
454
492
|
|
455
493
|
# Cloudpickle function serialization produces unstable values.
|
456
494
|
# TODO: better way to filter out types that don't have a stable hash?
|
457
|
-
build_function_globals = serialize(filtered_globals) if filtered_globals else
|
495
|
+
build_function_globals = serialize(filtered_globals) if filtered_globals else b""
|
458
496
|
_build_function = api_pb2.BuildFunction(
|
459
497
|
definition=build_function.get_build_def(),
|
460
498
|
globals=build_function_globals,
|
461
499
|
input=build_function_input,
|
462
500
|
)
|
463
501
|
else:
|
464
|
-
build_function_id =
|
502
|
+
build_function_id = ""
|
465
503
|
_build_function = None
|
466
504
|
|
467
505
|
image_definition = api_pb2.Image(
|
@@ -470,7 +508,7 @@ class _Image(_Object, type_prefix="im"):
|
|
470
508
|
context_files=context_file_pb2s,
|
471
509
|
secret_ids=[secret.object_id for secret in secrets],
|
472
510
|
gpu=bool(gpu_config.type), # Note: as of 2023-01-27, server still uses this
|
473
|
-
context_mount_id=(context_mount.object_id if context_mount else
|
511
|
+
context_mount_id=(context_mount.object_id if context_mount else ""),
|
474
512
|
gpu_config=gpu_config, # Note: as of 2023-01-27, server ignores this
|
475
513
|
image_registry_config=image_registry_config.get_proto(),
|
476
514
|
runtime=config.get("function_runtime"),
|
@@ -481,7 +519,7 @@ class _Image(_Object, type_prefix="im"):
|
|
481
519
|
req = api_pb2.ImageGetOrCreateRequest(
|
482
520
|
app_id=resolver.app_id,
|
483
521
|
image=image_definition,
|
484
|
-
existing_image_id=existing_object_id, # TODO: ignored
|
522
|
+
existing_image_id=existing_object_id or "", # TODO: ignored
|
485
523
|
build_function_id=build_function_id,
|
486
524
|
force_build=config.get("force_build") or force_build,
|
487
525
|
namespace=_namespace,
|
@@ -492,46 +530,22 @@ class _Image(_Object, type_prefix="im"):
|
|
492
530
|
)
|
493
531
|
resp = await retry_transient_errors(resolver.client.stub.ImageGetOrCreate, req)
|
494
532
|
image_id = resp.image_id
|
533
|
+
result: api_pb2.GenericResult
|
534
|
+
metadata: Optional[api_pb2.ImageMetadata] = None
|
535
|
+
|
536
|
+
if resp.result.status:
|
537
|
+
# image already built
|
538
|
+
result = resp.result
|
539
|
+
if resp.HasField("metadata"):
|
540
|
+
metadata = resp.metadata
|
541
|
+
else:
|
542
|
+
# not built or in the process of building - wait for build
|
543
|
+
logger.debug("Waiting for image %s" % image_id)
|
544
|
+
resp = await _image_await_build_result(image_id, resolver.client)
|
545
|
+
result = resp.result
|
546
|
+
if resp.HasField("metadata"):
|
547
|
+
metadata = resp.metadata
|
495
548
|
|
496
|
-
logger.debug("Waiting for image %s" % image_id)
|
497
|
-
last_entry_id: Optional[str] = None
|
498
|
-
result_response: Optional[api_pb2.ImageJoinStreamingResponse] = None
|
499
|
-
|
500
|
-
async def join():
|
501
|
-
nonlocal last_entry_id, result_response
|
502
|
-
|
503
|
-
request = api_pb2.ImageJoinStreamingRequest(image_id=image_id, timeout=55, last_entry_id=last_entry_id)
|
504
|
-
|
505
|
-
async for response in resolver.client.stub.ImageJoinStreaming.unary_stream(request):
|
506
|
-
if response.entry_id:
|
507
|
-
last_entry_id = response.entry_id
|
508
|
-
if response.result.status:
|
509
|
-
result_response = response
|
510
|
-
# can't return yet, since there may still be logs streaming back in subsequent responses
|
511
|
-
for task_log in response.task_logs:
|
512
|
-
if task_log.task_progress.pos or task_log.task_progress.len:
|
513
|
-
assert task_log.task_progress.progress_type == api_pb2.IMAGE_SNAPSHOT_UPLOAD
|
514
|
-
if output_mgr := _get_output_manager():
|
515
|
-
output_mgr.update_snapshot_progress(image_id, task_log.task_progress)
|
516
|
-
elif task_log.data:
|
517
|
-
if output_mgr := _get_output_manager():
|
518
|
-
await output_mgr.put_log_content(task_log)
|
519
|
-
if output_mgr := _get_output_manager():
|
520
|
-
output_mgr.flush_lines()
|
521
|
-
|
522
|
-
# Handle up to n exceptions while fetching logs
|
523
|
-
retry_count = 0
|
524
|
-
while result_response is None:
|
525
|
-
try:
|
526
|
-
await join()
|
527
|
-
except (StreamTerminatedError, GRPCError) as exc:
|
528
|
-
if isinstance(exc, GRPCError) and exc.status not in RETRYABLE_GRPC_STATUS_CODES:
|
529
|
-
raise exc
|
530
|
-
retry_count += 1
|
531
|
-
if retry_count >= 3:
|
532
|
-
raise exc
|
533
|
-
|
534
|
-
result = result_response.result
|
535
549
|
if result.status == api_pb2.GenericResult.GENERIC_STATUS_FAILURE:
|
536
550
|
raise RemoteError(f"Image build for {image_id} failed with the exception:\n{result.exception}")
|
537
551
|
elif result.status == api_pb2.GenericResult.GENERIC_STATUS_TERMINATED:
|
@@ -545,7 +559,7 @@ class _Image(_Object, type_prefix="im"):
|
|
545
559
|
else:
|
546
560
|
raise RemoteError("Unknown status %s!" % result.status)
|
547
561
|
|
548
|
-
self._hydrate(image_id, resolver.client,
|
562
|
+
self._hydrate(image_id, resolver.client, metadata)
|
549
563
|
local_mounts = set()
|
550
564
|
for base in base_images.values():
|
551
565
|
local_mounts |= base._serve_mounts
|
@@ -666,7 +680,7 @@ class _Image(_Object, type_prefix="im"):
|
|
666
680
|
context_mount=mount,
|
667
681
|
)
|
668
682
|
|
669
|
-
def _add_local_python_packages(self, *packages:
|
683
|
+
def _add_local_python_packages(self, *packages: str, copy: bool = False) -> "_Image":
|
670
684
|
"""Adds Python package files to containers
|
671
685
|
|
672
686
|
Adds all files from the specified Python packages to containers running the Image.
|
@@ -1632,7 +1646,7 @@ class _Image(_Object, type_prefix="im"):
|
|
1632
1646
|
function = _Function.from_args(
|
1633
1647
|
info,
|
1634
1648
|
app=None,
|
1635
|
-
image=self,
|
1649
|
+
image=self, # type: ignore[reportArgumentType] # TODO: probably conflict with type stub?
|
1636
1650
|
secrets=secrets,
|
1637
1651
|
gpu=gpu,
|
1638
1652
|
mounts=mounts,
|
@@ -1700,7 +1714,7 @@ class _Image(_Object, type_prefix="im"):
|
|
1700
1714
|
"""
|
1701
1715
|
|
1702
1716
|
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
1703
|
-
commands = ["FROM base", f"WORKDIR {shlex.quote(path)}"]
|
1717
|
+
commands = ["FROM base", f"WORKDIR {shlex.quote(str(path))}"]
|
1704
1718
|
return DockerfileSpec(commands=commands, context_files={})
|
1705
1719
|
|
1706
1720
|
return _Image._from_args(
|
@@ -1744,7 +1758,7 @@ class _Image(_Object, type_prefix="im"):
|
|
1744
1758
|
|
1745
1759
|
This method is considered private since its interface may change - use it at your own risk!
|
1746
1760
|
"""
|
1747
|
-
last_entry_id:
|
1761
|
+
last_entry_id: str = ""
|
1748
1762
|
|
1749
1763
|
request = api_pb2.ImageJoinStreamingRequest(
|
1750
1764
|
image_id=self._object_id, timeout=55, last_entry_id=last_entry_id, include_logs_for_finished=True
|
modal/image.pyi
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import google.protobuf.message
|
2
|
+
import modal.client
|
2
3
|
import modal.cloud_bucket_mount
|
3
4
|
import modal.functions
|
4
5
|
import modal.gpu
|
@@ -28,7 +29,7 @@ def _get_modal_requirements_path(
|
|
28
29
|
) -> str: ...
|
29
30
|
def _get_modal_requirements_command(version: typing.Literal["2023.12", "2024.04", "2024.10"]) -> str: ...
|
30
31
|
def _flatten_str_args(
|
31
|
-
function_name: str, arg_name: str, args: typing.
|
32
|
+
function_name: str, arg_name: str, args: typing.Sequence[typing.Union[str, typing.List[str]]]
|
32
33
|
) -> typing.List[str]: ...
|
33
34
|
def _validate_packages(packages: typing.List[str]) -> bool: ...
|
34
35
|
def _warn_invalid_packages(old_command: str) -> None: ...
|
@@ -55,6 +56,10 @@ class DockerfileSpec:
|
|
55
56
|
def __repr__(self): ...
|
56
57
|
def __eq__(self, other): ...
|
57
58
|
|
59
|
+
async def _image_await_build_result(
|
60
|
+
image_id: str, client: modal.client._Client
|
61
|
+
) -> modal_proto.api_pb2.ImageJoinStreamingResponse: ...
|
62
|
+
|
58
63
|
class _Image(modal.object._Object):
|
59
64
|
force_build: bool
|
60
65
|
inside_exceptions: typing.List[Exception]
|
@@ -64,10 +69,10 @@ class _Image(modal.object._Object):
|
|
64
69
|
|
65
70
|
def _initialize_from_empty(self): ...
|
66
71
|
def _initialize_from_other(self, other: _Image): ...
|
67
|
-
def _hydrate_metadata(self,
|
72
|
+
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
68
73
|
def _add_mount_layer_or_copy(self, mount: modal.mount._Mount, copy: bool = False): ...
|
69
74
|
@property
|
70
|
-
def _mount_layers(self) -> typing.
|
75
|
+
def _mount_layers(self) -> typing.Sequence[modal.mount._Mount]: ...
|
71
76
|
def _assert_no_mount_layers(self): ...
|
72
77
|
@staticmethod
|
73
78
|
def _from_args(
|
@@ -109,7 +114,7 @@ class _Image(modal.object._Object):
|
|
109
114
|
def copy_local_file(
|
110
115
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
|
111
116
|
) -> _Image: ...
|
112
|
-
def _add_local_python_packages(self, *module_names, copy: bool = False) -> _Image: ...
|
117
|
+
def _add_local_python_packages(self, *module_names: str, copy: bool = False) -> _Image: ...
|
113
118
|
def copy_local_dir(
|
114
119
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
|
115
120
|
) -> _Image: ...
|
@@ -321,10 +326,10 @@ class Image(modal.object.Object):
|
|
321
326
|
def __init__(self, *args, **kwargs): ...
|
322
327
|
def _initialize_from_empty(self): ...
|
323
328
|
def _initialize_from_other(self, other: Image): ...
|
324
|
-
def _hydrate_metadata(self,
|
329
|
+
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
325
330
|
def _add_mount_layer_or_copy(self, mount: modal.mount.Mount, copy: bool = False): ...
|
326
331
|
@property
|
327
|
-
def _mount_layers(self) -> typing.
|
332
|
+
def _mount_layers(self) -> typing.Sequence[modal.mount.Mount]: ...
|
328
333
|
def _assert_no_mount_layers(self): ...
|
329
334
|
@staticmethod
|
330
335
|
def _from_args(
|
@@ -366,7 +371,7 @@ class Image(modal.object.Object):
|
|
366
371
|
def copy_local_file(
|
367
372
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
|
368
373
|
) -> Image: ...
|
369
|
-
def _add_local_python_packages(self, *module_names, copy: bool = False) -> Image: ...
|
374
|
+
def _add_local_python_packages(self, *module_names: str, copy: bool = False) -> Image: ...
|
370
375
|
def copy_local_dir(
|
371
376
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
|
372
377
|
) -> Image: ...
|
modal/object.py
CHANGED
@@ -17,7 +17,7 @@ O = TypeVar("O", bound="_Object")
|
|
17
17
|
|
18
18
|
_BLOCKING_O = synchronize_api(O)
|
19
19
|
|
20
|
-
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP = 300
|
20
|
+
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int = 300
|
21
21
|
|
22
22
|
|
23
23
|
def _get_environment_name(environment_name: Optional[str] = None, resolver: Optional[Resolver] = None) -> Optional[str]:
|
@@ -205,7 +205,7 @@ class _Object:
|
|
205
205
|
return self._local_uuid
|
206
206
|
|
207
207
|
@property
|
208
|
-
def object_id(self):
|
208
|
+
def object_id(self) -> str:
|
209
209
|
"""mdmd:hidden"""
|
210
210
|
return self._object_id
|
211
211
|
|
modal/object.pyi
CHANGED
@@ -86,7 +86,7 @@ class _Object:
|
|
86
86
|
@property
|
87
87
|
def local_uuid(self): ...
|
88
88
|
@property
|
89
|
-
def object_id(self): ...
|
89
|
+
def object_id(self) -> str: ...
|
90
90
|
@property
|
91
91
|
def is_hydrated(self) -> bool: ...
|
92
92
|
@property
|
@@ -188,7 +188,7 @@ class Object:
|
|
188
188
|
@property
|
189
189
|
def local_uuid(self): ...
|
190
190
|
@property
|
191
|
-
def object_id(self): ...
|
191
|
+
def object_id(self) -> str: ...
|
192
192
|
@property
|
193
193
|
def is_hydrated(self) -> bool: ...
|
194
194
|
@property
|
@@ -202,3 +202,5 @@ class Object:
|
|
202
202
|
|
203
203
|
def live_method(method): ...
|
204
204
|
def live_method_gen(method): ...
|
205
|
+
|
206
|
+
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int
|
modal/parallel_map.py
CHANGED
@@ -78,7 +78,7 @@ async def _map_invocation(
|
|
78
78
|
):
|
79
79
|
assert client.stub
|
80
80
|
request = api_pb2.FunctionMapRequest(
|
81
|
-
function_id=function.
|
81
|
+
function_id=function.object_id,
|
82
82
|
parent_input_id=current_input_id() or "",
|
83
83
|
function_call_type=api_pb2.FUNCTION_CALL_TYPE_MAP,
|
84
84
|
return_exceptions=return_exceptions,
|
@@ -131,7 +131,7 @@ async def _map_invocation(
|
|
131
131
|
nonlocal have_all_inputs, num_inputs
|
132
132
|
async for items in queue_batch_iterator(input_queue, MAP_INVOCATION_CHUNK_SIZE):
|
133
133
|
request = api_pb2.FunctionPutInputsRequest(
|
134
|
-
function_id=function.
|
134
|
+
function_id=function.object_id, inputs=items, function_call_id=function_call_id
|
135
135
|
)
|
136
136
|
logger.debug(
|
137
137
|
f"Pushing {len(items)} inputs to server. Num queued inputs awaiting push is {input_queue.qsize()}."
|