modal 0.66.39__py3-none-any.whl → 0.66.46__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 +1 -1
- modal/_runtime/user_code_imports.py +1 -1
- modal/_utils/grpc_testing.py +33 -26
- modal/app.py +39 -24
- modal/app.pyi +4 -2
- modal/cli/import_refs.py +1 -1
- modal/cli/launch.py +6 -4
- modal/cli/run.py +2 -2
- modal/client.pyi +2 -2
- modal/cls.py +26 -19
- modal/cls.pyi +4 -4
- modal/functions.py +32 -29
- modal/functions.pyi +1 -5
- modal/image.py +49 -2
- modal/image.pyi +14 -2
- modal/io_streams.py +40 -33
- modal/io_streams.pyi +13 -13
- modal/mount.py +3 -1
- modal/partial_function.py +1 -1
- modal/runner.py +12 -6
- {modal-0.66.39.dist-info → modal-0.66.46.dist-info}/METADATA +1 -1
- {modal-0.66.39.dist-info → modal-0.66.46.dist-info}/RECORD +30 -30
- modal_proto/api.proto +2 -0
- modal_proto/api_pb2.py +244 -244
- modal_proto/api_pb2.pyi +7 -2
- modal_version/_version_generated.py +1 -1
- {modal-0.66.39.dist-info → modal-0.66.46.dist-info}/LICENSE +0 -0
- {modal-0.66.39.dist-info → modal-0.66.46.dist-info}/WHEEL +0 -0
- {modal-0.66.39.dist-info → modal-0.66.46.dist-info}/entry_points.txt +0 -0
- {modal-0.66.39.dist-info → modal-0.66.46.dist-info}/top_level.txt +0 -0
modal/functions.py
CHANGED
@@ -314,7 +314,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
314
314
|
_tag: str
|
315
315
|
_raw_f: Callable[..., Any]
|
316
316
|
_build_args: dict
|
317
|
-
|
317
|
+
|
318
318
|
_is_generator: Optional[bool] = None
|
319
319
|
_cluster_size: Optional[int] = None
|
320
320
|
|
@@ -323,10 +323,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
323
323
|
_use_function_id: str # The function to invoke
|
324
324
|
_use_method_name: str = ""
|
325
325
|
|
326
|
-
# TODO (elias): remove _parent. In case of instance functions, and methods bound on those,
|
327
|
-
# this references the parent class-function and is used to infer the client for lazy-loaded methods
|
328
|
-
_parent: Optional["_Function"] = None
|
329
|
-
|
330
326
|
_class_parameter_info: Optional["api_pb2.ClassParameterInfo"] = None
|
331
327
|
_method_handle_metadata: Optional[Dict[str, "api_pb2.FunctionHandleMetadata"]] = None
|
332
328
|
|
@@ -511,7 +507,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
511
507
|
fun._info = class_bound_method._info
|
512
508
|
fun._obj = instance_service_function._obj
|
513
509
|
fun._is_method = True
|
514
|
-
fun._parent = instance_service_function._parent
|
515
510
|
fun._app = class_bound_method._app
|
516
511
|
fun._spec = class_bound_method._spec
|
517
512
|
return fun
|
@@ -1019,27 +1014,37 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1019
1014
|
Binds a class-function to a specific instance of (init params, options) or a new workspace
|
1020
1015
|
"""
|
1021
1016
|
|
1022
|
-
|
1023
|
-
|
1017
|
+
# In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
|
1018
|
+
can_use_parent = len(args) + len(kwargs) == 0 and not from_other_workspace and options is None
|
1019
|
+
parent = self
|
1020
|
+
|
1021
|
+
async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
|
1022
|
+
if parent is None:
|
1024
1023
|
raise ExecutionError("Can't find the parent class' service function")
|
1025
1024
|
try:
|
1026
|
-
identity = f"{
|
1025
|
+
identity = f"{parent.info.function_name} class service function"
|
1027
1026
|
except Exception:
|
1028
1027
|
# Can't always look up the function name that way, so fall back to generic message
|
1029
1028
|
identity = "class service function for a parameterized class"
|
1030
|
-
if not
|
1031
|
-
if
|
1029
|
+
if not parent.is_hydrated:
|
1030
|
+
if parent.app._running_app is None:
|
1032
1031
|
reason = ", because the App it is defined on is not running"
|
1033
1032
|
else:
|
1034
1033
|
reason = ""
|
1035
1034
|
raise ExecutionError(
|
1036
1035
|
f"The {identity} has not been hydrated with the metadata it needs to run on Modal{reason}."
|
1037
1036
|
)
|
1038
|
-
|
1037
|
+
|
1038
|
+
assert parent._client.stub
|
1039
|
+
|
1040
|
+
if can_use_parent:
|
1041
|
+
# We can end up here if parent wasn't hydrated when class was instantiated, but has been since.
|
1042
|
+
param_bound_func._hydrate_from_other(parent)
|
1043
|
+
return
|
1044
|
+
|
1039
1045
|
if (
|
1040
|
-
|
1041
|
-
and
|
1042
|
-
== api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO
|
1046
|
+
parent._class_parameter_info
|
1047
|
+
and parent._class_parameter_info.format == api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO
|
1043
1048
|
):
|
1044
1049
|
if args:
|
1045
1050
|
# TODO(elias) - We could potentially support positional args as well, if we want to?
|
@@ -1047,34 +1052,30 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1047
1052
|
"Can't use positional arguments with modal.parameter-based synthetic constructors.\n"
|
1048
1053
|
"Use (<parameter_name>=value) keyword arguments when constructing classes instead."
|
1049
1054
|
)
|
1050
|
-
serialized_params = serialize_proto_params(kwargs,
|
1055
|
+
serialized_params = serialize_proto_params(kwargs, parent._class_parameter_info.schema)
|
1051
1056
|
else:
|
1052
1057
|
serialized_params = serialize((args, kwargs))
|
1053
1058
|
environment_name = _get_environment_name(None, resolver)
|
1054
|
-
assert
|
1059
|
+
assert parent is not None
|
1055
1060
|
req = api_pb2.FunctionBindParamsRequest(
|
1056
|
-
function_id=
|
1061
|
+
function_id=parent._object_id,
|
1057
1062
|
serialized_params=serialized_params,
|
1058
1063
|
function_options=options,
|
1059
1064
|
environment_name=environment_name
|
1060
1065
|
or "", # TODO: investigate shouldn't environment name always be specified here?
|
1061
1066
|
)
|
1062
1067
|
|
1063
|
-
response = await retry_transient_errors(
|
1064
|
-
|
1068
|
+
response = await retry_transient_errors(parent._client.stub.FunctionBindParams, req)
|
1069
|
+
param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
|
1065
1070
|
|
1066
1071
|
fun: _Function = _Function._from_loader(_load, "Function(parametrized)", hydrate_lazily=True)
|
1067
1072
|
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
# Edge case that lets us hydrate all objects right away
|
1072
|
-
# if the instance didn't use explicit constructor arguments
|
1073
|
-
fun._hydrate_from_other(self)
|
1073
|
+
if can_use_parent and parent.is_hydrated:
|
1074
|
+
# skip the resolver altogether:
|
1075
|
+
fun._hydrate_from_other(parent)
|
1074
1076
|
|
1075
1077
|
fun._info = self._info
|
1076
1078
|
fun._obj = obj
|
1077
|
-
fun._parent = self
|
1078
1079
|
return fun
|
1079
1080
|
|
1080
1081
|
@live_method
|
@@ -1265,8 +1266,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1265
1266
|
+ f"or call it locally: {self._function_name}.local()"
|
1266
1267
|
)
|
1267
1268
|
|
1269
|
+
# TODO (live_method on properties is not great, since it could be blocking the event loop from async contexts)
|
1268
1270
|
@property
|
1269
|
-
|
1271
|
+
@live_method
|
1272
|
+
async def web_url(self) -> str:
|
1270
1273
|
"""URL of a Function running as a web endpoint."""
|
1271
1274
|
if not self._web_url:
|
1272
1275
|
raise ValueError(
|
@@ -1439,7 +1442,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1439
1442
|
return fun(*args, **kwargs)
|
1440
1443
|
else:
|
1441
1444
|
# This is a method on a class, so bind the self to the function
|
1442
|
-
user_cls_instance = obj.
|
1445
|
+
user_cls_instance = obj._cached_user_cls_instance()
|
1443
1446
|
|
1444
1447
|
fun = info.raw_f.__get__(user_cls_instance)
|
1445
1448
|
|
modal/functions.pyi
CHANGED
@@ -120,12 +120,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
120
120
|
_tag: str
|
121
121
|
_raw_f: typing.Callable[..., typing.Any]
|
122
122
|
_build_args: dict
|
123
|
-
_can_use_base_function: bool
|
124
123
|
_is_generator: typing.Optional[bool]
|
125
124
|
_cluster_size: typing.Optional[int]
|
126
125
|
_use_function_id: str
|
127
126
|
_use_method_name: str
|
128
|
-
_parent: typing.Optional[_Function]
|
129
127
|
_class_parameter_info: typing.Optional[modal_proto.api_pb2.ClassParameterInfo]
|
130
128
|
_method_handle_metadata: typing.Optional[typing.Dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
131
129
|
|
@@ -218,7 +216,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
218
216
|
def _get_metadata(self): ...
|
219
217
|
def _check_no_web_url(self, fn_name: str): ...
|
220
218
|
@property
|
221
|
-
def web_url(self) -> str: ...
|
219
|
+
async def web_url(self) -> str: ...
|
222
220
|
@property
|
223
221
|
def is_generator(self) -> bool: ...
|
224
222
|
@property
|
@@ -296,12 +294,10 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
296
294
|
_tag: str
|
297
295
|
_raw_f: typing.Callable[..., typing.Any]
|
298
296
|
_build_args: dict
|
299
|
-
_can_use_base_function: bool
|
300
297
|
_is_generator: typing.Optional[bool]
|
301
298
|
_cluster_size: typing.Optional[int]
|
302
299
|
_use_function_id: str
|
303
300
|
_use_method_name: str
|
304
|
-
_parent: typing.Optional[Function]
|
305
301
|
_class_parameter_info: typing.Optional[modal_proto.api_pb2.ClassParameterInfo]
|
306
302
|
_method_handle_metadata: typing.Optional[typing.Dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
307
303
|
|
modal/image.py
CHANGED
@@ -344,7 +344,7 @@ class _Image(_Object, type_prefix="im"):
|
|
344
344
|
"\n"
|
345
345
|
"my_image = (\n"
|
346
346
|
" Image.debian_slim()\n"
|
347
|
-
'
|
347
|
+
' .add_local_file("data.json", copy=True)\n'
|
348
348
|
' .run_commands("python -m mypak") # this now works!\n'
|
349
349
|
")\n"
|
350
350
|
)
|
@@ -601,12 +601,59 @@ class _Image(_Object, type_prefix="im"):
|
|
601
601
|
context_mount=mount,
|
602
602
|
)
|
603
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
|
+
|
604
650
|
def copy_local_file(self, local_path: Union[str, Path], remote_path: Union[str, Path] = "./") -> "_Image":
|
605
651
|
"""Copy a file into the image as a part of building it.
|
606
652
|
|
607
653
|
This works in a similar way to [`COPY`](https://docs.docker.com/engine/reference/builder/#copy)
|
608
654
|
works in a `Dockerfile`.
|
609
655
|
"""
|
656
|
+
# TODO(elias): add pending deprecation with suggestion to use add_* instead
|
610
657
|
basename = str(Path(local_path).name)
|
611
658
|
mount = _Mount.from_local_file(local_path, remote_path=f"/{basename}")
|
612
659
|
|
@@ -1637,7 +1684,7 @@ class _Image(_Object, type_prefix="im"):
|
|
1637
1684
|
dockerfile_function=build_dockerfile,
|
1638
1685
|
)
|
1639
1686
|
|
1640
|
-
def workdir(self, path: str) -> "_Image":
|
1687
|
+
def workdir(self, path: Union[str, PurePosixPath]) -> "_Image":
|
1641
1688
|
"""Set the working directory for subsequent image build steps and function execution.
|
1642
1689
|
|
1643
1690
|
**Example**
|
modal/image.pyi
CHANGED
@@ -100,6 +100,12 @@ class _Image(modal.object._Object):
|
|
100
100
|
_do_assert_no_mount_layers: bool = True,
|
101
101
|
) -> _Image: ...
|
102
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: ...
|
103
109
|
def copy_local_file(
|
104
110
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
|
105
111
|
) -> _Image: ...
|
@@ -301,7 +307,7 @@ class _Image(modal.object._Object):
|
|
301
307
|
kwargs: typing.Dict[str, typing.Any] = {},
|
302
308
|
) -> _Image: ...
|
303
309
|
def env(self, vars: typing.Dict[str, str]) -> _Image: ...
|
304
|
-
def workdir(self, path: str) -> _Image: ...
|
310
|
+
def workdir(self, path: typing.Union[str, pathlib.PurePosixPath]) -> _Image: ...
|
305
311
|
def imports(self): ...
|
306
312
|
def _logs(self) -> typing.AsyncGenerator[str, None]: ...
|
307
313
|
|
@@ -351,6 +357,12 @@ class Image(modal.object.Object):
|
|
351
357
|
_do_assert_no_mount_layers: bool = True,
|
352
358
|
) -> Image: ...
|
353
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: ...
|
354
366
|
def copy_local_file(
|
355
367
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
|
356
368
|
) -> Image: ...
|
@@ -552,7 +564,7 @@ class Image(modal.object.Object):
|
|
552
564
|
kwargs: typing.Dict[str, typing.Any] = {},
|
553
565
|
) -> Image: ...
|
554
566
|
def env(self, vars: typing.Dict[str, str]) -> Image: ...
|
555
|
-
def workdir(self, path: str) -> Image: ...
|
567
|
+
def workdir(self, path: typing.Union[str, pathlib.PurePosixPath]) -> Image: ...
|
556
568
|
def imports(self): ...
|
557
569
|
|
558
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.
|
modal/partial_function.py
CHANGED
@@ -91,7 +91,7 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
91
91
|
if obj: # accessing the method on an instance of a class, e.g. `MyClass().fun``
|
92
92
|
if hasattr(obj, "_modal_functions"):
|
93
93
|
# This happens inside "local" user methods when they refer to other methods,
|
94
|
-
# e.g. Foo().parent_method()
|
94
|
+
# e.g. Foo().parent_method.remote() calling self.other_method.remote()
|
95
95
|
return getattr(obj, "_modal_functions")[k]
|
96
96
|
else:
|
97
97
|
# special edge case: referencing a method of an instance of an
|
modal/runner.py
CHANGED
@@ -327,11 +327,13 @@ async def _run_app(
|
|
327
327
|
)
|
328
328
|
|
329
329
|
try:
|
330
|
+
indexed_objects = dict(**app._functions, **app._classes) # TODO(erikbern): remove
|
331
|
+
|
330
332
|
# Create all members
|
331
|
-
await _create_all_objects(client, running_app,
|
333
|
+
await _create_all_objects(client, running_app, indexed_objects, environment_name)
|
332
334
|
|
333
335
|
# Publish the app
|
334
|
-
await _publish_app(client, running_app, app_state,
|
336
|
+
await _publish_app(client, running_app, app_state, indexed_objects)
|
335
337
|
except asyncio.CancelledError as e:
|
336
338
|
# this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
|
337
339
|
if output_mgr := _get_output_manager():
|
@@ -424,16 +426,18 @@ async def _serve_update(
|
|
424
426
|
try:
|
425
427
|
running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
|
426
428
|
|
429
|
+
indexed_objects = dict(**app._functions, **app._classes) # TODO(erikbern): remove
|
430
|
+
|
427
431
|
# Create objects
|
428
432
|
await _create_all_objects(
|
429
433
|
client,
|
430
434
|
running_app,
|
431
|
-
|
435
|
+
indexed_objects,
|
432
436
|
environment_name,
|
433
437
|
)
|
434
438
|
|
435
439
|
# Publish the updated app
|
436
|
-
await _publish_app(client, running_app, api_pb2.APP_STATE_UNSPECIFIED,
|
440
|
+
await _publish_app(client, running_app, api_pb2.APP_STATE_UNSPECIFIED, indexed_objects)
|
437
441
|
|
438
442
|
# Communicate to the parent process
|
439
443
|
is_ready.set()
|
@@ -521,17 +525,19 @@ async def _deploy_app(
|
|
521
525
|
|
522
526
|
tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL)
|
523
527
|
|
528
|
+
indexed_objects = dict(**app._functions, **app._classes) # TODO(erikbern): remove
|
529
|
+
|
524
530
|
try:
|
525
531
|
# Create all members
|
526
532
|
await _create_all_objects(
|
527
533
|
client,
|
528
534
|
running_app,
|
529
|
-
|
535
|
+
indexed_objects,
|
530
536
|
environment_name=environment_name,
|
531
537
|
)
|
532
538
|
|
533
539
|
app_url, warnings = await _publish_app(
|
534
|
-
client, running_app, api_pb2.APP_STATE_DEPLOYED,
|
540
|
+
client, running_app, api_pb2.APP_STATE_DEPLOYED, indexed_objects, name, tag
|
535
541
|
)
|
536
542
|
except Exception as e:
|
537
543
|
# Note that AppClientDisconnect only stops the app if it's still initializing, and is a no-op otherwise.
|