modal 1.6.3.dev11__tar.gz → 1.6.3.dev12__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.dev12}/PKG-INFO +4 -2
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_image.py +74 -19
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/grpc_utils.py +23 -4
- modal-1.6.3.dev12/modal/_utils/oauth.py +66 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/config.py +2 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/profile.py +1 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/token.py +1 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/client.py +39 -9
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/client.pyi +42 -8
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/config.py +3 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/image.pyi +15 -6
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/network_file_system.py +4 -8
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/network_file_system.pyi +10 -16
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/token_flow.py +1 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/PKG-INFO +4 -2
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/SOURCES.txt +1 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/requires.txt +4 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_grpc.py +32 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_pb2.py +1520 -1411
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_pb2.pyi +234 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_pb2_grpc.py +66 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_pb2_grpc.pyi +20 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/modal_api_grpc.py +2 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_version/__init__.py +1 -1
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/pyproject.toml +5 -3
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/LICENSE +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/README.md +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/__main__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_billing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_cluster.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_cluster.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_environments.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_function_variants.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_functions.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_grpc_client.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_ipython.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_load_context.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_location.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_logs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_logs_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_metrics.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_metrics.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_object.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_outbound_policy.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_outbound_policy.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/pty.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/rich.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/status.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_partial_function.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_resolver.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_resources.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_serialization.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_server.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_server.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_supports_logs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_traceback.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_tunnel.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_tunnel.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_type_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/async_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/blob_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/docker_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/logger.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/name_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/time_utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/version.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_watcher.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_workspace.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/app.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/app.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/billing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/billing.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2025.06.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/README.md +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/base-images.json +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/call_graph.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_download.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_help.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_logs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_stats.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_traceback.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/app.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/billing.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/changelog.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/container.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/curl.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/dashboard.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/dict.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/endpoint.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/entry_point.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/environment.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/function.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/image.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/import_refs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/logo.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/network_file_system.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/queues.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/run.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/secret.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/selector.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/server.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/shell.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/skills.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/utils.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/volume.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/workspace.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cls.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cls.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cluster.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cluster.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/container_process.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/container_process.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/dict.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/dict.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/environments.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/environments.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/exception.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/flash.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/ipython.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/functions.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/functions.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/image.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/io_streams.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/io_streams.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/mount.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/mount.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/object.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/object.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/output.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/parallel_map.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/parallel_map.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/partial_function.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/partial_function.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/proxy.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/proxy.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/py.typed +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/queue.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/queue.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/retries.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/runner.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/runner.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/running_app.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_common.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_filesystem.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_impl_persistent.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_impl_v1.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_impl_v2.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_sandbox.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_sidecar.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_snapshot.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_task_command_router_client.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/sandbox.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/sandbox.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/schedule.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/scheduler_placement.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/secret.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/secret.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/server.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/server.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/serving.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/serving.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/stream_type.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/token_flow.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/types.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/volume.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/volume.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/workspace.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/workspace.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/__init__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/py.typed +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_version/__main__.py +0 -0
- {modal-1.6.3.dev11 → modal-1.6.3.dev12}/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.dev12
|
|
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
|
|
@@ -445,6 +445,40 @@ def _create_context_mount_function(
|
|
|
445
445
|
return auto_created_context_mount_fn
|
|
446
446
|
|
|
447
447
|
|
|
448
|
+
_REGISTRY_AUTH_KEY_SETS: dict["api_pb2.RegistryAuthType.ValueType", list[set[str]]] = {
|
|
449
|
+
api_pb2.REGISTRY_AUTH_TYPE_AWS: [
|
|
450
|
+
{"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"},
|
|
451
|
+
{"AWS_ROLE_ARN", "AWS_REGION"},
|
|
452
|
+
],
|
|
453
|
+
api_pb2.REGISTRY_AUTH_TYPE_GCP: [{"SERVICE_ACCOUNT_JSON"}],
|
|
454
|
+
api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS: [{"REGISTRY_USERNAME", "REGISTRY_PASSWORD"}],
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
_REGISTRY_AUTH_NAMES = {
|
|
458
|
+
api_pb2.REGISTRY_AUTH_TYPE_AWS: "AWS",
|
|
459
|
+
api_pb2.REGISTRY_AUTH_TYPE_GCP: "GCP",
|
|
460
|
+
api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS: "username and password",
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
_REGISTRY_SECRET_KEYS_HELP = (
|
|
464
|
+
"Registry Secret must include exactly one of: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION "
|
|
465
|
+
"(or AWS_ROLE_ARN and AWS_REGION) for AWS ECR; SERVICE_ACCOUNT_JSON for GCP Artifact Registry; "
|
|
466
|
+
"or REGISTRY_USERNAME and REGISTRY_PASSWORD for other registries."
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _infer_registry_auth_type(keys: set[str]) -> "api_pb2.RegistryAuthType.ValueType":
|
|
471
|
+
matches = [auth for auth, key_sets in _REGISTRY_AUTH_KEY_SETS.items() if any(ks <= keys for ks in key_sets)]
|
|
472
|
+
if not matches:
|
|
473
|
+
raise InvalidError(f"{_REGISTRY_SECRET_KEYS_HELP} Found keys: {', '.join(sorted(keys)) or 'none'}.")
|
|
474
|
+
if len(matches) > 1:
|
|
475
|
+
names = ", ".join(_REGISTRY_AUTH_NAMES[auth] for auth in matches)
|
|
476
|
+
raise InvalidError(
|
|
477
|
+
f"Registry Secret has keys for more than one kind of authentication ({names}). {_REGISTRY_SECRET_KEYS_HELP}"
|
|
478
|
+
)
|
|
479
|
+
return matches[0]
|
|
480
|
+
|
|
481
|
+
|
|
448
482
|
class _ImageRegistryConfig:
|
|
449
483
|
"""mdmd:hidden"""
|
|
450
484
|
|
|
@@ -453,13 +487,21 @@ class _ImageRegistryConfig:
|
|
|
453
487
|
# TODO: change to _PUBLIC after worker starts handling it.
|
|
454
488
|
registry_auth_type: "api_pb2.RegistryAuthType.ValueType" = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
|
|
455
489
|
secret: _Secret | None = None,
|
|
490
|
+
*,
|
|
491
|
+
infer_auth_type: bool = False,
|
|
456
492
|
):
|
|
457
493
|
self.registry_auth_type = registry_auth_type
|
|
458
494
|
self.secret = secret
|
|
459
|
-
|
|
460
|
-
|
|
495
|
+
self.infer_auth_type = infer_auth_type
|
|
496
|
+
|
|
497
|
+
async def get_proto(self) -> api_pb2.ImageRegistryConfig:
|
|
498
|
+
registry_auth_type = self.registry_auth_type
|
|
499
|
+
if self.infer_auth_type and self.secret:
|
|
500
|
+
# The Secret is an Image dependency, so it's hydrated by now and its keys are cached
|
|
501
|
+
keys = await self.secret._get_keys()
|
|
502
|
+
registry_auth_type = _infer_registry_auth_type(keys)
|
|
461
503
|
return api_pb2.ImageRegistryConfig(
|
|
462
|
-
registry_auth_type=
|
|
504
|
+
registry_auth_type=registry_auth_type,
|
|
463
505
|
secret_id=(self.secret.object_id if self.secret else ""),
|
|
464
506
|
)
|
|
465
507
|
|
|
@@ -762,7 +804,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
762
804
|
secret_ids=[secret.object_id for secret in secrets],
|
|
763
805
|
context_mount_id=(context_mount.object_id if context_mount else ""),
|
|
764
806
|
gpu_config=gpu_config,
|
|
765
|
-
image_registry_config=image_registry_config.get_proto(),
|
|
807
|
+
image_registry_config=await image_registry_config.get_proto(),
|
|
766
808
|
runtime=config.get("function_runtime"),
|
|
767
809
|
runtime_debug=config.get("function_runtime_debug"),
|
|
768
810
|
build_function=_build_function,
|
|
@@ -2255,16 +2297,21 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2255
2297
|
remaining commands run. This might be useful if you want a custom Python installation or to
|
|
2256
2298
|
set a `SHELL`. Prefer `run_commands()` when possible though.
|
|
2257
2299
|
|
|
2258
|
-
To
|
|
2259
|
-
|
|
2260
|
-
|
|
2300
|
+
To pull from a private registry, set the `secret` parameter to a `modal.Secret` holding
|
|
2301
|
+
registry credentials. The kind of authentication follows from the keys in the Secret:
|
|
2302
|
+
|
|
2303
|
+
- AWS ECR: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` (optionally `AWS_SESSION_TOKEN`),
|
|
2304
|
+
or `AWS_ROLE_ARN` and `AWS_REGION` to assume a role with [OIDC](https://modal.com/docs/guide/oidc-integration).
|
|
2305
|
+
See `Image.from_aws_ecr()` for details.
|
|
2306
|
+
- GCP Artifact Registry: `SERVICE_ACCOUNT_JSON`, holding the service account key's JSON.
|
|
2307
|
+
See `Image.from_gcp_artifact_registry()` for details.
|
|
2308
|
+
- Other registries: a username (`REGISTRY_USERNAME`) and an access token or password (`REGISTRY_PASSWORD`).
|
|
2261
2309
|
|
|
2262
|
-
|
|
2263
|
-
use `Image.from_gcp_artifact_registry()` or `Image.from_aws_ecr()`.
|
|
2310
|
+
The Secret must have the keys for exactly one of these; otherwise an error is raised.
|
|
2264
2311
|
|
|
2265
2312
|
Args:
|
|
2266
2313
|
tag: Registry image reference (e.g. ``python:3.11-slim``).
|
|
2267
|
-
secret: Optional
|
|
2314
|
+
secret: Optional Secret with registry credentials; its keys determine the kind of authentication.
|
|
2268
2315
|
setup_dockerfile_commands: Extra Dockerfile lines run after ``FROM`` during base setup.
|
|
2269
2316
|
force_build: If True, skip cached image builds.
|
|
2270
2317
|
add_python: Optional standalone Python series to inject when the base image lacks Python.
|
|
@@ -2278,6 +2325,10 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2278
2325
|
modal.Image.from_registry("python:3.11-slim-bookworm")
|
|
2279
2326
|
modal.Image.from_registry("ubuntu:22.04", add_python="3.11")
|
|
2280
2327
|
modal.Image.from_registry("nvcr.io/nvidia/pytorch:22.12-py3")
|
|
2328
|
+
modal.Image.from_registry(
|
|
2329
|
+
"ghcr.io/my-org/my-image:latest",
|
|
2330
|
+
secret=modal.Secret.from_name("my-registry-secret"),
|
|
2331
|
+
)
|
|
2281
2332
|
```
|
|
2282
2333
|
"""
|
|
2283
2334
|
|
|
@@ -2292,7 +2343,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2292
2343
|
)
|
|
2293
2344
|
|
|
2294
2345
|
if "image_registry_config" not in kwargs and secret is not None:
|
|
2295
|
-
kwargs["image_registry_config"] = _ImageRegistryConfig(
|
|
2346
|
+
kwargs["image_registry_config"] = _ImageRegistryConfig(secret=secret, infer_auth_type=True)
|
|
2296
2347
|
|
|
2297
2348
|
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
2298
2349
|
commands = _Image._registry_setup_commands(tag, version, setup_dockerfile_commands, add_python)
|
|
@@ -2562,10 +2613,6 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2562
2613
|
force_build=force_build,
|
|
2563
2614
|
)
|
|
2564
2615
|
|
|
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
2616
|
def add_python_mount(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
2570
2617
|
return (
|
|
2571
2618
|
_Mount.from_name(
|
|
@@ -2606,9 +2653,12 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2606
2653
|
# Dockerfiles the split could change (see splittable_from_line_index) stay in one layer.
|
|
2607
2654
|
from_index = splittable_from_line_index(dockerfile_lines) if builder_version > "2025.06" else None
|
|
2608
2655
|
if from_index is None:
|
|
2609
|
-
image =
|
|
2656
|
+
image = dockerfile_image(dockerfile_lines)
|
|
2610
2657
|
else:
|
|
2611
|
-
|
|
2658
|
+
# Drop the lines above FROM so Dockerfiles that differ only in header comments or blank lines
|
|
2659
|
+
# share the cached image. Those lines can only be comments or blank lines, since
|
|
2660
|
+
# splittable_from_line_index rejects parser directives.
|
|
2661
|
+
from_lines = [dockerfile_lines[from_index]]
|
|
2612
2662
|
from_image = _Image._from_args(
|
|
2613
2663
|
dockerfile_function=lambda version: DockerfileSpec(commands=from_lines, context_files={}),
|
|
2614
2664
|
force_build=force_build,
|
|
@@ -2616,9 +2666,14 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2616
2666
|
rest = dockerfile_lines[from_index + 1 :]
|
|
2617
2667
|
rest_is_empty = all(not line.strip() or line.strip().startswith("#") for line in rest)
|
|
2618
2668
|
if rest_is_empty:
|
|
2619
|
-
image =
|
|
2669
|
+
image = from_image
|
|
2620
2670
|
else:
|
|
2621
|
-
image =
|
|
2671
|
+
image = dockerfile_image(["FROM base", *rest], base_images={"base": from_image})
|
|
2672
|
+
if add_python or builder_version <= "2025.06":
|
|
2673
|
+
# Before 2025.06 we always created a python_image() layer because it's where we installed the
|
|
2674
|
+
# client dependencies. From 2025.06, we no longer install client dependencies in base layers,
|
|
2675
|
+
# so this was a no-op layer when add_python= is not used. Skip it on later image builders.
|
|
2676
|
+
image = python_image(image)
|
|
2622
2677
|
await resolver.load(image, load_context)
|
|
2623
2678
|
self._hydrate_from_other(image)
|
|
2624
2679
|
self._serve_mounts = image._serve_mounts
|
|
@@ -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}
|
|
@@ -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))
|
|
@@ -35,7 +35,8 @@ HEARTBEAT_TIMEOUT: float = HEARTBEAT_INTERVAL + 0.1
|
|
|
35
35
|
class _OAuthCredentials:
|
|
36
36
|
refresh_token: str = field(repr=False)
|
|
37
37
|
client_id: str
|
|
38
|
-
client_secret: str = field(repr=False)
|
|
38
|
+
client_secret: str | None = field(default=None, repr=False)
|
|
39
|
+
jwt_key: str | None = field(default=None, repr=False)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def _get_metadata(
|
|
@@ -75,9 +76,10 @@ def _get_metadata(
|
|
|
75
76
|
{
|
|
76
77
|
"x-modal-refresh-token": oauth_credentials.refresh_token,
|
|
77
78
|
"x-modal-oauth-client-id": oauth_credentials.client_id,
|
|
78
|
-
"x-modal-oauth-client-secret": oauth_credentials.client_secret,
|
|
79
79
|
}
|
|
80
80
|
)
|
|
81
|
+
if oauth_credentials.client_secret:
|
|
82
|
+
metadata["x-modal-oauth-client-secret"] = oauth_credentials.client_secret
|
|
81
83
|
if agent_env := _agent_environment():
|
|
82
84
|
metadata["x-modal-agent-harness"] = urllib.parse.quote(agent_env)
|
|
83
85
|
|
|
@@ -209,7 +211,19 @@ class _Client:
|
|
|
209
211
|
self._cancellation_context = TaskContext(grace=0.5) # allow running rpcs to finish in 0.5s when closing client
|
|
210
212
|
self._cancellation_context_event_loop = asyncio.get_running_loop()
|
|
211
213
|
await self._cancellation_context.__aenter__()
|
|
212
|
-
|
|
214
|
+
metadata_provider = None
|
|
215
|
+
if self._oauth_credentials and self._oauth_credentials.jwt_key:
|
|
216
|
+
from ._utils.oauth import OAuthAssertionProvider
|
|
217
|
+
|
|
218
|
+
metadata_provider = OAuthAssertionProvider(
|
|
219
|
+
self._oauth_credentials.client_id,
|
|
220
|
+
self._oauth_credentials.jwt_key,
|
|
221
|
+
)
|
|
222
|
+
self._connection_manager = ConnectionManager(
|
|
223
|
+
client=self,
|
|
224
|
+
metadata=metadata,
|
|
225
|
+
metadata_provider=metadata_provider,
|
|
226
|
+
)
|
|
213
227
|
self._control_plane_stub = await self._get_stub(self.server_url)
|
|
214
228
|
self._auth_token_manager = _AuthTokenManager(self._stub)
|
|
215
229
|
self._owner_pid = os.getpid()
|
|
@@ -289,6 +303,7 @@ class _Client:
|
|
|
289
303
|
oauth_refresh_token = c.get("oauth_refresh_token")
|
|
290
304
|
oauth_client_id = c.get("oauth_client_id")
|
|
291
305
|
oauth_client_secret = c.get("oauth_client_secret")
|
|
306
|
+
oauth_jwt_key = c.get("oauth_jwt_key")
|
|
292
307
|
if _is_remote():
|
|
293
308
|
ignored_credential_settings = [
|
|
294
309
|
name
|
|
@@ -298,6 +313,7 @@ class _Client:
|
|
|
298
313
|
("MODAL_OAUTH_REFRESH_TOKEN", oauth_refresh_token),
|
|
299
314
|
("MODAL_OAUTH_CLIENT_ID", oauth_client_id),
|
|
300
315
|
("MODAL_OAUTH_CLIENT_SECRET", oauth_client_secret),
|
|
316
|
+
("MODAL_OAUTH_JWT_KEY", oauth_jwt_key),
|
|
301
317
|
)
|
|
302
318
|
if value
|
|
303
319
|
]
|
|
@@ -311,18 +327,26 @@ class _Client:
|
|
|
311
327
|
oauth_credentials = None
|
|
312
328
|
else:
|
|
313
329
|
has_token_config = bool(token_id or token_secret)
|
|
314
|
-
has_oauth_config = bool(oauth_refresh_token or oauth_client_id or oauth_client_secret)
|
|
330
|
+
has_oauth_config = bool(oauth_refresh_token or oauth_client_id or oauth_client_secret or oauth_jwt_key)
|
|
315
331
|
if has_token_config and has_oauth_config:
|
|
316
332
|
raise InvalidError("Modal token credentials and OAuth credentials cannot both be configured.")
|
|
317
333
|
if has_oauth_config:
|
|
318
|
-
if
|
|
319
|
-
|
|
334
|
+
if (
|
|
335
|
+
not oauth_refresh_token
|
|
336
|
+
or not oauth_client_id
|
|
337
|
+
or bool(oauth_client_secret) == bool(oauth_jwt_key)
|
|
338
|
+
):
|
|
339
|
+
raise InvalidError(
|
|
340
|
+
"OAuth refresh token, client ID, and exactly one of client secret or JWT key "
|
|
341
|
+
"must be configured."
|
|
342
|
+
)
|
|
320
343
|
client_type = api_pb2.CLIENT_TYPE_CLIENT
|
|
321
344
|
credentials = None
|
|
322
345
|
oauth_credentials = _OAuthCredentials(
|
|
323
346
|
refresh_token=oauth_refresh_token,
|
|
324
347
|
client_id=oauth_client_id,
|
|
325
348
|
client_secret=oauth_client_secret,
|
|
349
|
+
jwt_key=oauth_jwt_key,
|
|
326
350
|
)
|
|
327
351
|
elif token_id and token_secret:
|
|
328
352
|
client_type = api_pb2.CLIENT_TYPE_CLIENT
|
|
@@ -392,7 +416,8 @@ class _Client:
|
|
|
392
416
|
refresh_token: str,
|
|
393
417
|
*,
|
|
394
418
|
oauth_client_id: str,
|
|
395
|
-
oauth_client_secret: str,
|
|
419
|
+
oauth_client_secret: str | None = None,
|
|
420
|
+
oauth_jwt_key: str | None = None,
|
|
396
421
|
) -> "_Client":
|
|
397
422
|
"""
|
|
398
423
|
Constructor based on OAuth credentials; useful for managing Modal on behalf of third-party users.
|
|
@@ -401,6 +426,8 @@ class _Client:
|
|
|
401
426
|
refresh_token: OAuth refresh token returned by Modal's token endpoint.
|
|
402
427
|
oauth_client_id: Modal-issued OAuth client ID, with an `oc-` prefix.
|
|
403
428
|
oauth_client_secret: Modal-issued OAuth client secret, with an `ov-` prefix.
|
|
429
|
+
oauth_jwt_key: Unencrypted RSA private key encoded as PEM, with literal or escaped newlines.
|
|
430
|
+
Requires `modal[oauth]`.
|
|
404
431
|
|
|
405
432
|
Returns:
|
|
406
433
|
An authenticated `Client` with its connection opened.
|
|
@@ -417,8 +444,10 @@ class _Client:
|
|
|
417
444
|
```
|
|
418
445
|
"""
|
|
419
446
|
_check_config()
|
|
420
|
-
if not refresh_token or not oauth_client_id or
|
|
421
|
-
raise AuthError(
|
|
447
|
+
if not refresh_token or not oauth_client_id or bool(oauth_client_secret) == bool(oauth_jwt_key):
|
|
448
|
+
raise AuthError(
|
|
449
|
+
"OAuth refresh token, client ID, and exactly one of client secret or JWT key must be provided."
|
|
450
|
+
)
|
|
422
451
|
|
|
423
452
|
server_url = config["server_url"]
|
|
424
453
|
client_type = api_pb2.CLIENT_TYPE_CLIENT
|
|
@@ -426,6 +455,7 @@ class _Client:
|
|
|
426
455
|
refresh_token=refresh_token,
|
|
427
456
|
client_id=oauth_client_id,
|
|
428
457
|
client_secret=oauth_client_secret,
|
|
458
|
+
jwt_key=oauth_jwt_key,
|
|
429
459
|
)
|
|
430
460
|
client = _Client(
|
|
431
461
|
server_url,
|
|
@@ -12,13 +12,20 @@ import typing
|
|
|
12
12
|
import typing_extensions
|
|
13
13
|
|
|
14
14
|
class _OAuthCredentials:
|
|
15
|
-
"""_OAuthCredentials(refresh_token: str, client_id: str, client_secret: str)"""
|
|
15
|
+
"""_OAuthCredentials(refresh_token: str, client_id: str, client_secret: str | None = None, jwt_key: str | None = None)"""
|
|
16
16
|
|
|
17
17
|
refresh_token: str
|
|
18
18
|
client_id: str
|
|
19
|
-
client_secret: str
|
|
19
|
+
client_secret: typing.Optional[str]
|
|
20
|
+
jwt_key: typing.Optional[str]
|
|
20
21
|
|
|
21
|
-
def __init__(
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
refresh_token: str,
|
|
25
|
+
client_id: str,
|
|
26
|
+
client_secret: typing.Optional[str] = None,
|
|
27
|
+
jwt_key: typing.Optional[str] = None,
|
|
28
|
+
) -> None:
|
|
22
29
|
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
23
30
|
...
|
|
24
31
|
|
|
@@ -80,7 +87,7 @@ class _Client:
|
|
|
80
87
|
server_url: str,
|
|
81
88
|
client_type: int,
|
|
82
89
|
credentials: typing.Optional[tuple[str, str]],
|
|
83
|
-
version: str = "1.6.3.
|
|
90
|
+
version: str = "1.6.3.dev12",
|
|
84
91
|
*,
|
|
85
92
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
86
93
|
):
|
|
@@ -190,7 +197,12 @@ class _Client:
|
|
|
190
197
|
|
|
191
198
|
@classmethod
|
|
192
199
|
async def from_oauth_credentials(
|
|
193
|
-
cls,
|
|
200
|
+
cls,
|
|
201
|
+
refresh_token: str,
|
|
202
|
+
*,
|
|
203
|
+
oauth_client_id: str,
|
|
204
|
+
oauth_client_secret: typing.Optional[str] = None,
|
|
205
|
+
oauth_jwt_key: typing.Optional[str] = None,
|
|
194
206
|
) -> _Client:
|
|
195
207
|
"""Constructor based on OAuth credentials; useful for managing Modal on behalf of third-party users.
|
|
196
208
|
|
|
@@ -198,6 +210,8 @@ class _Client:
|
|
|
198
210
|
refresh_token: OAuth refresh token returned by Modal's token endpoint.
|
|
199
211
|
oauth_client_id: Modal-issued OAuth client ID, with an `oc-` prefix.
|
|
200
212
|
oauth_client_secret: Modal-issued OAuth client secret, with an `ov-` prefix.
|
|
213
|
+
oauth_jwt_key: Unencrypted RSA private key encoded as PEM, with literal or escaped newlines.
|
|
214
|
+
Requires `modal[oauth]`.
|
|
201
215
|
|
|
202
216
|
Returns:
|
|
203
217
|
An authenticated `Client` with its connection opened.
|
|
@@ -290,7 +304,7 @@ class Client:
|
|
|
290
304
|
server_url: str,
|
|
291
305
|
client_type: int,
|
|
292
306
|
credentials: typing.Optional[tuple[str, str]],
|
|
293
|
-
version: str = "1.6.3.
|
|
307
|
+
version: str = "1.6.3.dev12",
|
|
294
308
|
*,
|
|
295
309
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
296
310
|
):
|
|
@@ -490,13 +504,23 @@ class Client:
|
|
|
490
504
|
from_credentials: typing.ClassVar[__from_credentials_spec]
|
|
491
505
|
|
|
492
506
|
class __from_oauth_credentials_spec(typing_extensions.Protocol):
|
|
493
|
-
def __call__(
|
|
507
|
+
def __call__(
|
|
508
|
+
self,
|
|
509
|
+
/,
|
|
510
|
+
refresh_token: str,
|
|
511
|
+
*,
|
|
512
|
+
oauth_client_id: str,
|
|
513
|
+
oauth_client_secret: typing.Optional[str] = None,
|
|
514
|
+
oauth_jwt_key: typing.Optional[str] = None,
|
|
515
|
+
) -> Client:
|
|
494
516
|
"""Constructor based on OAuth credentials; useful for managing Modal on behalf of third-party users.
|
|
495
517
|
|
|
496
518
|
Args:
|
|
497
519
|
refresh_token: OAuth refresh token returned by Modal's token endpoint.
|
|
498
520
|
oauth_client_id: Modal-issued OAuth client ID, with an `oc-` prefix.
|
|
499
521
|
oauth_client_secret: Modal-issued OAuth client secret, with an `ov-` prefix.
|
|
522
|
+
oauth_jwt_key: Unencrypted RSA private key encoded as PEM, with literal or escaped newlines.
|
|
523
|
+
Requires `modal[oauth]`.
|
|
500
524
|
|
|
501
525
|
Returns:
|
|
502
526
|
An authenticated `Client` with its connection opened.
|
|
@@ -514,13 +538,23 @@ class Client:
|
|
|
514
538
|
"""
|
|
515
539
|
...
|
|
516
540
|
|
|
517
|
-
async def aio(
|
|
541
|
+
async def aio(
|
|
542
|
+
self,
|
|
543
|
+
/,
|
|
544
|
+
refresh_token: str,
|
|
545
|
+
*,
|
|
546
|
+
oauth_client_id: str,
|
|
547
|
+
oauth_client_secret: typing.Optional[str] = None,
|
|
548
|
+
oauth_jwt_key: typing.Optional[str] = None,
|
|
549
|
+
) -> Client:
|
|
518
550
|
"""Constructor based on OAuth credentials; useful for managing Modal on behalf of third-party users.
|
|
519
551
|
|
|
520
552
|
Args:
|
|
521
553
|
refresh_token: OAuth refresh token returned by Modal's token endpoint.
|
|
522
554
|
oauth_client_id: Modal-issued OAuth client ID, with an `oc-` prefix.
|
|
523
555
|
oauth_client_secret: Modal-issued OAuth client secret, with an `ov-` prefix.
|
|
556
|
+
oauth_jwt_key: Unencrypted RSA private key encoded as PEM, with literal or escaped newlines.
|
|
557
|
+
Requires `modal[oauth]`.
|
|
524
558
|
|
|
525
559
|
Returns:
|
|
526
560
|
An authenticated `Client` with its connection opened.
|
|
@@ -9,7 +9,8 @@ Client processes can authenticate in two ways:
|
|
|
9
9
|
1. Modal API tokens use `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET`. The
|
|
10
10
|
`modal token set` command stores these values in `.modal.toml`.
|
|
11
11
|
2. Third-party OAuth integrations typically inject `MODAL_OAUTH_REFRESH_TOKEN`,
|
|
12
|
-
`MODAL_OAUTH_CLIENT_ID`, and `MODAL_OAUTH_CLIENT_SECRET`
|
|
12
|
+
`MODAL_OAUTH_CLIENT_ID`, and either `MODAL_OAUTH_CLIENT_SECRET` or
|
|
13
|
+
`MODAL_OAUTH_JWT_KEY` into the process.
|
|
13
14
|
|
|
14
15
|
The two authentication methods cannot be combined, and all values for the
|
|
15
16
|
selected method must be provided.
|
|
@@ -370,6 +371,7 @@ _SETTINGS = {
|
|
|
370
371
|
"oauth_refresh_token": _Setting(),
|
|
371
372
|
"oauth_client_id": _Setting(),
|
|
372
373
|
"oauth_client_secret": _Setting(),
|
|
374
|
+
"oauth_jwt_key": _Setting(),
|
|
373
375
|
# --- Logging ----------------------------------------------------------------
|
|
374
376
|
"loglevel": _Setting("WARNING", lambda s: s.upper()),
|
|
375
377
|
"log_format": _Setting("STRING", lambda s: s.upper()),
|