modal 0.66.17__py3-none-any.whl → 0.66.44__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/_container_entrypoint.py +5 -342
- modal/_runtime/container_io_manager.py +6 -14
- modal/_runtime/user_code_imports.py +361 -0
- modal/_utils/function_utils.py +28 -8
- modal/_utils/grpc_testing.py +33 -26
- modal/app.py +13 -46
- modal/cli/import_refs.py +4 -38
- modal/client.pyi +2 -2
- modal/cls.py +26 -19
- modal/cls.pyi +4 -4
- modal/dict.py +0 -6
- modal/dict.pyi +0 -4
- modal/experimental.py +0 -3
- modal/functions.py +42 -38
- modal/functions.pyi +9 -13
- modal/gpu.py +8 -6
- modal/image.py +141 -7
- modal/image.pyi +34 -4
- modal/io_streams.py +40 -33
- modal/io_streams.pyi +13 -13
- modal/mount.py +5 -2
- modal/network_file_system.py +0 -28
- modal/network_file_system.pyi +0 -14
- modal/partial_function.py +12 -2
- modal/queue.py +0 -6
- modal/queue.pyi +0 -4
- modal/sandbox.py +1 -1
- modal/volume.py +0 -22
- modal/volume.pyi +0 -9
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/METADATA +1 -2
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/RECORD +43 -42
- modal_proto/api.proto +3 -20
- modal_proto/api_grpc.py +0 -16
- modal_proto/api_pb2.py +389 -413
- modal_proto/api_pb2.pyi +12 -58
- modal_proto/api_pb2_grpc.py +0 -33
- modal_proto/api_pb2_grpc.pyi +0 -10
- modal_proto/modal_api_grpc.py +0 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/LICENSE +0 -0
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/WHEEL +0 -0
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/entry_points.txt +0 -0
- {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/top_level.txt +0 -0
modal/image.py
CHANGED
@@ -273,12 +273,24 @@ class _Image(_Object, type_prefix="im"):
|
|
273
273
|
|
274
274
|
force_build: bool
|
275
275
|
inside_exceptions: List[Exception]
|
276
|
-
|
276
|
+
_serve_mounts: typing.FrozenSet[_Mount] # used for mounts watching in `modal serve`
|
277
|
+
_deferred_mounts: Sequence[
|
278
|
+
_Mount
|
279
|
+
] # added as mounts on any container referencing the Image, see `def _mount_layers`
|
277
280
|
_metadata: Optional[api_pb2.ImageMetadata] = None # set on hydration, private for now
|
278
281
|
|
279
282
|
def _initialize_from_empty(self):
|
280
283
|
self.inside_exceptions = []
|
281
|
-
self.
|
284
|
+
self._serve_mounts = frozenset()
|
285
|
+
self._deferred_mounts = ()
|
286
|
+
self.force_build = False
|
287
|
+
|
288
|
+
def _initialize_from_other(self, other: "_Image"):
|
289
|
+
# used by .clone()
|
290
|
+
self.inside_exceptions = other.inside_exceptions
|
291
|
+
self.force_build = other.force_build
|
292
|
+
self._serve_mounts = other._serve_mounts
|
293
|
+
self._deferred_mounts = other._deferred_mounts
|
282
294
|
|
283
295
|
def _hydrate_metadata(self, message: Optional[Message]):
|
284
296
|
env_image_id = config.get("image_id") # set as an env var in containers
|
@@ -292,6 +304,51 @@ class _Image(_Object, type_prefix="im"):
|
|
292
304
|
assert isinstance(message, api_pb2.ImageMetadata)
|
293
305
|
self._metadata = message
|
294
306
|
|
307
|
+
def _add_mount_layer_or_copy(self, mount: _Mount, copy: bool = False):
|
308
|
+
if copy:
|
309
|
+
return self.copy_mount(mount, remote_path="/")
|
310
|
+
|
311
|
+
base_image = self
|
312
|
+
|
313
|
+
async def _load(self2: "_Image", resolver: Resolver, existing_object_id: Optional[str]):
|
314
|
+
self2._hydrate_from_other(base_image) # same image id as base image as long as it's lazy
|
315
|
+
self2._deferred_mounts = tuple(base_image._deferred_mounts) + (mount,)
|
316
|
+
self2._serve_mounts = base_image._serve_mounts | ({mount} if mount.is_local() else set())
|
317
|
+
|
318
|
+
return _Image._from_loader(_load, "Image(local files)", deps=lambda: [base_image, mount])
|
319
|
+
|
320
|
+
@property
|
321
|
+
def _mount_layers(self) -> typing.Tuple[_Mount]:
|
322
|
+
"""Non-evaluated mount layers on the image
|
323
|
+
|
324
|
+
When the image is used by a Modal container, these mounts need to be attached as well to
|
325
|
+
represent the full image content, as they haven't yet been represented as a layer in the
|
326
|
+
image.
|
327
|
+
|
328
|
+
When the image is used as a base image for a new layer (that is not itself a mount layer)
|
329
|
+
these mounts need to first be inserted as a copy operation (.copy_mount) into the image.
|
330
|
+
"""
|
331
|
+
return self._deferred_mounts
|
332
|
+
|
333
|
+
def _assert_no_mount_layers(self):
|
334
|
+
if self._mount_layers:
|
335
|
+
raise InvalidError(
|
336
|
+
"An image tried to run a build step after using `image.add_local_*` to include local files.\n"
|
337
|
+
"\n"
|
338
|
+
"Run `image.add_local_*` commands last in your image build to avoid rebuilding images with every local "
|
339
|
+
"file change. Modal will then add these files to containers on startup instead, saving build time.\n"
|
340
|
+
"If you need to run other build steps after adding local files, set `copy=True` to copy the files"
|
341
|
+
"directly into the image, at the expense of some added build time.\n"
|
342
|
+
"\n"
|
343
|
+
"Example:\n"
|
344
|
+
"\n"
|
345
|
+
"my_image = (\n"
|
346
|
+
" Image.debian_slim()\n"
|
347
|
+
' .add_local_file("data.json", copy=True)\n'
|
348
|
+
' .run_commands("python -m mypak") # this now works!\n'
|
349
|
+
")\n"
|
350
|
+
)
|
351
|
+
|
295
352
|
@staticmethod
|
296
353
|
def _from_args(
|
297
354
|
*,
|
@@ -306,9 +363,11 @@ class _Image(_Object, type_prefix="im"):
|
|
306
363
|
force_build: bool = False,
|
307
364
|
# For internal use only.
|
308
365
|
_namespace: int = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
366
|
+
_do_assert_no_mount_layers: bool = True,
|
309
367
|
):
|
310
368
|
if base_images is None:
|
311
369
|
base_images = {}
|
370
|
+
|
312
371
|
if secrets is None:
|
313
372
|
secrets = []
|
314
373
|
if gpu_config is None:
|
@@ -334,6 +393,11 @@ class _Image(_Object, type_prefix="im"):
|
|
334
393
|
return deps
|
335
394
|
|
336
395
|
async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
|
396
|
+
if _do_assert_no_mount_layers:
|
397
|
+
for image in base_images.values():
|
398
|
+
# base images can't have
|
399
|
+
image._assert_no_mount_layers()
|
400
|
+
|
337
401
|
environment = await _get_environment_cached(resolver.environment_name or "", resolver.client)
|
338
402
|
# A bit hacky,but assume that the environment provides a valid builder version
|
339
403
|
image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
|
@@ -349,7 +413,9 @@ class _Image(_Object, type_prefix="im"):
|
|
349
413
|
"No commands were provided for the image — have you tried using modal.Image.debian_slim()?"
|
350
414
|
)
|
351
415
|
if dockerfile.commands and build_function:
|
352
|
-
raise InvalidError(
|
416
|
+
raise InvalidError(
|
417
|
+
"Cannot provide both build function and Dockerfile commands in the same image layer!"
|
418
|
+
)
|
353
419
|
|
354
420
|
base_images_pb2s = [
|
355
421
|
api_pb2.BaseImage(
|
@@ -482,12 +548,12 @@ class _Image(_Object, type_prefix="im"):
|
|
482
548
|
self._hydrate(image_id, resolver.client, result_response.metadata)
|
483
549
|
local_mounts = set()
|
484
550
|
for base in base_images.values():
|
485
|
-
local_mounts |= base.
|
551
|
+
local_mounts |= base._serve_mounts
|
486
552
|
if context_mount and context_mount.is_local():
|
487
553
|
local_mounts.add(context_mount)
|
488
|
-
self.
|
554
|
+
self._serve_mounts = frozenset(local_mounts)
|
489
555
|
|
490
|
-
rep = "Image()"
|
556
|
+
rep = f"Image({dockerfile_function})"
|
491
557
|
obj = _Image._from_loader(_load, rep, deps=_deps)
|
492
558
|
obj.force_build = force_build
|
493
559
|
return obj
|
@@ -535,12 +601,59 @@ class _Image(_Object, type_prefix="im"):
|
|
535
601
|
context_mount=mount,
|
536
602
|
)
|
537
603
|
|
604
|
+
def add_local_file(self, local_path: Union[str, Path], remote_path: str, *, copy: bool = False) -> "_Image":
|
605
|
+
"""Adds a local file to the image at `remote_path` within the container
|
606
|
+
|
607
|
+
By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
|
608
|
+
which speeds up deployment.
|
609
|
+
|
610
|
+
Set `copy=True` to copy the files into an Image layer at build time instead, similar to how
|
611
|
+
[`COPY`](https://docs.docker.com/engine/reference/builder/#copy) works in a `Dockerfile`.
|
612
|
+
|
613
|
+
copy=True can slow down iteration since it requires a rebuild of the Image and any subsequent
|
614
|
+
build steps whenever the included files change, but it is required if you want to run additional
|
615
|
+
build steps after this one.
|
616
|
+
"""
|
617
|
+
if not PurePosixPath(remote_path).is_absolute():
|
618
|
+
# TODO(elias): implement relative to absolute resolution using image workdir metadata
|
619
|
+
# + make default remote_path="./"
|
620
|
+
# This requires deferring the Mount creation until after "self" (the base image) has been resolved
|
621
|
+
# so we know the workdir of the operation.
|
622
|
+
raise InvalidError("image.add_local_file() currently only supports absolute remote_path values")
|
623
|
+
|
624
|
+
if remote_path.endswith("/"):
|
625
|
+
remote_path = remote_path + Path(local_path).name
|
626
|
+
|
627
|
+
mount = _Mount.from_local_file(local_path, remote_path)
|
628
|
+
return self._add_mount_layer_or_copy(mount, copy=copy)
|
629
|
+
|
630
|
+
def add_local_dir(self, local_path: Union[str, Path], remote_path: str, *, copy: bool = False) -> "_Image":
|
631
|
+
"""Adds a local directory's content to the image at `remote_path` within the container
|
632
|
+
|
633
|
+
By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
|
634
|
+
which speeds up deployment.
|
635
|
+
|
636
|
+
Set `copy=True` to copy the files into an Image layer at build time instead, similar to how
|
637
|
+
[`COPY`](https://docs.docker.com/engine/reference/builder/#copy) works in a `Dockerfile`.
|
638
|
+
|
639
|
+
copy=True can slow down iteration since it requires a rebuild of the Image and any subsequent
|
640
|
+
build steps whenever the included files change, but it is required if you want to run additional
|
641
|
+
build steps after this one.
|
642
|
+
"""
|
643
|
+
if not PurePosixPath(remote_path).is_absolute():
|
644
|
+
# TODO(elias): implement relative to absolute resolution using image workdir metadata
|
645
|
+
# + make default remote_path="./"
|
646
|
+
raise InvalidError("image.add_local_dir() currently only supports absolute remote_path values")
|
647
|
+
mount = _Mount.from_local_dir(local_path, remote_path=remote_path)
|
648
|
+
return self._add_mount_layer_or_copy(mount, copy=copy)
|
649
|
+
|
538
650
|
def copy_local_file(self, local_path: Union[str, Path], remote_path: Union[str, Path] = "./") -> "_Image":
|
539
651
|
"""Copy a file into the image as a part of building it.
|
540
652
|
|
541
653
|
This works in a similar way to [`COPY`](https://docs.docker.com/engine/reference/builder/#copy)
|
542
654
|
works in a `Dockerfile`.
|
543
655
|
"""
|
656
|
+
# TODO(elias): add pending deprecation with suggestion to use add_* instead
|
544
657
|
basename = str(Path(local_path).name)
|
545
658
|
mount = _Mount.from_local_file(local_path, remote_path=f"/{basename}")
|
546
659
|
|
@@ -553,6 +666,27 @@ class _Image(_Object, type_prefix="im"):
|
|
553
666
|
context_mount=mount,
|
554
667
|
)
|
555
668
|
|
669
|
+
def _add_local_python_packages(self, *packages: Union[str, Path], copy: bool = False) -> "_Image":
|
670
|
+
"""Adds Python package files to containers
|
671
|
+
|
672
|
+
Adds all files from the specified Python packages to containers running the Image.
|
673
|
+
|
674
|
+
Packages are added to the `/root` directory of containers, which is on the `PYTHONPATH`
|
675
|
+
of any executed Modal Functions.
|
676
|
+
|
677
|
+
By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
|
678
|
+
which speeds up deployment.
|
679
|
+
|
680
|
+
Set `copy=True` to copy the files into an Image layer at build time instead. This can slow down iteration since
|
681
|
+
it requires a rebuild of the Image and any subsequent build steps whenever the included files change, but it is
|
682
|
+
required if you want to run additional build steps after this one.
|
683
|
+
|
684
|
+
**Note:** This excludes all dot-prefixed subdirectories or files and all `.pyc`/`__pycache__` files.
|
685
|
+
To add full directories with finer control, use `.add_local_dir()` instead.
|
686
|
+
"""
|
687
|
+
mount = _Mount.from_local_python_packages(*packages)
|
688
|
+
return self._add_mount_layer_or_copy(mount, copy=copy)
|
689
|
+
|
556
690
|
def copy_local_dir(self, local_path: Union[str, Path], remote_path: Union[str, Path] = ".") -> "_Image":
|
557
691
|
"""Copy a directory into the image as a part of building the image.
|
558
692
|
|
@@ -1550,7 +1684,7 @@ class _Image(_Object, type_prefix="im"):
|
|
1550
1684
|
dockerfile_function=build_dockerfile,
|
1551
1685
|
)
|
1552
1686
|
|
1553
|
-
def workdir(self, path: str) -> "_Image":
|
1687
|
+
def workdir(self, path: Union[str, PurePosixPath]) -> "_Image":
|
1554
1688
|
"""Set the working directory for subsequent image build steps and function execution.
|
1555
1689
|
|
1556
1690
|
**Example**
|
modal/image.pyi
CHANGED
@@ -58,11 +58,17 @@ class DockerfileSpec:
|
|
58
58
|
class _Image(modal.object._Object):
|
59
59
|
force_build: bool
|
60
60
|
inside_exceptions: typing.List[Exception]
|
61
|
-
|
61
|
+
_serve_mounts: typing.FrozenSet[modal.mount._Mount]
|
62
|
+
_deferred_mounts: typing.Sequence[modal.mount._Mount]
|
62
63
|
_metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
|
63
64
|
|
64
65
|
def _initialize_from_empty(self): ...
|
66
|
+
def _initialize_from_other(self, other: _Image): ...
|
65
67
|
def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
|
68
|
+
def _add_mount_layer_or_copy(self, mount: modal.mount._Mount, copy: bool = False): ...
|
69
|
+
@property
|
70
|
+
def _mount_layers(self) -> typing.Tuple[modal.mount._Mount]: ...
|
71
|
+
def _assert_no_mount_layers(self): ...
|
66
72
|
@staticmethod
|
67
73
|
def _from_args(
|
68
74
|
*,
|
@@ -78,6 +84,7 @@ class _Image(modal.object._Object):
|
|
78
84
|
context_mount: typing.Optional[modal.mount._Mount] = None,
|
79
85
|
force_build: bool = False,
|
80
86
|
_namespace: int = 1,
|
87
|
+
_do_assert_no_mount_layers: bool = True,
|
81
88
|
): ...
|
82
89
|
def extend(
|
83
90
|
self,
|
@@ -90,11 +97,19 @@ class _Image(modal.object._Object):
|
|
90
97
|
context_mount: typing.Optional[modal.mount._Mount] = None,
|
91
98
|
force_build: bool = False,
|
92
99
|
_namespace: int = 1,
|
100
|
+
_do_assert_no_mount_layers: bool = True,
|
93
101
|
) -> _Image: ...
|
94
102
|
def copy_mount(self, mount: modal.mount._Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> _Image: ...
|
103
|
+
def add_local_file(
|
104
|
+
self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
|
105
|
+
) -> _Image: ...
|
106
|
+
def add_local_dir(
|
107
|
+
self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
|
108
|
+
) -> _Image: ...
|
95
109
|
def copy_local_file(
|
96
110
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
|
97
111
|
) -> _Image: ...
|
112
|
+
def _add_local_python_packages(self, *module_names, copy: bool = False) -> _Image: ...
|
98
113
|
def copy_local_dir(
|
99
114
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
|
100
115
|
) -> _Image: ...
|
@@ -292,19 +307,25 @@ class _Image(modal.object._Object):
|
|
292
307
|
kwargs: typing.Dict[str, typing.Any] = {},
|
293
308
|
) -> _Image: ...
|
294
309
|
def env(self, vars: typing.Dict[str, str]) -> _Image: ...
|
295
|
-
def workdir(self, path: str) -> _Image: ...
|
310
|
+
def workdir(self, path: typing.Union[str, pathlib.PurePosixPath]) -> _Image: ...
|
296
311
|
def imports(self): ...
|
297
312
|
def _logs(self) -> typing.AsyncGenerator[str, None]: ...
|
298
313
|
|
299
314
|
class Image(modal.object.Object):
|
300
315
|
force_build: bool
|
301
316
|
inside_exceptions: typing.List[Exception]
|
302
|
-
|
317
|
+
_serve_mounts: typing.FrozenSet[modal.mount.Mount]
|
318
|
+
_deferred_mounts: typing.Sequence[modal.mount.Mount]
|
303
319
|
_metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
|
304
320
|
|
305
321
|
def __init__(self, *args, **kwargs): ...
|
306
322
|
def _initialize_from_empty(self): ...
|
323
|
+
def _initialize_from_other(self, other: Image): ...
|
307
324
|
def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
|
325
|
+
def _add_mount_layer_or_copy(self, mount: modal.mount.Mount, copy: bool = False): ...
|
326
|
+
@property
|
327
|
+
def _mount_layers(self) -> typing.Tuple[modal.mount.Mount]: ...
|
328
|
+
def _assert_no_mount_layers(self): ...
|
308
329
|
@staticmethod
|
309
330
|
def _from_args(
|
310
331
|
*,
|
@@ -320,6 +341,7 @@ class Image(modal.object.Object):
|
|
320
341
|
context_mount: typing.Optional[modal.mount.Mount] = None,
|
321
342
|
force_build: bool = False,
|
322
343
|
_namespace: int = 1,
|
344
|
+
_do_assert_no_mount_layers: bool = True,
|
323
345
|
): ...
|
324
346
|
def extend(
|
325
347
|
self,
|
@@ -332,11 +354,19 @@ class Image(modal.object.Object):
|
|
332
354
|
context_mount: typing.Optional[modal.mount.Mount] = None,
|
333
355
|
force_build: bool = False,
|
334
356
|
_namespace: int = 1,
|
357
|
+
_do_assert_no_mount_layers: bool = True,
|
335
358
|
) -> Image: ...
|
336
359
|
def copy_mount(self, mount: modal.mount.Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> Image: ...
|
360
|
+
def add_local_file(
|
361
|
+
self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
|
362
|
+
) -> Image: ...
|
363
|
+
def add_local_dir(
|
364
|
+
self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
|
365
|
+
) -> Image: ...
|
337
366
|
def copy_local_file(
|
338
367
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
|
339
368
|
) -> Image: ...
|
369
|
+
def _add_local_python_packages(self, *module_names, copy: bool = False) -> Image: ...
|
340
370
|
def copy_local_dir(
|
341
371
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
|
342
372
|
) -> Image: ...
|
@@ -534,7 +564,7 @@ class Image(modal.object.Object):
|
|
534
564
|
kwargs: typing.Dict[str, typing.Any] = {},
|
535
565
|
) -> Image: ...
|
536
566
|
def env(self, vars: typing.Dict[str, str]) -> Image: ...
|
537
|
-
def workdir(self, path: str) -> Image: ...
|
567
|
+
def workdir(self, path: typing.Union[str, pathlib.PurePosixPath]) -> Image: ...
|
538
568
|
def imports(self): ...
|
539
569
|
|
540
570
|
class ___logs_spec(typing_extensions.Protocol):
|
modal/io_streams.py
CHANGED
@@ -66,9 +66,10 @@ T = TypeVar("T", str, bytes)
|
|
66
66
|
|
67
67
|
|
68
68
|
class _StreamReader(Generic[T]):
|
69
|
-
"""
|
69
|
+
"""Retrieve logs from a stream (`stdout` or `stderr`).
|
70
70
|
|
71
|
-
As an asynchronous iterable, the object supports the async for
|
71
|
+
As an asynchronous iterable, the object supports the `for` and `async for`
|
72
|
+
statements. Just loop over the object to read in chunks.
|
72
73
|
|
73
74
|
**Usage**
|
74
75
|
|
@@ -140,12 +141,12 @@ class _StreamReader(Generic[T]):
|
|
140
141
|
self._consume_container_process_task = asyncio.create_task(self._consume_container_process_stream())
|
141
142
|
|
142
143
|
@property
|
143
|
-
def file_descriptor(self):
|
144
|
+
def file_descriptor(self) -> int:
|
145
|
+
"""Possible values are `1` for stdout and `2` for stderr."""
|
144
146
|
return self._file_descriptor
|
145
147
|
|
146
148
|
async def read(self) -> T:
|
147
|
-
"""Fetch
|
148
|
-
return an empty string.
|
149
|
+
"""Fetch the entire contents of the stream until EOF.
|
149
150
|
|
150
151
|
**Usage**
|
151
152
|
|
@@ -157,7 +158,6 @@ class _StreamReader(Generic[T]):
|
|
157
158
|
|
158
159
|
print(sandbox.stdout.read())
|
159
160
|
```
|
160
|
-
|
161
161
|
"""
|
162
162
|
data_str = ""
|
163
163
|
data_bytes = b""
|
@@ -175,9 +175,7 @@ class _StreamReader(Generic[T]):
|
|
175
175
|
return cast(T, data_bytes)
|
176
176
|
|
177
177
|
async def _consume_container_process_stream(self):
|
178
|
-
"""
|
179
|
-
Consumes the container process stream and stores the messages in the buffer.
|
180
|
-
"""
|
178
|
+
"""Consume the container process stream and store messages in the buffer."""
|
181
179
|
if self._stream_type == StreamType.DEVNULL:
|
182
180
|
return
|
183
181
|
|
@@ -211,9 +209,7 @@ class _StreamReader(Generic[T]):
|
|
211
209
|
raise exc
|
212
210
|
|
213
211
|
async def _stream_container_process(self) -> AsyncGenerator[Tuple[Optional[bytes], str], None]:
|
214
|
-
"""
|
215
|
-
Streams the container process buffer to the reader.
|
216
|
-
"""
|
212
|
+
"""Streams the container process buffer to the reader."""
|
217
213
|
entry_id = 0
|
218
214
|
if self._last_entry_id:
|
219
215
|
entry_id = int(self._last_entry_id) + 1
|
@@ -232,8 +228,7 @@ class _StreamReader(Generic[T]):
|
|
232
228
|
entry_id += 1
|
233
229
|
|
234
230
|
async def _get_logs(self) -> AsyncGenerator[Optional[bytes], None]:
|
235
|
-
"""
|
236
|
-
Streams sandbox or process logs from the server to the reader.
|
231
|
+
"""Streams sandbox or process logs from the server to the reader.
|
237
232
|
|
238
233
|
Logs returned by this method may contain partial or multiple lines at a time.
|
239
234
|
|
@@ -278,9 +273,7 @@ class _StreamReader(Generic[T]):
|
|
278
273
|
raise
|
279
274
|
|
280
275
|
async def _get_logs_by_line(self) -> AsyncGenerator[Optional[bytes], None]:
|
281
|
-
"""
|
282
|
-
Processes logs from the server and yields complete lines only.
|
283
|
-
"""
|
276
|
+
"""Process logs from the server and yield complete lines only."""
|
284
277
|
async for message in self._get_logs():
|
285
278
|
if message is None:
|
286
279
|
if self._line_buffer:
|
@@ -325,7 +318,8 @@ MAX_BUFFER_SIZE = 2 * 1024 * 1024
|
|
325
318
|
class _StreamWriter:
|
326
319
|
"""Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
|
327
320
|
|
328
|
-
def __init__(self, object_id: str, object_type: Literal["sandbox", "container_process"], client: _Client):
|
321
|
+
def __init__(self, object_id: str, object_type: Literal["sandbox", "container_process"], client: _Client) -> None:
|
322
|
+
"""mdmd:hidden"""
|
329
323
|
self._index = 1
|
330
324
|
self._object_id = object_id
|
331
325
|
self._object_type = object_type
|
@@ -333,17 +327,16 @@ class _StreamWriter:
|
|
333
327
|
self._is_closed = False
|
334
328
|
self._buffer = bytearray()
|
335
329
|
|
336
|
-
def
|
337
|
-
"""mdmd:hidden"""
|
330
|
+
def _get_next_index(self) -> int:
|
338
331
|
index = self._index
|
339
332
|
self._index += 1
|
340
333
|
return index
|
341
334
|
|
342
|
-
def write(self, data: Union[bytes, bytearray, memoryview, str]):
|
343
|
-
"""
|
344
|
-
Writes data to stream's internal buffer, but does not drain/flush the write.
|
335
|
+
def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None:
|
336
|
+
"""Write data to the stream but does not send it immediately.
|
345
337
|
|
346
|
-
This
|
338
|
+
This is non-blocking and queues the data to an internal buffer. Must be
|
339
|
+
used along with the `drain()` method, which flushes the buffer.
|
347
340
|
|
348
341
|
**Usage**
|
349
342
|
|
@@ -375,22 +368,36 @@ class _StreamWriter:
|
|
375
368
|
else:
|
376
369
|
raise TypeError(f"data argument must be a bytes-like object, not {type(data).__name__}")
|
377
370
|
|
378
|
-
def write_eof(self):
|
379
|
-
"""
|
380
|
-
Closes the write end of the stream after the buffered write data is drained.
|
381
|
-
If the process was blocked on input, it will become unblocked after `write_eof()`.
|
371
|
+
def write_eof(self) -> None:
|
372
|
+
"""Close the write end of the stream after the buffered data is drained.
|
382
373
|
|
383
|
-
|
374
|
+
If the process was blocked on input, it will become unblocked after
|
375
|
+
`write_eof()`. This method needs to be used along with the `drain()`
|
376
|
+
method, which flushes the EOF to the process.
|
384
377
|
"""
|
385
378
|
self._is_closed = True
|
386
379
|
|
387
|
-
async def drain(self):
|
388
|
-
"""
|
389
|
-
|
380
|
+
async def drain(self) -> None:
|
381
|
+
"""Flush the write buffer and send data to the running process.
|
382
|
+
|
383
|
+
This is a flow control method that blocks until data is sent. It returns
|
384
|
+
when it is appropriate to continue writing data to the stream.
|
385
|
+
|
386
|
+
**Usage**
|
387
|
+
|
388
|
+
```python
|
389
|
+
# Synchronous
|
390
|
+
writer.write(data)
|
391
|
+
writer.drain()
|
392
|
+
|
393
|
+
# Async
|
394
|
+
writer.write(data)
|
395
|
+
await writer.drain.aio()
|
396
|
+
```
|
390
397
|
"""
|
391
398
|
data = bytes(self._buffer)
|
392
399
|
self._buffer.clear()
|
393
|
-
index = self.
|
400
|
+
index = self._get_next_index()
|
394
401
|
|
395
402
|
try:
|
396
403
|
if self._object_type == "sandbox":
|
modal/io_streams.pyi
CHANGED
@@ -26,7 +26,7 @@ class _StreamReader(typing.Generic[T]):
|
|
26
26
|
by_line: bool = False,
|
27
27
|
) -> None: ...
|
28
28
|
@property
|
29
|
-
def file_descriptor(self): ...
|
29
|
+
def file_descriptor(self) -> int: ...
|
30
30
|
async def read(self) -> T: ...
|
31
31
|
async def _consume_container_process_stream(self): ...
|
32
32
|
def _stream_container_process(self) -> typing.AsyncGenerator[typing.Tuple[typing.Optional[bytes], str], None]: ...
|
@@ -38,11 +38,11 @@ class _StreamReader(typing.Generic[T]):
|
|
38
38
|
class _StreamWriter:
|
39
39
|
def __init__(
|
40
40
|
self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client._Client
|
41
|
-
): ...
|
42
|
-
def
|
43
|
-
def write(self, data: typing.Union[bytes, bytearray, memoryview, str]): ...
|
44
|
-
def write_eof(self): ...
|
45
|
-
async def drain(self): ...
|
41
|
+
) -> None: ...
|
42
|
+
def _get_next_index(self) -> int: ...
|
43
|
+
def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None: ...
|
44
|
+
def write_eof(self) -> None: ...
|
45
|
+
async def drain(self) -> None: ...
|
46
46
|
|
47
47
|
T_INNER = typing.TypeVar("T_INNER", covariant=True)
|
48
48
|
|
@@ -60,7 +60,7 @@ class StreamReader(typing.Generic[T]):
|
|
60
60
|
by_line: bool = False,
|
61
61
|
) -> None: ...
|
62
62
|
@property
|
63
|
-
def file_descriptor(self): ...
|
63
|
+
def file_descriptor(self) -> int: ...
|
64
64
|
|
65
65
|
class __read_spec(typing_extensions.Protocol[T_INNER]):
|
66
66
|
def __call__(self) -> T_INNER: ...
|
@@ -100,13 +100,13 @@ class StreamReader(typing.Generic[T]):
|
|
100
100
|
class StreamWriter:
|
101
101
|
def __init__(
|
102
102
|
self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client.Client
|
103
|
-
): ...
|
104
|
-
def
|
105
|
-
def write(self, data: typing.Union[bytes, bytearray, memoryview, str]): ...
|
106
|
-
def write_eof(self): ...
|
103
|
+
) -> None: ...
|
104
|
+
def _get_next_index(self) -> int: ...
|
105
|
+
def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None: ...
|
106
|
+
def write_eof(self) -> None: ...
|
107
107
|
|
108
108
|
class __drain_spec(typing_extensions.Protocol):
|
109
|
-
def __call__(self): ...
|
110
|
-
async def aio(self): ...
|
109
|
+
def __call__(self) -> None: ...
|
110
|
+
async def aio(self) -> None: ...
|
111
111
|
|
112
112
|
drain: __drain_spec
|
modal/mount.py
CHANGED
@@ -377,7 +377,9 @@ class _Mount(_Object, type_prefix="mo"):
|
|
377
377
|
)
|
378
378
|
|
379
379
|
def add_local_file(
|
380
|
-
self,
|
380
|
+
self,
|
381
|
+
local_path: Union[str, Path],
|
382
|
+
remote_path: Union[str, PurePosixPath, None] = None,
|
381
383
|
) -> "_Mount":
|
382
384
|
"""
|
383
385
|
Add a local file to the `Mount` object.
|
@@ -622,12 +624,13 @@ class _Mount(_Object, type_prefix="mo"):
|
|
622
624
|
client: Optional[_Client] = None,
|
623
625
|
) -> None:
|
624
626
|
check_object_name(deployment_name, "Mount")
|
627
|
+
environment_name = _get_environment_name(environment_name, resolver=None)
|
625
628
|
self._deployment_name = deployment_name
|
626
629
|
self._namespace = namespace
|
627
630
|
self._environment_name = environment_name
|
628
631
|
if client is None:
|
629
632
|
client = await _Client.from_env()
|
630
|
-
resolver = Resolver(client=client)
|
633
|
+
resolver = Resolver(client=client, environment_name=environment_name)
|
631
634
|
await resolver.load(self)
|
632
635
|
|
633
636
|
def _get_metadata(self) -> api_pb2.MountHandleMetadata:
|
modal/network_file_system.py
CHANGED
@@ -171,34 +171,6 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
171
171
|
tc.infinite_loop(lambda: client.stub.SharedVolumeHeartbeat(request), sleep=_heartbeat_sleep)
|
172
172
|
yield cls._new_hydrated(response.shared_volume_id, client, None, is_another_app=True)
|
173
173
|
|
174
|
-
@staticmethod
|
175
|
-
def persisted(
|
176
|
-
label: str,
|
177
|
-
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
178
|
-
environment_name: Optional[str] = None,
|
179
|
-
cloud: Optional[str] = None,
|
180
|
-
):
|
181
|
-
"""mdmd:hidden"""
|
182
|
-
message = (
|
183
|
-
"`NetworkFileSystem.persisted` is deprecated."
|
184
|
-
" Please use `NetworkFileSystem.from_name(name, create_if_missing=True)` instead."
|
185
|
-
)
|
186
|
-
deprecation_error((2024, 3, 1), message)
|
187
|
-
|
188
|
-
def persist(
|
189
|
-
self,
|
190
|
-
label: str,
|
191
|
-
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
192
|
-
environment_name: Optional[str] = None,
|
193
|
-
cloud: Optional[str] = None,
|
194
|
-
):
|
195
|
-
"""mdmd:hidden"""
|
196
|
-
message = (
|
197
|
-
"`NetworkFileSystem().persist('my-volume')` is deprecated."
|
198
|
-
" Please use `NetworkFileSystem.from_name('my-volume', create_if_missing=True)` instead."
|
199
|
-
)
|
200
|
-
deprecation_error((2024, 2, 29), message)
|
201
|
-
|
202
174
|
@staticmethod
|
203
175
|
async def lookup(
|
204
176
|
label: str,
|
modal/network_file_system.pyi
CHANGED
@@ -26,13 +26,6 @@ class _NetworkFileSystem(modal.object._Object):
|
|
26
26
|
_heartbeat_sleep: float = 300,
|
27
27
|
) -> typing.AsyncContextManager[_NetworkFileSystem]: ...
|
28
28
|
@staticmethod
|
29
|
-
def persisted(
|
30
|
-
label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
|
31
|
-
): ...
|
32
|
-
def persist(
|
33
|
-
self, label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
|
34
|
-
): ...
|
35
|
-
@staticmethod
|
36
29
|
async def lookup(
|
37
30
|
label: str,
|
38
31
|
namespace=1,
|
@@ -85,13 +78,6 @@ class NetworkFileSystem(modal.object.Object):
|
|
85
78
|
environment_name: typing.Optional[str] = None,
|
86
79
|
_heartbeat_sleep: float = 300,
|
87
80
|
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[NetworkFileSystem]: ...
|
88
|
-
@staticmethod
|
89
|
-
def persisted(
|
90
|
-
label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
|
91
|
-
): ...
|
92
|
-
def persist(
|
93
|
-
self, label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
|
94
|
-
): ...
|
95
81
|
|
96
82
|
class __lookup_spec(typing_extensions.Protocol):
|
97
83
|
def __call__(
|