modal 1.6.3.dev11__tar.gz → 1.6.3.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.6.3.dev11 → modal-1.6.3.dev13}/PKG-INFO +4 -2
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_image.py +110 -29
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/docker_utils.py +10 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/grpc_utils.py +23 -4
- modal-1.6.3.dev13/modal/_utils/oauth.py +66 -0
- modal-1.6.3.dev13/modal/builder/PREVIEW.txt +17 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/base-images.json +3 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/config.py +2 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/endpoint.py +49 -7
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/profile.py +2 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/token.py +1 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/workspace.py +1 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/client.py +39 -9
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/client.pyi +42 -8
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/config.py +3 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/image.pyi +39 -18
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/io_streams.py +26 -14
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/io_streams.pyi +2 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/mount.py +9 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/network_file_system.py +4 -8
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/network_file_system.pyi +10 -16
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/token_flow.py +1 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/PKG-INFO +4 -2
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/SOURCES.txt +2 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/requires.txt +4 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_grpc.py +48 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_pb2.py +1550 -1411
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_pb2.pyi +286 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_pb2_grpc.py +99 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_pb2_grpc.pyi +30 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/modal_api_grpc.py +3 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_version/__init__.py +1 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/pyproject.toml +5 -3
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/LICENSE +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/README.md +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/__main__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_billing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_cluster.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_cluster.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_environments.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_function_variants.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_functions.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_grpc_client.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_ipython.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_load_context.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_location.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_logs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_logs_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_metrics.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_metrics.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_object.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_outbound_policy.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_outbound_policy.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/pty.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/rich.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/status.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_partial_function.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_resolver.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_resources.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_serialization.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_server.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_server.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_supports_logs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_traceback.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_tunnel.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_tunnel.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_type_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/async_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/blob_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/logger.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/name_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/time_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/version.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_watcher.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_workspace.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/app.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/app.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/billing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/billing.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2025.06.txt +0 -0
- /modal-1.6.3.dev11/modal/builder/PREVIEW.txt → /modal-1.6.3.dev13/modal/builder/2026.10.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/README.md +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/call_graph.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_download.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_help.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_logs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_stats.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_traceback.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/app.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/billing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/changelog.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/container.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/curl.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/dashboard.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/dict.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/entry_point.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/environment.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/function.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/image.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/import_refs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/logo.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/network_file_system.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/queues.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/run.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/secret.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/selector.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/server.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/shell.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/skills.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/volume.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cls.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cls.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cluster.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cluster.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/container_process.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/container_process.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/dict.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/dict.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/environments.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/environments.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/exception.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/flash.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/ipython.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/functions.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/functions.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/image.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/mount.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/object.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/object.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/output.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/parallel_map.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/parallel_map.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/partial_function.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/partial_function.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/proxy.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/proxy.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/py.typed +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/queue.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/queue.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/retries.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/runner.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/runner.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/running_app.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_common.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_filesystem.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_impl_persistent.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_impl_v1.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_impl_v2.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_sandbox.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_sidecar.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_snapshot.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_task_command_router_client.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/sandbox.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/sandbox.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/schedule.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/scheduler_placement.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/secret.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/secret.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/server.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/server.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/serving.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/serving.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/stream_type.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/token_flow.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/types.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/volume.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/volume.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/workspace.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/workspace.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/py.typed +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_version/__main__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev13}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.6.3.
|
|
3
|
+
Version: 1.6.3.dev13
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,7 +23,7 @@ Requires-Dist: grpclib<0.4.10,>=0.4.7; python_version < "3.14"
|
|
|
23
23
|
Requires-Dist: grpclib<0.4.10,>=0.4.9; python_version >= "3.14"
|
|
24
24
|
Requires-Dist: protobuf!=4.24.0,<8.0,>=3.19
|
|
25
25
|
Requires-Dist: rich>=12.0.0
|
|
26
|
-
Requires-Dist: synchronicity~=0.12.
|
|
26
|
+
Requires-Dist: synchronicity~=0.12.7
|
|
27
27
|
Requires-Dist: toml
|
|
28
28
|
Requires-Dist: types-certifi
|
|
29
29
|
Requires-Dist: types-toml
|
|
@@ -33,6 +33,8 @@ Requires-Dist: typing_extensions>=4.14; python_version >= "3.15"
|
|
|
33
33
|
Provides-Extra: api-proxy-support
|
|
34
34
|
Requires-Dist: python-socks[asyncio]>=2.5; extra == "api-proxy-support"
|
|
35
35
|
Requires-Dist: aiohttp-socks>=0.8; extra == "api-proxy-support"
|
|
36
|
+
Provides-Extra: oauth
|
|
37
|
+
Requires-Dist: pyjwt[crypto]>=2.7; extra == "oauth"
|
|
36
38
|
Dynamic: license-file
|
|
37
39
|
|
|
38
40
|
# Modal Python SDK
|
|
@@ -40,6 +40,7 @@ from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
|
|
40
40
|
from ._utils.docker_utils import (
|
|
41
41
|
extract_copy_command_patterns,
|
|
42
42
|
find_dockerignore_file,
|
|
43
|
+
is_multi_stage_dockerfile,
|
|
43
44
|
splittable_from_line_index,
|
|
44
45
|
)
|
|
45
46
|
from ._utils.function_utils import FunctionSourceInfo, parse_gpu_config
|
|
@@ -70,8 +71,8 @@ if typing.TYPE_CHECKING:
|
|
|
70
71
|
import modal.client
|
|
71
72
|
|
|
72
73
|
# This is used for both type checking and runtime validation
|
|
73
|
-
ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]
|
|
74
|
-
LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "
|
|
74
|
+
ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "2026.10", "PREVIEW"]
|
|
75
|
+
LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2026.10"
|
|
75
76
|
|
|
76
77
|
# Note: we also define supported Python versions via logic at the top of the package __init__.py
|
|
77
78
|
# so that we fail fast / clearly in unsupported containers. Additionally, we enumerate the supported
|
|
@@ -79,6 +80,7 @@ LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2025.06"
|
|
|
79
80
|
# Consider consolidating these multiple sources of truth?
|
|
80
81
|
SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
|
|
81
82
|
"PREVIEW": ["3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
|
|
83
|
+
"2026.10": ["3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
|
|
82
84
|
"2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
|
|
83
85
|
"2024.10": ["3.10", "3.11", "3.12", "3.13"],
|
|
84
86
|
"2024.04": ["3.10", "3.11", "3.12"],
|
|
@@ -445,6 +447,40 @@ def _create_context_mount_function(
|
|
|
445
447
|
return auto_created_context_mount_fn
|
|
446
448
|
|
|
447
449
|
|
|
450
|
+
_REGISTRY_AUTH_KEY_SETS: dict["api_pb2.RegistryAuthType.ValueType", list[set[str]]] = {
|
|
451
|
+
api_pb2.REGISTRY_AUTH_TYPE_AWS: [
|
|
452
|
+
{"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"},
|
|
453
|
+
{"AWS_ROLE_ARN", "AWS_REGION"},
|
|
454
|
+
],
|
|
455
|
+
api_pb2.REGISTRY_AUTH_TYPE_GCP: [{"SERVICE_ACCOUNT_JSON"}],
|
|
456
|
+
api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS: [{"REGISTRY_USERNAME", "REGISTRY_PASSWORD"}],
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_REGISTRY_AUTH_NAMES = {
|
|
460
|
+
api_pb2.REGISTRY_AUTH_TYPE_AWS: "AWS",
|
|
461
|
+
api_pb2.REGISTRY_AUTH_TYPE_GCP: "GCP",
|
|
462
|
+
api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS: "username and password",
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
_REGISTRY_SECRET_KEYS_HELP = (
|
|
466
|
+
"Registry Secret must include exactly one of: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION "
|
|
467
|
+
"(or AWS_ROLE_ARN and AWS_REGION) for AWS ECR; SERVICE_ACCOUNT_JSON for GCP Artifact Registry; "
|
|
468
|
+
"or REGISTRY_USERNAME and REGISTRY_PASSWORD for other registries."
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _infer_registry_auth_type(keys: set[str]) -> "api_pb2.RegistryAuthType.ValueType":
|
|
473
|
+
matches = [auth for auth, key_sets in _REGISTRY_AUTH_KEY_SETS.items() if any(ks <= keys for ks in key_sets)]
|
|
474
|
+
if not matches:
|
|
475
|
+
raise InvalidError(f"{_REGISTRY_SECRET_KEYS_HELP} Found keys: {', '.join(sorted(keys)) or 'none'}.")
|
|
476
|
+
if len(matches) > 1:
|
|
477
|
+
names = ", ".join(_REGISTRY_AUTH_NAMES[auth] for auth in matches)
|
|
478
|
+
raise InvalidError(
|
|
479
|
+
f"Registry Secret has keys for more than one kind of authentication ({names}). {_REGISTRY_SECRET_KEYS_HELP}"
|
|
480
|
+
)
|
|
481
|
+
return matches[0]
|
|
482
|
+
|
|
483
|
+
|
|
448
484
|
class _ImageRegistryConfig:
|
|
449
485
|
"""mdmd:hidden"""
|
|
450
486
|
|
|
@@ -453,13 +489,21 @@ class _ImageRegistryConfig:
|
|
|
453
489
|
# TODO: change to _PUBLIC after worker starts handling it.
|
|
454
490
|
registry_auth_type: "api_pb2.RegistryAuthType.ValueType" = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
|
|
455
491
|
secret: _Secret | None = None,
|
|
492
|
+
*,
|
|
493
|
+
infer_auth_type: bool = False,
|
|
456
494
|
):
|
|
457
495
|
self.registry_auth_type = registry_auth_type
|
|
458
496
|
self.secret = secret
|
|
459
|
-
|
|
460
|
-
|
|
497
|
+
self.infer_auth_type = infer_auth_type
|
|
498
|
+
|
|
499
|
+
async def get_proto(self) -> api_pb2.ImageRegistryConfig:
|
|
500
|
+
registry_auth_type = self.registry_auth_type
|
|
501
|
+
if self.infer_auth_type and self.secret:
|
|
502
|
+
# The Secret is an Image dependency, so it's hydrated by now and its keys are cached
|
|
503
|
+
keys = await self.secret._get_keys()
|
|
504
|
+
registry_auth_type = _infer_registry_auth_type(keys)
|
|
461
505
|
return api_pb2.ImageRegistryConfig(
|
|
462
|
-
registry_auth_type=
|
|
506
|
+
registry_auth_type=registry_auth_type,
|
|
463
507
|
secret_id=(self.secret.object_id if self.secret else ""),
|
|
464
508
|
)
|
|
465
509
|
|
|
@@ -762,7 +806,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
762
806
|
secret_ids=[secret.object_id for secret in secrets],
|
|
763
807
|
context_mount_id=(context_mount.object_id if context_mount else ""),
|
|
764
808
|
gpu_config=gpu_config,
|
|
765
|
-
image_registry_config=image_registry_config.get_proto(),
|
|
809
|
+
image_registry_config=await image_registry_config.get_proto(),
|
|
766
810
|
runtime=config.get("function_runtime"),
|
|
767
811
|
runtime_debug=config.get("function_runtime_debug"),
|
|
768
812
|
build_function=_build_function,
|
|
@@ -2255,16 +2299,21 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2255
2299
|
remaining commands run. This might be useful if you want a custom Python installation or to
|
|
2256
2300
|
set a `SHELL`. Prefer `run_commands()` when possible though.
|
|
2257
2301
|
|
|
2258
|
-
To
|
|
2259
|
-
|
|
2260
|
-
|
|
2302
|
+
To pull from a private registry, set the `secret` parameter to a `modal.Secret` holding
|
|
2303
|
+
registry credentials. The kind of authentication follows from the keys in the Secret:
|
|
2304
|
+
|
|
2305
|
+
- AWS ECR: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` (optionally `AWS_SESSION_TOKEN`),
|
|
2306
|
+
or `AWS_ROLE_ARN` and `AWS_REGION` to assume a role with [OIDC](https://modal.com/docs/guide/oidc-integration).
|
|
2307
|
+
See `Image.from_aws_ecr()` for details.
|
|
2308
|
+
- GCP Artifact Registry: `SERVICE_ACCOUNT_JSON`, holding the service account key's JSON.
|
|
2309
|
+
See `Image.from_gcp_artifact_registry()` for details.
|
|
2310
|
+
- Other registries: a username (`REGISTRY_USERNAME`) and an access token or password (`REGISTRY_PASSWORD`).
|
|
2261
2311
|
|
|
2262
|
-
|
|
2263
|
-
use `Image.from_gcp_artifact_registry()` or `Image.from_aws_ecr()`.
|
|
2312
|
+
The Secret must have the keys for exactly one of these; otherwise an error is raised.
|
|
2264
2313
|
|
|
2265
2314
|
Args:
|
|
2266
2315
|
tag: Registry image reference (e.g. ``python:3.11-slim``).
|
|
2267
|
-
secret: Optional
|
|
2316
|
+
secret: Optional Secret with registry credentials; its keys determine the kind of authentication.
|
|
2268
2317
|
setup_dockerfile_commands: Extra Dockerfile lines run after ``FROM`` during base setup.
|
|
2269
2318
|
force_build: If True, skip cached image builds.
|
|
2270
2319
|
add_python: Optional standalone Python series to inject when the base image lacks Python.
|
|
@@ -2278,6 +2327,10 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2278
2327
|
modal.Image.from_registry("python:3.11-slim-bookworm")
|
|
2279
2328
|
modal.Image.from_registry("ubuntu:22.04", add_python="3.11")
|
|
2280
2329
|
modal.Image.from_registry("nvcr.io/nvidia/pytorch:22.12-py3")
|
|
2330
|
+
modal.Image.from_registry(
|
|
2331
|
+
"ghcr.io/my-org/my-image:latest",
|
|
2332
|
+
secret=modal.Secret.from_name("my-registry-secret"),
|
|
2333
|
+
)
|
|
2281
2334
|
```
|
|
2282
2335
|
"""
|
|
2283
2336
|
|
|
@@ -2292,7 +2345,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2292
2345
|
)
|
|
2293
2346
|
|
|
2294
2347
|
if "image_registry_config" not in kwargs and secret is not None:
|
|
2295
|
-
kwargs["image_registry_config"] = _ImageRegistryConfig(
|
|
2348
|
+
kwargs["image_registry_config"] = _ImageRegistryConfig(secret=secret, infer_auth_type=True)
|
|
2296
2349
|
|
|
2297
2350
|
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
2298
2351
|
commands = _Image._registry_setup_commands(tag, version, setup_dockerfile_commands, add_python)
|
|
@@ -2472,28 +2525,33 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2472
2525
|
*,
|
|
2473
2526
|
force_build: bool = False,
|
|
2474
2527
|
context_dir: Path | str | None = None,
|
|
2475
|
-
|
|
2528
|
+
ignore: Sequence[str] | Callable[[Path], bool] = AUTO_DOCKERIGNORE,
|
|
2529
|
+
build_args: dict[str, str] = {},
|
|
2530
|
+
registry_secret: _Secret | None = None,
|
|
2476
2531
|
secrets: Collection[_Secret] | None = None,
|
|
2532
|
+
env: dict[str, str | None] | None = None,
|
|
2477
2533
|
gpu: str | None = None,
|
|
2478
2534
|
add_python: str | None = None,
|
|
2479
|
-
build_args: dict[str, str] = {},
|
|
2480
|
-
ignore: Sequence[str] | Callable[[Path], bool] = AUTO_DOCKERIGNORE,
|
|
2481
2535
|
) -> "_Image":
|
|
2482
2536
|
"""Build a Modal image from a local Dockerfile.
|
|
2483
2537
|
|
|
2484
2538
|
If your Dockerfile does not have Python installed, you can use the `add_python` parameter
|
|
2485
2539
|
to specify a version of Python to add to the image.
|
|
2486
2540
|
|
|
2541
|
+
A `registry_secret` can be provided to pull the base image from a private registry.
|
|
2542
|
+
Currently this is supported only for single-stage Dockerfiles.
|
|
2543
|
+
|
|
2487
2544
|
Args:
|
|
2488
2545
|
path: Path to the Dockerfile on the local machine.
|
|
2489
2546
|
force_build: If True, skip cached image builds.
|
|
2490
2547
|
context_dir: Build context directory for resolving relative COPY paths.
|
|
2548
|
+
ignore: Ignore rules for the implicit context mount (defaults to auto ``.dockerignore`` behavior).
|
|
2549
|
+
build_args: Dockerfile ``ARG`` values forwarded to the build.
|
|
2550
|
+
registry_secret: Optional Secret with credentials for pulling the base image from a private registry.
|
|
2551
|
+
secrets: Secrets injected as environment variables in the build container.
|
|
2491
2552
|
env: Environment variables set in the build container.
|
|
2492
|
-
|
|
2493
|
-
gpu: GPU type to attach to the builder container.
|
|
2553
|
+
gpu: GPU type to attach to the build container.
|
|
2494
2554
|
add_python: Standalone Python version to add when the Dockerfile does not install Python.
|
|
2495
|
-
build_args: Dockerfile ``ARG`` values forwarded to the build.
|
|
2496
|
-
ignore: Ignore rules for the implicit context mount (defaults to auto ``.dockerignore`` behavior).
|
|
2497
2555
|
|
|
2498
2556
|
Returns:
|
|
2499
2557
|
An `Image` built from the Dockerfile plus Modal runtime dependencies.
|
|
@@ -2539,6 +2597,12 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2539
2597
|
add_python="3.12",
|
|
2540
2598
|
ignore=FilePatternMatcher.from_file("/path/to/dockerignore"),
|
|
2541
2599
|
)
|
|
2600
|
+
|
|
2601
|
+
# Pull the Dockerfile's base image from a private registry.
|
|
2602
|
+
image = modal.Image.from_dockerfile(
|
|
2603
|
+
"./Dockerfile",
|
|
2604
|
+
registry_secret=modal.Secret.from_name("my-registry-secret"),
|
|
2605
|
+
)
|
|
2542
2606
|
```
|
|
2543
2607
|
"""
|
|
2544
2608
|
|
|
@@ -2551,7 +2615,15 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2551
2615
|
ignore=ignore, dockerfile_path=Path(path), context_dir=context_dir
|
|
2552
2616
|
)
|
|
2553
2617
|
|
|
2554
|
-
|
|
2618
|
+
registry_config = (
|
|
2619
|
+
_ImageRegistryConfig(secret=registry_secret, infer_auth_type=True) if registry_secret is not None else None
|
|
2620
|
+
)
|
|
2621
|
+
|
|
2622
|
+
def dockerfile_image(
|
|
2623
|
+
commands: list[str],
|
|
2624
|
+
base_images: dict[str, _Image] | None = None,
|
|
2625
|
+
image_registry_config: _ImageRegistryConfig | None = None,
|
|
2626
|
+
) -> _Image:
|
|
2555
2627
|
return _Image._from_args(
|
|
2556
2628
|
base_images=base_images,
|
|
2557
2629
|
dockerfile_function=lambda version: DockerfileSpec(commands=commands, context_files={}),
|
|
@@ -2559,13 +2631,10 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2559
2631
|
gpu_config=gpu_config,
|
|
2560
2632
|
secrets=secrets,
|
|
2561
2633
|
build_args=build_args,
|
|
2634
|
+
image_registry_config=image_registry_config,
|
|
2562
2635
|
force_build=force_build,
|
|
2563
2636
|
)
|
|
2564
2637
|
|
|
2565
|
-
# --- Now add in the modal dependencies, and, optionally a Python distribution
|
|
2566
|
-
# This happening in two steps is probably a vestigial consequence of previous limitations,
|
|
2567
|
-
# but it will be difficult to merge them without forcing rebuilds of images.
|
|
2568
|
-
|
|
2569
2638
|
def add_python_mount(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
2570
2639
|
return (
|
|
2571
2640
|
_Mount.from_name(
|
|
@@ -2601,24 +2670,36 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2601
2670
|
)
|
|
2602
2671
|
with open(os.path.expanduser(path)) as f:
|
|
2603
2672
|
dockerfile_lines = f.read().split("\n")
|
|
2673
|
+
if registry_secret is not None and is_multi_stage_dockerfile(dockerfile_lines):
|
|
2674
|
+
raise InvalidError("`registry_secret` is not supported with multi-stage Dockerfiles.")
|
|
2604
2675
|
|
|
2605
2676
|
# Builder versions after 2025.06 put the FROM line of a single-stage Dockerfile in its own image.
|
|
2606
2677
|
# Dockerfiles the split could change (see splittable_from_line_index) stay in one layer.
|
|
2607
2678
|
from_index = splittable_from_line_index(dockerfile_lines) if builder_version > "2025.06" else None
|
|
2608
2679
|
if from_index is None:
|
|
2609
|
-
image =
|
|
2680
|
+
image = dockerfile_image(dockerfile_lines, image_registry_config=registry_config)
|
|
2610
2681
|
else:
|
|
2611
|
-
|
|
2682
|
+
# Drop the lines above FROM so Dockerfiles that differ only in header comments or blank lines
|
|
2683
|
+
# share the cached image. Those lines can only be comments or blank lines, since
|
|
2684
|
+
# splittable_from_line_index rejects parser directives.
|
|
2685
|
+
from_lines = [dockerfile_lines[from_index]]
|
|
2686
|
+
# Only this layer pulls from the registry; the layers above it build on top of it.
|
|
2612
2687
|
from_image = _Image._from_args(
|
|
2613
2688
|
dockerfile_function=lambda version: DockerfileSpec(commands=from_lines, context_files={}),
|
|
2689
|
+
image_registry_config=registry_config,
|
|
2614
2690
|
force_build=force_build,
|
|
2615
2691
|
)
|
|
2616
2692
|
rest = dockerfile_lines[from_index + 1 :]
|
|
2617
2693
|
rest_is_empty = all(not line.strip() or line.strip().startswith("#") for line in rest)
|
|
2618
2694
|
if rest_is_empty:
|
|
2619
|
-
image =
|
|
2695
|
+
image = from_image
|
|
2620
2696
|
else:
|
|
2621
|
-
image =
|
|
2697
|
+
image = dockerfile_image(["FROM base", *rest], base_images={"base": from_image})
|
|
2698
|
+
if add_python or builder_version <= "2025.06":
|
|
2699
|
+
# Before 2025.06 we always created a python_image() layer because it's where we installed the
|
|
2700
|
+
# client dependencies. From 2025.06, we no longer install client dependencies in base layers,
|
|
2701
|
+
# so this was a no-op layer when add_python= is not used. Skip it on later image builders.
|
|
2702
|
+
image = python_image(image)
|
|
2622
2703
|
await resolver.load(image, load_context)
|
|
2623
2704
|
self._hydrate_from_other(image)
|
|
2624
2705
|
self._serve_mounts = image._serve_mounts
|
|
@@ -79,6 +79,15 @@ _FROM_ALIAS_PATTERN = re.compile(r"\sAS\s+\S+\s*$", re.IGNORECASE)
|
|
|
79
79
|
_PARSER_DIRECTIVE_PATTERN = re.compile(r"^#\s*[a-zA-Z][a-zA-Z0-9]*\s*=")
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def is_multi_stage_dockerfile(dockerfile_lines: Sequence[str]) -> bool:
|
|
83
|
+
"""Return whether more than one line starts with a FROM instruction.
|
|
84
|
+
|
|
85
|
+
Can have false-positives for Dockerfiles with a Heredoc bodies of continuation lines
|
|
86
|
+
that contain "from" tokens.
|
|
87
|
+
"""
|
|
88
|
+
return sum(1 for line in dockerfile_lines if _FROM_PATTERN.match(line)) > 1
|
|
89
|
+
|
|
90
|
+
|
|
82
91
|
def splittable_from_line_index(dockerfile_lines: Sequence[str]) -> int | None:
|
|
83
92
|
"""Return the line index of the FROM instruction if it can be built as its own image.
|
|
84
93
|
|
|
@@ -96,7 +105,7 @@ def splittable_from_line_index(dockerfile_lines: Sequence[str]) -> int | None:
|
|
|
96
105
|
return None
|
|
97
106
|
if any(_PARSER_DIRECTIVE_PATTERN.match(line.strip()) for line in dockerfile_lines[:start]):
|
|
98
107
|
return None
|
|
99
|
-
if
|
|
108
|
+
if is_multi_stage_dockerfile(dockerfile_lines):
|
|
100
109
|
return None
|
|
101
110
|
return start
|
|
102
111
|
|
|
@@ -9,7 +9,7 @@ import time
|
|
|
9
9
|
import typing
|
|
10
10
|
import urllib.parse
|
|
11
11
|
import uuid
|
|
12
|
-
from collections.abc import AsyncIterator, Sequence
|
|
12
|
+
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from functools import cache
|
|
15
15
|
from typing import Any, Optional, TypeVar, Union
|
|
@@ -42,6 +42,7 @@ from .logger import logger
|
|
|
42
42
|
|
|
43
43
|
RequestType = TypeVar("RequestType", bound=Message)
|
|
44
44
|
ResponseType = TypeVar("ResponseType", bound=Message)
|
|
45
|
+
MetadataProvider = Callable[[], Mapping[str, str]]
|
|
45
46
|
|
|
46
47
|
if typing.TYPE_CHECKING:
|
|
47
48
|
import modal._grpc_client
|
|
@@ -224,17 +225,29 @@ class ConnectionManager:
|
|
|
224
225
|
should share the same connection.
|
|
225
226
|
"""
|
|
226
227
|
|
|
227
|
-
def __init__(
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
client: "modal.client._Client",
|
|
231
|
+
metadata: dict[str, str] = {},
|
|
232
|
+
metadata_provider: MetadataProvider | None = None,
|
|
233
|
+
):
|
|
228
234
|
self._client = client
|
|
229
235
|
# This metadata is injected into all requests on all channels created by this manager.
|
|
230
236
|
self._metadata = metadata
|
|
237
|
+
self._metadata_provider = metadata_provider
|
|
231
238
|
self._channels: dict[str, asyncio.Future[grpclib.client.Channel]] = {}
|
|
232
239
|
|
|
233
240
|
async def get_or_create_channel(self, server_url: str) -> grpclib.client.Channel:
|
|
234
241
|
fut = self._channels.get(server_url)
|
|
235
242
|
if fut is None or (fut.done() and (fut.cancelled() or fut.exception() is not None)):
|
|
236
243
|
# Cache the attempt rather than the channel, so callers racing to connect share one.
|
|
237
|
-
fut = asyncio.ensure_future(
|
|
244
|
+
fut = asyncio.ensure_future(
|
|
245
|
+
create_channel_with_fallbacks(
|
|
246
|
+
server_url,
|
|
247
|
+
self._metadata,
|
|
248
|
+
metadata_provider=self._metadata_provider,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
238
251
|
self._channels[server_url] = fut
|
|
239
252
|
# Shielded so one caller giving up doesn't cancel the connection for the others.
|
|
240
253
|
return await asyncio.shield(fut)
|
|
@@ -357,6 +370,7 @@ def listen_for_server_warnings(channel: grpclib.client.Channel) -> None:
|
|
|
357
370
|
def create_channel(
|
|
358
371
|
server_url: str,
|
|
359
372
|
metadata: dict[str, str] = {},
|
|
373
|
+
metadata_provider: MetadataProvider | None = None,
|
|
360
374
|
) -> grpclib.client.Channel:
|
|
361
375
|
"""Creates a grpclib.Channel to be used by a GRPC stub.
|
|
362
376
|
|
|
@@ -387,6 +401,10 @@ def create_channel(
|
|
|
387
401
|
async def send_request(event: grpclib.events.SendRequest) -> None:
|
|
388
402
|
for k, v in metadata.items():
|
|
389
403
|
event.metadata[k] = v
|
|
404
|
+
if metadata_provider is not None:
|
|
405
|
+
provided_metadata = await asyncio.to_thread(metadata_provider)
|
|
406
|
+
for k, v in provided_metadata.items():
|
|
407
|
+
event.metadata[k] = v
|
|
390
408
|
|
|
391
409
|
if o.hostname is not None:
|
|
392
410
|
# GRPC actually injects a :authority pseudo-header containing the hostname.
|
|
@@ -412,6 +430,7 @@ async def create_channel_with_fallbacks(
|
|
|
412
430
|
server_url: str,
|
|
413
431
|
metadata: dict[str, str] = {},
|
|
414
432
|
stagger_delay: float = 3.0,
|
|
433
|
+
metadata_provider: MetadataProvider | None = None,
|
|
415
434
|
) -> grpclib.client.Channel:
|
|
416
435
|
"""Create a connected channel, supporting comma-separated fallback URLs.
|
|
417
436
|
|
|
@@ -440,7 +459,7 @@ async def create_channel_with_fallbacks(
|
|
|
440
459
|
await asyncio.wait_for(failed[index - 1].wait(), timeout=index * stagger_delay)
|
|
441
460
|
except asyncio.TimeoutError:
|
|
442
461
|
pass
|
|
443
|
-
channel = create_channel(url, metadata)
|
|
462
|
+
channel = create_channel(url, metadata, metadata_provider)
|
|
444
463
|
try:
|
|
445
464
|
await channel.__connect__()
|
|
446
465
|
except BaseException:
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from modal.exception import InvalidError
|
|
11
|
+
|
|
12
|
+
_OAUTH_CLIENT_ASSERTION_AUDIENCE = "modal-server"
|
|
13
|
+
_OAUTH_CLIENT_ASSERTION_LIFETIME_SECONDS = 5 * 60
|
|
14
|
+
_MIN_RSA_KEY_SIZE = 2048
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OAuthAssertionProvider:
|
|
18
|
+
"""Mint short-lived private_key_jwt assertions for OAuth client authentication."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, client_id: str, private_key_pem: str):
|
|
21
|
+
try:
|
|
22
|
+
import jwt
|
|
23
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
24
|
+
from jwt.algorithms import RSAAlgorithm
|
|
25
|
+
from jwt.exceptions import InvalidKeyError
|
|
26
|
+
except ImportError:
|
|
27
|
+
raise ImportError(
|
|
28
|
+
"Private-key OAuth authentication requires the 'oauth' extra. "
|
|
29
|
+
"Install it with `pip install 'modal[oauth]'`."
|
|
30
|
+
) from None
|
|
31
|
+
|
|
32
|
+
private_key_pem = private_key_pem.replace("\\n", "\n")
|
|
33
|
+
try:
|
|
34
|
+
private_key = RSAAlgorithm(RSAAlgorithm.SHA256).prepare_key(private_key_pem)
|
|
35
|
+
except (TypeError, InvalidKeyError):
|
|
36
|
+
raise InvalidError("oauth_jwt_key must be an unencrypted RSA private key encoded as PEM.") from None
|
|
37
|
+
if not isinstance(private_key, RSAPrivateKey):
|
|
38
|
+
raise InvalidError("oauth_jwt_key must be an RSA private key.")
|
|
39
|
+
if private_key.key_size < _MIN_RSA_KEY_SIZE:
|
|
40
|
+
raise InvalidError("oauth_jwt_key must be at least 2048 bits.")
|
|
41
|
+
|
|
42
|
+
public_jwk: dict[str, Any] = RSAAlgorithm.to_jwk(private_key.public_key(), as_dict=True)
|
|
43
|
+
thumbprint_jwk = {field: public_jwk[field] for field in ("e", "kty", "n")}
|
|
44
|
+
canonical_jwk = json.dumps(thumbprint_jwk, sort_keys=True, separators=(",", ":")).encode()
|
|
45
|
+
|
|
46
|
+
self._client_id = client_id
|
|
47
|
+
self._private_key = private_key
|
|
48
|
+
self._kid = base64.urlsafe_b64encode(hashlib.sha256(canonical_jwk).digest()).rstrip(b"=").decode()
|
|
49
|
+
self._jwt = jwt
|
|
50
|
+
|
|
51
|
+
def __call__(self) -> Mapping[str, str]:
|
|
52
|
+
issued_at = int(time.time())
|
|
53
|
+
assertion = self._jwt.encode(
|
|
54
|
+
{
|
|
55
|
+
"iss": self._client_id,
|
|
56
|
+
"sub": self._client_id,
|
|
57
|
+
"aud": _OAUTH_CLIENT_ASSERTION_AUDIENCE,
|
|
58
|
+
"iat": issued_at,
|
|
59
|
+
"exp": issued_at + _OAUTH_CLIENT_ASSERTION_LIFETIME_SECONDS,
|
|
60
|
+
"jti": str(uuid.uuid4()),
|
|
61
|
+
},
|
|
62
|
+
self._private_key,
|
|
63
|
+
algorithm="RS256",
|
|
64
|
+
headers={"kid": self._kid},
|
|
65
|
+
)
|
|
66
|
+
return {"x-modal-oauth-client-assertion": assertion}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
aiohappyeyeballs==2.7.1
|
|
2
|
+
aiohttp==3.14.5
|
|
3
|
+
aiosignal==1.4.0
|
|
4
|
+
attrs==26.1.0
|
|
5
|
+
cbor2==5.9.0
|
|
6
|
+
certifi==2026.10.19
|
|
7
|
+
frozenlist==1.8.0
|
|
8
|
+
grpclib==0.4.9
|
|
9
|
+
h2==4.4.1
|
|
10
|
+
hpack==4.2.0
|
|
11
|
+
hyperframe==6.1.0
|
|
12
|
+
idna==3.20
|
|
13
|
+
multidict==7.1.0
|
|
14
|
+
propcache==0.5.4
|
|
15
|
+
protobuf==7.36.2
|
|
16
|
+
typing_extensions==4.16.0
|
|
17
|
+
yarl==1.25.1
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"debian": {
|
|
3
3
|
"PREVIEW": "trixie",
|
|
4
|
+
"2026.10": "trixie",
|
|
4
5
|
"2025.06": "bookworm",
|
|
5
6
|
"2024.10": "bookworm",
|
|
6
7
|
"2024.04": "bookworm",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
},
|
|
39
40
|
"micromamba": {
|
|
40
41
|
"PREVIEW": "2.9.0-debian13-slim",
|
|
42
|
+
"2026.10": "2.9.0-debian13-slim",
|
|
41
43
|
"2025.06": "2.1.1-debian12-slim",
|
|
42
44
|
"2024.10": "1.5.10-bookworm-slim",
|
|
43
45
|
"2024.04": "1.5.8-bookworm-slim",
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
},
|
|
46
48
|
"package_tools": {
|
|
47
49
|
"PREVIEW": "pip wheel uv",
|
|
50
|
+
"2026.10": "pip wheel uv",
|
|
48
51
|
"2025.06": "pip wheel uv",
|
|
49
52
|
"2024.10": "pip wheel uv",
|
|
50
53
|
"2024.04": "pip wheel uv",
|
|
@@ -33,6 +33,8 @@ def show(redact: bool, include_internal: bool, json: bool):
|
|
|
33
33
|
config_dict["oauth_refresh_token"] = "***"
|
|
34
34
|
if redact and config_dict.get("oauth_client_secret"):
|
|
35
35
|
config_dict["oauth_client_secret"] = "***"
|
|
36
|
+
if redact and config_dict.get("oauth_jwt_key"):
|
|
37
|
+
config_dict["oauth_jwt_key"] = "***"
|
|
36
38
|
|
|
37
39
|
# Accept --json as a no-op for consistency; we may change the default formatting later
|
|
38
40
|
OutputManager.get().print_json(dumps(config_dict))
|
|
@@ -260,26 +260,35 @@ async def list_(*, json: bool = False, env: Optional[str] = None):
|
|
|
260
260
|
@click.option(
|
|
261
261
|
"--name",
|
|
262
262
|
default=None,
|
|
263
|
-
help="Endpoint name.
|
|
263
|
+
help="Endpoint name. If omitted, Modal chooses a name.",
|
|
264
264
|
)
|
|
265
265
|
@click.option(
|
|
266
266
|
"--model",
|
|
267
267
|
required=True,
|
|
268
268
|
help="Hugging Face repo ID for the base model architecture (e.g., 'Qwen/Qwen3.6-27B-FP8').",
|
|
269
269
|
)
|
|
270
|
+
@click.option(
|
|
271
|
+
"--shared",
|
|
272
|
+
is_flag=True,
|
|
273
|
+
default=False,
|
|
274
|
+
help="Create a Shared Endpoint. Defaults to a Dedicated Endpoint.",
|
|
275
|
+
)
|
|
270
276
|
@click.option(
|
|
271
277
|
"--routing-region",
|
|
272
278
|
"routing_region",
|
|
273
279
|
multiple=True,
|
|
274
280
|
callback=_single_routing_region_callback,
|
|
275
|
-
help=
|
|
281
|
+
help=(
|
|
282
|
+
f"Region to route inference requests through. Defaults to {_DEFAULT_ROUTING_REGION}. "
|
|
283
|
+
"Shared Endpoint routing cannot currently be customized."
|
|
284
|
+
),
|
|
276
285
|
)
|
|
277
286
|
@click.option(
|
|
278
287
|
"--compute-region",
|
|
279
288
|
"compute_regions",
|
|
280
289
|
multiple=True,
|
|
281
290
|
help=(
|
|
282
|
-
"Region to run Endpoint containers in. May be specified multiple times. "
|
|
291
|
+
"Region to run Dedicated Endpoint containers in. May be specified multiple times. "
|
|
283
292
|
"This incurs a region selection price multiplier."
|
|
284
293
|
),
|
|
285
294
|
)
|
|
@@ -287,13 +296,16 @@ async def list_(*, json: bool = False, env: Optional[str] = None):
|
|
|
287
296
|
"--colocate-compute",
|
|
288
297
|
is_flag=True,
|
|
289
298
|
default=False,
|
|
290
|
-
help=
|
|
299
|
+
help=(
|
|
300
|
+
"Run all Dedicated Endpoint containers within the routing region. "
|
|
301
|
+
"This incurs a region selection price multiplier."
|
|
302
|
+
),
|
|
291
303
|
)
|
|
292
304
|
@click.option(
|
|
293
305
|
"--unauthenticated",
|
|
294
306
|
is_flag=True,
|
|
295
307
|
default=False,
|
|
296
|
-
help="Allow unauthenticated HTTP requests to
|
|
308
|
+
help="Allow unauthenticated HTTP requests to a Dedicated Endpoint.",
|
|
297
309
|
)
|
|
298
310
|
@click.option("--custom-hf-repo", default=None, help="Hugging Face repo ID for fine-tuned model weights.")
|
|
299
311
|
@click.option("--custom-hf-revision", default=None, help="Git revision for --custom-hf-repo.")
|
|
@@ -304,6 +316,7 @@ async def list_(*, json: bool = False, env: Optional[str] = None):
|
|
|
304
316
|
async def create(
|
|
305
317
|
name: Optional[str],
|
|
306
318
|
model: Optional[str] = None,
|
|
319
|
+
shared: bool = False,
|
|
307
320
|
routing_region: str = _DEFAULT_ROUTING_REGION,
|
|
308
321
|
compute_regions: tuple[str, ...] = (),
|
|
309
322
|
colocate_compute: bool = False,
|
|
@@ -317,13 +330,21 @@ async def create(
|
|
|
317
330
|
):
|
|
318
331
|
"""Deploy a new Endpoint.
|
|
319
332
|
|
|
333
|
+
Creates a Dedicated Endpoint by default, with support for custom weights and compute placement.
|
|
334
|
+
Use --shared to serve a supported base model with Modal-managed capacity.
|
|
335
|
+
|
|
320
336
|
Examples:
|
|
321
337
|
|
|
322
|
-
Create
|
|
338
|
+
Create a Dedicated Endpoint from a base model:
|
|
323
339
|
```bash
|
|
324
340
|
modal endpoint create --model Qwen/Qwen3.6-27B-FP8
|
|
325
341
|
```
|
|
326
342
|
|
|
343
|
+
Create a Shared Endpoint from a supported base model:
|
|
344
|
+
```bash
|
|
345
|
+
modal endpoint create --shared --model moonshotai/Kimi-K3
|
|
346
|
+
```
|
|
347
|
+
|
|
327
348
|
Create an Endpoint with an explicit name:
|
|
328
349
|
```bash
|
|
329
350
|
modal endpoint create --name qwen-chat --model Qwen/Qwen3.6-27B-FP8
|
|
@@ -348,6 +369,22 @@ async def create(
|
|
|
348
369
|
```
|
|
349
370
|
|
|
350
371
|
"""
|
|
372
|
+
if shared:
|
|
373
|
+
for option, provided in (
|
|
374
|
+
("--compute-region", bool(compute_regions)),
|
|
375
|
+
("--colocate-compute", colocate_compute),
|
|
376
|
+
("--unauthenticated", unauthenticated),
|
|
377
|
+
("--custom-hf-repo", custom_hf_repo is not None),
|
|
378
|
+
("--custom-hf-revision", custom_hf_revision is not None),
|
|
379
|
+
("--custom-hf-token", custom_hf_token is not None),
|
|
380
|
+
("--custom-volume-name", custom_volume_name is not None),
|
|
381
|
+
("--custom-volume-path", custom_volume_path is not None),
|
|
382
|
+
):
|
|
383
|
+
if provided:
|
|
384
|
+
raise click.UsageError(f"{option} cannot be used with --shared.")
|
|
385
|
+
if routing_region != _DEFAULT_ROUTING_REGION:
|
|
386
|
+
raise click.UsageError(f"Shared Endpoints require --routing-region {_DEFAULT_ROUTING_REGION}.")
|
|
387
|
+
|
|
351
388
|
if compute_regions and colocate_compute:
|
|
352
389
|
raise click.UsageError("--compute-region and --colocate-compute are mutually exclusive.")
|
|
353
390
|
if custom_hf_repo and custom_volume_name:
|
|
@@ -403,13 +440,18 @@ async def create(
|
|
|
403
440
|
model=model_source,
|
|
404
441
|
environment_name=environment_name,
|
|
405
442
|
unauthenticated=unauthenticated,
|
|
443
|
+
serving_mode=(api_pb2.ENDPOINT_SERVING_MODE_SHARED if shared else api_pb2.ENDPOINT_SERVING_MODE_DEDICATED),
|
|
406
444
|
)
|
|
407
445
|
if name:
|
|
408
446
|
req.name = name
|
|
409
447
|
resp = await client._stub.EndpointCreate(req)
|
|
410
448
|
|
|
411
449
|
output = OutputManager.get()
|
|
412
|
-
|
|
450
|
+
serving_mode = "Shared" if shared else "Dedicated"
|
|
451
|
+
output.print(
|
|
452
|
+
f"[green]✓[/green] {serving_mode} Endpoint '{resp.name}' ({resp.endpoint_id}) "
|
|
453
|
+
"was created and started provisioning."
|
|
454
|
+
)
|
|
413
455
|
if resp.model_name:
|
|
414
456
|
output.print(f' → Send [cyan]"model": "{resp.model_name}"[/cyan] in requests to the Endpoint.')
|
|
415
457
|
if resp.endpoint_page_url:
|