modal 1.6.2.dev13__tar.gz → 1.6.3__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.2.dev13 → modal-1.6.3}/PKG-INFO +4 -2
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_environments.py +5 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_image.py +251 -72
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/auth_token_manager.py +8 -4
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/docker_utils.py +36 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/grpc_testing.py +3 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/grpc_utils.py +23 -4
- modal-1.6.3/modal/_utils/oauth.py +66 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_workspace.py +65 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/app.py +39 -19
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/app.pyi +59 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2025.06.txt +1 -1
- modal-1.6.3/modal/builder/2026.10.txt +17 -0
- modal-1.6.3/modal/builder/PREVIEW.txt +17 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/base-images.json +5 -11
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/config.py +2 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/endpoint.py +49 -7
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/environment.py +21 -5
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/profile.py +2 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/token.py +1 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/workspace.py +101 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/client.py +39 -9
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/client.pyi +42 -8
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/config.py +3 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/environments.pyi +2 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/__init__.py +6 -5
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/image.pyi +52 -20
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/io_streams.py +26 -14
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/io_streams.pyi +2 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/mount.py +43 -13
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/mount.pyi +10 -8
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/network_file_system.py +4 -8
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/network_file_system.pyi +10 -16
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/runner.py +10 -25
- modal-1.6.3/modal/sandbox/_common.py +1134 -0
- modal-1.6.3/modal/sandbox/_impl_persistent.py +470 -0
- modal-1.6.3/modal/sandbox/_impl_v1.py +523 -0
- modal-1.6.3/modal/sandbox/_impl_v2.py +623 -0
- modal-1.6.3/modal/sandbox/_sandbox.py +1492 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/_sidecar.py +67 -214
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/_task_command_router_client.py +88 -128
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/sandbox.pyi +114 -318
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/token_flow.py +1 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/types.py +23 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/workspace.py +3 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/workspace.pyi +93 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/PKG-INFO +4 -2
- {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/SOURCES.txt +5 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/requires.txt +4 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_grpc.py +160 -16
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_pb2.py +1711 -1436
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_pb2.pyi +794 -115
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_pb2_grpc.py +332 -35
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_pb2_grpc.pyi +105 -15
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/modal_api_grpc.py +10 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_version/__init__.py +1 -1
- {modal-1.6.2.dev13 → modal-1.6.3}/pyproject.toml +8 -4
- modal-1.6.2.dev13/modal/builder/PREVIEW.txt +0 -21
- modal-1.6.2.dev13/modal/sandbox/_common.py +0 -248
- modal-1.6.2.dev13/modal/sandbox/_sandbox.py +0 -2590
- {modal-1.6.2.dev13 → modal-1.6.3}/LICENSE +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/README.md +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/__main__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_billing.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_cluster.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_cluster.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_function_variants.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_functions.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_grpc_client.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_ipython.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_load_context.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_location.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_logs.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_logs_manager.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_metrics.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_metrics.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_object.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_outbound_policy.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_outbound_policy.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/manager.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/pty.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/rich.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/status.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_partial_function.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_resolver.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_resources.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_serialization.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_server.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_server.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_supports_logs.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_traceback.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_tunnel.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_tunnel.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_type_manager.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/async_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/blob_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/logger.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/name_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/time_utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/version.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/_watcher.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/billing.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/billing.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/README.md +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/call_graph.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_download.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_help.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_logs.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_stats.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_traceback.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/app.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/billing.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/changelog.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/container.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/curl.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/dashboard.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/dict.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/entry_point.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/function.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/image.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/import_refs.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/logo.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/network_file_system.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/queues.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/run.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/secret.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/selector.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/server.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/shell.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/skills.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/utils.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/volume.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cls.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cls.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cluster.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/cluster.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/container_process.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/container_process.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/dict.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/dict.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/environments.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/exception.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/flash.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/ipython.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/functions.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/functions.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/image.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/object.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/object.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/output.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/parallel_map.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/parallel_map.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/partial_function.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/partial_function.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/proxy.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/proxy.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/py.typed +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/queue.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/queue.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/retries.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/runner.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/running_app.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/_filesystem.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/_snapshot.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/sandbox.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/schedule.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/scheduler_placement.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/secret.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/secret.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/server.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/server.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/serving.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/serving.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/stream_type.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/token_flow.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/volume.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal/volume.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/__init__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/py.typed +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/modal_version/__main__.py +0 -0
- {modal-1.6.2.dev13 → modal-1.6.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.3
|
|
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
|
|
@@ -648,6 +648,7 @@ async def _update_environment(
|
|
|
648
648
|
*,
|
|
649
649
|
new_name: str | None = None,
|
|
650
650
|
new_web_suffix: str | None = None,
|
|
651
|
+
new_image_builder_version: str | None = None,
|
|
651
652
|
client: _Client | None = None,
|
|
652
653
|
):
|
|
653
654
|
new_name_pb2 = None
|
|
@@ -662,7 +663,10 @@ async def _update_environment(
|
|
|
662
663
|
new_web_suffix_pb2 = StringValue(value=new_web_suffix)
|
|
663
664
|
|
|
664
665
|
update_payload = api_pb2.EnvironmentUpdateRequest(
|
|
665
|
-
current_name=current_name,
|
|
666
|
+
current_name=current_name,
|
|
667
|
+
name=new_name_pb2,
|
|
668
|
+
web_suffix=new_web_suffix_pb2,
|
|
669
|
+
image_builder_version=new_image_builder_version,
|
|
666
670
|
)
|
|
667
671
|
if client is None:
|
|
668
672
|
client = await _Client.from_env()
|
|
@@ -40,6 +40,8 @@ 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,
|
|
44
|
+
splittable_from_line_index,
|
|
43
45
|
)
|
|
44
46
|
from ._utils.function_utils import FunctionSourceInfo, parse_gpu_config
|
|
45
47
|
from ._utils.mount_utils import validate_only_modal_volumes, validate_volumes_by_object_id
|
|
@@ -57,7 +59,7 @@ from .exception import (
|
|
|
57
59
|
VersionError,
|
|
58
60
|
)
|
|
59
61
|
from .file_pattern_matcher import NON_PYTHON_FILES, FilePatternMatcher, _ignore_fn
|
|
60
|
-
from .mount import _Mount, python_standalone_mount_name
|
|
62
|
+
from .mount import PYTHON_STANDALONE_VERSIONS, _Mount, python_standalone_mount_name
|
|
61
63
|
from .network_file_system import _NetworkFileSystem
|
|
62
64
|
from .output import OutputManager
|
|
63
65
|
from .secret import _Secret
|
|
@@ -69,14 +71,16 @@ if typing.TYPE_CHECKING:
|
|
|
69
71
|
import modal.client
|
|
70
72
|
|
|
71
73
|
# This is used for both type checking and runtime validation
|
|
72
|
-
ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]
|
|
74
|
+
ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "2026.10", "PREVIEW"]
|
|
75
|
+
LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2026.10"
|
|
73
76
|
|
|
74
77
|
# Note: we also define supported Python versions via logic at the top of the package __init__.py
|
|
75
78
|
# so that we fail fast / clearly in unsupported containers. Additionally, we enumerate the supported
|
|
76
79
|
# Python versions in mount.py where we specify the "standalone Python versions" we create mounts for.
|
|
77
80
|
# Consider consolidating these multiple sources of truth?
|
|
78
81
|
SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
|
|
79
|
-
"PREVIEW": ["3.
|
|
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"],
|
|
80
84
|
"2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
|
|
81
85
|
"2024.10": ["3.10", "3.11", "3.12", "3.13"],
|
|
82
86
|
"2024.04": ["3.10", "3.11", "3.12"],
|
|
@@ -173,6 +177,7 @@ def _validate_python_version(
|
|
|
173
177
|
allow_free_threading: bool = False,
|
|
174
178
|
caller_name: str = "",
|
|
175
179
|
) -> str:
|
|
180
|
+
python_version_unspecified = python_version is None
|
|
176
181
|
if python_version is None:
|
|
177
182
|
# If Python version is unspecified, match the local version, up to the minor component
|
|
178
183
|
python_version = series_version = "{}.{}".format(*sys.version_info)
|
|
@@ -195,6 +200,12 @@ def _validate_python_version(
|
|
|
195
200
|
|
|
196
201
|
supported_series = SUPPORTED_PYTHON_SERIES[builder_version]
|
|
197
202
|
if series_version not in supported_series:
|
|
203
|
+
if python_version_unspecified:
|
|
204
|
+
raise InvalidError(
|
|
205
|
+
f"Python {series_version} is not supported by the {builder_version!r} Image builder."
|
|
206
|
+
f" Since `python_version` was not set, Modal tried to match your local Python ({series_version})."
|
|
207
|
+
f" Set `python_version` to one of the supported series: {supported_series!r}."
|
|
208
|
+
)
|
|
198
209
|
raise InvalidError(
|
|
199
210
|
f"Unsupported Python version: {python_version!r}."
|
|
200
211
|
f" When using the {builder_version!r} Image builder, Modal supports the following series:"
|
|
@@ -212,23 +223,32 @@ def _dockerhub_python_version(
|
|
|
212
223
|
python_version = _validate_python_version(
|
|
213
224
|
python_version, builder_version, allow_free_threading=allow_free_threading, caller_name=caller_name
|
|
214
225
|
)
|
|
226
|
+
|
|
215
227
|
version_components = python_version.split(".")
|
|
216
228
|
|
|
217
|
-
#
|
|
218
|
-
|
|
229
|
+
# - Users that want a specific patch version for reproducibility guarantees can specify that up
|
|
230
|
+
# front
|
|
231
|
+
# - Users on the newest builder can force-rebuild to get the latest patch versions, allowing
|
|
232
|
+
# quick responses to upstream bug fixes / CVEs.
|
|
233
|
+
if builder_version > "2025.06" or len(version_components) > 2:
|
|
219
234
|
return python_version
|
|
220
235
|
|
|
221
236
|
# Otherwise, use the same series, but a specific micro version, corresponding to the latest
|
|
222
237
|
# available from https://hub.docker.com/_/python at the time of each image builder release.
|
|
223
238
|
# This allows us to publish one pre-built debian-slim image per Python series.
|
|
224
239
|
python_versions = _base_image_config("python", builder_version)
|
|
225
|
-
series_to_micro_version
|
|
240
|
+
series_to_micro_version: dict[str, str] = {}
|
|
241
|
+
for v in python_versions:
|
|
242
|
+
series, micro = v.rsplit(".", 1)
|
|
243
|
+
series_to_micro_version[series] = micro
|
|
226
244
|
python_series_requested = "{}.{}".format(*version_components)
|
|
227
245
|
micro_version = series_to_micro_version[python_series_requested]
|
|
228
246
|
return f"{python_series_requested}.{micro_version}"
|
|
229
247
|
|
|
230
248
|
|
|
231
249
|
def _base_image_config(group: str, builder_version: ImageBuilderVersion) -> Any:
|
|
250
|
+
# data["python"] does not include image builder versions more recent than `2025.06`, as those do
|
|
251
|
+
# not rely on hardcoded patch versions.
|
|
232
252
|
with open(LOCAL_REQUIREMENTS_DIR / "base-images.json") as f:
|
|
233
253
|
data = json.load(f)
|
|
234
254
|
return data[group][builder_version]
|
|
@@ -339,6 +359,13 @@ def _get_image_builder_version(server_version: ImageBuilderVersion) -> ImageBuil
|
|
|
339
359
|
return version
|
|
340
360
|
|
|
341
361
|
|
|
362
|
+
async def _resolve_image_builder_version(load_context: LoadContext) -> ImageBuilderVersion:
|
|
363
|
+
environment = await _get_environment_cached(load_context.environment_name or "", load_context.client)
|
|
364
|
+
# A bit hacky,but assume that the environment provides a valid builder version
|
|
365
|
+
image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
|
|
366
|
+
return _get_image_builder_version(image_builder_version)
|
|
367
|
+
|
|
368
|
+
|
|
342
369
|
def _create_context_mount(
|
|
343
370
|
docker_commands: Sequence[str],
|
|
344
371
|
ignore_fn: Callable[[Path], bool],
|
|
@@ -393,7 +420,7 @@ def _create_context_mount_function(
|
|
|
393
420
|
|
|
394
421
|
if ignore is AUTO_DOCKERIGNORE:
|
|
395
422
|
|
|
396
|
-
def auto_created_context_mount_fn() -> _Mount | None:
|
|
423
|
+
def auto_created_context_mount_fn(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
397
424
|
nonlocal context_dir
|
|
398
425
|
context_dir = Path.cwd() if context_dir is None else Path(context_dir).absolute()
|
|
399
426
|
dockerignore_file = find_dockerignore_file(context_dir, dockerfile_path)
|
|
@@ -410,7 +437,7 @@ def _create_context_mount_function(
|
|
|
410
437
|
|
|
411
438
|
else:
|
|
412
439
|
|
|
413
|
-
def auto_created_context_mount_fn() -> _Mount | None:
|
|
440
|
+
def auto_created_context_mount_fn(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
414
441
|
# use COPY commands and ignore patterns to construct implicit context mount
|
|
415
442
|
nonlocal context_dir
|
|
416
443
|
context_dir = Path.cwd() if context_dir is None else Path(context_dir).absolute()
|
|
@@ -420,6 +447,40 @@ def _create_context_mount_function(
|
|
|
420
447
|
return auto_created_context_mount_fn
|
|
421
448
|
|
|
422
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
|
+
|
|
423
484
|
class _ImageRegistryConfig:
|
|
424
485
|
"""mdmd:hidden"""
|
|
425
486
|
|
|
@@ -428,13 +489,21 @@ class _ImageRegistryConfig:
|
|
|
428
489
|
# TODO: change to _PUBLIC after worker starts handling it.
|
|
429
490
|
registry_auth_type: "api_pb2.RegistryAuthType.ValueType" = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
|
|
430
491
|
secret: _Secret | None = None,
|
|
492
|
+
*,
|
|
493
|
+
infer_auth_type: bool = False,
|
|
431
494
|
):
|
|
432
495
|
self.registry_auth_type = registry_auth_type
|
|
433
496
|
self.secret = secret
|
|
434
|
-
|
|
435
|
-
|
|
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)
|
|
436
505
|
return api_pb2.ImageRegistryConfig(
|
|
437
|
-
registry_auth_type=
|
|
506
|
+
registry_auth_type=registry_auth_type,
|
|
438
507
|
secret_id=(self.secret.object_id if self.secret else ""),
|
|
439
508
|
)
|
|
440
509
|
|
|
@@ -613,7 +682,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
613
682
|
build_function: "modal._functions._Function | None" = None,
|
|
614
683
|
build_function_input: api_pb2.FunctionInput | None = None,
|
|
615
684
|
image_registry_config: _ImageRegistryConfig | None = None,
|
|
616
|
-
context_mount_function: Callable[[], _Mount | None] | None = None,
|
|
685
|
+
context_mount_function: Callable[[ImageBuilderVersion], _Mount | None] | None = None,
|
|
617
686
|
force_build: bool = False,
|
|
618
687
|
build_args: dict[str, str] = {},
|
|
619
688
|
validated_volumes: Sequence[tuple[str, _Volume]] | None = None,
|
|
@@ -652,7 +721,10 @@ class _Image(_Object, type_prefix="im"):
|
|
|
652
721
|
return deps
|
|
653
722
|
|
|
654
723
|
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
655
|
-
|
|
724
|
+
assert load_context.app_id # type narrowing
|
|
725
|
+
builder_version = await _resolve_image_builder_version(load_context)
|
|
726
|
+
|
|
727
|
+
context_mount = context_mount_function(builder_version) if context_mount_function else None
|
|
656
728
|
if context_mount:
|
|
657
729
|
await resolver.load(context_mount, load_context)
|
|
658
730
|
|
|
@@ -661,12 +733,6 @@ class _Image(_Object, type_prefix="im"):
|
|
|
661
733
|
# base images can't have
|
|
662
734
|
image._assert_no_mount_layers()
|
|
663
735
|
|
|
664
|
-
assert load_context.app_id # type narrowing
|
|
665
|
-
environment = await _get_environment_cached(load_context.environment_name or "", load_context.client)
|
|
666
|
-
# A bit hacky,but assume that the environment provides a valid builder version
|
|
667
|
-
image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
|
|
668
|
-
builder_version = _get_image_builder_version(image_builder_version)
|
|
669
|
-
|
|
670
736
|
if dockerfile_function is None:
|
|
671
737
|
dockerfile = DockerfileSpec(commands=[], context_files={})
|
|
672
738
|
else:
|
|
@@ -740,7 +806,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
740
806
|
secret_ids=[secret.object_id for secret in secrets],
|
|
741
807
|
context_mount_id=(context_mount.object_id if context_mount else ""),
|
|
742
808
|
gpu_config=gpu_config,
|
|
743
|
-
image_registry_config=image_registry_config.get_proto(),
|
|
809
|
+
image_registry_config=await image_registry_config.get_proto(),
|
|
744
810
|
runtime=config.get("function_runtime"),
|
|
745
811
|
runtime_debug=config.get("function_runtime_debug"),
|
|
746
812
|
build_function=_build_function,
|
|
@@ -838,7 +904,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
838
904
|
return _Image._from_args(
|
|
839
905
|
base_images={"base": self},
|
|
840
906
|
dockerfile_function=build_dockerfile,
|
|
841
|
-
context_mount_function=lambda: mount,
|
|
907
|
+
context_mount_function=lambda builder_version: mount,
|
|
842
908
|
)
|
|
843
909
|
|
|
844
910
|
@_requires_image_instance
|
|
@@ -2182,12 +2248,9 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2182
2248
|
"COPY /python/. /usr/local",
|
|
2183
2249
|
"ENV TERMINFO_DIRS=/etc/terminfo:/lib/terminfo:/usr/share/terminfo:/usr/lib/terminfo",
|
|
2184
2250
|
]
|
|
2185
|
-
|
|
2186
|
-
if
|
|
2187
|
-
|
|
2188
|
-
if int(python_minor) < 13:
|
|
2189
|
-
# Previous versions did not include the `python` binary, but later ones do.
|
|
2190
|
-
# (The important factor is not the Python version itself, but the standalone dist version.)
|
|
2251
|
+
release, _ = PYTHON_STANDALONE_VERSIONS[builder_version][add_python]
|
|
2252
|
+
if release < "20241008":
|
|
2253
|
+
# Older standalone releases do not include the `python` binary.
|
|
2191
2254
|
# We insert the command in the list at the position it was previously always added
|
|
2192
2255
|
# for backwards compatibility with existing images.
|
|
2193
2256
|
add_python_commands.insert(1, "RUN ln -s /usr/local/bin/python3 /usr/local/bin/python")
|
|
@@ -2236,16 +2299,21 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2236
2299
|
remaining commands run. This might be useful if you want a custom Python installation or to
|
|
2237
2300
|
set a `SHELL`. Prefer `run_commands()` when possible though.
|
|
2238
2301
|
|
|
2239
|
-
To
|
|
2240
|
-
|
|
2241
|
-
|
|
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`).
|
|
2242
2311
|
|
|
2243
|
-
|
|
2244
|
-
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.
|
|
2245
2313
|
|
|
2246
2314
|
Args:
|
|
2247
2315
|
tag: Registry image reference (e.g. ``python:3.11-slim``).
|
|
2248
|
-
secret: Optional
|
|
2316
|
+
secret: Optional Secret with registry credentials; its keys determine the kind of authentication.
|
|
2249
2317
|
setup_dockerfile_commands: Extra Dockerfile lines run after ``FROM`` during base setup.
|
|
2250
2318
|
force_build: If True, skip cached image builds.
|
|
2251
2319
|
add_python: Optional standalone Python series to inject when the base image lacks Python.
|
|
@@ -2259,13 +2327,17 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2259
2327
|
modal.Image.from_registry("python:3.11-slim-bookworm")
|
|
2260
2328
|
modal.Image.from_registry("ubuntu:22.04", add_python="3.11")
|
|
2261
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
|
+
)
|
|
2262
2334
|
```
|
|
2263
2335
|
"""
|
|
2264
2336
|
|
|
2265
|
-
def context_mount_function() -> _Mount | None:
|
|
2337
|
+
def context_mount_function(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
2266
2338
|
return (
|
|
2267
2339
|
_Mount.from_name(
|
|
2268
|
-
python_standalone_mount_name(add_python),
|
|
2340
|
+
python_standalone_mount_name(add_python, builder_version),
|
|
2269
2341
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
2270
2342
|
)
|
|
2271
2343
|
if add_python
|
|
@@ -2273,7 +2345,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2273
2345
|
)
|
|
2274
2346
|
|
|
2275
2347
|
if "image_registry_config" not in kwargs and secret is not None:
|
|
2276
|
-
kwargs["image_registry_config"] = _ImageRegistryConfig(
|
|
2348
|
+
kwargs["image_registry_config"] = _ImageRegistryConfig(secret=secret, infer_auth_type=True)
|
|
2277
2349
|
|
|
2278
2350
|
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
2279
2351
|
commands = _Image._registry_setup_commands(tag, version, setup_dockerfile_commands, add_python)
|
|
@@ -2282,12 +2354,48 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2282
2354
|
context_files = {CONTAINER_REQUIREMENTS_PATH: _get_modal_requirements_path(version, add_python)}
|
|
2283
2355
|
return DockerfileSpec(commands=commands, context_files=context_files)
|
|
2284
2356
|
|
|
2285
|
-
|
|
2357
|
+
single_layer_image = _Image._from_args(
|
|
2286
2358
|
dockerfile_function=build_dockerfile,
|
|
2287
2359
|
context_mount_function=context_mount_function,
|
|
2288
2360
|
force_build=force_build,
|
|
2289
2361
|
**kwargs,
|
|
2290
2362
|
)
|
|
2363
|
+
if not add_python:
|
|
2364
|
+
return single_layer_image
|
|
2365
|
+
|
|
2366
|
+
# Builder versions after 2025.06 put the registry image and the standalone Python in separate layers.
|
|
2367
|
+
registry_image = _Image._from_args(
|
|
2368
|
+
dockerfile_function=lambda version: DockerfileSpec(commands=[f"FROM {tag}"], context_files={}),
|
|
2369
|
+
image_registry_config=kwargs.get("image_registry_config"),
|
|
2370
|
+
force_build=force_build,
|
|
2371
|
+
)
|
|
2372
|
+
|
|
2373
|
+
def build_python_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
2374
|
+
commands = _Image._registry_setup_commands("base", version, setup_dockerfile_commands, add_python)
|
|
2375
|
+
return DockerfileSpec(commands=commands, context_files={})
|
|
2376
|
+
|
|
2377
|
+
split_image = _Image._from_args(
|
|
2378
|
+
base_images={"base": registry_image},
|
|
2379
|
+
dockerfile_function=build_python_dockerfile,
|
|
2380
|
+
context_mount_function=context_mount_function,
|
|
2381
|
+
force_build=force_build,
|
|
2382
|
+
**kwargs,
|
|
2383
|
+
)
|
|
2384
|
+
|
|
2385
|
+
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
2386
|
+
builder_version = await _resolve_image_builder_version(load_context)
|
|
2387
|
+
# Validate before the registry base image of split_image is built.
|
|
2388
|
+
_validate_python_version(
|
|
2389
|
+
add_python, builder_version, allow_micro_granularity=False, allow_free_threading=True
|
|
2390
|
+
)
|
|
2391
|
+
image = split_image if builder_version > "2025.06" else single_layer_image
|
|
2392
|
+
await resolver.load(image, load_context)
|
|
2393
|
+
self._hydrate_from_other(image)
|
|
2394
|
+
self._serve_mounts = image._serve_mounts
|
|
2395
|
+
|
|
2396
|
+
obj = _Image._from_loader(_load, f"Image.from_registry({tag!r})", load_context_overrides=LoadContext.empty())
|
|
2397
|
+
obj.force_build = force_build
|
|
2398
|
+
return obj
|
|
2291
2399
|
|
|
2292
2400
|
@staticmethod
|
|
2293
2401
|
def from_gcp_artifact_registry(
|
|
@@ -2299,7 +2407,11 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2299
2407
|
add_python: str | None = None,
|
|
2300
2408
|
**kwargs,
|
|
2301
2409
|
) -> "_Image":
|
|
2302
|
-
"""
|
|
2410
|
+
"""mdmd:hidden
|
|
2411
|
+
Build a Modal image from a private image in Google Cloud Platform (GCP) Artifact Registry.
|
|
2412
|
+
|
|
2413
|
+
**Note:** This constructor is superseded by `Image.from_registry()`, which can also authenticate
|
|
2414
|
+
to private repositories when provided with the appropriate keys in a `modal.Secret`.
|
|
2303
2415
|
|
|
2304
2416
|
You will need to pass a `modal.Secret` containing [your GCP service account key data](https://cloud.google.com/iam/docs/keys-create-delete#creating)
|
|
2305
2417
|
as `SERVICE_ACCOUNT_JSON`. This can be done from the [Secrets](https://modal.com/secrets) page.
|
|
@@ -2360,11 +2472,15 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2360
2472
|
add_python: str | None = None,
|
|
2361
2473
|
**kwargs,
|
|
2362
2474
|
) -> "_Image":
|
|
2363
|
-
"""
|
|
2475
|
+
"""mdmd:hidden
|
|
2476
|
+
Build a Modal image from a private image in AWS Elastic Container Registry (ECR).
|
|
2364
2477
|
|
|
2365
2478
|
You will need to pass a `modal.Secret` containing either IAM user credentials or OIDC
|
|
2366
2479
|
configuration to access the target ECR registry.
|
|
2367
2480
|
|
|
2481
|
+
**Note:** This constructor is superseded by `Image.from_registry()`, which can also authenticate
|
|
2482
|
+
to private repositories when provided with the appropriate keys in a `modal.Secret`.
|
|
2483
|
+
|
|
2368
2484
|
For IAM user authentication, set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION`.
|
|
2369
2485
|
|
|
2370
2486
|
For OIDC authentication, set `AWS_ROLE_ARN` and `AWS_REGION`.
|
|
@@ -2417,28 +2533,33 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2417
2533
|
*,
|
|
2418
2534
|
force_build: bool = False,
|
|
2419
2535
|
context_dir: Path | str | None = None,
|
|
2420
|
-
|
|
2536
|
+
ignore: Sequence[str] | Callable[[Path], bool] = AUTO_DOCKERIGNORE,
|
|
2537
|
+
build_args: dict[str, str] = {},
|
|
2538
|
+
registry_secret: _Secret | None = None,
|
|
2421
2539
|
secrets: Collection[_Secret] | None = None,
|
|
2540
|
+
env: dict[str, str | None] | None = None,
|
|
2422
2541
|
gpu: str | None = None,
|
|
2423
2542
|
add_python: str | None = None,
|
|
2424
|
-
build_args: dict[str, str] = {},
|
|
2425
|
-
ignore: Sequence[str] | Callable[[Path], bool] = AUTO_DOCKERIGNORE,
|
|
2426
2543
|
) -> "_Image":
|
|
2427
2544
|
"""Build a Modal image from a local Dockerfile.
|
|
2428
2545
|
|
|
2429
2546
|
If your Dockerfile does not have Python installed, you can use the `add_python` parameter
|
|
2430
2547
|
to specify a version of Python to add to the image.
|
|
2431
2548
|
|
|
2549
|
+
A `registry_secret` can be provided to pull the base image from a private registry.
|
|
2550
|
+
Currently this is supported only for single-stage Dockerfiles.
|
|
2551
|
+
|
|
2432
2552
|
Args:
|
|
2433
2553
|
path: Path to the Dockerfile on the local machine.
|
|
2434
2554
|
force_build: If True, skip cached image builds.
|
|
2435
2555
|
context_dir: Build context directory for resolving relative COPY paths.
|
|
2556
|
+
ignore: Ignore rules for the implicit context mount (defaults to auto ``.dockerignore`` behavior).
|
|
2557
|
+
build_args: Dockerfile ``ARG`` values forwarded to the build.
|
|
2558
|
+
registry_secret: Optional Secret with credentials for pulling the base image from a private registry.
|
|
2559
|
+
secrets: Secrets injected as environment variables in the build container.
|
|
2436
2560
|
env: Environment variables set in the build container.
|
|
2437
|
-
|
|
2438
|
-
gpu: GPU type to attach to the builder container.
|
|
2561
|
+
gpu: GPU type to attach to the build container.
|
|
2439
2562
|
add_python: Standalone Python version to add when the Dockerfile does not install Python.
|
|
2440
|
-
build_args: Dockerfile ``ARG`` values forwarded to the build.
|
|
2441
|
-
ignore: Ignore rules for the implicit context mount (defaults to auto ``.dockerignore`` behavior).
|
|
2442
2563
|
|
|
2443
2564
|
Returns:
|
|
2444
2565
|
An `Image` built from the Dockerfile plus Modal runtime dependencies.
|
|
@@ -2484,6 +2605,12 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2484
2605
|
add_python="3.12",
|
|
2485
2606
|
ignore=FilePatternMatcher.from_file("/path/to/dockerignore"),
|
|
2486
2607
|
)
|
|
2608
|
+
|
|
2609
|
+
# Pull the Dockerfile's base image from a private registry.
|
|
2610
|
+
image = modal.Image.from_dockerfile(
|
|
2611
|
+
"./Dockerfile",
|
|
2612
|
+
registry_secret=modal.Secret.from_name("my-registry-secret"),
|
|
2613
|
+
)
|
|
2487
2614
|
```
|
|
2488
2615
|
"""
|
|
2489
2616
|
|
|
@@ -2491,32 +2618,35 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2491
2618
|
if env:
|
|
2492
2619
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
2493
2620
|
|
|
2494
|
-
# --- Build the base dockerfile
|
|
2495
|
-
|
|
2496
|
-
def build_dockerfile_base(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
2497
|
-
with open(os.path.expanduser(path)) as f:
|
|
2498
|
-
commands = f.read().split("\n")
|
|
2499
|
-
return DockerfileSpec(commands=commands, context_files={})
|
|
2500
|
-
|
|
2501
2621
|
gpu_config = parse_gpu_config(gpu)
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
context_mount_function=_create_context_mount_function(
|
|
2505
|
-
ignore=ignore, dockerfile_path=Path(path), context_dir=context_dir
|
|
2506
|
-
),
|
|
2507
|
-
gpu_config=gpu_config,
|
|
2508
|
-
secrets=secrets,
|
|
2509
|
-
build_args=build_args,
|
|
2622
|
+
context_mount_function = _create_context_mount_function(
|
|
2623
|
+
ignore=ignore, dockerfile_path=Path(path), context_dir=context_dir
|
|
2510
2624
|
)
|
|
2511
2625
|
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2626
|
+
registry_config = (
|
|
2627
|
+
_ImageRegistryConfig(secret=registry_secret, infer_auth_type=True) if registry_secret is not None else None
|
|
2628
|
+
)
|
|
2515
2629
|
|
|
2516
|
-
def
|
|
2630
|
+
def dockerfile_image(
|
|
2631
|
+
commands: list[str],
|
|
2632
|
+
base_images: dict[str, _Image] | None = None,
|
|
2633
|
+
image_registry_config: _ImageRegistryConfig | None = None,
|
|
2634
|
+
) -> _Image:
|
|
2635
|
+
return _Image._from_args(
|
|
2636
|
+
base_images=base_images,
|
|
2637
|
+
dockerfile_function=lambda version: DockerfileSpec(commands=commands, context_files={}),
|
|
2638
|
+
context_mount_function=context_mount_function,
|
|
2639
|
+
gpu_config=gpu_config,
|
|
2640
|
+
secrets=secrets,
|
|
2641
|
+
build_args=build_args,
|
|
2642
|
+
image_registry_config=image_registry_config,
|
|
2643
|
+
force_build=force_build,
|
|
2644
|
+
)
|
|
2645
|
+
|
|
2646
|
+
def add_python_mount(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
2517
2647
|
return (
|
|
2518
2648
|
_Mount.from_name(
|
|
2519
|
-
python_standalone_mount_name(add_python),
|
|
2649
|
+
python_standalone_mount_name(add_python, builder_version),
|
|
2520
2650
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
2521
2651
|
)
|
|
2522
2652
|
if add_python
|
|
@@ -2531,12 +2661,61 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2531
2661
|
context_files = {CONTAINER_REQUIREMENTS_PATH: requirements_path}
|
|
2532
2662
|
return DockerfileSpec(commands=commands, context_files=context_files)
|
|
2533
2663
|
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2664
|
+
def python_image(base_image: _Image) -> _Image:
|
|
2665
|
+
return _Image._from_args(
|
|
2666
|
+
base_images={"base": base_image},
|
|
2667
|
+
dockerfile_function=build_dockerfile_python,
|
|
2668
|
+
context_mount_function=add_python_mount,
|
|
2669
|
+
force_build=force_build,
|
|
2670
|
+
)
|
|
2671
|
+
|
|
2672
|
+
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
2673
|
+
builder_version = await _resolve_image_builder_version(load_context)
|
|
2674
|
+
if add_python:
|
|
2675
|
+
# Validate before any Dockerfile layer is built.
|
|
2676
|
+
_validate_python_version(
|
|
2677
|
+
add_python, builder_version, allow_micro_granularity=False, allow_free_threading=True
|
|
2678
|
+
)
|
|
2679
|
+
with open(os.path.expanduser(path)) as f:
|
|
2680
|
+
dockerfile_lines = f.read().split("\n")
|
|
2681
|
+
if registry_secret is not None and is_multi_stage_dockerfile(dockerfile_lines):
|
|
2682
|
+
raise InvalidError("`registry_secret` is not supported with multi-stage Dockerfiles.")
|
|
2683
|
+
|
|
2684
|
+
# Builder versions after 2025.06 put the FROM line of a single-stage Dockerfile in its own image.
|
|
2685
|
+
# Dockerfiles the split could change (see splittable_from_line_index) stay in one layer.
|
|
2686
|
+
from_index = splittable_from_line_index(dockerfile_lines) if builder_version > "2025.06" else None
|
|
2687
|
+
if from_index is None:
|
|
2688
|
+
image = dockerfile_image(dockerfile_lines, image_registry_config=registry_config)
|
|
2689
|
+
else:
|
|
2690
|
+
# Drop the lines above FROM so Dockerfiles that differ only in header comments or blank lines
|
|
2691
|
+
# share the cached image. Those lines can only be comments or blank lines, since
|
|
2692
|
+
# splittable_from_line_index rejects parser directives.
|
|
2693
|
+
from_lines = [dockerfile_lines[from_index]]
|
|
2694
|
+
# Only this layer pulls from the registry; the layers above it build on top of it.
|
|
2695
|
+
from_image = _Image._from_args(
|
|
2696
|
+
dockerfile_function=lambda version: DockerfileSpec(commands=from_lines, context_files={}),
|
|
2697
|
+
image_registry_config=registry_config,
|
|
2698
|
+
force_build=force_build,
|
|
2699
|
+
)
|
|
2700
|
+
rest = dockerfile_lines[from_index + 1 :]
|
|
2701
|
+
rest_is_empty = all(not line.strip() or line.strip().startswith("#") for line in rest)
|
|
2702
|
+
if rest_is_empty:
|
|
2703
|
+
image = from_image
|
|
2704
|
+
else:
|
|
2705
|
+
image = dockerfile_image(["FROM base", *rest], base_images={"base": from_image})
|
|
2706
|
+
if add_python or builder_version <= "2025.06":
|
|
2707
|
+
# Before 2025.06 we always created a python_image() layer because it's where we installed the
|
|
2708
|
+
# client dependencies. From 2025.06, we no longer install client dependencies in base layers,
|
|
2709
|
+
# so this was a no-op layer when add_python= is not used. Skip it on later image builders.
|
|
2710
|
+
image = python_image(image)
|
|
2711
|
+
await resolver.load(image, load_context)
|
|
2712
|
+
self._hydrate_from_other(image)
|
|
2713
|
+
self._serve_mounts = image._serve_mounts
|
|
2714
|
+
|
|
2715
|
+
rep = f"Image.from_dockerfile({str(path)!r})"
|
|
2716
|
+
obj = _Image._from_loader(_load, rep, load_context_overrides=LoadContext.empty())
|
|
2717
|
+
obj.force_build = force_build
|
|
2718
|
+
return obj
|
|
2540
2719
|
|
|
2541
2720
|
@staticmethod
|
|
2542
2721
|
def from_scratch(force_build: bool = False) -> "_Image":
|
|
@@ -98,8 +98,9 @@ class _AuthTokenManager:
|
|
|
98
98
|
caller goes away.
|
|
99
99
|
"""
|
|
100
100
|
if self._refresh_task is None:
|
|
101
|
-
# Maybe another coroutine refreshed already, or we're cooling down after a failure.
|
|
102
|
-
|
|
101
|
+
# Maybe another coroutine refreshed already, or we're cooling down after a failure. The cooldown
|
|
102
|
+
# only applies while the cached token is still usable.
|
|
103
|
+
if self._has_valid_token() and not self._should_refresh():
|
|
103
104
|
return
|
|
104
105
|
# Note: the task is created in async code so it binds to the synchronicity event loop.
|
|
105
106
|
self._refresh_task = asyncio.create_task(self._fetch_token())
|
|
@@ -121,12 +122,12 @@ class _AuthTokenManager:
|
|
|
121
122
|
async def _fetch_token(self):
|
|
122
123
|
"""Fetch a new token and cache it, or arm the failure backoff."""
|
|
123
124
|
try:
|
|
124
|
-
#
|
|
125
|
+
# Retry transient errors if the cached token is missing or expired.
|
|
125
126
|
# Otherwise one attempt, and _retry_after handles the cooldown.
|
|
126
127
|
retry = Retry(
|
|
127
128
|
attempt_timeout=self.AUTH_TOKEN_TIMEOUT,
|
|
128
129
|
total_timeout=self.AUTH_TOKEN_RETRY_TOTAL_TIMEOUT,
|
|
129
|
-
max_retries=0 if self.
|
|
130
|
+
max_retries=0 if self._has_valid_token() else DEFAULT_MAX_RETRIES,
|
|
130
131
|
)
|
|
131
132
|
token = await self._fetch(retry)
|
|
132
133
|
if not token:
|
|
@@ -166,6 +167,9 @@ class _AuthTokenManager:
|
|
|
166
167
|
except Exception as e:
|
|
167
168
|
raise ValueError("Internal error: Cannot parse auth token. Please contact Modal support.") from e
|
|
168
169
|
|
|
170
|
+
def _has_valid_token(self):
|
|
171
|
+
return bool(self._token) and not self._is_expired()
|
|
172
|
+
|
|
169
173
|
def _in_backoff(self):
|
|
170
174
|
return time.time() < self._retry_after
|
|
171
175
|
|