modal 1.5.6.dev12__tar.gz → 1.5.6.dev14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/PKG-INFO +1 -1
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_functions.py +57 -9
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_output/status.py +5 -6
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/auth_token_manager.py +28 -17
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/grpc_utils.py +20 -6
- modal-1.5.6.dev14/modal/_utils/idle_countdown.py +64 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/task_command_router_client.py +192 -82
- modal-1.5.6.dev14/modal/cli/_logs.py +124 -0
- modal-1.5.6.dev14/modal/cli/_stats.py +141 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/app.py +16 -111
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/container.py +1 -1
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/entry_point.py +2 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/function.py +145 -119
- modal-1.5.6.dev14/modal/cli/server.py +519 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/utils.py +89 -1
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/client.py +0 -1
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/client.pyi +2 -2
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/config.py +8 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/exception.py +0 -4
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/functions.pyi +58 -6
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/io_streams.py +18 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/io_streams.pyi +18 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/sandbox.py +50 -152
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/sandbox.pyi +15 -188
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/sandbox_fs.py +62 -54
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/sandbox_fs.pyi +182 -158
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/types.py +61 -2
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/volume.py +101 -48
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/volume.pyi +16 -2
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal.egg-info/SOURCES.txt +3 -2
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/gen_reference_docs.py +0 -1
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/reference_intro.md +0 -1
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/api_grpc.py +64 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/api_pb2.py +1595 -1374
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/api_pb2.pyi +494 -27
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/api_pb2_grpc.py +133 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/api_pb2_grpc.pyi +42 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/modal_api_grpc.py +4 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_version/__init__.py +1 -1
- modal-1.5.6.dev12/modal/cli/_stats.py +0 -50
- modal-1.5.6.dev12/modal/file_io.py +0 -564
- modal-1.5.6.dev12/modal/file_io.pyi +0 -447
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/LICENSE +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/README.md +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/__main__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_billing.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_clustered_functions.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_environments.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_function_variants.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_image.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_location.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_logs.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_logs_manager.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_object.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_output/manager.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_output/pty.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_output/rich.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_partial_function.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_resources.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_server.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_traceback.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_tunnel.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/_workspace.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/app.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/app.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/billing.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/cluster.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/config.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/curl.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/dashboard.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/endpoint.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/image.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/import_refs.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/launch.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/profile.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/queues.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/secret.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/shell.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/token.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cli/workspace.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cls.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/cls.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/container_process.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/dict.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/dict.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/environments.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/environments.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/experimental/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/experimental/flash.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/functions.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/image.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/image.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/mount.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/network_file_system.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/network_file_system.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/object.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/object.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/output.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/parallel_map.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/parallel_map.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/partial_function.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/partial_function.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/proxy.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/py.typed +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/queue.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/retries.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/runner.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/running_app.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/schedule.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/secret.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/secret.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/server.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/server.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/serving.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/snapshot.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/token_flow.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/token_flow.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/workspace.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal/workspace.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/pyproject.toml +0 -0
- {modal-1.5.6.dev12 → modal-1.5.6.dev14}/setup.cfg +0 -0
|
@@ -8,6 +8,7 @@ import warnings
|
|
|
8
8
|
from collections import OrderedDict
|
|
9
9
|
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Collection, Sequence
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
11
12
|
from pathlib import PurePosixPath
|
|
12
13
|
from typing import TYPE_CHECKING, Any
|
|
13
14
|
|
|
@@ -64,7 +65,7 @@ from ._utils.function_utils import (
|
|
|
64
65
|
parse_gpu_config,
|
|
65
66
|
validate_target_concurrency,
|
|
66
67
|
)
|
|
67
|
-
from ._utils.grpc_utils import Retry, RetryWarningMessage
|
|
68
|
+
from ._utils.grpc_utils import Retry, RetryWarningMessage, is_class_function_lookup_error
|
|
68
69
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes, validate_volumes_by_object_id
|
|
69
70
|
from .call_graph import InputInfo, _reconstruct_call_graph
|
|
70
71
|
from .client import _Client
|
|
@@ -99,7 +100,12 @@ from .proxy import _Proxy
|
|
|
99
100
|
from .retries import Retries, RetryManager
|
|
100
101
|
from .schedule import Schedule
|
|
101
102
|
from .secret import _Secret
|
|
102
|
-
from .types import
|
|
103
|
+
from .types import (
|
|
104
|
+
FunctionAutoscalerSettings,
|
|
105
|
+
FunctionCurrentStats,
|
|
106
|
+
FunctionStats,
|
|
107
|
+
ServerAutoscalerSettings,
|
|
108
|
+
)
|
|
103
109
|
from .volume import _Volume, _volume_to_mount_proto
|
|
104
110
|
|
|
105
111
|
if TYPE_CHECKING:
|
|
@@ -1361,9 +1367,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1361
1367
|
env_context = (
|
|
1362
1368
|
f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
|
|
1363
1369
|
)
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1370
|
+
msg = f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}."
|
|
1371
|
+
if is_class_function_lookup_error(exc):
|
|
1372
|
+
msg = (
|
|
1373
|
+
f"Function '{name}' from the '{app_name}' app{env_context} is a modal.Cls. "
|
|
1374
|
+
f"Use `modal.Cls.from_name` instead."
|
|
1375
|
+
)
|
|
1376
|
+
raise NotFoundError(msg) from None
|
|
1367
1377
|
|
|
1368
1378
|
print_server_warnings(response.server_warnings)
|
|
1369
1379
|
|
|
@@ -2144,23 +2154,61 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
2144
2154
|
return self._raw_f
|
|
2145
2155
|
|
|
2146
2156
|
@live_method
|
|
2147
|
-
async def get_current_stats(self) ->
|
|
2148
|
-
"""Return a
|
|
2157
|
+
async def get_current_stats(self) -> FunctionCurrentStats:
|
|
2158
|
+
"""Return a snapshot of the Function's current input and container state.
|
|
2149
2159
|
|
|
2150
2160
|
Returns:
|
|
2151
|
-
|
|
2161
|
+
A `FunctionCurrentStats` object containing live input and container counts.
|
|
2152
2162
|
"""
|
|
2153
2163
|
resp = await self.client.stub.FunctionGetCurrentStats(
|
|
2154
2164
|
api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
|
|
2155
2165
|
retry=Retry(total_timeout=10.0),
|
|
2156
2166
|
)
|
|
2157
|
-
return
|
|
2167
|
+
return FunctionCurrentStats(
|
|
2158
2168
|
backlog=resp.backlog,
|
|
2159
2169
|
num_total_runners=resp.num_total_tasks,
|
|
2160
2170
|
num_running_inputs=resp.num_running_inputs,
|
|
2161
2171
|
input_headroom=resp.input_headroom,
|
|
2162
2172
|
)
|
|
2163
2173
|
|
|
2174
|
+
@live_method
|
|
2175
|
+
async def stats(
|
|
2176
|
+
self, *, since: datetime | None = None, until: datetime | None = None, container: str | None = None
|
|
2177
|
+
) -> FunctionStats:
|
|
2178
|
+
"""Return statistics for a modal Function.
|
|
2179
|
+
|
|
2180
|
+
The default time range is the most recent hour. The maximum time range is 7 days.
|
|
2181
|
+
|
|
2182
|
+
Args:
|
|
2183
|
+
since: The beginning of the time range, inclusive. If omitted, this defaults to an hour before `until`.
|
|
2184
|
+
Values without a timezone are interpeted as local time.
|
|
2185
|
+
until: The end of the time range, exclusive. If omitted, this defaults to current time.
|
|
2186
|
+
Values without a timezone are interpeted as local time.
|
|
2187
|
+
container: If passed in, the stats are computed for only this container. Default None.
|
|
2188
|
+
|
|
2189
|
+
Returns:
|
|
2190
|
+
A `FunctionStats` object
|
|
2191
|
+
"""
|
|
2192
|
+
until = until or datetime.now(timezone.utc)
|
|
2193
|
+
if until.tzinfo is None:
|
|
2194
|
+
until = until.astimezone()
|
|
2195
|
+
until = until.astimezone(timezone.utc)
|
|
2196
|
+
|
|
2197
|
+
since = since or until - timedelta(hours=1)
|
|
2198
|
+
if since.tzinfo is None:
|
|
2199
|
+
since = since.astimezone()
|
|
2200
|
+
since = since.astimezone(timezone.utc)
|
|
2201
|
+
if since >= until:
|
|
2202
|
+
raise InvalidError("`since` must be before `until`.")
|
|
2203
|
+
|
|
2204
|
+
historical_request = api_pb2.FunctionGetTimeRangeStatsRequest(function_id=self.object_id)
|
|
2205
|
+
historical_request.since.FromDatetime(since)
|
|
2206
|
+
historical_request.until.FromDatetime(until)
|
|
2207
|
+
if container:
|
|
2208
|
+
historical_request.container_id = container
|
|
2209
|
+
stats = await self.client.stub.FunctionGetTimeRangeStats(historical_request)
|
|
2210
|
+
return FunctionStats._from_proto(stats)
|
|
2211
|
+
|
|
2164
2212
|
@live_method
|
|
2165
2213
|
async def _get_schema(self) -> api_pb2.FunctionSchema:
|
|
2166
2214
|
"""Returns recorded schema for function, internal use only for now"""
|
|
@@ -46,16 +46,17 @@ class FunctionCreationStatus:
|
|
|
46
46
|
|
|
47
47
|
if not self.response:
|
|
48
48
|
self.status_row.finish(f"Unknown error when creating function {self.tag}")
|
|
49
|
+
return
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
for warning in self.response.server_warnings:
|
|
52
|
+
self.status_row.warn(warning)
|
|
53
|
+
|
|
54
|
+
if web_url := self.response.handle_metadata.web_url:
|
|
51
55
|
url_info = self.response.function.web_url_info
|
|
52
56
|
requires_proxy_auth = self.response.function.webhook_config.requires_proxy_auth
|
|
53
57
|
proxy_auth_suffix = " 🔑" if requires_proxy_auth else ""
|
|
54
58
|
# Ensure terms used here match terms used in modal.com/docs/guide/webhook-urls doc.
|
|
55
59
|
suffix = _get_suffix_from_web_url_info(url_info)
|
|
56
|
-
# TODO: this is only printed when we're showing progress. Maybe move this somewhere else.
|
|
57
|
-
for warning in self.response.server_warnings:
|
|
58
|
-
self.status_row.warn(warning)
|
|
59
60
|
self.status_row.finish(
|
|
60
61
|
f"Created web function {self.tag} => [magenta underline]{web_url}[/magenta underline]"
|
|
61
62
|
f"{proxy_auth_suffix}{suffix}"
|
|
@@ -78,8 +79,6 @@ class FunctionCreationStatus:
|
|
|
78
79
|
)
|
|
79
80
|
|
|
80
81
|
else:
|
|
81
|
-
for warning in self.response.server_warnings:
|
|
82
|
-
self.status_row.warn(warning)
|
|
83
82
|
self.status_row.finish(f"Created function {self.tag}.")
|
|
84
83
|
if self.response.function.method_definitions_set:
|
|
85
84
|
for method_definition in self.response.function.method_definitions.values():
|
|
@@ -4,6 +4,7 @@ import base64
|
|
|
4
4
|
import json
|
|
5
5
|
import random
|
|
6
6
|
import time
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from modal._utils.grpc_utils import DEFAULT_MAX_RETRIES, Retry
|
|
@@ -15,8 +16,15 @@ from .logger import logger
|
|
|
15
16
|
AUTH_DENIED_EXCEPTIONS = (AuthError, PermissionDeniedError)
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
TokenFetcher = Callable[[Retry], Awaitable[str]]
|
|
20
|
+
|
|
21
|
+
|
|
18
22
|
class _AuthTokenManager:
|
|
19
|
-
"""Handles fetching and refreshing of
|
|
23
|
+
"""Handles fetching and refreshing of a short-lived JWT.
|
|
24
|
+
|
|
25
|
+
Defaults to the input plane auth token (`AuthTokenGet`); pass `fetch` to manage a different token with the
|
|
26
|
+
same caching and refresh behavior.
|
|
27
|
+
"""
|
|
20
28
|
|
|
21
29
|
# Fraction of the token's lifetime to use before refreshing.
|
|
22
30
|
REFRESH_FRACTION = 0.5
|
|
@@ -26,7 +34,7 @@ class _AuthTokenManager:
|
|
|
26
34
|
# only ever delays a refresh, so the margin before expiry stays at no less than
|
|
27
35
|
# 1 - REFRESH_FRACTION - REFRESH_JITTER of the lifetime.
|
|
28
36
|
REFRESH_JITTER = 0.1
|
|
29
|
-
# Bound each
|
|
37
|
+
# Bound each fetch attempt so a refresh can't hang.
|
|
30
38
|
AUTH_TOKEN_TIMEOUT = 5.0
|
|
31
39
|
AUTH_TOKEN_RETRY_TOTAL_TIMEOUT = 3 * AUTH_TOKEN_TIMEOUT
|
|
32
40
|
# After a failed refresh, wait before hitting the server again, growing exponentially with
|
|
@@ -36,8 +44,9 @@ class _AuthTokenManager:
|
|
|
36
44
|
# If the token doesn't have an expiry field, default to current time plus this value (not expected).
|
|
37
45
|
DEFAULT_EXPIRY_OFFSET = 20 * 60
|
|
38
46
|
|
|
39
|
-
def __init__(self, stub: "modal_api_grpc.ModalClientModal"):
|
|
47
|
+
def __init__(self, stub: "modal_api_grpc.ModalClientModal", fetch: TokenFetcher | None = None):
|
|
40
48
|
self._stub = stub
|
|
49
|
+
self._fetch = fetch or self._fetch_auth_token
|
|
41
50
|
self._token = ""
|
|
42
51
|
self._expiry = 0.0
|
|
43
52
|
self._refresh_at = 0.0
|
|
@@ -105,31 +114,33 @@ class _AuthTokenManager:
|
|
|
105
114
|
# Retrieve the exception, if any, so a failure isn't logged as unhandled when no caller is waiting.
|
|
106
115
|
task.exception()
|
|
107
116
|
|
|
117
|
+
async def _fetch_auth_token(self, retry: Retry) -> str:
|
|
118
|
+
resp: api_pb2.AuthTokenGetResponse = await self._stub.AuthTokenGet(api_pb2.AuthTokenGetRequest(), retry=retry)
|
|
119
|
+
return resp.token
|
|
120
|
+
|
|
108
121
|
async def _fetch_token(self):
|
|
109
|
-
"""
|
|
122
|
+
"""Fetch a new token and cache it, or arm the failure backoff."""
|
|
110
123
|
try:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
total_timeout=self.AUTH_TOKEN_RETRY_TOTAL_TIMEOUT,
|
|
118
|
-
max_retries=0 if self._token else DEFAULT_MAX_RETRIES,
|
|
119
|
-
),
|
|
124
|
+
# No cached token to fall back on, so a failure is user-visible: retry transient errors.
|
|
125
|
+
# Otherwise one attempt, and _retry_after handles the cooldown.
|
|
126
|
+
retry = Retry(
|
|
127
|
+
attempt_timeout=self.AUTH_TOKEN_TIMEOUT,
|
|
128
|
+
total_timeout=self.AUTH_TOKEN_RETRY_TOTAL_TIMEOUT,
|
|
129
|
+
max_retries=0 if self._token else DEFAULT_MAX_RETRIES,
|
|
120
130
|
)
|
|
121
|
-
|
|
131
|
+
token = await self._fetch(retry)
|
|
132
|
+
if not token:
|
|
122
133
|
# Not expected
|
|
123
134
|
raise ExecutionError(
|
|
124
135
|
"Internal error: Did not receive auth token from server. Please contact Modal support."
|
|
125
136
|
)
|
|
126
|
-
if exp := self._decode_jwt(
|
|
137
|
+
if exp := self._decode_jwt(token).get("exp"):
|
|
127
138
|
expiry = float(exp)
|
|
128
139
|
else:
|
|
129
140
|
# This should never happen.
|
|
130
|
-
logger.warning("
|
|
141
|
+
logger.warning("Auth token does not contain exp field")
|
|
131
142
|
expiry = time.time() + self.DEFAULT_EXPIRY_OFFSET
|
|
132
|
-
self._token =
|
|
143
|
+
self._token = token
|
|
133
144
|
self._set_expiry(expiry)
|
|
134
145
|
self._retry_after = 0.0
|
|
135
146
|
self._backoff = self.FAILURE_BACKOFF_BASE
|
|
@@ -25,7 +25,7 @@ from grpclib.encoding.base import CodecBase, StatusDetailsCodecBase
|
|
|
25
25
|
from grpclib.exceptions import StreamTerminatedError
|
|
26
26
|
from grpclib.protocol import H2Protocol
|
|
27
27
|
|
|
28
|
-
from modal.exception import ClientClosed, ConnectionError
|
|
28
|
+
from modal.exception import ClientClosed, ConnectionError, NotFoundError
|
|
29
29
|
from modal_proto import api_pb2
|
|
30
30
|
from modal_version import __version__
|
|
31
31
|
|
|
@@ -121,12 +121,12 @@ class ModalChannel(grpclib.client.Channel):
|
|
|
121
121
|
self.__target_host = host
|
|
122
122
|
self.__target_port = port
|
|
123
123
|
self.__ssl_context = ssl_context
|
|
124
|
-
self.
|
|
125
|
-
self.
|
|
124
|
+
self._permanently_closed = False
|
|
125
|
+
self._closed_error_message = closed_error_message
|
|
126
126
|
|
|
127
127
|
async def __connect__(self):
|
|
128
|
-
if self.
|
|
129
|
-
raise ClientClosed(self.
|
|
128
|
+
if self._permanently_closed and self._closed_error_message is not None:
|
|
129
|
+
raise ClientClosed(self._closed_error_message)
|
|
130
130
|
return await super().__connect__()
|
|
131
131
|
|
|
132
132
|
async def _create_connection(self) -> H2Protocol:
|
|
@@ -161,8 +161,13 @@ class ModalChannel(grpclib.client.Channel):
|
|
|
161
161
|
and not self._protocol.connection.is_closing()
|
|
162
162
|
)
|
|
163
163
|
|
|
164
|
+
def release_connection(self):
|
|
165
|
+
"""Release the connection while leaving the channel reusable."""
|
|
166
|
+
return super().close()
|
|
167
|
+
|
|
164
168
|
def close(self):
|
|
165
|
-
|
|
169
|
+
"""Close the channel permanently."""
|
|
170
|
+
self._permanently_closed = True
|
|
166
171
|
return super().close()
|
|
167
172
|
|
|
168
173
|
|
|
@@ -450,6 +455,15 @@ async def retry_transient_errors(
|
|
|
450
455
|
return await _retry_transient_errors(fn, req, retry=Retry(max_retries=max_retries))
|
|
451
456
|
|
|
452
457
|
|
|
458
|
+
def is_class_function_lookup_error(exc: NotFoundError) -> bool:
|
|
459
|
+
"""Whether a function lookup failed because the requested name identifies a class."""
|
|
460
|
+
return any(
|
|
461
|
+
isinstance(detail, api_pb2.FunctionLookupError)
|
|
462
|
+
and detail.reason == api_pb2.FunctionLookupError.REASON_CLASS_NAME_USED
|
|
463
|
+
for detail in (getattr(exc, "_grpc_details", None) or [])
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
|
|
453
467
|
def get_server_retry_policy(exc: Exception) -> api_pb2.RPCRetryPolicy | None:
|
|
454
468
|
"""Get server retry policy."""
|
|
455
469
|
if not isinstance(exc, GRPCError) or not exc.details:
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright Modal Labs 2025
|
|
2
|
+
import asyncio
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class IdleCountdown:
|
|
7
|
+
"""An extendable idle timeout tracker."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
10
|
+
self._loop = loop
|
|
11
|
+
self._handle: asyncio.TimerHandle | None = None
|
|
12
|
+
self._retired = False
|
|
13
|
+
self._expire_on_idle = False
|
|
14
|
+
self._release: Callable[[], None] | None = None
|
|
15
|
+
|
|
16
|
+
def _assert_owning_loop(self) -> None:
|
|
17
|
+
assert asyncio.get_running_loop() is self._loop, "Idle bookkeeping must run on its owning event loop"
|
|
18
|
+
|
|
19
|
+
def expire_when_idle(self) -> None:
|
|
20
|
+
"""Release at the current or next idle boundary, even if the timeout is disabled.
|
|
21
|
+
|
|
22
|
+
Must run on the owning event loop without yielding control.
|
|
23
|
+
"""
|
|
24
|
+
self._assert_owning_loop()
|
|
25
|
+
self._expire_on_idle = True
|
|
26
|
+
if self._release is not None and not self._retired:
|
|
27
|
+
release = self._release
|
|
28
|
+
self.stop()
|
|
29
|
+
release()
|
|
30
|
+
|
|
31
|
+
def stop(self) -> None:
|
|
32
|
+
"""Cancel the countdown on the owning event loop without yielding control."""
|
|
33
|
+
self._assert_owning_loop()
|
|
34
|
+
self._release = None
|
|
35
|
+
if self._handle is not None:
|
|
36
|
+
self._handle.cancel()
|
|
37
|
+
self._handle = None
|
|
38
|
+
|
|
39
|
+
def retire(self) -> None:
|
|
40
|
+
"""Stop the countdown and refuse every subsequent arm.
|
|
41
|
+
|
|
42
|
+
Must run on the owning event loop without yielding control.
|
|
43
|
+
"""
|
|
44
|
+
self._assert_owning_loop()
|
|
45
|
+
self._retired = True
|
|
46
|
+
self.stop()
|
|
47
|
+
|
|
48
|
+
def arm(self, timeout_secs: float, release: Callable[[], None]) -> None:
|
|
49
|
+
"""Start the countdown again, calling `release` when it elapses.
|
|
50
|
+
|
|
51
|
+
Must run on the owning event loop without yielding control.
|
|
52
|
+
A timeout of zero or less never fires unless `expire_when_idle` was
|
|
53
|
+
requested. `release` runs as a plain callback on the event loop, so it
|
|
54
|
+
must not block.
|
|
55
|
+
"""
|
|
56
|
+
self.stop()
|
|
57
|
+
if self._retired:
|
|
58
|
+
return
|
|
59
|
+
self._release = release
|
|
60
|
+
if self._expire_on_idle:
|
|
61
|
+
timeout_secs = 0
|
|
62
|
+
elif timeout_secs <= 0:
|
|
63
|
+
return
|
|
64
|
+
self._handle = self._loop.call_later(timeout_secs, release)
|