modal 1.5.3.dev13__tar.gz → 1.5.4.dev1__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.3.dev13 → modal-1.5.4.dev1}/PKG-INFO +1 -1
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_clustered_functions.py +11 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_clustered_functions.pyi +6 -2
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_environments.py +72 -38
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_image.py +50 -41
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/environment.py +107 -44
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/image.py +13 -25
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/client.pyi +2 -2
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/environments.py +3 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/environments.pyi +82 -55
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/experimental/__init__.py +9 -1
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/functions.pyi +6 -6
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/image.pyi +6 -4
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_pb2.py +702 -702
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_pb2.pyi +22 -4
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_version/__init__.py +1 -1
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/LICENSE +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/README.md +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/__main__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_billing.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_function_variants.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_functions.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_grpc_client.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_ipython.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_load_context.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_location.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_logs.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_logs_manager.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_object.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/manager.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/pty.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/rich.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/status.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_partial_function.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_resolver.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_resources.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_serialization.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_server.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_supports_logs.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_traceback.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_tunnel.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_tunnel.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_type_manager.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/logger.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/version.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_watcher.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_workspace.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/app.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/app.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/billing.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/billing.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/README.md +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/base-images.json +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/call_graph.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/_download.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/_help.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/_traceback.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/app.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/billing.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/changelog.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/cluster.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/config.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/container.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/curl.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/dashboard.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/dict.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/endpoint.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/entry_point.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/import_refs.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/launch.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/logo.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/profile.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/queues.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/run.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/secret.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/selector.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/shell.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/skills.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/token.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/utils.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/volume.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/workspace.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/client.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cls.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cls.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/config.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/container_process.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/container_process.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/dict.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/dict.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/exception.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/experimental/flash.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/experimental/ipython.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/file_io.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/file_io.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/functions.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/image.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/io_streams.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/io_streams.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/mount.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/mount.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/network_file_system.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/network_file_system.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/object.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/object.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/output.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/parallel_map.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/parallel_map.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/partial_function.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/partial_function.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/proxy.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/proxy.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/py.typed +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/queue.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/queue.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/retries.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/runner.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/runner.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/running_app.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/sandbox.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/sandbox.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/sandbox_fs.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/schedule.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/scheduler_placement.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/secret.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/secret.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/server.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/server.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/serving.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/serving.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/snapshot.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/snapshot.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/stream_type.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/token_flow.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/token_flow.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/types.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/volume.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/volume.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/workspace.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/workspace.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/__init__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_grpc.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/py.typed +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_version/__main__.py +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/pyproject.toml +0 -0
- {modal-1.5.3.dev13 → modal-1.5.4.dev1}/setup.cfg +0 -0
|
@@ -15,6 +15,7 @@ class ClusterInfo:
|
|
|
15
15
|
cluster_id: str
|
|
16
16
|
container_ips: list[str]
|
|
17
17
|
container_ipv4_ips: list[str]
|
|
18
|
+
fabric_ids: list[str]
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
cluster_info: ClusterInfo | None = None
|
|
@@ -29,6 +30,14 @@ def get_cluster_info() -> ClusterInfo:
|
|
|
29
30
|
return cluster_info
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def get_fabric_peers() -> list[int]:
|
|
34
|
+
info = get_cluster_info()
|
|
35
|
+
if info.rank >= len(info.fabric_ids) or not info.fabric_ids[info.rank]:
|
|
36
|
+
return [info.rank]
|
|
37
|
+
own_fabric_id = info.fabric_ids[info.rank]
|
|
38
|
+
return [rank for rank, fabric_id in enumerate(info.fabric_ids) if fabric_id == own_fabric_id]
|
|
39
|
+
|
|
40
|
+
|
|
32
41
|
async def _initialize_clustered_function(client: _Client, task_id: str, world_size: int):
|
|
33
42
|
global cluster_info
|
|
34
43
|
|
|
@@ -70,6 +79,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
|
|
|
70
79
|
cluster_id=resp.cluster_id,
|
|
71
80
|
container_ips=resp.container_ips,
|
|
72
81
|
container_ipv4_ips=resp.container_ipv4_ips,
|
|
82
|
+
fabric_ids=resp.fabric_ids,
|
|
73
83
|
)
|
|
74
84
|
else:
|
|
75
85
|
cluster_info = ClusterInfo(
|
|
@@ -77,6 +87,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
|
|
|
77
87
|
cluster_id="", # No cluster ID for single-node # TODO(irfansharif): Is this right?
|
|
78
88
|
container_ips=[container_ip],
|
|
79
89
|
container_ipv4_ips=[], # No IPv4 IPs for single-node
|
|
90
|
+
fabric_ids=[],
|
|
80
91
|
)
|
|
81
92
|
|
|
82
93
|
|
|
@@ -3,14 +3,17 @@ import types
|
|
|
3
3
|
import typing_extensions
|
|
4
4
|
|
|
5
5
|
class ClusterInfo:
|
|
6
|
-
"""ClusterInfo(rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str])"""
|
|
6
|
+
"""ClusterInfo(rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str], fabric_ids: list[str])"""
|
|
7
7
|
|
|
8
8
|
rank: int
|
|
9
9
|
cluster_id: str
|
|
10
10
|
container_ips: list[str]
|
|
11
11
|
container_ipv4_ips: list[str]
|
|
12
|
+
fabric_ids: list[str]
|
|
12
13
|
|
|
13
|
-
def __init__(
|
|
14
|
+
def __init__(
|
|
15
|
+
self, rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str], fabric_ids: list[str]
|
|
16
|
+
) -> None:
|
|
14
17
|
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
15
18
|
...
|
|
16
19
|
|
|
@@ -23,6 +26,7 @@ class ClusterInfo:
|
|
|
23
26
|
...
|
|
24
27
|
|
|
25
28
|
def get_cluster_info() -> ClusterInfo: ...
|
|
29
|
+
def get_fabric_peers() -> list[int]: ...
|
|
26
30
|
async def _initialize_clustered_function(client: modal.client._Client, task_id: str, world_size: int): ...
|
|
27
31
|
|
|
28
32
|
class __initialize_clustered_function_spec(typing_extensions.Protocol):
|
|
@@ -4,7 +4,7 @@ import builtins
|
|
|
4
4
|
from collections.abc import Iterable, Mapping
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
|
-
from typing import Literal
|
|
7
|
+
from typing import Any, Literal
|
|
8
8
|
|
|
9
9
|
from google.protobuf.empty_pb2 import Empty
|
|
10
10
|
from google.protobuf.message import Message
|
|
@@ -16,6 +16,7 @@ from modal_proto import api_pb2
|
|
|
16
16
|
from ._load_context import LoadContext
|
|
17
17
|
from ._object import _Object
|
|
18
18
|
from ._resolver import Resolver
|
|
19
|
+
from ._utils.deprecation import deprecation_warning
|
|
19
20
|
from ._utils.name_utils import check_environment_name
|
|
20
21
|
from ._utils.time_utils import is_utc_month_aligned, parse_billing_cycle
|
|
21
22
|
from .client import _Client
|
|
@@ -38,6 +39,7 @@ class _EnvironmentManager:
|
|
|
38
39
|
name: str, # Name to use for the new Environment
|
|
39
40
|
*,
|
|
40
41
|
restricted: bool = False, # If True, enable RBAC restrictions on the Environment
|
|
42
|
+
experimental_options: dict[str, Any] | None = None, # Experimental options for Environment creation
|
|
41
43
|
client: _Client | None = None, # Optional client with Modal credentials
|
|
42
44
|
) -> None:
|
|
43
45
|
"""Create a new Environment.
|
|
@@ -50,7 +52,10 @@ class _EnvironmentManager:
|
|
|
50
52
|
"""
|
|
51
53
|
check_environment_name(name)
|
|
52
54
|
client = await _Client.from_env() if client is None else client
|
|
53
|
-
|
|
55
|
+
request = api_pb2.EnvironmentCreateRequest(name=name, is_managed=restricted)
|
|
56
|
+
if (experimental_options or {}).get("is_public", False):
|
|
57
|
+
request.environment_type = api_pb2.ENVIRONMENT_TYPE_PUBLIC
|
|
58
|
+
await client.stub.EnvironmentCreate(request)
|
|
54
59
|
|
|
55
60
|
async def list(
|
|
56
61
|
self,
|
|
@@ -104,7 +109,7 @@ class _EnvironmentManager:
|
|
|
104
109
|
await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
|
|
105
110
|
|
|
106
111
|
|
|
107
|
-
class
|
|
112
|
+
class _EnvironmentRolesManager:
|
|
108
113
|
"""mdmd:namespace"""
|
|
109
114
|
|
|
110
115
|
def __init__(self, environment: "_Environment"):
|
|
@@ -112,13 +117,13 @@ class _EnvironmentMembersManager:
|
|
|
112
117
|
self._environment = environment
|
|
113
118
|
|
|
114
119
|
async def list(self) -> dict[Literal["users", "service_users"], dict[str, str]]:
|
|
115
|
-
"""Enumerate the
|
|
120
|
+
"""Enumerate the Environment Role for each user and service user in the workspace.
|
|
116
121
|
|
|
117
122
|
**Examples:**
|
|
118
123
|
|
|
119
124
|
```python notest
|
|
120
|
-
|
|
121
|
-
print(
|
|
125
|
+
roles = modal.Environment.from_name("my-env").roles.list()
|
|
126
|
+
print(roles)
|
|
122
127
|
# {
|
|
123
128
|
# "users": {"alice": "contributor", "bob": "viewer", "carol": "contributor"},
|
|
124
129
|
# "service_users": {"alice-bot": "contributor", "ops-bot": "viewer", "ci-bot": "no-access"},
|
|
@@ -145,16 +150,17 @@ class _EnvironmentMembersManager:
|
|
|
145
150
|
users: Mapping[str, str] | None = None,
|
|
146
151
|
service_users: Mapping[str, str] | None = None,
|
|
147
152
|
) -> None:
|
|
148
|
-
"""
|
|
153
|
+
"""Update the Environment Role of users and service users.
|
|
149
154
|
|
|
150
|
-
Each
|
|
151
|
-
|
|
155
|
+
Each role is one of 'contributor', 'viewer', or 'no-access'. Service users can be
|
|
156
|
+
assigned a role on any Environment, while workspace members can only be assigned a
|
|
157
|
+
role on restricted Environments.
|
|
152
158
|
|
|
153
159
|
**Examples:**
|
|
154
160
|
|
|
155
161
|
```python notest
|
|
156
162
|
env = modal.Environment.from_name("my-restricted-env")
|
|
157
|
-
env.
|
|
163
|
+
env.roles.update(
|
|
158
164
|
users={"alice": "contributor", "bob": "viewer"},
|
|
159
165
|
service_users={"alice-bot": "contributor"},
|
|
160
166
|
)
|
|
@@ -196,24 +202,59 @@ class _EnvironmentMembersManager:
|
|
|
196
202
|
|
|
197
203
|
await self._dispatch_role_updates(requests)
|
|
198
204
|
|
|
205
|
+
async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
|
|
206
|
+
"""Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
|
|
207
|
+
results = await asyncio.gather(
|
|
208
|
+
*(self._environment.client.stub.EnvironmentRoleSet(req) for req in requests.values()),
|
|
209
|
+
return_exceptions=True,
|
|
210
|
+
)
|
|
211
|
+
errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
|
|
212
|
+
if errors:
|
|
213
|
+
n = len(errors)
|
|
214
|
+
header = f"{n} error{'s' if n != 1 else ''} occurred while updating Environment roles:"
|
|
215
|
+
details = "\n".join(f" - {label}: {e}" for label, e in errors)
|
|
216
|
+
raise WorkspaceManagementError(f"{header}\n{details}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# Deprecated alias for `_EnvironmentRolesManager`; each method warns and delegates.
|
|
220
|
+
class _EnvironmentMembersManager:
|
|
221
|
+
"""mdmd:hidden"""
|
|
222
|
+
|
|
223
|
+
def __init__(self, environment: "_Environment"):
|
|
224
|
+
"""mdmd:hidden"""
|
|
225
|
+
self._environment = environment
|
|
226
|
+
|
|
227
|
+
async def list(self) -> dict[Literal["users", "service_users"], dict[str, str]]:
|
|
228
|
+
deprecation_warning(
|
|
229
|
+
(2026, 7, 23),
|
|
230
|
+
"`Environment.members.list()` is deprecated; use `Environment.roles.list()` instead.",
|
|
231
|
+
)
|
|
232
|
+
return await _EnvironmentRolesManager(self._environment).list()
|
|
233
|
+
|
|
234
|
+
async def update(
|
|
235
|
+
self,
|
|
236
|
+
*,
|
|
237
|
+
users: Mapping[str, str] | None = None,
|
|
238
|
+
service_users: Mapping[str, str] | None = None,
|
|
239
|
+
) -> None:
|
|
240
|
+
deprecation_warning(
|
|
241
|
+
(2026, 7, 23),
|
|
242
|
+
"`Environment.members.update()` is deprecated; use `Environment.roles.update()` instead.",
|
|
243
|
+
)
|
|
244
|
+
await _EnvironmentRolesManager(self._environment).update(users=users, service_users=service_users)
|
|
245
|
+
|
|
199
246
|
async def remove(
|
|
200
247
|
self,
|
|
201
248
|
*,
|
|
202
249
|
users: Iterable[str] | None = None,
|
|
203
250
|
service_users: Iterable[str] | None = None,
|
|
204
251
|
) -> None:
|
|
205
|
-
"""Remove
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
env = modal.Environment.from_name("my-restricted-env")
|
|
211
|
-
env.members.remove(
|
|
212
|
-
users=["alice"],
|
|
213
|
-
service_users=["alice-bot"],
|
|
252
|
+
"""Remove the Environment Role of users and service users, reverting them to the default."""
|
|
253
|
+
deprecation_warning(
|
|
254
|
+
(2026, 7, 23),
|
|
255
|
+
"`Environment.members.remove()` is deprecated. Environment Roles are now explicit; "
|
|
256
|
+
"set a role (e.g. 'no-access') with `Environment.roles.update()` instead.",
|
|
214
257
|
)
|
|
215
|
-
```
|
|
216
|
-
"""
|
|
217
258
|
await self._environment.hydrate()
|
|
218
259
|
users = users or []
|
|
219
260
|
service_users = service_users or []
|
|
@@ -247,20 +288,7 @@ class _EnvironmentMembersManager:
|
|
|
247
288
|
role=api_pb2.ENVIRONMENT_ROLE_UNSPECIFIED,
|
|
248
289
|
)
|
|
249
290
|
|
|
250
|
-
await self._dispatch_role_updates(requests)
|
|
251
|
-
|
|
252
|
-
async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
|
|
253
|
-
"""Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
|
|
254
|
-
results = await asyncio.gather(
|
|
255
|
-
*(self._environment.client.stub.EnvironmentRoleSet(req) for req in requests.values()),
|
|
256
|
-
return_exceptions=True,
|
|
257
|
-
)
|
|
258
|
-
errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
|
|
259
|
-
if errors:
|
|
260
|
-
n = len(errors)
|
|
261
|
-
header = f"{n} error{'s' if n != 1 else ''} occurred while updating Environment members:"
|
|
262
|
-
details = "\n".join(f" - {label}: {e}" for label, e in errors)
|
|
263
|
-
raise WorkspaceManagementError(f"{header}\n{details}")
|
|
291
|
+
await _EnvironmentRolesManager(self._environment)._dispatch_role_updates(requests)
|
|
264
292
|
|
|
265
293
|
|
|
266
294
|
class _Environment(_Object, type_prefix="en"):
|
|
@@ -284,11 +312,17 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
284
312
|
return _EnvironmentManager()
|
|
285
313
|
|
|
286
314
|
@property
|
|
287
|
-
def
|
|
288
|
-
"""Namespace with methods for managing the
|
|
315
|
+
def roles(self) -> "_EnvironmentRolesManager":
|
|
316
|
+
"""Namespace with methods for managing the Environment Roles of users and service users.
|
|
289
317
|
|
|
290
|
-
See https://modal.com/docs/guide/rbac for more information on
|
|
318
|
+
See https://modal.com/docs/guide/rbac for more information on Environment Roles.
|
|
291
319
|
"""
|
|
320
|
+
return _EnvironmentRolesManager(self)
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def members(self) -> "_EnvironmentMembersManager":
|
|
324
|
+
"""mdmd:hidden"""
|
|
325
|
+
# Deprecated alias for `Environment.roles`.
|
|
292
326
|
return _EnvironmentMembersManager(self)
|
|
293
327
|
|
|
294
328
|
# TODO(michael) Keeping this private for now until we decide what else should be in it
|
|
@@ -117,34 +117,51 @@ def _validate_image_tag(tag: str) -> None:
|
|
|
117
117
|
check_object_name(tag, "Image tag")
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
If the name contains a '/', the part before the last '/' is extracted as
|
|
124
|
-
a namespace prefix (intended for environment/name or workspace/env/name
|
|
125
|
-
syntax). The actual image name (after the last '/') is validated as a
|
|
126
|
-
standard object name.
|
|
127
|
-
|
|
128
|
-
Returns a tuple of (prefix, "full_name:tag") where prefix is empty string
|
|
129
|
-
if no '/' is present.
|
|
130
|
-
"""
|
|
131
|
-
image_name, sep, tag = name.partition(":")
|
|
120
|
+
def _parse_image_name(partial_name: str) -> tuple[str | None, str]:
|
|
121
|
+
partial_name_no_env, sep, tag = partial_name.partition(":")
|
|
132
122
|
if not sep:
|
|
133
123
|
tag = "latest"
|
|
134
124
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
raise InvalidError("Invalid Image name: '/' prefix must be non-empty.")
|
|
125
|
+
if "/" in partial_name_no_env:
|
|
126
|
+
environment, image_name = partial_name_no_env.rsplit("/", 1)
|
|
127
|
+
if not environment:
|
|
128
|
+
raise InvalidError("Invalid Image name: environment prefix before '/' must be non-empty.")
|
|
140
129
|
if not image_name:
|
|
141
|
-
raise InvalidError("Invalid Image name: name after '/' must be non-empty.")
|
|
130
|
+
raise InvalidError("Invalid Image name: name after last '/' must be non-empty.")
|
|
131
|
+
else:
|
|
132
|
+
environment = None
|
|
133
|
+
image_name = partial_name_no_env
|
|
142
134
|
|
|
143
135
|
_validate_image_name(image_name)
|
|
144
136
|
_validate_image_tag(tag)
|
|
145
137
|
|
|
146
|
-
|
|
147
|
-
|
|
138
|
+
return environment, f"{image_name}:{tag}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _upgrade_image_name(
|
|
142
|
+
partial_name: str,
|
|
143
|
+
*,
|
|
144
|
+
explicit_environment_name: str | None = None,
|
|
145
|
+
fallback_environment_name: str | None = None,
|
|
146
|
+
) -> str:
|
|
147
|
+
"""Ensures an Image name has a tag and an environment name when possible
|
|
148
|
+
|
|
149
|
+
* If no tag is present, add :latest
|
|
150
|
+
* If an explicit environment name is passed, add it as a prefix and error
|
|
151
|
+
if there is already a prefix
|
|
152
|
+
* If a fallback environment name is passed, add it only when there is no prefix
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
prefix_environment, name_tag = _parse_image_name(partial_name)
|
|
156
|
+
if prefix_environment:
|
|
157
|
+
if explicit_environment_name is not None:
|
|
158
|
+
raise InvalidError("environment_name cannot be used when an Image name includes an environment prefix.")
|
|
159
|
+
return f"{prefix_environment}/{name_tag}"
|
|
160
|
+
|
|
161
|
+
environment_name = explicit_environment_name if explicit_environment_name is not None else fallback_environment_name
|
|
162
|
+
if environment_name:
|
|
163
|
+
return f"{environment_name}/{name_tag}"
|
|
164
|
+
return name_tag # no explicit environment - use just the unprefixed name:tag
|
|
148
165
|
|
|
149
166
|
|
|
150
167
|
def _validate_python_version(
|
|
@@ -2932,8 +2949,8 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2932
2949
|
) -> "_Image":
|
|
2933
2950
|
"""Reference a named Image that was previously published with `.publish()`.
|
|
2934
2951
|
|
|
2935
|
-
Names can contain an optional `:tag` part
|
|
2936
|
-
|
|
2952
|
+
Names can contain an optional `:tag` part. If no tag part is included, `":latest"` is used, matching
|
|
2953
|
+
Docker conventions.
|
|
2937
2954
|
|
|
2938
2955
|
```python notest
|
|
2939
2956
|
image = modal.Image.from_name("my-image") # references my-image:latest
|
|
@@ -2944,20 +2961,16 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2944
2961
|
...
|
|
2945
2962
|
```
|
|
2946
2963
|
"""
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
if namespace_prefix and environment_name:
|
|
2950
|
-
raise InvalidError("Cannot specify 'environment_name' when the image name contains a '/'.")
|
|
2964
|
+
_upgrade_image_name(name, explicit_environment_name=environment_name) # validate
|
|
2951
2965
|
|
|
2952
2966
|
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
2953
2967
|
req = api_pb2.ImageGetByTagRequest(
|
|
2954
|
-
tag=
|
|
2955
|
-
environment_name="" if namespace_prefix else load_context.environment_name,
|
|
2968
|
+
tag=_upgrade_image_name(name, fallback_environment_name=load_context.environment_name),
|
|
2956
2969
|
)
|
|
2957
2970
|
response = await load_context.client.stub.ImageGetByTag(req)
|
|
2958
2971
|
self._hydrate(response.image_id, load_context.client, None)
|
|
2959
2972
|
|
|
2960
|
-
rep = _Image._repr(
|
|
2973
|
+
rep = _Image._repr(name, environment_name)
|
|
2961
2974
|
return _Image._from_loader(
|
|
2962
2975
|
_load,
|
|
2963
2976
|
rep,
|
|
@@ -2971,13 +2984,14 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2971
2984
|
name: str,
|
|
2972
2985
|
*,
|
|
2973
2986
|
environment_name: str | None = None,
|
|
2987
|
+
experimental_options: dict[str, Any] | None = None,
|
|
2974
2988
|
client: _Client | None = None,
|
|
2975
2989
|
) -> None:
|
|
2976
2990
|
"""Publish this image under the given name
|
|
2977
2991
|
|
|
2978
2992
|
The Image must already be created (typically by calling `image.build()` or `sandbox.snapshot_filesystem()`).
|
|
2979
2993
|
|
|
2980
|
-
Image names can contain an explicit tag designation
|
|
2994
|
+
Image names can contain an explicit tag designation using `name:tag`. If no tag is included in the name,
|
|
2981
2995
|
`":latest"` is used, matching Docker conventions. To publish multiple tags, call `.publish()` once per tag.
|
|
2982
2996
|
|
|
2983
2997
|
```python notest
|
|
@@ -2987,25 +3001,20 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2987
3001
|
image.publish("my-image-with-numpy:v1")
|
|
2988
3002
|
```
|
|
2989
3003
|
"""
|
|
2990
|
-
namespace_prefix, tag = _parse_named_image_ref(name)
|
|
2991
|
-
|
|
2992
|
-
if namespace_prefix:
|
|
2993
|
-
if environment_name:
|
|
2994
|
-
raise InvalidError("Cannot specify 'environment_name' when the image name contains a '/'.")
|
|
2995
|
-
resolved_env = ""
|
|
2996
|
-
else:
|
|
2997
|
-
resolved_env = environment_name or config.get("environment") or ""
|
|
2998
|
-
|
|
2999
3004
|
if self._object_id is None:
|
|
3000
3005
|
raise InvalidError("Cannot publish an image that has not been created yet. Call `.build()` first.")
|
|
3001
3006
|
|
|
3002
3007
|
_client = client or await _Client.from_env()
|
|
3008
|
+
tag = _upgrade_image_name(
|
|
3009
|
+
name,
|
|
3010
|
+
explicit_environment_name=environment_name,
|
|
3011
|
+
fallback_environment_name=config.get("environment"),
|
|
3012
|
+
)
|
|
3003
3013
|
|
|
3004
3014
|
await _client.stub.ImagePublish(
|
|
3005
3015
|
api_pb2.ImagePublishRequest(
|
|
3006
3016
|
image_id=self._object_id,
|
|
3007
|
-
|
|
3008
|
-
allow_public=False,
|
|
3017
|
+
allow_public=bool((experimental_options or {}).get("is_public", False)),
|
|
3009
3018
|
tag=tag,
|
|
3010
3019
|
)
|
|
3011
3020
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2023
|
|
2
2
|
import json as json_mod
|
|
3
|
+
import warnings
|
|
3
4
|
|
|
4
5
|
import click
|
|
5
6
|
import rich
|
|
@@ -11,6 +12,7 @@ from rich.text import Text
|
|
|
11
12
|
from modal import environments
|
|
12
13
|
from modal._environments import _Environment
|
|
13
14
|
from modal._utils.async_utils import synchronizer
|
|
15
|
+
from modal._utils.deprecation import deprecation_warning
|
|
14
16
|
from modal._utils.display_utils import pretty_decimal
|
|
15
17
|
from modal._utils.name_utils import check_environment_name
|
|
16
18
|
from modal._utils.time_utils import format_interval
|
|
@@ -18,7 +20,7 @@ from modal.cli.billing import DATE_HELP, _validate_and_parse_interval
|
|
|
18
20
|
from modal.cli.utils import _col_name_to_json_key, confirm_or_suggest_yes, display_table, yes_option
|
|
19
21
|
from modal.config import config
|
|
20
22
|
from modal.environments import Environment
|
|
21
|
-
from modal.exception import InvalidError
|
|
23
|
+
from modal.exception import DeprecationError, InvalidError
|
|
22
24
|
from modal.output import OutputManager
|
|
23
25
|
|
|
24
26
|
from ._help import ModalGroup
|
|
@@ -55,7 +57,7 @@ def list_(json: bool = False):
|
|
|
55
57
|
# over the server default
|
|
56
58
|
active_env = config.get("environment") or ""
|
|
57
59
|
for env in envs:
|
|
58
|
-
if env.default is True and active_env
|
|
60
|
+
if env.default is True and not active_env:
|
|
59
61
|
active_env = env.name
|
|
60
62
|
|
|
61
63
|
table_data = []
|
|
@@ -70,10 +72,11 @@ def list_(json: bool = False):
|
|
|
70
72
|
@environment_cli.command("create", help="Create a new environment in the current workspace.", no_args_is_help=True)
|
|
71
73
|
@click.argument("name")
|
|
72
74
|
@click.option("--restricted", is_flag=True, default=False, help="Enable RBAC restrictions on the new environment")
|
|
73
|
-
|
|
75
|
+
@click.option("--public", is_flag=True, default=False, hidden=True)
|
|
76
|
+
def create(name: str, restricted: bool = False, public: bool = False):
|
|
74
77
|
check_environment_name(name)
|
|
75
|
-
Environment.objects.create(name, restricted=restricted)
|
|
76
|
-
prefix = "Restricted " if restricted else ""
|
|
78
|
+
Environment.objects.create(name, restricted=restricted, experimental_options={"is_public": public})
|
|
79
|
+
prefix = "Public " if public else "Restricted " if restricted else ""
|
|
77
80
|
rich.print(f"[green]✓[/green] {prefix}Environment created: {name}")
|
|
78
81
|
|
|
79
82
|
|
|
@@ -123,67 +126,127 @@ def update(
|
|
|
123
126
|
rich.print("[green]✓[/green] Environment updated")
|
|
124
127
|
|
|
125
128
|
|
|
126
|
-
MEMBERS_HELP_TEXT = """Manage members and their roles in a restricted Environment.
|
|
127
|
-
|
|
128
|
-
Restricted Environments use RBAC to limit the actions that can be performed by
|
|
129
|
-
users (and service users) based on roles: https://modal.com/docs/guide/rbac.
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
members_cli = ModalGroup(name="members", help=MEMBERS_HELP_TEXT)
|
|
133
|
-
environment_cli.add_command(members_cli)
|
|
134
|
-
|
|
135
129
|
service_user_option = click.option(
|
|
136
|
-
"--service-user", is_flag=True, default=False, help="Treat
|
|
130
|
+
"--service-user", is_flag=True, default=False, help="Treat PRINCIPAL as the name of a service user"
|
|
137
131
|
)
|
|
138
132
|
|
|
139
133
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
@click.option("--json", is_flag=True, default=False)
|
|
143
|
-
def members_list(environment: str, json: bool = False):
|
|
144
|
-
members = Environment.from_name(environment).members.list()
|
|
134
|
+
def _render_roles_list(environment: str, json: bool) -> None:
|
|
135
|
+
roles = Environment.from_name(environment).roles.list()
|
|
145
136
|
if json:
|
|
146
137
|
# Mirror the API output shape: a nested dict keyed by users / service_users.
|
|
147
|
-
OutputManager.get().print_json(json_mod.dumps(
|
|
138
|
+
OutputManager.get().print_json(json_mod.dumps(roles))
|
|
148
139
|
return
|
|
149
140
|
rows = []
|
|
150
|
-
for name, role in
|
|
141
|
+
for name, role in roles.get("users", {}).items():
|
|
151
142
|
rows.append([name, role])
|
|
152
|
-
for name, role in
|
|
143
|
+
for name, role in roles.get("service_users", {}).items():
|
|
153
144
|
rows.append([f"{name} (service user)", role])
|
|
154
145
|
rows = sorted(rows, key=lambda x: x[0])
|
|
155
146
|
display_table(["name", "role"], rows)
|
|
156
147
|
|
|
157
148
|
|
|
158
|
-
|
|
159
|
-
@click.argument("environment")
|
|
160
|
-
@click.argument("member")
|
|
161
|
-
@click.option(
|
|
162
|
-
"--role", type=click.Choice(["contributor", "viewer"]), required=True, help="Role to assign to the member"
|
|
163
|
-
)
|
|
164
|
-
@service_user_option
|
|
165
|
-
def members_update(environment: str, member: str, role: str, service_user: bool = False):
|
|
149
|
+
def _apply_role_update(environment: str, principal: str, role: str, service_user: bool) -> None:
|
|
166
150
|
env = Environment.from_name(environment)
|
|
167
151
|
if service_user:
|
|
168
|
-
env.
|
|
152
|
+
env.roles.update(service_users={principal: role})
|
|
169
153
|
else:
|
|
170
|
-
env.
|
|
154
|
+
env.roles.update(users={principal: role})
|
|
171
155
|
kind = "service user" if service_user else "user"
|
|
172
|
-
rich.print(f"[green]✓[/green] Set {kind} {
|
|
156
|
+
rich.print(f"[green]✓[/green] Set {kind} {principal!r} to role {role!r} in environment {environment!r}")
|
|
173
157
|
|
|
174
158
|
|
|
175
|
-
|
|
176
|
-
@click.argument("environment")
|
|
177
|
-
@click.argument("member")
|
|
178
|
-
@service_user_option
|
|
179
|
-
def members_remove(environment: str, member: str, service_user: bool = False):
|
|
159
|
+
def _apply_role_remove(environment: str, principal: str, service_user: bool) -> None:
|
|
180
160
|
env = Environment.from_name(environment)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
161
|
+
# `remove` only lives on the deprecated `members` manager, which warns. The caller (a
|
|
162
|
+
# deprecated `members` command) has already warned, so suppress the redundant SDK warning.
|
|
163
|
+
with warnings.catch_warnings():
|
|
164
|
+
warnings.simplefilter("ignore", DeprecationError)
|
|
165
|
+
if service_user:
|
|
166
|
+
env.members.remove(service_users=[principal])
|
|
167
|
+
else:
|
|
168
|
+
env.members.remove(users=[principal])
|
|
185
169
|
kind = "service user" if service_user else "user"
|
|
186
|
-
rich.print(f"[green]✓[/green] Removed {kind} {
|
|
170
|
+
rich.print(f"[green]✓[/green] Removed {kind} {principal!r} from environment {environment!r}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
ROLES_HELP_TEXT = """Manage the Environment Roles of users and service users.
|
|
174
|
+
|
|
175
|
+
An Environment Role is one of 'contributor' (read-write), 'viewer' (read-only), or
|
|
176
|
+
'no-access', and dictates access to Environments. See
|
|
177
|
+
https://modal.com/docs/guide/rbac for details on which principals can be assigned
|
|
178
|
+
which roles.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
roles_cli = ModalGroup(name="roles", help=ROLES_HELP_TEXT)
|
|
182
|
+
environment_cli.add_command(roles_cli)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@roles_cli.command("list", help="List the roles of each user and service user in an Environment", no_args_is_help=True)
|
|
186
|
+
@click.argument("environment")
|
|
187
|
+
@click.option("--json", is_flag=True, default=False)
|
|
188
|
+
def roles_list(environment: str, json: bool = False):
|
|
189
|
+
_render_roles_list(environment, json)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@roles_cli.command("update", help="Update a user's or service user's role in an Environment", no_args_is_help=True)
|
|
193
|
+
@click.argument("environment")
|
|
194
|
+
@click.argument("principal")
|
|
195
|
+
@click.option("--role", type=click.Choice(["contributor", "viewer", "no-access"]), required=True, help="Role to assign")
|
|
196
|
+
@service_user_option
|
|
197
|
+
def roles_update(environment: str, principal: str, role: str, service_user: bool = False):
|
|
198
|
+
_apply_role_update(environment, principal, role, service_user)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# The deprecated `members` command group keeps working for existing automation, but is hidden
|
|
202
|
+
# from `--help` so it is not discoverable. Each subcommand warns with its own guidance.
|
|
203
|
+
members_cli = ModalGroup(name="members", help="[Deprecated] Use `modal environment roles`.", hidden=True)
|
|
204
|
+
environment_cli.add_command(members_cli)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@members_cli.command("list", help="[Deprecated] Use `modal environment roles list`", no_args_is_help=True)
|
|
208
|
+
@click.argument("environment")
|
|
209
|
+
@click.option("--json", is_flag=True, default=False)
|
|
210
|
+
def members_list(environment: str, json: bool = False):
|
|
211
|
+
deprecation_warning(
|
|
212
|
+
(2026, 7, 23),
|
|
213
|
+
"`modal environment members list` is deprecated; use `modal environment roles list`.",
|
|
214
|
+
show_source=False,
|
|
215
|
+
)
|
|
216
|
+
_render_roles_list(environment, json)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@members_cli.command("update", help="[Deprecated] Use `modal environment roles update`", no_args_is_help=True)
|
|
220
|
+
@click.argument("environment")
|
|
221
|
+
@click.argument("principal")
|
|
222
|
+
@click.option(
|
|
223
|
+
"--role",
|
|
224
|
+
type=click.Choice(["contributor", "viewer", "no-access"]),
|
|
225
|
+
required=True,
|
|
226
|
+
help="Role to assign to the principal",
|
|
227
|
+
)
|
|
228
|
+
@service_user_option
|
|
229
|
+
def members_update(environment: str, principal: str, role: str, service_user: bool = False):
|
|
230
|
+
deprecation_warning(
|
|
231
|
+
(2026, 7, 23),
|
|
232
|
+
"`modal environment members update` is deprecated; use `modal environment roles update`.",
|
|
233
|
+
show_source=False,
|
|
234
|
+
)
|
|
235
|
+
_apply_role_update(environment, principal, role, service_user)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@members_cli.command("remove", help="[Deprecated] Use `modal environment roles update`", no_args_is_help=True)
|
|
239
|
+
@click.argument("environment")
|
|
240
|
+
@click.argument("principal")
|
|
241
|
+
@service_user_option
|
|
242
|
+
def members_remove(environment: str, principal: str, service_user: bool = False):
|
|
243
|
+
deprecation_warning(
|
|
244
|
+
(2026, 7, 23),
|
|
245
|
+
"`modal environment members remove` is deprecated; roles are now explicit — set a role "
|
|
246
|
+
"(e.g. `--role no-access`) with `modal environment roles update` instead.",
|
|
247
|
+
show_source=False,
|
|
248
|
+
)
|
|
249
|
+
_apply_role_remove(environment, principal, service_user)
|
|
187
250
|
|
|
188
251
|
|
|
189
252
|
billing_cli = ModalGroup(name="billing", help="View billing and usage info for the given Environment.")
|