modal 1.5.6.dev31__tar.gz → 1.6.1__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- {modal-1.5.6.dev31 → modal-1.6.1}/PKG-INFO +3 -3
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_environments.py +2 -1
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_functions.py +4 -4
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_object.py +17 -9
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/status.py +1 -3
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_partial_function.py +1 -1
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_server.py +44 -17
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_server.pyi +40 -17
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/grpc_utils.py +11 -12
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/task_command_router_client.py +29 -8
- modal-1.6.1/modal/cli/endpoint.py +779 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/function.py +5 -3
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/server.py +175 -111
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/client.pyi +2 -2
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/config.py +28 -6
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/dict.py +4 -4
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/dict.pyi +9 -9
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/functions.pyi +12 -10
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/network_file_system.py +3 -3
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/network_file_system.pyi +6 -6
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/object.pyi +1 -3
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/parallel_map.py +4 -4
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/parallel_map.pyi +3 -3
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/partial_function.pyi +1 -1
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/sandbox.py +62 -46
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/sandbox.pyi +28 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/server.pyi +1 -1
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/skills/modal/SKILL.md +19 -24
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/token_flow.py +2 -1
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/types.py +8 -1
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/volume.py +6 -2
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/volume.pyi +21 -3
- {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/PKG-INFO +3 -3
- {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/requires.txt +2 -2
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_pb2.py +1383 -1373
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_pb2.pyi +64 -8
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_pb2.py +96 -96
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_pb2.pyi +12 -2
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_version/__init__.py +1 -1
- {modal-1.5.6.dev31 → modal-1.6.1}/pyproject.toml +3 -3
- modal-1.5.6.dev31/modal/cli/endpoint.py +0 -372
- {modal-1.5.6.dev31 → modal-1.6.1}/LICENSE +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/README.md +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/__main__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_billing.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_cluster.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_cluster.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_function_variants.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_image.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_location.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_logs.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_logs_manager.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_outbound_policy.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_outbound_policy.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/manager.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/pty.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/rich.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_resources.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_traceback.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_tunnel.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/_workspace.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/app.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/app.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/billing.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_logs.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_stats.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/app.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/config.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/container.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/curl.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/dashboard.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/entry_point.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/image.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/import_refs.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/profile.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/queues.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/secret.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/shell.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/token.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/utils.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/workspace.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/client.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cls.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cls.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cluster.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/cluster.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/container_process.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/environments.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/environments.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/exception.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/flash.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/flash_server.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/functions.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/image.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/image.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/io_streams.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/io_streams.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/mount.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/object.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/output.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/partial_function.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/proxy.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/py.typed +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/queue.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/retries.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/runner.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/running_app.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/sandbox_fs.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/schedule.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/secret.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/secret.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/server.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/serving.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/snapshot.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/token_flow.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/workspace.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal/workspace.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_grpc.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev31 → modal-1.6.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.1
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,9 +21,9 @@ Requires-Dist: certifi
|
|
|
21
21
|
Requires-Dist: click~=8.1
|
|
22
22
|
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
|
-
Requires-Dist: protobuf!=4.24.0,<
|
|
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.6
|
|
27
27
|
Requires-Dist: toml
|
|
28
28
|
Requires-Dist: types-certifi
|
|
29
29
|
Requires-Dist: types-toml
|
|
@@ -22,7 +22,7 @@ from ._utils.time_utils import is_utc_month_aligned, parse_billing_cycle
|
|
|
22
22
|
from .client import _Client
|
|
23
23
|
from .config import config, logger
|
|
24
24
|
from .exception import InvalidError, WorkspaceManagementError
|
|
25
|
-
from .types import BillingReportItem, EnvironmentBillingSummary
|
|
25
|
+
from .types import AppInfo, BillingReportItem, EnvironmentBillingSummary
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
import modal
|
|
@@ -346,6 +346,7 @@ class _EnvironmentAppsManager:
|
|
|
346
346
|
self._environment.name,
|
|
347
347
|
self._environment.client,
|
|
348
348
|
description=item.description or None,
|
|
349
|
+
info=AppInfo._from_proto(item.metadata, item.app_id),
|
|
349
350
|
)
|
|
350
351
|
for item in response.apps
|
|
351
352
|
if item.state not in stopped_app_states
|
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
import typing
|
|
8
8
|
import warnings
|
|
9
9
|
from collections import OrderedDict
|
|
10
|
-
from collections.abc import AsyncGenerator,
|
|
10
|
+
from collections.abc import AsyncGenerator, Callable, Collection, Sequence
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from datetime import datetime, timedelta, timezone
|
|
13
13
|
from pathlib import PurePosixPath
|
|
@@ -1749,14 +1749,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1749
1749
|
return list(self._experimental_flash_urls) if self._experimental_flash_urls else None
|
|
1750
1750
|
|
|
1751
1751
|
async def _fetch_flash_auth_token(self, retry: Retry) -> str:
|
|
1752
|
-
resp = await self.client.
|
|
1752
|
+
resp = await self.client._stub.FunctionGetFlashAuthToken(
|
|
1753
1753
|
api_pb2.FunctionGetFlashAuthTokenRequest(function_id=self.object_id), retry=retry
|
|
1754
1754
|
)
|
|
1755
1755
|
return resp.token
|
|
1756
1756
|
|
|
1757
1757
|
async def _get_flash_auth_token(self) -> str:
|
|
1758
1758
|
if self._flash_token_manager is None:
|
|
1759
|
-
self._flash_token_manager = _AuthTokenManager(self.client.
|
|
1759
|
+
self._flash_token_manager = _AuthTokenManager(self.client._stub, fetch=self._fetch_flash_auth_token)
|
|
1760
1760
|
return await self._flash_token_manager.get_token()
|
|
1761
1761
|
|
|
1762
1762
|
def _apply_dynamic_config(
|
|
@@ -2405,7 +2405,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2405
2405
|
return await self._invocation().poll_function(timeout=timeout, index=index)
|
|
2406
2406
|
|
|
2407
2407
|
@live_method_gen
|
|
2408
|
-
async def iter(self, *, start: int = 0, end: int | None = None) ->
|
|
2408
|
+
async def iter(self, *, start: int = 0, end: int | None = None) -> AsyncGenerator[ReturnType, None]:
|
|
2409
2409
|
"""Iterate in-order over the results of the function call.
|
|
2410
2410
|
|
|
2411
2411
|
Optionally, specify a range [start, end) to iterate over.
|
|
@@ -4,10 +4,10 @@ import typing
|
|
|
4
4
|
import uuid
|
|
5
5
|
from collections.abc import Awaitable, Callable, Hashable, Sequence
|
|
6
6
|
from functools import wraps
|
|
7
|
-
from typing import ClassVar
|
|
7
|
+
from typing import Any, AsyncContextManager, AsyncGenerator, AsyncIterator, ClassVar, ParamSpec, TypeVar
|
|
8
8
|
|
|
9
9
|
from google.protobuf.message import Message
|
|
10
|
-
from typing_extensions import Self
|
|
10
|
+
from typing_extensions import Concatenate, Self
|
|
11
11
|
|
|
12
12
|
from ._load_context import LoadContext
|
|
13
13
|
from ._resolver import Resolver
|
|
@@ -38,18 +38,24 @@ def _get_environment_name(
|
|
|
38
38
|
return config.get("environment") or ""
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
P = ParamSpec("P")
|
|
42
|
+
T = TypeVar("T")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def live_method(method: Callable[Concatenate[Any, P], Awaitable[T]]) -> Callable[Concatenate[Any, P], Awaitable[T]]:
|
|
42
46
|
@wraps(method)
|
|
43
|
-
async def wrapped(self, *args, **kwargs):
|
|
47
|
+
async def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
|
44
48
|
await self.hydrate()
|
|
45
49
|
return await method(self, *args, **kwargs)
|
|
46
50
|
|
|
47
51
|
return wrapped
|
|
48
52
|
|
|
49
53
|
|
|
50
|
-
def live_method_gen(
|
|
54
|
+
def live_method_gen(
|
|
55
|
+
method: Callable[Concatenate[Any, P], AsyncGenerator[T, None]],
|
|
56
|
+
) -> Callable[Concatenate[Any, P], AsyncIterator[T]]:
|
|
51
57
|
@wraps(method)
|
|
52
|
-
async def wrapped(self, *args, **kwargs):
|
|
58
|
+
async def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> AsyncIterator[T]:
|
|
53
59
|
await self.hydrate()
|
|
54
60
|
async with aclosing(method(self, *args, **kwargs)) as stream:
|
|
55
61
|
async for item in stream:
|
|
@@ -58,14 +64,16 @@ def live_method_gen(method):
|
|
|
58
64
|
return wrapped
|
|
59
65
|
|
|
60
66
|
|
|
61
|
-
def live_method_contextmanager(
|
|
67
|
+
def live_method_contextmanager(
|
|
68
|
+
method: Callable[Concatenate[Any, P], AsyncContextManager[T]],
|
|
69
|
+
) -> Callable[Concatenate[Any, P], AsyncContextManager[T]]:
|
|
62
70
|
# make sure a wrapped function returning an async context manager
|
|
63
71
|
# will not require both an `await func.aio()` and `async with`
|
|
64
72
|
# which would have been the case if it was wrapped in live_method
|
|
65
73
|
|
|
66
74
|
@wraps(method)
|
|
67
75
|
@contextlib.asynccontextmanager
|
|
68
|
-
async def wrapped(self, *args, **kwargs):
|
|
76
|
+
async def wrapped(self, *args: P.args, **kwargs: P.kwargs):
|
|
69
77
|
await self.hydrate()
|
|
70
78
|
async with method(self, *args, **kwargs) as ctx:
|
|
71
79
|
yield ctx
|
|
@@ -75,7 +83,7 @@ def live_method_contextmanager(method):
|
|
|
75
83
|
|
|
76
84
|
class _Object:
|
|
77
85
|
_type_prefix: ClassVar[str | None] = None
|
|
78
|
-
_prefix_to_type: ClassVar[dict[str, type]] = {}
|
|
86
|
+
_prefix_to_type: ClassVar[dict[str, type["_Object"]]] = {}
|
|
79
87
|
|
|
80
88
|
# For constructors
|
|
81
89
|
_load: Callable[[Self, Resolver, LoadContext, str | None], Awaitable[None]] | None = None
|
|
@@ -13,9 +13,7 @@ from .manager import OutputManager
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _get_annotation_for_web_url(url_info: api_pb2.WebUrlInfo) -> str:
|
|
16
|
-
if url_info.
|
|
17
|
-
suffix = " [grey70](label truncated)[/grey70]"
|
|
18
|
-
elif url_info.label_stolen:
|
|
16
|
+
if url_info.label_stolen:
|
|
19
17
|
suffix = " [grey70](label stolen)[/grey70]"
|
|
20
18
|
else:
|
|
21
19
|
suffix = ""
|
|
@@ -732,7 +732,7 @@ def _sessioned() -> Callable[
|
|
|
732
732
|
[Callable[P, ReturnType] | _PartialFunction[P, ReturnType, ReturnType]],
|
|
733
733
|
_PartialFunction[P, ReturnType, ReturnType],
|
|
734
734
|
]:
|
|
735
|
-
"""Decorator that enables sessions on a Server.
|
|
735
|
+
"""Decorator that enables sticky sessions on a Server.
|
|
736
736
|
|
|
737
737
|
Every request must carry a session token obtained from a session start request; requests with the same token are
|
|
738
738
|
routed to the same container until the session is idle for `idle_timeout` seconds or explicitly terminated. A
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
|
+
import asyncio
|
|
2
3
|
import inspect
|
|
3
4
|
import json
|
|
5
|
+
import time
|
|
4
6
|
import typing
|
|
7
|
+
import uuid
|
|
5
8
|
from datetime import datetime, timedelta, timezone
|
|
6
9
|
|
|
7
10
|
from typing_extensions import Self
|
|
@@ -113,7 +116,7 @@ class _Server:
|
|
|
113
116
|
|
|
114
117
|
@property
|
|
115
118
|
def sessions(self) -> "_ServerSessionsManager":
|
|
116
|
-
"""Start and terminate sessions on a Server decorated with `@modal.sessioned()`."""
|
|
119
|
+
"""Start and terminate sticky sessions on a Server decorated with `@modal.sessioned()`."""
|
|
117
120
|
return _ServerSessionsManager(self)
|
|
118
121
|
|
|
119
122
|
async def info(self, *, refresh: bool = False) -> ServerInfo:
|
|
@@ -160,7 +163,7 @@ class _Server:
|
|
|
160
163
|
This interface is experimental and may change or be removed without warning.
|
|
161
164
|
"""
|
|
162
165
|
service_function = self._get_service_function()
|
|
163
|
-
response = await service_function.client.
|
|
166
|
+
response = await service_function.client._stub.FlashContainerList(
|
|
164
167
|
api_pb2.FlashContainerListRequest(function_id=service_function.object_id)
|
|
165
168
|
)
|
|
166
169
|
return [
|
|
@@ -473,13 +476,12 @@ class _Server:
|
|
|
473
476
|
request.until.FromDatetime(until)
|
|
474
477
|
if container:
|
|
475
478
|
request.container_id = container
|
|
476
|
-
stats = await self._get_service_function().client.
|
|
479
|
+
stats = await self._get_service_function().client._stub.ServerGetTimeRangeStats(request)
|
|
477
480
|
return ServerStats._from_proto(stats)
|
|
478
481
|
|
|
479
482
|
|
|
480
|
-
@retry(n_attempts=5, base_delay=0.5, attempt_timeout=65, total_timeout=200)
|
|
481
483
|
async def _post_session_control(url: str, headers: dict[str, str]) -> tuple[int, str, str]:
|
|
482
|
-
"""POST to a session control endpoint,
|
|
484
|
+
"""POST to a sticky session control endpoint, raising on 5xx."""
|
|
483
485
|
async with ClientSessionRegistry.get_session().post(url, headers=headers) as resp:
|
|
484
486
|
body = await resp.text()
|
|
485
487
|
if resp.status >= 400:
|
|
@@ -489,6 +491,28 @@ async def _post_session_control(url: str, headers: dict[str, str]) -> tuple[int,
|
|
|
489
491
|
return resp.status, resp.reason or "", body
|
|
490
492
|
|
|
491
493
|
|
|
494
|
+
async def _post_session_start(base_url: str, fn: _Function, idle_timeout: int) -> tuple[int, str, str]:
|
|
495
|
+
idempotency_key = str(uuid.uuid4())
|
|
496
|
+
start_timestamp = str(time.time())
|
|
497
|
+
|
|
498
|
+
@retry(n_attempts=5, base_delay=0.5, attempt_timeout=None, total_timeout=1515)
|
|
499
|
+
async def attempt() -> tuple[int, str, str]:
|
|
500
|
+
headers = {
|
|
501
|
+
"Modal-Authorization": f"Bearer {await fn._get_flash_auth_token()}",
|
|
502
|
+
"x-idempotency-key": idempotency_key,
|
|
503
|
+
"x-modal-timestamp": start_timestamp,
|
|
504
|
+
"x-modal-server-session-idle-timeout": str(idle_timeout),
|
|
505
|
+
}
|
|
506
|
+
return await _post_session_control(f"{base_url}/_modal/sessions/start", headers)
|
|
507
|
+
|
|
508
|
+
return await attempt()
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@retry(n_attempts=3, base_delay=0.5, attempt_timeout=None, total_timeout=60)
|
|
512
|
+
async def _post_session_terminate(base_url: str, headers: dict[str, str]) -> tuple[int, str, str]:
|
|
513
|
+
return await _post_session_control(f"{base_url}/_modal/sessions/terminate", headers)
|
|
514
|
+
|
|
515
|
+
|
|
492
516
|
class _ServerSessionsManager:
|
|
493
517
|
"""mdmd:namespace"""
|
|
494
518
|
|
|
@@ -501,11 +525,15 @@ class _ServerSessionsManager:
|
|
|
501
525
|
raise InvalidError("`sessions` requires `@modal.sessioned()` on the Server.")
|
|
502
526
|
|
|
503
527
|
async def start(self, idle_timeout: int = 600) -> ServerSessionCredentials:
|
|
504
|
-
"""Start a session and return its ID and token.
|
|
528
|
+
"""Start a sticky session and return its ID and token.
|
|
529
|
+
|
|
530
|
+
Requests to the server URL that carry the returned token are routed to the same container until the
|
|
531
|
+
session has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
|
|
532
|
+
for as long as it holds a live session.
|
|
505
533
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
534
|
+
If no container has room for the session, the call waits for additional capacity. It will block for
|
|
535
|
+
up to 25 minutes before giving up. To control the wait per call, use the HTTP API and apply your own retry
|
|
536
|
+
policy.
|
|
509
537
|
|
|
510
538
|
Args:
|
|
511
539
|
idle_timeout: Seconds without an in-flight request before the session ends.
|
|
@@ -531,13 +559,10 @@ class _ServerSessionsManager:
|
|
|
531
559
|
self._validate()
|
|
532
560
|
assert url is not None, "Server has no URL."
|
|
533
561
|
|
|
534
|
-
headers = {
|
|
535
|
-
"Modal-Authorization": f"Bearer {await fn._get_flash_auth_token()}",
|
|
536
|
-
"x-modal-server-session-idle-timeout": str(idle_timeout),
|
|
537
|
-
}
|
|
538
|
-
|
|
539
562
|
try:
|
|
540
|
-
status, reason, body = await
|
|
563
|
+
status, reason, body = await _post_session_start(url, fn, idle_timeout)
|
|
564
|
+
except asyncio.TimeoutError:
|
|
565
|
+
raise asyncio.TimeoutError("Failed to start session: timed out") from None
|
|
541
566
|
except ServiceError as exc:
|
|
542
567
|
raise ExecutionError(f"Failed to start session: {exc}") from None
|
|
543
568
|
if status >= 400:
|
|
@@ -550,7 +575,9 @@ class _ServerSessionsManager:
|
|
|
550
575
|
raise ExecutionError("Failed to start session: unexpected response from server") from None
|
|
551
576
|
|
|
552
577
|
async def terminate(self, token: str) -> None:
|
|
553
|
-
"""Terminate a session.
|
|
578
|
+
"""Terminate a sticky session.
|
|
579
|
+
|
|
580
|
+
New requests to it will be rejected. The container continues serving other sessions.
|
|
554
581
|
|
|
555
582
|
Args:
|
|
556
583
|
token: The `token` of the `ServerSessionCredentials` to terminate.
|
|
@@ -574,7 +601,7 @@ class _ServerSessionsManager:
|
|
|
574
601
|
"x-modal-server-session-token": token,
|
|
575
602
|
}
|
|
576
603
|
try:
|
|
577
|
-
status, reason, _body = await
|
|
604
|
+
status, reason, _body = await _post_session_terminate(url, headers)
|
|
578
605
|
except ServiceError as exc:
|
|
579
606
|
raise ExecutionError(f"Failed to terminate session: {exc}") from None
|
|
580
607
|
if status == 404:
|
|
@@ -67,7 +67,7 @@ class _Server:
|
|
|
67
67
|
|
|
68
68
|
@property
|
|
69
69
|
def sessions(self) -> _ServerSessionsManager:
|
|
70
|
-
"""Start and terminate sessions on a Server decorated with `@modal.sessioned()`."""
|
|
70
|
+
"""Start and terminate sticky sessions on a Server decorated with `@modal.sessioned()`."""
|
|
71
71
|
...
|
|
72
72
|
|
|
73
73
|
async def info(self, *, refresh: bool = False) -> modal.types.ServerInfo:
|
|
@@ -272,9 +272,14 @@ class _Server:
|
|
|
272
272
|
...
|
|
273
273
|
|
|
274
274
|
async def _post_session_control(url: str, headers: dict[str, str]) -> tuple[int, str, str]:
|
|
275
|
-
"""POST to a session control endpoint,
|
|
275
|
+
"""POST to a sticky session control endpoint, raising on 5xx."""
|
|
276
276
|
...
|
|
277
277
|
|
|
278
|
+
async def _post_session_start(
|
|
279
|
+
base_url: str, fn: modal._functions._Function, idle_timeout: int
|
|
280
|
+
) -> tuple[int, str, str]: ...
|
|
281
|
+
async def _post_session_terminate(base_url: str, headers: dict[str, str]) -> tuple[int, str, str]: ...
|
|
282
|
+
|
|
278
283
|
class _ServerSessionsManager:
|
|
279
284
|
"""mdmd:namespace"""
|
|
280
285
|
def __init__(self, server: _Server):
|
|
@@ -283,11 +288,15 @@ class _ServerSessionsManager:
|
|
|
283
288
|
|
|
284
289
|
def _validate(self) -> None: ...
|
|
285
290
|
async def start(self, idle_timeout: int = 600) -> modal.types.ServerSessionCredentials:
|
|
286
|
-
"""Start a session and return its ID and token.
|
|
291
|
+
"""Start a sticky session and return its ID and token.
|
|
292
|
+
|
|
293
|
+
Requests to the server URL that carry the returned token are routed to the same container until the
|
|
294
|
+
session has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
|
|
295
|
+
for as long as it holds a live session.
|
|
287
296
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
297
|
+
If no container has room for the session, the call waits for additional capacity. It will block for
|
|
298
|
+
up to 25 minutes before giving up. To control the wait per call, use the HTTP API and apply your own retry
|
|
299
|
+
policy.
|
|
291
300
|
|
|
292
301
|
Args:
|
|
293
302
|
idle_timeout: Seconds without an in-flight request before the session ends.
|
|
@@ -308,7 +317,9 @@ class _ServerSessionsManager:
|
|
|
308
317
|
...
|
|
309
318
|
|
|
310
319
|
async def terminate(self, token: str) -> None:
|
|
311
|
-
"""Terminate a session.
|
|
320
|
+
"""Terminate a sticky session.
|
|
321
|
+
|
|
322
|
+
New requests to it will be rejected. The container continues serving other sessions.
|
|
312
323
|
|
|
313
324
|
Args:
|
|
314
325
|
token: The `token` of the `ServerSessionCredentials` to terminate.
|
|
@@ -334,11 +345,15 @@ class ServerSessionsManager:
|
|
|
334
345
|
|
|
335
346
|
class __start_spec(typing_extensions.Protocol):
|
|
336
347
|
def __call__(self, /, idle_timeout: int = 600) -> modal.types.ServerSessionCredentials:
|
|
337
|
-
"""Start a session and return its ID and token.
|
|
348
|
+
"""Start a sticky session and return its ID and token.
|
|
349
|
+
|
|
350
|
+
Requests to the server URL that carry the returned token are routed to the same container until the
|
|
351
|
+
session has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
|
|
352
|
+
for as long as it holds a live session.
|
|
338
353
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
354
|
+
If no container has room for the session, the call waits for additional capacity. It will block for
|
|
355
|
+
up to 25 minutes before giving up. To control the wait per call, use the HTTP API and apply your own retry
|
|
356
|
+
policy.
|
|
342
357
|
|
|
343
358
|
Args:
|
|
344
359
|
idle_timeout: Seconds without an in-flight request before the session ends.
|
|
@@ -359,11 +374,15 @@ class ServerSessionsManager:
|
|
|
359
374
|
...
|
|
360
375
|
|
|
361
376
|
async def aio(self, /, idle_timeout: int = 600) -> modal.types.ServerSessionCredentials:
|
|
362
|
-
"""Start a session and return its ID and token.
|
|
377
|
+
"""Start a sticky session and return its ID and token.
|
|
363
378
|
|
|
364
|
-
Requests to the server URL that carry the returned token are routed to the same container until the
|
|
365
|
-
has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
|
|
366
|
-
as it holds a live session.
|
|
379
|
+
Requests to the server URL that carry the returned token are routed to the same container until the
|
|
380
|
+
session has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
|
|
381
|
+
for as long as it holds a live session.
|
|
382
|
+
|
|
383
|
+
If no container has room for the session, the call waits for additional capacity. It will block for
|
|
384
|
+
up to 25 minutes before giving up. To control the wait per call, use the HTTP API and apply your own retry
|
|
385
|
+
policy.
|
|
367
386
|
|
|
368
387
|
Args:
|
|
369
388
|
idle_timeout: Seconds without an in-flight request before the session ends.
|
|
@@ -387,7 +406,9 @@ class ServerSessionsManager:
|
|
|
387
406
|
|
|
388
407
|
class __terminate_spec(typing_extensions.Protocol):
|
|
389
408
|
def __call__(self, /, token: str) -> None:
|
|
390
|
-
"""Terminate a session.
|
|
409
|
+
"""Terminate a sticky session.
|
|
410
|
+
|
|
411
|
+
New requests to it will be rejected. The container continues serving other sessions.
|
|
391
412
|
|
|
392
413
|
Args:
|
|
393
414
|
token: The `token` of the `ServerSessionCredentials` to terminate.
|
|
@@ -404,7 +425,9 @@ class ServerSessionsManager:
|
|
|
404
425
|
...
|
|
405
426
|
|
|
406
427
|
async def aio(self, /, token: str) -> None:
|
|
407
|
-
"""Terminate a session.
|
|
428
|
+
"""Terminate a sticky session.
|
|
429
|
+
|
|
430
|
+
New requests to it will be rejected. The container continues serving other sessions.
|
|
408
431
|
|
|
409
432
|
Args:
|
|
410
433
|
token: The `token` of the `ServerSessionCredentials` to terminate.
|
|
@@ -307,20 +307,19 @@ def create_channel_config(*, sustained_keepalive: bool = False) -> grpclib.confi
|
|
|
307
307
|
HTTP/2 keepalive probes keep idle transports warm through stateful
|
|
308
308
|
middleboxes and surface dead connections before retries reuse them.
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
open.
|
|
310
|
+
A probe goes out every `_keepalive_time` while the connection keeps sending
|
|
311
|
+
requests. `sustained_keepalive` also keeps probing a connection that only
|
|
312
|
+
awaits responses, for as long as it is open.
|
|
313
313
|
"""
|
|
314
|
-
ping_throttle_overrides: dict[str, Any] = {
|
|
314
|
+
ping_throttle_overrides: dict[str, Any] = {
|
|
315
|
+
# Default 300s floor between probes, applied regardless of data
|
|
316
|
+
# despite the name, so it has to stay under `_keepalive_time`.
|
|
317
|
+
"_http2_min_sent_ping_interval_without_data": GRPC_KEEPALIVE_TIME_SECS / 2,
|
|
318
|
+
}
|
|
315
319
|
if sustained_keepalive:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
"_http2_max_pings_without_data": 0,
|
|
320
|
-
# Default 300s floor between probes, applied regardless of data
|
|
321
|
-
# despite the name, so it has to stay under `_keepalive_time`.
|
|
322
|
-
"_http2_min_sent_ping_interval_without_data": GRPC_KEEPALIVE_TIME_SECS / 2,
|
|
323
|
-
}
|
|
320
|
+
# Default 2: probing stops after two probes with no request sent in
|
|
321
|
+
# between. Requests reset the count, acks don't. 0 = no cap.
|
|
322
|
+
ping_throttle_overrides["_http2_max_pings_without_data"] = 0
|
|
324
323
|
|
|
325
324
|
return grpclib.config.Configuration(
|
|
326
325
|
_keepalive_time=GRPC_KEEPALIVE_TIME_SECS,
|
|
@@ -226,6 +226,7 @@ async def fetch_command_router_access_v2(
|
|
|
226
226
|
*,
|
|
227
227
|
sandbox_id: str | None = None,
|
|
228
228
|
task_id: str | None = None,
|
|
229
|
+
sandbox_token: str | None = None,
|
|
229
230
|
) -> api_pb2.SandboxGetCommandRouterAccessResponse:
|
|
230
231
|
"""Fetch direct command router access info from Modal server for a V2 sandbox."""
|
|
231
232
|
if sandbox_id is not None:
|
|
@@ -236,10 +237,10 @@ async def fetch_command_router_access_v2(
|
|
|
236
237
|
raise ValueError("Either sandbox_id or task_id must be provided")
|
|
237
238
|
assert server_client._auth_token_manager
|
|
238
239
|
auth_token = await server_client._auth_token_manager.get_token()
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
metadata
|
|
242
|
-
)
|
|
240
|
+
metadata = [("x-modal-auth-token", auth_token)]
|
|
241
|
+
if sandbox_token:
|
|
242
|
+
metadata.append(("x-modal-sandbox-token", sandbox_token))
|
|
243
|
+
return await server_client._stub.SandboxGetCommandRouterAccess(request, metadata=metadata)
|
|
243
244
|
|
|
244
245
|
|
|
245
246
|
def _finalize_channel(loop, channel):
|
|
@@ -267,6 +268,7 @@ class TaskCommandRouterClient:
|
|
|
267
268
|
jwt: str,
|
|
268
269
|
*,
|
|
269
270
|
sandbox_id: str | None = None,
|
|
271
|
+
sandbox_token: str | None = None,
|
|
270
272
|
) -> "TaskCommandRouterClient":
|
|
271
273
|
"""Build a connected client from a jwt and url."""
|
|
272
274
|
o = urllib.parse.urlparse(url)
|
|
@@ -315,7 +317,17 @@ class TaskCommandRouterClient:
|
|
|
315
317
|
loop = asyncio.get_running_loop()
|
|
316
318
|
jwt_refresh_lock = asyncio.Lock()
|
|
317
319
|
|
|
318
|
-
client = cls(
|
|
320
|
+
client = cls(
|
|
321
|
+
server_client,
|
|
322
|
+
task_id,
|
|
323
|
+
url,
|
|
324
|
+
jwt,
|
|
325
|
+
channel,
|
|
326
|
+
loop,
|
|
327
|
+
jwt_refresh_lock,
|
|
328
|
+
sandbox_id=sandbox_id,
|
|
329
|
+
sandbox_token=sandbox_token,
|
|
330
|
+
)
|
|
319
331
|
# An unused client releases its connection too.
|
|
320
332
|
client._arm_idle_release()
|
|
321
333
|
return client
|
|
@@ -337,6 +349,8 @@ class TaskCommandRouterClient:
|
|
|
337
349
|
sandbox_id: str,
|
|
338
350
|
task_id: str,
|
|
339
351
|
access: api_pb2.CommandRouterAccess | None = None,
|
|
352
|
+
*,
|
|
353
|
+
sandbox_token: str | None = None,
|
|
340
354
|
) -> "TaskCommandRouterClient":
|
|
341
355
|
"""Initialize a TaskCommandRouterClient for a V2 sandbox.
|
|
342
356
|
|
|
@@ -348,10 +362,12 @@ class TaskCommandRouterClient:
|
|
|
348
362
|
if access is not None:
|
|
349
363
|
url, jwt = access.url, access.jwt
|
|
350
364
|
else:
|
|
351
|
-
resp = await fetch_command_router_access_v2(
|
|
365
|
+
resp = await fetch_command_router_access_v2(
|
|
366
|
+
server_client, sandbox_id=sandbox_id, sandbox_token=sandbox_token
|
|
367
|
+
)
|
|
352
368
|
url, jwt = resp.url, resp.jwt
|
|
353
369
|
logger.debug(f"Using command router access for sandbox {sandbox_id}")
|
|
354
|
-
return await cls._connect(server_client, task_id, url, jwt, sandbox_id=sandbox_id)
|
|
370
|
+
return await cls._connect(server_client, task_id, url, jwt, sandbox_id=sandbox_id, sandbox_token=sandbox_token)
|
|
355
371
|
|
|
356
372
|
@classmethod
|
|
357
373
|
async def init_v2_by_task_id(
|
|
@@ -375,6 +391,7 @@ class TaskCommandRouterClient:
|
|
|
375
391
|
jwt_refresh_lock: asyncio.Lock,
|
|
376
392
|
*,
|
|
377
393
|
sandbox_id: str | None = None,
|
|
394
|
+
sandbox_token: str | None = None,
|
|
378
395
|
stream_stdio_retry_delay_secs: float = 0.01,
|
|
379
396
|
stream_stdio_retry_delay_factor: float = 2,
|
|
380
397
|
stream_stdio_max_retries: int = 10,
|
|
@@ -388,6 +405,7 @@ class TaskCommandRouterClient:
|
|
|
388
405
|
self._server_client = server_client
|
|
389
406
|
self._task_id = task_id
|
|
390
407
|
self._sandbox_id = sandbox_id
|
|
408
|
+
self._sandbox_token = sandbox_token
|
|
391
409
|
self._server_url = server_url
|
|
392
410
|
self._jwt = jwt
|
|
393
411
|
self._channel = channel
|
|
@@ -902,7 +920,10 @@ class TaskCommandRouterClient:
|
|
|
902
920
|
if self._is_v2_sandbox:
|
|
903
921
|
logger.debug(f"Refreshing JWT for V2 exec with task ID {self._task_id}")
|
|
904
922
|
v2_resp = await fetch_command_router_access_v2(
|
|
905
|
-
self._server_client,
|
|
923
|
+
self._server_client,
|
|
924
|
+
sandbox_id=self._sandbox_id,
|
|
925
|
+
task_id=self._task_id,
|
|
926
|
+
sandbox_token=self._sandbox_token,
|
|
906
927
|
)
|
|
907
928
|
logger.debug(f"Finished refreshing JWT for V2 exec with task ID {self._task_id}")
|
|
908
929
|
jwt, url = v2_resp.jwt, v2_resp.url
|