modal 1.1.2.dev11__tar.gz → 1.1.2.dev13__tar.gz
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-1.1.2.dev11 → modal-1.1.2.dev13}/PKG-INFO +2 -2
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/container_io_manager.py +0 -1
- modal-1.1.2.dev13/modal/_utils/time_utils.py +43 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/dict.py +6 -7
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/launch.py +78 -2
- modal-1.1.2.dev13/modal/cli/programs/launch_instance_ssh.py +94 -0
- modal-1.1.2.dev13/modal/cli/programs/run_marimo.py +95 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/queues.py +40 -15
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/secret.py +33 -7
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/volume.py +6 -9
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/client.pyi +2 -2
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/dict.py +78 -5
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/dict.pyi +115 -1
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/experimental/__init__.py +104 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/functions.pyi +6 -6
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/queue.py +76 -3
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/queue.pyi +114 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/secret.py +68 -1
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/secret.pyi +114 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/volume.py +79 -6
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/volume.pyi +118 -4
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal.egg-info/PKG-INFO +2 -2
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal.egg-info/SOURCES.txt +2 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal.egg-info/requires.txt +1 -1
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_docs/mdmd/mdmd.py +11 -1
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/api.proto +4 -4
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/api_pb2.pyi +4 -4
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_version/__init__.py +1 -1
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/pyproject.toml +1 -1
- modal-1.1.2.dev11/modal/_utils/time_utils.py +0 -19
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/LICENSE +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/README.md +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/__main__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_clustered_functions.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_clustered_functions.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_container_entrypoint.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_functions.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_ipython.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_location.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_object.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_output.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_partial_function.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_pty.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_resolver.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_resources.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/asgi.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/execution_context.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/telemetry.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_serialization.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_traceback.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_tunnel.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_tunnel.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_type_manager.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/app_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/async_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/blob_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/deprecation.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/docker_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/function_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/git_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/hash_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/http_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/logger.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/mount_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/name_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/package_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_utils/shell_utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_vendor/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_vendor/tblib.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/_watcher.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/app.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/app.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/builder/2023.12.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/builder/2024.04.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/builder/2024.10.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/builder/2025.06.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/builder/README.md +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/builder/base-images.json +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/call_graph.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/_download.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/_traceback.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/app.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/cluster.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/config.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/container.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/entry_point.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/environment.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/import_refs.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/network_file_system.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/profile.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/programs/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/programs/vscode.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/run.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/token.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cli/utils.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/client.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cls.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/cls.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/config.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/container_process.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/container_process.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/environments.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/environments.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/exception.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/experimental/flash.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/experimental/flash.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/experimental/ipython.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/file_io.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/file_io.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/file_pattern_matcher.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/functions.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/gpu.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/image.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/image.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/io_streams.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/io_streams.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/mount.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/mount.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/network_file_system.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/network_file_system.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/object.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/object.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/output.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/parallel_map.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/parallel_map.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/partial_function.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/partial_function.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/proxy.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/proxy.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/py.typed +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/retries.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/runner.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/runner.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/running_app.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/sandbox.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/sandbox.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/schedule.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/scheduler_placement.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/serving.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/serving.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/snapshot.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/snapshot.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/stream_type.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/token_flow.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal/token_flow.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal.egg-info/top_level.txt +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_docs/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/__init__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/api_grpc.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/api_pb2.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/modal_options_grpc.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/options.proto +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/options_grpc.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/options_pb2.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/options_pb2.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/options_pb2_grpc.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/options_pb2_grpc.pyi +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_proto/py.typed +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/modal_version/__main__.py +0 -0
- {modal-1.1.2.dev11 → modal-1.1.2.dev13}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: modal
|
3
|
-
Version: 1.1.2.
|
3
|
+
Version: 1.1.2.dev13
|
4
4
|
Summary: Python client library for Modal
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
6
6
|
License: Apache-2.0
|
@@ -22,7 +22,7 @@ Requires-Dist: click~=8.1
|
|
22
22
|
Requires-Dist: grpclib<0.4.9,>=0.4.7
|
23
23
|
Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
|
24
24
|
Requires-Dist: rich>=12.0.0
|
25
|
-
Requires-Dist: synchronicity~=0.10.
|
25
|
+
Requires-Dist: synchronicity~=0.10.2
|
26
26
|
Requires-Dist: toml
|
27
27
|
Requires-Dist: typer>=0.9
|
28
28
|
Requires-Dist: types-certifi
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Copyright Modal Labs 2025
|
2
|
+
from datetime import datetime, tzinfo
|
3
|
+
from typing import Optional, Union
|
4
|
+
|
5
|
+
|
6
|
+
def locale_tz() -> tzinfo:
|
7
|
+
return datetime.now().astimezone().tzinfo
|
8
|
+
|
9
|
+
|
10
|
+
def as_timestamp(arg: Optional[Union[datetime, str]]) -> float:
|
11
|
+
"""Coerce a user-provided argument to a timestamp.
|
12
|
+
|
13
|
+
An argument provided without timezone information will be treated as local time.
|
14
|
+
|
15
|
+
When the argument is null, returns the current time.
|
16
|
+
"""
|
17
|
+
if arg is None:
|
18
|
+
dt = datetime.now().astimezone()
|
19
|
+
elif isinstance(arg, str):
|
20
|
+
dt = datetime.fromisoformat(arg)
|
21
|
+
elif isinstance(arg, datetime):
|
22
|
+
dt = arg
|
23
|
+
else:
|
24
|
+
raise TypeError(f"Invalid argument: {arg}")
|
25
|
+
|
26
|
+
if dt.tzinfo is None:
|
27
|
+
dt = dt.replace(tzinfo=locale_tz())
|
28
|
+
return dt.timestamp()
|
29
|
+
|
30
|
+
|
31
|
+
def timestamp_to_localized_dt(ts: float) -> datetime:
|
32
|
+
return datetime.fromtimestamp(ts, tz=locale_tz())
|
33
|
+
|
34
|
+
|
35
|
+
def timestamp_to_localized_str(ts: float, isotz: bool = True) -> Optional[str]:
|
36
|
+
if ts > 0:
|
37
|
+
dt = timestamp_to_localized_dt(ts)
|
38
|
+
if isotz:
|
39
|
+
return dt.isoformat(sep=" ", timespec="seconds")
|
40
|
+
else:
|
41
|
+
return f"{dt:%Y-%m-%d %H:%M %Z}"
|
42
|
+
else:
|
43
|
+
return None
|
@@ -7,13 +7,11 @@ from typer import Argument, Option, Typer
|
|
7
7
|
from modal._output import make_console
|
8
8
|
from modal._resolver import Resolver
|
9
9
|
from modal._utils.async_utils import synchronizer
|
10
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
11
10
|
from modal._utils.time_utils import timestamp_to_localized_str
|
12
11
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
13
12
|
from modal.client import _Client
|
14
13
|
from modal.dict import _Dict
|
15
14
|
from modal.environments import ensure_env
|
16
|
-
from modal_proto import api_pb2
|
17
15
|
|
18
16
|
dict_cli = Typer(
|
19
17
|
name="dict",
|
@@ -40,12 +38,13 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
|
40
38
|
async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
41
39
|
"""List all named Dicts."""
|
42
40
|
env = ensure_env(env)
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
dicts = await _Dict.objects.list(environment_name=env)
|
42
|
+
rows = []
|
43
|
+
for obj in dicts:
|
44
|
+
info = await obj.info()
|
45
|
+
rows.append((info.name, timestamp_to_localized_str(info.created_at.timestamp(), json), info.created_by))
|
46
46
|
|
47
|
-
|
48
|
-
display_table(["Name", "Created at"], rows, json)
|
47
|
+
display_table(["Name", "Created at", "Created by"], rows, json)
|
49
48
|
|
50
49
|
|
51
50
|
@dict_cli.command("clear", rich_help_panel="Management")
|
@@ -3,6 +3,8 @@ import asyncio
|
|
3
3
|
import inspect
|
4
4
|
import json
|
5
5
|
import os
|
6
|
+
import subprocess
|
7
|
+
import tempfile
|
6
8
|
from pathlib import Path
|
7
9
|
from typing import Any, Optional
|
8
10
|
|
@@ -26,7 +28,9 @@ launch_cli = Typer(
|
|
26
28
|
)
|
27
29
|
|
28
30
|
|
29
|
-
def _launch_program(
|
31
|
+
def _launch_program(
|
32
|
+
name: str, filename: str, detach: bool, args: dict[str, Any], *, description: Optional[str] = None
|
33
|
+
) -> None:
|
30
34
|
os.environ["MODAL_LAUNCH_ARGS"] = json.dumps(args)
|
31
35
|
|
32
36
|
program_path = str(Path(__file__).parent / "programs" / filename)
|
@@ -35,7 +39,7 @@ def _launch_program(name: str, filename: str, detach: bool, args: dict[str, Any]
|
|
35
39
|
entrypoint = module.main
|
36
40
|
|
37
41
|
app = _get_runnable_app(entrypoint)
|
38
|
-
app.set_description(base_cmd)
|
42
|
+
app.set_description(description if description else base_cmd)
|
39
43
|
|
40
44
|
# `launch/` scripts must have a `local_entrypoint()` with no args, for simplicity here.
|
41
45
|
func = entrypoint.info.raw_f
|
@@ -108,3 +112,75 @@ def vscode(
|
|
108
112
|
"volume": volume,
|
109
113
|
}
|
110
114
|
_launch_program("vscode", "vscode.py", detach, args)
|
115
|
+
|
116
|
+
|
117
|
+
@launch_cli.command(name="machine", help="Start an instance on Modal, with direct SSH access.")
|
118
|
+
def machine(
|
119
|
+
name: str, # Name of the machine App.
|
120
|
+
cpu: int = 8, # Reservation of CPU cores (can burst above this value).
|
121
|
+
memory: int = 32768, # Reservation of memory in MiB (can burst above this value).
|
122
|
+
gpu: Optional[str] = None, # GPU type and count, e.g. "t4" or "h100:2".
|
123
|
+
image: Optional[str] = None, # Image tag to use from registry. Defaults to the notebook base image.
|
124
|
+
timeout: int = 3600 * 24, # Timeout in seconds for the instance.
|
125
|
+
volume: str = "machine-vol", # Attach a persisted `modal.Volume` at /workspace (created if missing).
|
126
|
+
):
|
127
|
+
tempdir = Path(tempfile.gettempdir())
|
128
|
+
key_path = tempdir / "modal-machine-keyfile.pem"
|
129
|
+
# Generate a new SSH key pair for this machine instance.
|
130
|
+
if not key_path.exists():
|
131
|
+
subprocess.run(
|
132
|
+
["ssh-keygen", "-t", "ed25519", "-f", str(key_path), "-N", ""],
|
133
|
+
check=True,
|
134
|
+
stdout=subprocess.DEVNULL,
|
135
|
+
)
|
136
|
+
# Add the key with expiry 1d to ssh agent.
|
137
|
+
subprocess.run(
|
138
|
+
["ssh-add", "-t", "1d", str(key_path)],
|
139
|
+
check=True,
|
140
|
+
stdout=subprocess.DEVNULL,
|
141
|
+
stderr=subprocess.DEVNULL,
|
142
|
+
)
|
143
|
+
|
144
|
+
os.environ["SSH_PUBLIC_KEY"] = Path(str(key_path) + ".pub").read_text()
|
145
|
+
os.environ["MODAL_LOGS_TIMEOUT"] = "0" # hack to work with --detach
|
146
|
+
|
147
|
+
args = {
|
148
|
+
"cpu": cpu,
|
149
|
+
"memory": memory,
|
150
|
+
"gpu": gpu,
|
151
|
+
"image": image,
|
152
|
+
"timeout": timeout,
|
153
|
+
"volume": volume,
|
154
|
+
}
|
155
|
+
_launch_program(
|
156
|
+
"machine",
|
157
|
+
"launch_instance_ssh.py",
|
158
|
+
True,
|
159
|
+
args,
|
160
|
+
description=name,
|
161
|
+
)
|
162
|
+
|
163
|
+
|
164
|
+
@launch_cli.command(name="marimo", help="Start a remote Marimo notebook on Modal.")
|
165
|
+
def marimo(
|
166
|
+
cpu: int = 8,
|
167
|
+
memory: int = 32768,
|
168
|
+
gpu: Optional[str] = None,
|
169
|
+
image: str = "debian:12",
|
170
|
+
timeout: int = 3600,
|
171
|
+
add_python: Optional[str] = "3.12",
|
172
|
+
mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
|
173
|
+
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
174
|
+
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
175
|
+
):
|
176
|
+
args = {
|
177
|
+
"cpu": cpu,
|
178
|
+
"memory": memory,
|
179
|
+
"gpu": gpu,
|
180
|
+
"timeout": timeout,
|
181
|
+
"image": image,
|
182
|
+
"add_python": add_python,
|
183
|
+
"mount": mount,
|
184
|
+
"volume": volume,
|
185
|
+
}
|
186
|
+
_launch_program("marimo", "run_marimo.py", detach, args)
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Copyright Modal Labs 2023
|
2
|
+
# type: ignore
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
import rich
|
9
|
+
import rich.panel
|
10
|
+
import rich.rule
|
11
|
+
|
12
|
+
import modal
|
13
|
+
import modal.experimental
|
14
|
+
|
15
|
+
# Passed by `modal launch` locally via CLI.
|
16
|
+
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
17
|
+
|
18
|
+
app = modal.App()
|
19
|
+
|
20
|
+
image: modal.Image
|
21
|
+
if args.get("image"):
|
22
|
+
image = modal.Image.from_registry(args.get("image"))
|
23
|
+
else:
|
24
|
+
# Must be set to the same image builder version as the notebook base image.
|
25
|
+
os.environ["MODAL_IMAGE_BUILDER_VERSION"] = "2024.10"
|
26
|
+
image = modal.experimental.notebook_base_image(python_version="3.12")
|
27
|
+
|
28
|
+
volume = (
|
29
|
+
modal.Volume.from_name(
|
30
|
+
args.get("volume"),
|
31
|
+
create_if_missing=True,
|
32
|
+
)
|
33
|
+
if args.get("volume")
|
34
|
+
else None
|
35
|
+
)
|
36
|
+
volumes = {"/workspace": volume} if volume else {}
|
37
|
+
|
38
|
+
|
39
|
+
startup_script = """
|
40
|
+
set -eu
|
41
|
+
mkdir -p /run/sshd
|
42
|
+
|
43
|
+
# Check if sshd is installed, install if not
|
44
|
+
test -x /usr/sbin/sshd || (apt-get update && apt-get install -y openssh-server)
|
45
|
+
|
46
|
+
# Change default working directory to /workspace
|
47
|
+
echo "cd /workspace" >> /root/.profile
|
48
|
+
|
49
|
+
mkdir -p /root/.ssh
|
50
|
+
echo "$SSH_PUBLIC_KEY" >> /root/.ssh/authorized_keys
|
51
|
+
/usr/sbin/sshd -D -e
|
52
|
+
"""
|
53
|
+
|
54
|
+
|
55
|
+
@app.local_entrypoint()
|
56
|
+
def main():
|
57
|
+
if not os.environ.get("SSH_PUBLIC_KEY"):
|
58
|
+
raise ValueError("SSH_PUBLIC_KEY environment variable is not set")
|
59
|
+
|
60
|
+
sb = modal.Sandbox.create(
|
61
|
+
*("sh", "-c", startup_script),
|
62
|
+
app=app,
|
63
|
+
image=image,
|
64
|
+
cpu=args.get("cpu"),
|
65
|
+
memory=args.get("memory"),
|
66
|
+
gpu=args.get("gpu"),
|
67
|
+
timeout=args.get("timeout"),
|
68
|
+
volumes=volumes,
|
69
|
+
unencrypted_ports=[22], # Forward SSH port
|
70
|
+
secrets=[modal.Secret.from_dict({"SSH_PUBLIC_KEY": os.environ.get("SSH_PUBLIC_KEY")})],
|
71
|
+
)
|
72
|
+
hostname, port = sb.tunnels()[22].tcp_socket
|
73
|
+
connection_cmd = f"ssh -A -p {port} root@{hostname}"
|
74
|
+
|
75
|
+
rich.print(
|
76
|
+
rich.rule.Rule(style="yellow"),
|
77
|
+
rich.panel.Panel(
|
78
|
+
f"""Your instance is ready! You can SSH into it using the following command:
|
79
|
+
|
80
|
+
[dim gray]>[/dim gray] [bold cyan]{connection_cmd}[/bold cyan]
|
81
|
+
|
82
|
+
[italic]Details:[/italic]
|
83
|
+
• Name: [magenta]{app.description}[/magenta]
|
84
|
+
• CPU: [yellow]{args.get("cpu")} cores[/yellow]
|
85
|
+
• Memory: [yellow]{args.get("memory")} MiB[/yellow]
|
86
|
+
• Timeout: [yellow]{args.get("timeout")} seconds[/yellow]
|
87
|
+
• GPU: [green]{(args.get("gpu") or "N/A").upper()}[/green]""",
|
88
|
+
title="SSH Connection",
|
89
|
+
expand=False,
|
90
|
+
),
|
91
|
+
rich.rule.Rule(style="yellow"),
|
92
|
+
)
|
93
|
+
|
94
|
+
sys.exit(0) # Exit immediately to prevent "Timed out waiting for final apps log."
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# Copyright Modal Labs 2025
|
2
|
+
# type: ignore
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import secrets
|
6
|
+
import socket
|
7
|
+
import subprocess
|
8
|
+
import threading
|
9
|
+
import time
|
10
|
+
import webbrowser
|
11
|
+
from typing import Any
|
12
|
+
|
13
|
+
from modal import App, Image, Queue, Secret, Volume, forward
|
14
|
+
|
15
|
+
# Args injected by `modal launch` CLI.
|
16
|
+
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
17
|
+
|
18
|
+
app = App()
|
19
|
+
|
20
|
+
image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).uv_pip_install("marimo")
|
21
|
+
|
22
|
+
# Optional host-filesystem mount (read-only snapshot of your project, useful for editing)
|
23
|
+
if args.get("mount"):
|
24
|
+
image = image.add_local_dir(args["mount"], remote_path="/root/marimo/mount")
|
25
|
+
|
26
|
+
# Optional persistent Modal volume
|
27
|
+
volume = Volume.from_name(args["volume"], create_if_missing=True) if args.get("volume") else None
|
28
|
+
volumes = {"/root/marimo/volume": volume} if volume else {}
|
29
|
+
|
30
|
+
|
31
|
+
def _wait_for_port(url: str, q: Queue) -> None:
|
32
|
+
start = time.monotonic()
|
33
|
+
while True:
|
34
|
+
try:
|
35
|
+
with socket.create_connection(("localhost", 8888), timeout=30):
|
36
|
+
break
|
37
|
+
except OSError as exc:
|
38
|
+
if time.monotonic() - start > 30:
|
39
|
+
raise TimeoutError("marimo server did not start within 30 s") from exc
|
40
|
+
time.sleep(0.05)
|
41
|
+
q.put(url)
|
42
|
+
|
43
|
+
|
44
|
+
@app.function(
|
45
|
+
image=image,
|
46
|
+
cpu=args.get("cpu"),
|
47
|
+
memory=args.get("memory"),
|
48
|
+
gpu=args.get("gpu"),
|
49
|
+
timeout=args.get("timeout"),
|
50
|
+
secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
|
51
|
+
volumes=volumes,
|
52
|
+
max_containers=1 if volume else None,
|
53
|
+
)
|
54
|
+
def run_marimo(q: Queue):
|
55
|
+
os.makedirs("/root/marimo", exist_ok=True)
|
56
|
+
|
57
|
+
# marimo supports token-based auth; generate one so only you can connect
|
58
|
+
token = secrets.token_urlsafe(12)
|
59
|
+
|
60
|
+
with forward(8888) as tunnel:
|
61
|
+
url = f"{tunnel.url}/?access_token={token}"
|
62
|
+
threading.Thread(target=_wait_for_port, args=(url, q), daemon=True).start()
|
63
|
+
|
64
|
+
print("\nmarimo on Modal, opening in browser …")
|
65
|
+
print(f" -> {url}\n")
|
66
|
+
|
67
|
+
# Launch the headless edit server
|
68
|
+
subprocess.run(
|
69
|
+
[
|
70
|
+
"marimo",
|
71
|
+
"edit",
|
72
|
+
"--headless", # don't open browser in container
|
73
|
+
"--host",
|
74
|
+
"0.0.0.0", # bind all interfaces
|
75
|
+
"--port",
|
76
|
+
"8888",
|
77
|
+
"--token-password",
|
78
|
+
token, # enable session-based auth
|
79
|
+
"--skip-update-check",
|
80
|
+
"/root/marimo", # workspace directory
|
81
|
+
],
|
82
|
+
env={**os.environ, "SHELL": "/bin/bash"},
|
83
|
+
)
|
84
|
+
|
85
|
+
q.put("done")
|
86
|
+
|
87
|
+
|
88
|
+
@app.local_entrypoint()
|
89
|
+
def main():
|
90
|
+
with Queue.ephemeral() as q:
|
91
|
+
run_marimo.spawn(q)
|
92
|
+
url = q.get() # first message = connect URL
|
93
|
+
time.sleep(1) # give server a heartbeat
|
94
|
+
webbrowser.open(url)
|
95
|
+
assert q.get() == "done"
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# Copyright Modal Labs 2024
|
2
|
+
from datetime import datetime
|
2
3
|
from typing import Optional
|
3
4
|
|
4
5
|
import typer
|
@@ -62,22 +63,46 @@ async def delete(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_
|
|
62
63
|
async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
63
64
|
"""List all named Queues."""
|
64
65
|
env = ensure_env(env)
|
65
|
-
|
66
|
-
max_total_size = 100_000
|
67
66
|
client = await _Client.from_env()
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
67
|
+
max_total_size = 100_000 # Limit on the *Queue size* that we report
|
68
|
+
|
69
|
+
items: list[api_pb2.QueueListResponse.QueueInfo] = []
|
70
|
+
|
71
|
+
# Note that we need to continue using the gRPC API directly here rather than using Queue.objects.list.
|
72
|
+
# There is some metadata that historically appears in the CLI output (num_partitions, total_size) that
|
73
|
+
# doesn't make sense to transmit as hydration metadata, because the values can change over time and
|
74
|
+
# the metadata retrieved at hydration time could get stale. Alternatively, we could rewrite this using
|
75
|
+
# only public API by sequentially retrieving the queues and then querying their dynamic metadata, but
|
76
|
+
# that would require multiple round trips and would add lag to the CLI.
|
77
|
+
async def retrieve_page(created_before: float) -> bool:
|
78
|
+
max_page_size = 100
|
79
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
80
|
+
req = api_pb2.QueueListRequest(environment_name=env, pagination=pagination, total_size_limit=max_total_size)
|
81
|
+
resp = await retry_transient_errors(client.stub.QueueList, req)
|
82
|
+
items.extend(resp.queues)
|
83
|
+
return len(resp.queues) < max_page_size
|
84
|
+
|
85
|
+
finished = await retrieve_page(datetime.now().timestamp())
|
86
|
+
while True:
|
87
|
+
if finished:
|
88
|
+
break
|
89
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
90
|
+
|
91
|
+
queues = [_Queue._new_hydrated(item.queue_id, client, item.metadata, is_another_app=True) for item in items]
|
92
|
+
|
93
|
+
rows = []
|
94
|
+
for obj, resp_data in zip(queues, items):
|
95
|
+
info = await obj.info()
|
96
|
+
rows.append(
|
97
|
+
(
|
98
|
+
obj.name,
|
99
|
+
timestamp_to_localized_str(info.created_at.timestamp(), json),
|
100
|
+
info.created_by,
|
101
|
+
str(resp_data.num_partitions),
|
102
|
+
str(resp_data.total_size) if resp_data.total_size <= max_total_size else f">{max_total_size}",
|
103
|
+
)
|
77
104
|
)
|
78
|
-
|
79
|
-
]
|
80
|
-
display_table(["Name", "Created at", "Partitions", "Total size"], rows, json)
|
105
|
+
display_table(["Name", "Created at", "Created by", "Partitions", "Total size"], rows, json)
|
81
106
|
|
82
107
|
|
83
108
|
@queue_cli.command(name="clear", rich_help_panel="Management")
|
@@ -119,7 +144,7 @@ async def peek(
|
|
119
144
|
|
120
145
|
@queue_cli.command(name="len", rich_help_panel="Inspection")
|
121
146
|
@synchronizer.create_blocking
|
122
|
-
async def
|
147
|
+
async def len_(
|
123
148
|
name: str,
|
124
149
|
partition: Optional[str] = PARTITION_OPTION,
|
125
150
|
total: bool = Option(False, "-t", "--total", help="Compute the sum of the queue lengths across all partitions"),
|
@@ -3,6 +3,7 @@ import json
|
|
3
3
|
import os
|
4
4
|
import platform
|
5
5
|
import subprocess
|
6
|
+
from datetime import datetime
|
6
7
|
from pathlib import Path
|
7
8
|
from tempfile import NamedTemporaryFile
|
8
9
|
from typing import Optional
|
@@ -30,20 +31,45 @@ secret_cli = typer.Typer(name="secret", help="Manage secrets.", no_args_is_help=
|
|
30
31
|
async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
31
32
|
env = ensure_env(env)
|
32
33
|
client = await _Client.from_env()
|
33
|
-
response = await retry_transient_errors(client.stub.SecretList, api_pb2.SecretListRequest(environment_name=env))
|
34
|
-
column_names = ["Name", "Created at", "Last used at"]
|
35
|
-
rows = []
|
36
34
|
|
37
|
-
|
35
|
+
items: list[api_pb2.SecretListItem] = []
|
36
|
+
|
37
|
+
# Note that we need to continue using the gRPC API directly here rather than using Secret.objects.list.
|
38
|
+
# There is some metadata that historically appears in the CLI output (last_used_at) that
|
39
|
+
# doesn't make sense to transmit as hydration metadata, because the value can change over time and
|
40
|
+
# the metadata retrieved at hydration time could get stale. Alternatively, we could rewrite this using
|
41
|
+
# only public API by sequentially retrieving the secrets and then querying their dynamic metadata, but
|
42
|
+
# that would require multiple round trips and would add lag to the CLI.
|
43
|
+
async def retrieve_page(created_before: float) -> bool:
|
44
|
+
max_page_size = 100
|
45
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
46
|
+
req = api_pb2.SecretListRequest(environment_name=env, pagination=pagination)
|
47
|
+
resp = await retry_transient_errors(client.stub.SecretList, req)
|
48
|
+
items.extend(resp.items)
|
49
|
+
return len(resp.items) < max_page_size
|
50
|
+
|
51
|
+
finished = await retrieve_page(datetime.now().timestamp())
|
52
|
+
while True:
|
53
|
+
if finished:
|
54
|
+
break
|
55
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
56
|
+
|
57
|
+
secrets = [_Secret._new_hydrated(item.secret_id, client, item.metadata, is_another_app=True) for item in items]
|
58
|
+
|
59
|
+
rows = []
|
60
|
+
for obj, resp_data in zip(secrets, items):
|
61
|
+
info = await obj.info()
|
38
62
|
rows.append(
|
39
63
|
[
|
40
|
-
|
41
|
-
timestamp_to_localized_str(
|
42
|
-
|
64
|
+
obj.name,
|
65
|
+
timestamp_to_localized_str(info.created_at.timestamp(), json),
|
66
|
+
info.created_by,
|
67
|
+
timestamp_to_localized_str(resp_data.last_used_at, json) if resp_data.last_used_at else "-",
|
43
68
|
]
|
44
69
|
)
|
45
70
|
|
46
71
|
env_part = f" in environment '{env}'" if env else ""
|
72
|
+
column_names = ["Name", "Created at", "Created by", "Last used at"]
|
47
73
|
display_table(column_names, rows, json, title=f"Secrets{env_part}")
|
48
74
|
|
49
75
|
|
@@ -13,11 +13,9 @@ from typer import Argument, Option, Typer
|
|
13
13
|
import modal
|
14
14
|
from modal._output import OutputManager, ProgressHandler, make_console
|
15
15
|
from modal._utils.async_utils import synchronizer
|
16
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
17
16
|
from modal._utils.time_utils import timestamp_to_localized_str
|
18
17
|
from modal.cli._download import _volume_download
|
19
18
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
20
|
-
from modal.client import _Client
|
21
19
|
from modal.environments import ensure_env
|
22
20
|
from modal.volume import _AbstractVolumeUploadContextManager, _Volume
|
23
21
|
from modal_proto import api_pb2
|
@@ -110,14 +108,13 @@ async def get(
|
|
110
108
|
@synchronizer.create_blocking
|
111
109
|
async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
112
110
|
env = ensure_env(env)
|
113
|
-
|
114
|
-
response = await retry_transient_errors(client.stub.VolumeList, api_pb2.VolumeListRequest(environment_name=env))
|
115
|
-
env_part = f" in environment '{env}'" if env else ""
|
116
|
-
column_names = ["Name", "Created at"]
|
111
|
+
volumes = await _Volume.objects.list(environment_name=env)
|
117
112
|
rows = []
|
118
|
-
for
|
119
|
-
|
120
|
-
|
113
|
+
for obj in volumes:
|
114
|
+
info = await obj.info()
|
115
|
+
rows.append((info.name, timestamp_to_localized_str(info.created_at.timestamp(), json), info.created_by))
|
116
|
+
|
117
|
+
display_table(["Name", "Created at", "Created by"], rows, json)
|
121
118
|
|
122
119
|
|
123
120
|
@volume_cli.command(
|
@@ -33,7 +33,7 @@ class _Client:
|
|
33
33
|
server_url: str,
|
34
34
|
client_type: int,
|
35
35
|
credentials: typing.Optional[tuple[str, str]],
|
36
|
-
version: str = "1.1.2.
|
36
|
+
version: str = "1.1.2.dev13",
|
37
37
|
):
|
38
38
|
"""mdmd:hidden
|
39
39
|
The Modal client object is not intended to be instantiated directly by users.
|
@@ -164,7 +164,7 @@ class Client:
|
|
164
164
|
server_url: str,
|
165
165
|
client_type: int,
|
166
166
|
credentials: typing.Optional[tuple[str, str]],
|
167
|
-
version: str = "1.1.2.
|
167
|
+
version: str = "1.1.2.dev13",
|
168
168
|
):
|
169
169
|
"""mdmd:hidden
|
170
170
|
The Modal client object is not intended to be instantiated directly by users.
|