modal 1.5.1.dev13__tar.gz → 1.5.1.dev15__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.5.1.dev13 → modal-1.5.1.dev15}/PKG-INFO +1 -1
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_image.py +3 -4
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/gpu_memory_snapshot.py +5 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/task_lifecycle_manager.py +9 -4
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_workspace.py +145 -1
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/app.py +24 -3
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/entry_point.py +4 -2
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/environment.py +10 -11
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/utils.py +8 -3
- modal-1.5.1.dev15/modal/cli/workspace.py +129 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/client.pyi +2 -2
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/sandbox.py +11 -20
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/sandbox.pyi +12 -9
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/secret.py +39 -9
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/secret.pyi +19 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/workspace.py +13 -1
- modal-1.5.1.dev15/modal/workspace.pyi +395 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal.egg-info/SOURCES.txt +1 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/api_grpc.py +16 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/api_pb2.py +969 -949
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/api_pb2.pyi +36 -2
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/api_pb2_grpc.py +34 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/api_pb2_grpc.pyi +12 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_version/__init__.py +1 -1
- modal-1.5.1.dev13/modal/workspace.pyi +0 -139
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/LICENSE +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/README.md +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/__main__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_billing.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_clustered_functions.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_environments.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_function_variants.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_functions.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_grpc_client.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_ipython.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_load_context.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_location.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_logs.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_object.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_output/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_output/manager.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_output/pty.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_output/rich.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_output/status.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_partial_function.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_resolver.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_resources.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_serialization.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_server.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_traceback.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_tunnel.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_tunnel.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_type_manager.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/logger.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_vendor/version.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/_watcher.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/app.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/app.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/billing.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/billing.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/builder/README.md +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/builder/base-images.json +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/call_graph.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/_download.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/_help.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/_traceback.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/billing.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/changelog.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/cluster.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/config.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/container.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/dashboard.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/dict.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/image.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/import_refs.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/launch.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/logo.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/profile.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/queues.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/run.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/secret.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/selector.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/shell.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/skills.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/token.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cli/volume.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/client.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cls.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/cls.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/config.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/container_process.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/container_process.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/dict.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/dict.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/environments.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/environments.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/exception.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/experimental/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/experimental/flash.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/experimental/ipython.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/file_io.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/file_io.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/functions.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/functions.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/image.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/image.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/io_streams.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/io_streams.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/mount.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/mount.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/network_file_system.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/network_file_system.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/object.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/object.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/output.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/parallel_map.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/parallel_map.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/partial_function.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/partial_function.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/proxy.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/proxy.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/py.typed +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/queue.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/queue.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/retries.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/runner.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/runner.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/running_app.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/sandbox_fs.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/schedule.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/scheduler_placement.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/server.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/server.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/serving.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/serving.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/snapshot.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/snapshot.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/stream_type.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/token_flow.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/token_flow.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/volume.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal/volume.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/__init__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/py.typed +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/modal_version/__main__.py +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/pyproject.toml +0 -0
- {modal-1.5.1.dev13 → modal-1.5.1.dev15}/setup.cfg +0 -0
|
@@ -2965,17 +2965,16 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2965
2965
|
"""
|
|
2966
2966
|
tag = _parse_named_image_ref(name)
|
|
2967
2967
|
|
|
2968
|
-
if
|
|
2968
|
+
if self._object_id is None:
|
|
2969
2969
|
raise InvalidError("Cannot publish an image that has not been created yet. Call `.build()` first.")
|
|
2970
2970
|
|
|
2971
|
-
_client = client or
|
|
2972
|
-
assert _client is not None
|
|
2971
|
+
_client = client or await _Client.from_env()
|
|
2973
2972
|
|
|
2974
2973
|
resolved_env = environment_name or config.get("environment") or ""
|
|
2975
2974
|
|
|
2976
2975
|
await _client.stub.ImagePublish(
|
|
2977
2976
|
api_pb2.ImagePublishRequest(
|
|
2978
|
-
image_id=self.
|
|
2977
|
+
image_id=self._object_id,
|
|
2979
2978
|
environment_name=resolved_env,
|
|
2980
2979
|
is_public=False,
|
|
2981
2980
|
tag=tag,
|
|
@@ -17,6 +17,11 @@ from modal.config import config, logger
|
|
|
17
17
|
|
|
18
18
|
CUDA_CHECKPOINT_PATH: str = config.get("cuda_checkpoint_path")
|
|
19
19
|
|
|
20
|
+
# Exit code used to signal to modal-runtime that a GPU memory snapshot restore failed
|
|
21
|
+
# and the task should be retried without a snapshot. This is a sentinel value that must
|
|
22
|
+
# stay in sync with `GPU_SNAPSHOT_RESTORE_FAILED_EXIT_CODE` in runner.rs.
|
|
23
|
+
SNAPSHOT_RESTORE_FAILED_EXIT_CODE: int = 222
|
|
24
|
+
|
|
20
25
|
# Maximum total duration for each individual `cuda-checkpoint` invocation.
|
|
21
26
|
CUDA_CHECKPOINT_TIMEOUT: float = 3 * 60.0
|
|
22
27
|
|
|
@@ -180,10 +180,15 @@ class _TaskLifecycleManager:
|
|
|
180
180
|
if self.function_def._experimental_enable_gpu_snapshot and self.function_def.resources.gpu_config.gpu_type:
|
|
181
181
|
logger.debug("GPU memory snapshot enabled. Attempting to restore GPU memory.")
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
try:
|
|
184
|
+
if self._cuda_checkpoint_session is None:
|
|
185
|
+
raise gpu_memory_snapshot.CudaCheckpointException("CudaCheckpointSession not found")
|
|
186
|
+
self._cuda_checkpoint_session.restore()
|
|
187
|
+
except gpu_memory_snapshot.CudaCheckpointException as exc:
|
|
188
|
+
logger.warning(f"GPU memory snapshot restore failed; retrying task without snapshot. Error: {exc}")
|
|
189
|
+
sys.stderr.flush()
|
|
190
|
+
# Exit with a sentinel code that the runtime will use to retry the task without a snapshot.
|
|
191
|
+
os._exit(gpu_memory_snapshot.SNAPSHOT_RESTORE_FAILED_EXIT_CODE)
|
|
187
192
|
|
|
188
193
|
self._client = await _Client.from_env()
|
|
189
194
|
|
|
@@ -121,10 +121,154 @@ class _Workspace(_Object, type_prefix="ac"):
|
|
|
121
121
|
def billing(self) -> "_WorkspaceBillingManager":
|
|
122
122
|
return _WorkspaceBillingManager(self)
|
|
123
123
|
|
|
124
|
+
@property
|
|
125
|
+
def proxy_tokens(self) -> "_WorkspaceProxyTokenManager":
|
|
126
|
+
return _WorkspaceProxyTokenManager(self)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class TokenData:
|
|
131
|
+
"""A token ID / secret pair."""
|
|
132
|
+
|
|
133
|
+
token_id: str
|
|
134
|
+
token_secret: str
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class ProxyTokenInfo:
|
|
139
|
+
"""Metadata about a proxy token, not including the token secret."""
|
|
140
|
+
|
|
141
|
+
token_id: str
|
|
142
|
+
created_at: datetime
|
|
143
|
+
scoped: bool
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _WorkspaceProxyTokenManager:
|
|
147
|
+
"""mdmd:namespace
|
|
148
|
+
Namespace with methods for managing the proxy tokens in a Workspace.
|
|
149
|
+
|
|
150
|
+
See [the guide](https://modal.com/docs/guide/webhook-proxy-auth) for more information on proxy tokens.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, workspace: "_Workspace"):
|
|
154
|
+
"""mdmd:hidden"""
|
|
155
|
+
self._workspace = workspace
|
|
156
|
+
|
|
157
|
+
async def create(self) -> TokenData:
|
|
158
|
+
"""Create a new proxy token for the Workspace.
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
```python notest
|
|
162
|
+
token = modal.Workspace.from_context().proxy_tokens.create()
|
|
163
|
+
print(token.token_id, token.token_secret)
|
|
164
|
+
```
|
|
165
|
+
"""
|
|
166
|
+
await self._workspace.hydrate()
|
|
167
|
+
resp = await self._workspace.client.stub.WebhookTokenCreate(api_pb2.WebhookTokenCreateRequest())
|
|
168
|
+
return TokenData(token_id=resp.token_id, token_secret=resp.token_secret)
|
|
169
|
+
|
|
170
|
+
async def list(self, environment_name: Optional[str] = None) -> builtins.list[ProxyTokenInfo]:
|
|
171
|
+
"""List proxy tokens in the Workspace.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
environment_name: When provided, list only the tokens associated with this environment.
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
```python notest
|
|
178
|
+
ws = modal.Workspace.from_context()
|
|
179
|
+
|
|
180
|
+
# List all proxy tokens in the Workspace
|
|
181
|
+
tokens = ws.proxy_tokens.list()
|
|
182
|
+
print([t.token_id for t in tokens])
|
|
183
|
+
|
|
184
|
+
# List only the proxy tokens associated with a specific Environment
|
|
185
|
+
env_tokens = ws.proxy_tokens.list(environment_name="prod")
|
|
186
|
+
```
|
|
187
|
+
"""
|
|
188
|
+
await self._workspace.hydrate()
|
|
189
|
+
if environment_name is None:
|
|
190
|
+
resp = await self._workspace.client.stub.WebhookTokenList(Empty())
|
|
191
|
+
else:
|
|
192
|
+
resp = await self._workspace.client.stub.WebhookTokenListForEnvironment(
|
|
193
|
+
api_pb2.WebhookTokenListForEnvironmentRequest(environment_name=environment_name)
|
|
194
|
+
)
|
|
195
|
+
return [
|
|
196
|
+
ProxyTokenInfo(
|
|
197
|
+
token_id=token.token_id,
|
|
198
|
+
created_at=timestamp_to_localized_dt(token.created_at),
|
|
199
|
+
scoped=token.scoped,
|
|
200
|
+
)
|
|
201
|
+
for token in resp.tokens
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
async def allow(self, proxy_token_id: str, environment_name: str) -> None:
|
|
205
|
+
"""Allow a proxy token to authenticate requests to a given Environment.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
proxy_token_id: The token ID (`wk-...`) to operate on.
|
|
209
|
+
environment_name: The name of the environment to allow access to.
|
|
210
|
+
|
|
211
|
+
Examples:
|
|
212
|
+
```python notest
|
|
213
|
+
ws = modal.Workspace.from_context()
|
|
214
|
+
token = ws.proxy_tokens.create()
|
|
215
|
+
ws.proxy_tokens.allow(token.token_id, "prod")
|
|
216
|
+
```
|
|
217
|
+
"""
|
|
218
|
+
await self._workspace.hydrate()
|
|
219
|
+
environment_id = await self._environment_id(environment_name)
|
|
220
|
+
req = api_pb2.WebhookTokenEnvironmentAddRequest(token_id=proxy_token_id, environment_id=environment_id)
|
|
221
|
+
await self._workspace.client.stub.WebhookTokenEnvironmentAdd(req)
|
|
222
|
+
|
|
223
|
+
async def revoke(self, proxy_token_id: str, environment_name: str) -> None:
|
|
224
|
+
"""Revoke a proxy token's access to a given Environment.
|
|
225
|
+
|
|
226
|
+
The proxy token is not deleted, and it will continue to authenticate requests to any
|
|
227
|
+
other Environments it is associated with.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
proxy_token_id: The token ID (`wk-...`) to operate on.
|
|
231
|
+
environment_name: The name of the environment to revoke access from.
|
|
232
|
+
|
|
233
|
+
Examples:
|
|
234
|
+
```python notest
|
|
235
|
+
ws = modal.Workspace.from_context()
|
|
236
|
+
ws.proxy_tokens.revoke(token_id, "prod")
|
|
237
|
+
```
|
|
238
|
+
"""
|
|
239
|
+
await self._workspace.hydrate()
|
|
240
|
+
environment_id = await self._environment_id(environment_name)
|
|
241
|
+
req = api_pb2.WebhookTokenEnvironmentRemoveRequest(token_id=proxy_token_id, environment_id=environment_id)
|
|
242
|
+
await self._workspace.client.stub.WebhookTokenEnvironmentRemove(req)
|
|
243
|
+
|
|
244
|
+
async def delete(self, proxy_token_id: str) -> None:
|
|
245
|
+
"""Delete a proxy token from the Workspace.
|
|
246
|
+
|
|
247
|
+
This cannot be reverted. Any clients currently using the token will immediately
|
|
248
|
+
lose access to associated resources.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
proxy_token_id: The token ID (`wk-...`) to delete.
|
|
252
|
+
|
|
253
|
+
Examples:
|
|
254
|
+
```python notest
|
|
255
|
+
modal.Workspace.from_context().proxy_tokens.delete(token_id)
|
|
256
|
+
```
|
|
257
|
+
"""
|
|
258
|
+
await self._workspace.hydrate()
|
|
259
|
+
await self._workspace.client.stub.WebhookTokenDelete(api_pb2.TokenDeleteRequest(token_id=proxy_token_id))
|
|
260
|
+
|
|
261
|
+
async def _environment_id(self, environment_name: str) -> str:
|
|
262
|
+
# The environment-association RPCs key on environment ID, so resolve the name first.
|
|
263
|
+
from ._environments import _Environment
|
|
264
|
+
|
|
265
|
+
environment = await _Environment.from_name(environment_name, client=self._workspace.client).hydrate()
|
|
266
|
+
return environment.object_id
|
|
267
|
+
|
|
124
268
|
|
|
125
269
|
class _WorkspaceBillingManager:
|
|
126
270
|
"""mdmd:namespace
|
|
127
|
-
Namespace for Workspace billing APIs
|
|
271
|
+
Namespace for Workspace billing APIs.
|
|
128
272
|
"""
|
|
129
273
|
|
|
130
274
|
def __init__(self, workspace: _Workspace):
|
|
@@ -4,7 +4,7 @@ import sys
|
|
|
4
4
|
import time
|
|
5
5
|
import warnings
|
|
6
6
|
from datetime import datetime, timedelta, timezone
|
|
7
|
-
from typing import get_args
|
|
7
|
+
from typing import Literal, get_args
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
10
|
import rich
|
|
@@ -268,6 +268,7 @@ async def logs(
|
|
|
268
268
|
```
|
|
269
269
|
|
|
270
270
|
"""
|
|
271
|
+
env = ensure_env(env)
|
|
271
272
|
|
|
272
273
|
if follow and (since or until or tail):
|
|
273
274
|
raise UsageError("--follow cannot be combined with --since, --until, or --tail.")
|
|
@@ -353,11 +354,18 @@ async def logs(
|
|
|
353
354
|
@app_cli.command("rollback", no_args_is_help=True, context_settings={"ignore_unknown_options": True})
|
|
354
355
|
@click.argument("app_identifier")
|
|
355
356
|
@click.argument("version", default="")
|
|
357
|
+
@click.option(
|
|
358
|
+
"--strategy",
|
|
359
|
+
default="rolling",
|
|
360
|
+
type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
|
|
361
|
+
help="Strategy for rollback",
|
|
362
|
+
)
|
|
356
363
|
@env_option
|
|
357
364
|
@synchronizer.create_blocking
|
|
358
365
|
async def rollback(
|
|
359
366
|
app_identifier: str,
|
|
360
367
|
version: str = "",
|
|
368
|
+
strategy: Literal["recreate", "rolling"] = "rolling",
|
|
361
369
|
*,
|
|
362
370
|
env: str | None = None,
|
|
363
371
|
):
|
|
@@ -403,8 +411,20 @@ async def rollback(
|
|
|
403
411
|
else:
|
|
404
412
|
raise UsageError(f"Invalid version specifier: {version}")
|
|
405
413
|
req = api_pb2.AppRollbackRequest(app_id=app_id, version=version_number)
|
|
406
|
-
await client.stub.AppRollback(req)
|
|
407
|
-
|
|
414
|
+
resp = await client.stub.AppRollback(req)
|
|
415
|
+
print_server_warnings(resp.server_warnings)
|
|
416
|
+
output_mgr = OutputManager.get()
|
|
417
|
+
if strategy == "recreate":
|
|
418
|
+
try:
|
|
419
|
+
await _stop_and_wait_for_containers(client, app_id, resp.deployed_at, env)
|
|
420
|
+
except Exception as exc:
|
|
421
|
+
warnings.warn(
|
|
422
|
+
f"App rollback executed successfully, but containers did not all terminate. {exc}", UserWarning
|
|
423
|
+
)
|
|
424
|
+
output_mgr.print(f"\nView Deployment: [magenta]{resp.url}[/magenta]")
|
|
425
|
+
sys.exit(1)
|
|
426
|
+
output_mgr.print("[green]✓[/green] Deployment rollback successful!")
|
|
427
|
+
output_mgr.print(f"\nView Deployment: [magenta]{resp.url}[/magenta]")
|
|
408
428
|
|
|
409
429
|
|
|
410
430
|
@app_cli.command("rollover", no_args_is_help=True)
|
|
@@ -626,6 +646,7 @@ async def dashboard(
|
|
|
626
646
|
modal app dashboard my-app --env dev
|
|
627
647
|
```
|
|
628
648
|
"""
|
|
649
|
+
env = ensure_env(env)
|
|
629
650
|
client = await _Client.from_env()
|
|
630
651
|
app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
|
|
631
652
|
url = f"https://modal.com/id/{app_id}"
|
|
@@ -29,6 +29,7 @@ from .secret import secret_cli
|
|
|
29
29
|
from .skills import skills_cli
|
|
30
30
|
from .token import _new_token, token_cli
|
|
31
31
|
from .volume import volume_cli
|
|
32
|
+
from .workspace import workspace_cli
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
@click.group(
|
|
@@ -117,11 +118,12 @@ entrypoint_cli.add_command(volume_cli, panel="Storage")
|
|
|
117
118
|
entrypoint_cli.add_command(setup, panel="Onboarding")
|
|
118
119
|
entrypoint_cli.add_command(bootstrap, panel="Onboarding")
|
|
119
120
|
|
|
120
|
-
entrypoint_cli.add_command(
|
|
121
|
+
entrypoint_cli.add_command(workspace_cli, panel="Configuration")
|
|
121
122
|
entrypoint_cli.add_command(environment_cli, panel="Configuration")
|
|
122
123
|
entrypoint_cli.add_command(profile_cli, panel="Configuration")
|
|
123
|
-
entrypoint_cli.add_command(
|
|
124
|
+
entrypoint_cli.add_command(config_cli, panel="Configuration")
|
|
124
125
|
entrypoint_cli.add_command(token_cli, panel="Configuration")
|
|
126
|
+
entrypoint_cli.add_command(skills_cli, panel="Configuration")
|
|
125
127
|
|
|
126
128
|
entrypoint_cli.add_command(billing_cli, panel="Observability")
|
|
127
129
|
entrypoint_cli.add_command(changelog, panel="Observability")
|
|
@@ -187,11 +187,10 @@ environment_cli.add_command(billing_cli)
|
|
|
187
187
|
|
|
188
188
|
|
|
189
189
|
@billing_cli.command("report", no_args_is_help=True)
|
|
190
|
-
@click.
|
|
191
|
-
"
|
|
192
|
-
|
|
190
|
+
@click.argument(
|
|
191
|
+
"environment_name",
|
|
192
|
+
required=False,
|
|
193
193
|
default=None,
|
|
194
|
-
help="Name of the environment to build a report for. If no argument is provided, uses the current default.",
|
|
195
194
|
)
|
|
196
195
|
@click.option("--start", default=None, help=f"Start date. {DATE_HELP}")
|
|
197
196
|
@click.option("--end", default=None, help=f"End date. {DATE_HELP} Defaults to now.")
|
|
@@ -244,19 +243,19 @@ async def environment_billing(
|
|
|
244
243
|
Examples:
|
|
245
244
|
|
|
246
245
|
```bash
|
|
247
|
-
modal environment billing report
|
|
246
|
+
modal environment billing report --start 2025-12-01 --end 2026-01-01
|
|
248
247
|
|
|
249
|
-
modal environment billing report
|
|
248
|
+
modal environment billing report --for "last month" --tag-names team,project
|
|
250
249
|
|
|
251
|
-
modal environment billing report
|
|
250
|
+
modal environment billing report test_env --for today --resolution h
|
|
252
251
|
|
|
253
|
-
modal environment billing report
|
|
252
|
+
modal environment billing report test_env --for "this month" --show-resources
|
|
254
253
|
|
|
255
|
-
modal environment billing report
|
|
254
|
+
modal environment billing report prod_env --for yesterday -r h --tz local
|
|
256
255
|
|
|
257
|
-
modal environment billing report
|
|
256
|
+
modal environment billing report main_env --for "last month" --csv > report.csv
|
|
258
257
|
|
|
259
|
-
modal environment billing report
|
|
258
|
+
modal environment billing report main_env --start 2025-12-01 --json > report.json
|
|
260
259
|
```
|
|
261
260
|
|
|
262
261
|
"""
|
|
@@ -123,7 +123,7 @@ async def fetch_app_logs(
|
|
|
123
123
|
await _drain_batches(output_mgr, batches, prefix_fields or [], filters.search_text)
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
def _plain(text: Text | str) -> str:
|
|
126
|
+
def _plain(text: "Text | str | bool | None") -> "str | bool | None":
|
|
127
127
|
return text.plain if isinstance(text, Text) else text
|
|
128
128
|
|
|
129
129
|
|
|
@@ -138,7 +138,7 @@ def is_tty() -> bool:
|
|
|
138
138
|
|
|
139
139
|
def display_table(
|
|
140
140
|
columns: Sequence[Column | str],
|
|
141
|
-
rows: Sequence[Sequence[Text | str]],
|
|
141
|
+
rows: Sequence[Sequence["Text | str | bool | None"]],
|
|
142
142
|
json: bool = False,
|
|
143
143
|
csv: bool = False,
|
|
144
144
|
title: str = "",
|
|
@@ -164,7 +164,12 @@ def display_table(
|
|
|
164
164
|
else:
|
|
165
165
|
table = Table(*columns, title=title)
|
|
166
166
|
for row in rows:
|
|
167
|
-
|
|
167
|
+
# rich can't render bare scalars like bools; stringify anything that isn't already
|
|
168
|
+
# a renderable (str/Text) or None (which rich treats as an empty cell).
|
|
169
|
+
cells: list[Text | str | None] = [
|
|
170
|
+
cell if cell is None or isinstance(cell, (str, Text)) else str(cell) for cell in row
|
|
171
|
+
]
|
|
172
|
+
table.add_row(*cells)
|
|
168
173
|
output.print(table)
|
|
169
174
|
|
|
170
175
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Copyright Modal Labs 2025
|
|
2
|
+
import json as json_mod
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import rich
|
|
6
|
+
|
|
7
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
8
|
+
from modal.cli.utils import confirm_or_suggest_yes, display_table, yes_option
|
|
9
|
+
from modal.output import OutputManager
|
|
10
|
+
from modal.workspace import Workspace
|
|
11
|
+
|
|
12
|
+
from ._help import ModalGroup
|
|
13
|
+
|
|
14
|
+
WORKSPACE_HELP_TEXT = """Interact with the current Modal Workspace.
|
|
15
|
+
|
|
16
|
+
A Workspace is the top-level account that owns your Modal resources. Use these commands
|
|
17
|
+
to manage workspace-level settings such as proxy tokens.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
workspace_cli = ModalGroup(name="workspace", help=WORKSPACE_HELP_TEXT)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MEMBERS_HELP_TEXT = """View the members of the current Workspace."""
|
|
24
|
+
|
|
25
|
+
members_cli = ModalGroup(name="members", help=MEMBERS_HELP_TEXT)
|
|
26
|
+
workspace_cli.add_command(members_cli)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@members_cli.command("list", help="List the members of the current Workspace.")
|
|
30
|
+
@click.option("--json", is_flag=True, default=False)
|
|
31
|
+
def members_list(json: bool = False):
|
|
32
|
+
members = Workspace.from_context().members.list()
|
|
33
|
+
rows = [
|
|
34
|
+
[
|
|
35
|
+
member.name,
|
|
36
|
+
member.email,
|
|
37
|
+
member.role,
|
|
38
|
+
member.user_id,
|
|
39
|
+
timestamp_to_localized_str(member.joined_at.timestamp(), json),
|
|
40
|
+
timestamp_to_localized_str(member.last_active_at.timestamp(), json) if member.last_active_at else None,
|
|
41
|
+
]
|
|
42
|
+
for member in members
|
|
43
|
+
]
|
|
44
|
+
display_table(["Name", "Email", "Role", "User ID", "Joined", "Last active"], rows, json=json)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
PROXY_TOKENS_HELP_TEXT = """Manage the proxy tokens of the current Workspace.
|
|
48
|
+
|
|
49
|
+
Proxy tokens provide authentication to HTTP interfaces on Modal Servers and Web Functions.
|
|
50
|
+
They are passed as request headers (`Modal-Key` and `Modal-Secret`). See
|
|
51
|
+
https://modal.com/docs/guide/webhook-proxy-auth for more information.
|
|
52
|
+
|
|
53
|
+
Proxy tokens and secrets have `wk-` and `ws-` prefixes, respectively. The cannot be
|
|
54
|
+
interchanged with API tokens (which use `ak-` and `as-` prefixes).
|
|
55
|
+
|
|
56
|
+
On workspaces with RBAC enabled, tokens are scoped to specific environments;
|
|
57
|
+
use the `allow` and `revoke` commands to manage environment associations.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
proxy_tokens_cli = ModalGroup(name="proxy-tokens", help=PROXY_TOKENS_HELP_TEXT)
|
|
61
|
+
workspace_cli.add_command(proxy_tokens_cli)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@proxy_tokens_cli.command("create", help="Create a proxy token in the current Workspace.")
|
|
65
|
+
@click.option("--json", is_flag=True, default=False)
|
|
66
|
+
def proxy_tokens_create(*, json: bool = False):
|
|
67
|
+
"""Create a proxy token in the current Workspace.
|
|
68
|
+
|
|
69
|
+
The new token's ID and secret will be printed to stdout. The secret is only
|
|
70
|
+
shown at creation time and cannot be retrieved later.
|
|
71
|
+
"""
|
|
72
|
+
token = Workspace.from_context().proxy_tokens.create()
|
|
73
|
+
output_manager = OutputManager.get()
|
|
74
|
+
if json:
|
|
75
|
+
output_manager.print_json(json_mod.dumps({"Modal-Key": token.token_id, "Modal-Secret": token.token_secret}))
|
|
76
|
+
else:
|
|
77
|
+
output_manager.print(f"Modal-Key: {token.token_id}\nModal-Secret: {token.token_secret}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@proxy_tokens_cli.command("list", help="List the proxy tokens of the current Workspace.")
|
|
81
|
+
@click.option(
|
|
82
|
+
"-e",
|
|
83
|
+
"--environment",
|
|
84
|
+
default=None,
|
|
85
|
+
help="Only list tokens associated with this environment. Lists all tokens when omitted.",
|
|
86
|
+
)
|
|
87
|
+
@click.option("--json", is_flag=True, default=False)
|
|
88
|
+
def proxy_tokens_list(environment: str | None = None, json: bool = False):
|
|
89
|
+
tokens = Workspace.from_context().proxy_tokens.list(environment_name=environment)
|
|
90
|
+
# Emit a real boolean for JSON output, but a string for the rich table (which can't render a bare bool).
|
|
91
|
+
rows = [
|
|
92
|
+
[
|
|
93
|
+
token.token_id,
|
|
94
|
+
timestamp_to_localized_str(token.created_at.timestamp(), json),
|
|
95
|
+
token.scoped if json else str(token.scoped),
|
|
96
|
+
]
|
|
97
|
+
for token in tokens
|
|
98
|
+
]
|
|
99
|
+
display_table(["Token ID", "Created at", "Scoped"], rows, json=json)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@proxy_tokens_cli.command("allow", help="Allow a proxy token to authenticate to an environment.", no_args_is_help=True)
|
|
103
|
+
@click.argument("token_id")
|
|
104
|
+
@click.argument("environment_name")
|
|
105
|
+
def proxy_tokens_allow(token_id: str, environment_name: str):
|
|
106
|
+
Workspace.from_context().proxy_tokens.allow(token_id, environment_name)
|
|
107
|
+
rich.print(f"[green]✓[/green] Allowed proxy token {token_id!r} to authenticate to environment {environment_name!r}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@proxy_tokens_cli.command("revoke", help="Revoke a proxy token's access to an environment.", no_args_is_help=True)
|
|
111
|
+
@click.argument("token_id")
|
|
112
|
+
@click.argument("environment_name")
|
|
113
|
+
def proxy_tokens_revoke(token_id: str, environment_name: str):
|
|
114
|
+
Workspace.from_context().proxy_tokens.revoke(token_id, environment_name)
|
|
115
|
+
rich.print(f"[green]✓[/green] Revoked proxy token {token_id!r} access to environment {environment_name!r}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@proxy_tokens_cli.command("delete", help="Delete a proxy token from the current Workspace.", no_args_is_help=True)
|
|
119
|
+
@click.argument("token_id")
|
|
120
|
+
@yes_option
|
|
121
|
+
def proxy_tokens_delete(token_id: str, yes: bool = False):
|
|
122
|
+
if not yes:
|
|
123
|
+
message = (
|
|
124
|
+
f"Are you sure you want to delete proxy token {token_id!r}? "
|
|
125
|
+
"Any web endpoints relying on it will no longer accept it."
|
|
126
|
+
)
|
|
127
|
+
confirm_or_suggest_yes(message)
|
|
128
|
+
Workspace.from_context().proxy_tokens.delete(token_id)
|
|
129
|
+
rich.print(f"[green]✓[/green] Deleted proxy token {token_id!r}")
|
|
@@ -35,7 +35,7 @@ class _Client:
|
|
|
35
35
|
server_url: str,
|
|
36
36
|
client_type: int,
|
|
37
37
|
credentials: typing.Optional[tuple[str, str]],
|
|
38
|
-
version: str = "1.5.1.
|
|
38
|
+
version: str = "1.5.1.dev15",
|
|
39
39
|
):
|
|
40
40
|
"""mdmd:hidden
|
|
41
41
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -205,7 +205,7 @@ class Client:
|
|
|
205
205
|
server_url: str,
|
|
206
206
|
client_type: int,
|
|
207
207
|
credentials: typing.Optional[tuple[str, str]],
|
|
208
|
-
version: str = "1.5.1.
|
|
208
|
+
version: str = "1.5.1.dev15",
|
|
209
209
|
):
|
|
210
210
|
"""mdmd:hidden
|
|
211
211
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -16,6 +16,8 @@ from dataclasses import dataclass
|
|
|
16
16
|
from pathlib import PurePosixPath
|
|
17
17
|
from typing import TYPE_CHECKING, Any, Literal, Union, overload
|
|
18
18
|
|
|
19
|
+
from modal.secret import _split_env_dict_and_resolvable_secrets
|
|
20
|
+
|
|
19
21
|
from ._output.pty import get_pty_info
|
|
20
22
|
from .config import config, logger
|
|
21
23
|
|
|
@@ -843,6 +845,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
843
845
|
encrypted_ports: Sequence[int] = [],
|
|
844
846
|
h2_ports: Sequence[int] = [],
|
|
845
847
|
unencrypted_ports: Sequence[int] = [],
|
|
848
|
+
readiness_probe: Probe | None = None,
|
|
846
849
|
include_oidc_identity_token: bool = False,
|
|
847
850
|
verbose: bool = False,
|
|
848
851
|
client: _Client | None = None,
|
|
@@ -851,11 +854,11 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
851
854
|
|
|
852
855
|
Supported features include exec, encrypted tunnels, wait/poll/terminate,
|
|
853
856
|
CPU and memory configuration, region placement, volumes, cloud bucket mounts
|
|
854
|
-
(with static credentials via `secret=...`),
|
|
857
|
+
(with static credentials via `secret=...` or `oidc_auth_role_arn`), OIDC
|
|
858
|
+
identity tokens, and filesystem snapshots.
|
|
855
859
|
|
|
856
860
|
Features like tags, memory snapshots, network file systems, GPUs, custom
|
|
857
|
-
domains,
|
|
858
|
-
CloudBucketMount), and proxies are not supported.
|
|
861
|
+
domains, and proxies are not supported.
|
|
859
862
|
|
|
860
863
|
V2 sandboxes created with this method are not currently returned by
|
|
861
864
|
`Sandbox.list()` and cannot be looked up with `Sandbox.from_name()`.
|
|
@@ -878,20 +881,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
878
881
|
cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
|
|
879
882
|
validated_volumes = [(k, v) for k, v in validated_volumes if isinstance(v, _Volume)]
|
|
880
883
|
|
|
881
|
-
# The V2 backend does not yet propagate `oidc_identity_token` from server to worker - it is silently dropped
|
|
882
|
-
# TODO(akshay) add support for OIDC tokens
|
|
883
|
-
if include_oidc_identity_token:
|
|
884
|
-
raise InvalidError(
|
|
885
|
-
"Sandbox._experimental_create does not support include_oidc_identity_token=True. "
|
|
886
|
-
"Use Sandbox.create instead."
|
|
887
|
-
)
|
|
888
|
-
for path, cbm in cloud_bucket_mounts:
|
|
889
|
-
if cbm.oidc_auth_role_arn:
|
|
890
|
-
raise InvalidError(
|
|
891
|
-
"Sandbox._experimental_create does not support CloudBucketMount with oidc_auth_role_arn "
|
|
892
|
-
f"(at mount path {path!r}). Use static credentials via `secret=...`, or use Sandbox.create."
|
|
893
|
-
)
|
|
894
|
-
|
|
895
884
|
secrets = secrets or []
|
|
896
885
|
ephemeral_env: dict[str, str] = {}
|
|
897
886
|
if env:
|
|
@@ -981,6 +970,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
981
970
|
inbound_cidr_allowlist=list(inbound_cidr_allowlist) if inbound_cidr_allowlist is not None else [],
|
|
982
971
|
volume_mounts=volume_mounts,
|
|
983
972
|
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
|
|
973
|
+
readiness_probe=(readiness_probe._to_proto() if readiness_probe else None),
|
|
984
974
|
)
|
|
985
975
|
|
|
986
976
|
create_req = api_pb2.SandboxCreateV2Request(
|
|
@@ -1857,10 +1847,11 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1857
1847
|
_validate_exec_args(args)
|
|
1858
1848
|
|
|
1859
1849
|
secrets = list(secrets or [])
|
|
1860
|
-
env_dict
|
|
1850
|
+
env_dict, resolvable_secrets = _split_env_dict_and_resolvable_secrets(secrets)
|
|
1851
|
+
env_dict |= {k: v for k, v in (env or {}).items() if v is not None}
|
|
1861
1852
|
|
|
1862
1853
|
# Force explicit secret resolution so we can pass the secret IDs to the backend.
|
|
1863
|
-
secret_coros = [secret.hydrate(client=self._client) for secret in
|
|
1854
|
+
secret_coros = [secret.hydrate(client=self._client) for secret in resolvable_secrets]
|
|
1864
1855
|
await TaskContext.gather(*secret_coros)
|
|
1865
1856
|
|
|
1866
1857
|
task_id = await self._get_task_id(raise_if_task_complete=True)
|
|
@@ -1878,7 +1869,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1878
1869
|
stderr=stderr,
|
|
1879
1870
|
timeout=timeout,
|
|
1880
1871
|
workdir=workdir,
|
|
1881
|
-
secret_ids=[secret.object_id for secret in
|
|
1872
|
+
secret_ids=[secret.object_id for secret in resolvable_secrets],
|
|
1882
1873
|
env=env_dict,
|
|
1883
1874
|
text=text,
|
|
1884
1875
|
bufsize=bufsize,
|