modal 1.5.6.dev23__tar.gz → 1.5.6.dev25__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.dev23 → modal-1.5.6.dev25}/PKG-INFO +1 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/__init__.py +2 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_functions.py +68 -2
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_output/manager.py +8 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_output/rich.py +5 -1
- modal-1.5.6.dev25/modal/_output/status.py +92 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_server.py +32 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_traceback.py +14 -2
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/grpc_utils.py +43 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/task_command_router_client.py +45 -5
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/config.py +8 -6
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/function.py +225 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/server.py +127 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/token.py +20 -2
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/utils.py +28 -3
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/client.pyi +2 -2
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/config.py +57 -46
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/functions.pyi +36 -6
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/mount.py +1 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/secret.py +69 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/secret.pyi +70 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/server.pyi +19 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/token_flow.py +4 -2
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/token_flow.pyi +15 -3
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/types.py +1 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_grpc.py +16 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_pb2.py +409 -389
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_pb2.pyi +34 -1
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_pb2_grpc.py +33 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_pb2_grpc.pyi +10 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_version/__init__.py +1 -1
- modal-1.5.6.dev23/modal/_output/status.py +0 -98
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/LICENSE +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/README.md +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/__main__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_billing.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_clustered_functions.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_environments.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_function_variants.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_image.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_location.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_logs.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_logs_manager.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_object.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_output/pty.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_partial_function.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_resources.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_tunnel.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_workspace.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/app.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/app.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/billing.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_logs.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_stats.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/app.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/cluster.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/container.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/curl.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/dashboard.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/endpoint.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/entry_point.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/image.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/import_refs.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/profile.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/queues.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/secret.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/shell.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/workspace.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/client.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cls.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cls.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/container_process.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/dict.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/dict.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/environments.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/environments.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/exception.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/flash.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/flash_server.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/functions.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/image.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/image.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/io_streams.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/io_streams.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/network_file_system.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/network_file_system.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/object.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/object.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/outbound_policy.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/outbound_policy.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/output.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/parallel_map.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/parallel_map.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/partial_function.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/partial_function.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/proxy.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/py.typed +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/queue.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/retries.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/runner.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/running_app.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/sandbox.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/sandbox.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/sandbox_fs.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/schedule.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/server.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/serving.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/snapshot.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/volume.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/volume.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/workspace.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/workspace.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/pyproject.toml +0 -0
- {modal-1.5.6.dev23 → modal-1.5.6.dev25}/setup.cfg +0 -0
|
@@ -9,7 +9,7 @@ if sys.version_info[:2] >= (3, 15):
|
|
|
9
9
|
from modal_version import __version__
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
|
-
from . import billing, types
|
|
12
|
+
from . import billing, experimental, types
|
|
13
13
|
from ._runtime.execution_context import current_function_call_id, current_input_id, interact, is_local
|
|
14
14
|
from ._tunnel import Tunnel, forward
|
|
15
15
|
from .app import App
|
|
@@ -96,6 +96,7 @@ __all__ = [
|
|
|
96
96
|
"enable_output",
|
|
97
97
|
"enter",
|
|
98
98
|
"exit",
|
|
99
|
+
"experimental",
|
|
99
100
|
"fastapi_endpoint",
|
|
100
101
|
"forward",
|
|
101
102
|
"is_local",
|
|
@@ -10,7 +10,7 @@ from collections.abc import AsyncGenerator, AsyncIterator, Callable, Collection,
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from datetime import datetime, timedelta, timezone
|
|
12
12
|
from pathlib import PurePosixPath
|
|
13
|
-
from typing import TYPE_CHECKING, Any
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
14
14
|
|
|
15
15
|
import typing_extensions
|
|
16
16
|
from google.protobuf.message import Message
|
|
@@ -945,7 +945,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
945
945
|
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
946
946
|
|
|
947
947
|
async def _load(self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
948
|
-
with FunctionCreationStatus(tag) as function_creation_status:
|
|
948
|
+
with FunctionCreationStatus("Server" if is_server else "Function", tag) as function_creation_status:
|
|
949
949
|
timeout_secs = timeout
|
|
950
950
|
|
|
951
951
|
if app and app._is_interactive_ and not is_builder_function:
|
|
@@ -1388,6 +1388,72 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1388
1388
|
_load_remote, rep, skip_reload=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
|
|
1389
1389
|
)
|
|
1390
1390
|
|
|
1391
|
+
@classmethod
|
|
1392
|
+
def _from_id(
|
|
1393
|
+
cls,
|
|
1394
|
+
function_id: str,
|
|
1395
|
+
*,
|
|
1396
|
+
load_context_overrides: LoadContext,
|
|
1397
|
+
called_from: Literal["Function", "Server"] = "Function",
|
|
1398
|
+
):
|
|
1399
|
+
# internal function lookup implementation that allows lookup of class "service functions"
|
|
1400
|
+
# in addition to non-class functions
|
|
1401
|
+
async def _load_remote(
|
|
1402
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None
|
|
1403
|
+
):
|
|
1404
|
+
request = api_pb2.FunctionGetByIdRequest(function_id=function_id)
|
|
1405
|
+
try:
|
|
1406
|
+
response = await load_context.client.stub.FunctionGetById(request)
|
|
1407
|
+
except NotFoundError as exc:
|
|
1408
|
+
# refine the error message
|
|
1409
|
+
msg = f"Lookup failed for {called_from} '{function_id}': {exc}."
|
|
1410
|
+
raise NotFoundError(msg) from None
|
|
1411
|
+
if called_from == "Function" and (response.function.is_server or response.function.is_class):
|
|
1412
|
+
if response.function.is_server:
|
|
1413
|
+
raise InvalidError(f"{function_id} is a Server. Use \n ``modal.Server.from_id('{function_id}')``")
|
|
1414
|
+
raise InvalidError(f"{function_id} is a Cls and cannot be loaded with Function.from_id().")
|
|
1415
|
+
if called_from == "Server" and not response.function.is_server:
|
|
1416
|
+
if response.function.is_class:
|
|
1417
|
+
raise InvalidError(f"{function_id} is a Cls and cannot be loaded with Server.from_id().")
|
|
1418
|
+
raise InvalidError(f"{function_id} is a Function. Use \n ``modal.Function.from_id('{function_id}')``")
|
|
1419
|
+
self._hydrate(function_id, load_context.client, response.handle_metadata)
|
|
1420
|
+
self._function_info = FunctionInfo._from_function_proto(response.function)
|
|
1421
|
+
|
|
1422
|
+
rep = f"modal.Function.from_id('{function_id}')"
|
|
1423
|
+
return cls._from_loader(
|
|
1424
|
+
_load_remote, rep, skip_reload=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
@classmethod
|
|
1428
|
+
def from_id(
|
|
1429
|
+
cls: type["_Function"],
|
|
1430
|
+
function_id: str,
|
|
1431
|
+
*,
|
|
1432
|
+
client: _Client | None = None,
|
|
1433
|
+
):
|
|
1434
|
+
"""Reference a Function from a deployed or running App by its ID.
|
|
1435
|
+
|
|
1436
|
+
This is a lazy method that defers hydrating the local
|
|
1437
|
+
object with metadata from Modal servers until the first
|
|
1438
|
+
time it is actually used.
|
|
1439
|
+
|
|
1440
|
+
Args:
|
|
1441
|
+
function_id: ID of the function.
|
|
1442
|
+
client: Modal client to use; defaults to `Client.from_env()` when omitted.
|
|
1443
|
+
|
|
1444
|
+
Returns:
|
|
1445
|
+
A lazy `Function` handle.
|
|
1446
|
+
|
|
1447
|
+
Examples:
|
|
1448
|
+
```python
|
|
1449
|
+
f = modal.Function.from_id("fu-123")
|
|
1450
|
+
```
|
|
1451
|
+
"""
|
|
1452
|
+
return cls._from_id(
|
|
1453
|
+
function_id,
|
|
1454
|
+
load_context_overrides=LoadContext(client=client),
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1391
1457
|
@classmethod
|
|
1392
1458
|
def from_name(
|
|
1393
1459
|
cls: type["_Function"],
|
|
@@ -58,6 +58,11 @@ class StatusRow(ABC):
|
|
|
58
58
|
"""Append a warning message associated with this row."""
|
|
59
59
|
...
|
|
60
60
|
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def details(self, message: str) -> None:
|
|
63
|
+
"""Append additional details to the row."""
|
|
64
|
+
...
|
|
65
|
+
|
|
61
66
|
@abstractmethod
|
|
62
67
|
def finish(self, message: str) -> None:
|
|
63
68
|
"""Mark the row as finished with the given message."""
|
|
@@ -73,6 +78,9 @@ class DisabledStatusRow(StatusRow):
|
|
|
73
78
|
def warn(self, warning: "api_pb2.Warning") -> None:
|
|
74
79
|
pass
|
|
75
80
|
|
|
81
|
+
def details(self, message: str) -> None:
|
|
82
|
+
pass
|
|
83
|
+
|
|
76
84
|
def finish(self, message: str) -> None:
|
|
77
85
|
pass
|
|
78
86
|
|
|
@@ -194,6 +194,10 @@ class RichStatusRow(StatusRow):
|
|
|
194
194
|
if self._spinner is not None:
|
|
195
195
|
self._spinner.update(text=message)
|
|
196
196
|
|
|
197
|
+
def details(self, message: str) -> None:
|
|
198
|
+
if self._step_node is not None:
|
|
199
|
+
self._step_node.add(f"{message}")
|
|
200
|
+
|
|
197
201
|
def warn(self, warning: api_pb2.Warning) -> None:
|
|
198
202
|
if self._step_node is not None:
|
|
199
203
|
self._step_node.add(f"[yellow]:warning:[/yellow] {warning.message}")
|
|
@@ -201,7 +205,7 @@ class RichStatusRow(StatusRow):
|
|
|
201
205
|
def finish(self, message: str) -> None:
|
|
202
206
|
if self._step_node is not None and self._spinner is not None:
|
|
203
207
|
self._spinner.update(text=message)
|
|
204
|
-
self._step_node.label = f"
|
|
208
|
+
self._step_node.label = f"[green]✓[/green] {message}"
|
|
205
209
|
|
|
206
210
|
|
|
207
211
|
_LOG_FLUSH_THRESHOLD = 32768 # 32KB — flush batched log output roughly every ~400 lines
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Copyright Modal Labs 2022
|
|
2
|
+
"""Function creation status tracking for Modal CLI.
|
|
3
|
+
|
|
4
|
+
This module contains the FunctionCreationStatus context manager used to
|
|
5
|
+
track and display function creation progress.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from modal_proto import api_pb2
|
|
11
|
+
|
|
12
|
+
from .manager import OutputManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_annotation_for_web_url(url_info: api_pb2.WebUrlInfo) -> str:
|
|
16
|
+
if url_info.truncated:
|
|
17
|
+
suffix = " [grey70](label truncated)[/grey70]"
|
|
18
|
+
elif url_info.label_stolen:
|
|
19
|
+
suffix = " [grey70](label stolen)[/grey70]"
|
|
20
|
+
else:
|
|
21
|
+
suffix = ""
|
|
22
|
+
return suffix
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FunctionCreationStatus:
|
|
26
|
+
"""Context manager for tracking and displaying function creation progress."""
|
|
27
|
+
|
|
28
|
+
type: str
|
|
29
|
+
name: str
|
|
30
|
+
response: api_pb2.FunctionCreateResponse | None = None
|
|
31
|
+
|
|
32
|
+
def __init__(self, type: str, name: str):
|
|
33
|
+
self.type = type
|
|
34
|
+
self.name = name
|
|
35
|
+
self._output_mgr = OutputManager.get()
|
|
36
|
+
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
self.status_row = self._output_mgr.add_status_row()
|
|
39
|
+
self.status_row.message(f"Creating {self.type} {self.name}...")
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def set_response(self, resp: api_pb2.FunctionCreateResponse):
|
|
43
|
+
self.response = resp
|
|
44
|
+
|
|
45
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
46
|
+
if exc_type:
|
|
47
|
+
raise exc_val
|
|
48
|
+
|
|
49
|
+
if not self.response:
|
|
50
|
+
self.status_row.finish(f"Unknown error when creating {self.type} {self.name}")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
for warning in self.response.server_warnings:
|
|
54
|
+
self.status_row.warn(warning)
|
|
55
|
+
|
|
56
|
+
if web_url := self.response.handle_metadata.web_url:
|
|
57
|
+
url_info = self.response.function.web_url_info
|
|
58
|
+
authenticated = self.response.function.webhook_config.requires_proxy_auth
|
|
59
|
+
auth_suffix = " 🔑" if authenticated else " [yellow](unauthenticated)[/yellow]"
|
|
60
|
+
suffix = _get_annotation_for_web_url(url_info)
|
|
61
|
+
|
|
62
|
+
self.status_row.finish(f"Created {self.type} [green]{self.name}[/green]")
|
|
63
|
+
self.status_row.details(f"[dim underline]{web_url}[/dim underline]{auth_suffix}{suffix}")
|
|
64
|
+
for custom_domain in self.response.function.custom_domain_info:
|
|
65
|
+
self.status_row.details(f"Custom domain: [dim underline]{custom_domain.url}[/dim underline]")
|
|
66
|
+
|
|
67
|
+
elif self.response.function.flash_service_urls:
|
|
68
|
+
# Despite the gRPC API types, Servers only have one URL
|
|
69
|
+
authenticated = self.response.function.is_server and not self.response.function.http_config.unauthenticated
|
|
70
|
+
auth_suffix = " 🔑" if authenticated else " [yellow](unauthenticated)[/yellow]"
|
|
71
|
+
self.status_row.finish(f"Created {self.type} [green]{self.name}[/green]")
|
|
72
|
+
url = self.response.function.flash_service_urls[0]
|
|
73
|
+
self.status_row.details(f"[dim underline]{url}[/dim underline]{auth_suffix}")
|
|
74
|
+
|
|
75
|
+
else:
|
|
76
|
+
self.status_row.finish(f"Created {self.type} [green]{self.name}[/green].")
|
|
77
|
+
if self.response.function.method_definitions_set:
|
|
78
|
+
for method_definition in self.response.function.method_definitions.values():
|
|
79
|
+
if method_definition.web_url:
|
|
80
|
+
url_info = method_definition.web_url_info
|
|
81
|
+
suffix = _get_annotation_for_web_url(url_info)
|
|
82
|
+
authenticated = method_definition.webhook_config.requires_proxy_auth
|
|
83
|
+
auth_suffix = " 🔑" if authenticated else " [yellow](unauthenticated)[/yellow]"
|
|
84
|
+
self.status_row.details(
|
|
85
|
+
f"{method_definition.function_name} -> "
|
|
86
|
+
f"[dim underline]{method_definition.web_url}[/dim underline]{auth_suffix}{suffix}"
|
|
87
|
+
)
|
|
88
|
+
for custom_domain in method_definition.custom_domain_info:
|
|
89
|
+
indent = len(method_definition.function_name) * " "
|
|
90
|
+
self.status_row.details(
|
|
91
|
+
f"{indent} -> [dim underline]{custom_domain.url}[/dim underline]{auth_suffix}"
|
|
92
|
+
)
|
|
@@ -273,6 +273,38 @@ class _Server:
|
|
|
273
273
|
)
|
|
274
274
|
return server
|
|
275
275
|
|
|
276
|
+
@classmethod
|
|
277
|
+
def from_id(
|
|
278
|
+
cls: type["_Server"],
|
|
279
|
+
server_id: str,
|
|
280
|
+
*,
|
|
281
|
+
client: _Client | None = None,
|
|
282
|
+
):
|
|
283
|
+
"""Reference a Server from a deployed or running App by its ID.
|
|
284
|
+
|
|
285
|
+
This is a lazy method that defers hydrating the local
|
|
286
|
+
object with metadata from Modal servers until the first
|
|
287
|
+
time it is actually used.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
server_id: The ID of the server.
|
|
291
|
+
client: Modal client instance for this session.
|
|
292
|
+
|
|
293
|
+
Examples:
|
|
294
|
+
```python notest
|
|
295
|
+
server = modal.Server.from_id("fu-456")
|
|
296
|
+
```
|
|
297
|
+
"""
|
|
298
|
+
load_context_overrides = LoadContext(client=client)
|
|
299
|
+
|
|
300
|
+
server = _Server()
|
|
301
|
+
server._service_function = _Function._from_id(
|
|
302
|
+
server_id,
|
|
303
|
+
load_context_overrides=load_context_overrides,
|
|
304
|
+
called_from="Server",
|
|
305
|
+
)
|
|
306
|
+
return server
|
|
307
|
+
|
|
276
308
|
def _is_local(self) -> bool:
|
|
277
309
|
"""Returns True if this Server has local source code available."""
|
|
278
310
|
return self._user_cls is not None
|
|
@@ -123,13 +123,25 @@ def print_exception(exc: type[BaseException] | None, value: BaseException | None
|
|
|
123
123
|
print(*notes, sep="\n", file=sys.stderr) # noqa: T201
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
_server_warning_registry: dict[Any, Any] = {}
|
|
127
|
+
SERVER_WARNING_REGISTRY_LIMIT = 2048
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def print_server_warning(message: str):
|
|
127
131
|
"""Issue a warning originating from the server with empty metadata about local origin.
|
|
128
132
|
|
|
129
133
|
When using the Modal CLI, these warnings should get caught and coerced into Rich panels.
|
|
130
134
|
"""
|
|
135
|
+
if len(_server_warning_registry) > SERVER_WARNING_REGISTRY_LIMIT:
|
|
136
|
+
# Start over instead of going quiet: overflow costs a repeat, never a hidden warning.
|
|
137
|
+
_server_warning_registry.clear()
|
|
138
|
+
warnings.warn_explicit(message, ServerWarning, "<modal-server>", 0, registry=_server_warning_registry)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
|
|
142
|
+
"""Issue warnings that the server attached to a response message."""
|
|
131
143
|
for warning in server_warnings:
|
|
132
|
-
|
|
144
|
+
print_server_warning(warning.message)
|
|
133
145
|
|
|
134
146
|
|
|
135
147
|
# for some reason, the traceback cleanup here can't be moved into a context manager :(
|
|
@@ -24,12 +24,13 @@ from grpclib.config import Configuration
|
|
|
24
24
|
from grpclib.encoding.base import CodecBase, StatusDetailsCodecBase
|
|
25
25
|
from grpclib.exceptions import StreamTerminatedError
|
|
26
26
|
from grpclib.protocol import H2Protocol
|
|
27
|
+
from h2.exceptions import ProtocolError as H2ProtocolError
|
|
27
28
|
|
|
28
29
|
from modal.exception import ClientClosed, ConnectionError, NotFoundError
|
|
29
30
|
from modal_proto import api_pb2
|
|
30
31
|
from modal_version import __version__
|
|
31
32
|
|
|
32
|
-
from .._traceback import suppress_tb_frame
|
|
33
|
+
from .._traceback import print_server_warning, suppress_tb_frame
|
|
33
34
|
from ..config import config
|
|
34
35
|
from .async_utils import TaskContext, retry
|
|
35
36
|
from .logger import logger
|
|
@@ -130,6 +131,24 @@ class ModalChannel(grpclib.client.Channel):
|
|
|
130
131
|
return await super().__connect__()
|
|
131
132
|
|
|
132
133
|
async def _create_connection(self) -> H2Protocol:
|
|
134
|
+
target = self._path if self.__use_unix_socket else f"{self.__target_host}:{self.__target_port}"
|
|
135
|
+
tls = self.__ssl_context is not None
|
|
136
|
+
started_at = time.monotonic()
|
|
137
|
+
# Logged before awaiting: a reconnect is invisible until it resolves, which
|
|
138
|
+
# can be long after the RPC waiting on it appears to hang.
|
|
139
|
+
logger.debug(f"Starting connection attempt to {target} (TLS={tls})")
|
|
140
|
+
try:
|
|
141
|
+
protocol = await self._open_connection()
|
|
142
|
+
except (asyncio.CancelledError, Exception) as exc:
|
|
143
|
+
outcome = "cancelled" if isinstance(exc, asyncio.CancelledError) else "failed"
|
|
144
|
+
elapsed = time.monotonic() - started_at
|
|
145
|
+
logger.debug(f"Connection attempt to {target} {outcome} after {elapsed:.3f}s: {exc!r}")
|
|
146
|
+
raise
|
|
147
|
+
elapsed = time.monotonic() - started_at
|
|
148
|
+
logger.debug(f"Connection to {target} established after {elapsed:.3f}s")
|
|
149
|
+
return protocol
|
|
150
|
+
|
|
151
|
+
async def _open_connection(self) -> H2Protocol:
|
|
133
152
|
if self.__use_unix_socket:
|
|
134
153
|
return await super()._create_connection()
|
|
135
154
|
|
|
@@ -308,6 +327,20 @@ def create_channel_config(*, sustained_keepalive: bool = False) -> grpclib.confi
|
|
|
308
327
|
)
|
|
309
328
|
|
|
310
329
|
|
|
330
|
+
# Header the server uses to attach a non-fatal message to any response, one entry per
|
|
331
|
+
# percent-encoded warning. It rides in the trailing metadata, which grpclib also
|
|
332
|
+
# surfaces for trailers-only responses, i.e. failed requests.
|
|
333
|
+
SERVER_WARNING_HEADER = "x-modal-warning"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _issue_server_warnings(metadata: Any) -> None:
|
|
337
|
+
"""Issue the warnings in a response's metadata, which `print_server_warning` deduplicates."""
|
|
338
|
+
for key, encoded_message in metadata.items():
|
|
339
|
+
if key != SERVER_WARNING_HEADER:
|
|
340
|
+
continue
|
|
341
|
+
print_server_warning(urllib.parse.unquote(encoded_message))
|
|
342
|
+
|
|
343
|
+
|
|
311
344
|
def create_channel(
|
|
312
345
|
server_url: str,
|
|
313
346
|
metadata: dict[str, str] = {},
|
|
@@ -356,6 +389,12 @@ def create_channel(
|
|
|
356
389
|
|
|
357
390
|
grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
|
|
358
391
|
|
|
392
|
+
# Surface warnings the server attached to any response on this channel.
|
|
393
|
+
async def recv_trailing_metadata(event: grpclib.events.RecvTrailingMetadata) -> None:
|
|
394
|
+
_issue_server_warnings(event.metadata)
|
|
395
|
+
|
|
396
|
+
grpclib.events.listen(channel, grpclib.events.RecvTrailingMetadata, recv_trailing_metadata)
|
|
397
|
+
|
|
359
398
|
return channel
|
|
360
399
|
|
|
361
400
|
|
|
@@ -496,6 +535,8 @@ def process_exception_before_retry(
|
|
|
496
535
|
raise ConnectionError(str(exc))
|
|
497
536
|
elif isinstance(exc, asyncio.TimeoutError):
|
|
498
537
|
raise ConnectionError(str(exc))
|
|
538
|
+
elif isinstance(exc, H2ProtocolError):
|
|
539
|
+
raise ConnectionError(str(exc))
|
|
499
540
|
else:
|
|
500
541
|
raise exc
|
|
501
542
|
|
|
@@ -583,6 +624,7 @@ async def _retry_transient_errors(
|
|
|
583
624
|
OSError,
|
|
584
625
|
asyncio.TimeoutError,
|
|
585
626
|
AttributeError,
|
|
627
|
+
H2ProtocolError,
|
|
586
628
|
) as exc:
|
|
587
629
|
# Note that we only catch AttributeError to handle a specific case that works around a bug
|
|
588
630
|
# in grpclib<=0.4.7. See above (search for `write_appdata`).
|
|
@@ -89,6 +89,11 @@ def _parse_jwt_expiration(jwt_token: str) -> float | None:
|
|
|
89
89
|
return None
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
# Enough consecutive failures to distinguish a connection that keeps breaking from
|
|
93
|
+
# an isolated blip, without leaving a stuck call silent for long.
|
|
94
|
+
RETRY_WARNING_THRESHOLD = 3
|
|
95
|
+
|
|
96
|
+
|
|
92
97
|
async def call_with_retries_on_transient_errors(
|
|
93
98
|
func,
|
|
94
99
|
*,
|
|
@@ -97,6 +102,9 @@ async def call_with_retries_on_transient_errors(
|
|
|
97
102
|
max_retries: int | None = 10,
|
|
98
103
|
exclude_status_codes: list[Status] | None = None,
|
|
99
104
|
timeout_deadline: float | None = None,
|
|
105
|
+
rpc_name: str = "RPC",
|
|
106
|
+
exec_id: str | None = None,
|
|
107
|
+
warning_message: str | None = None,
|
|
100
108
|
):
|
|
101
109
|
"""Call func() with transient error retries and exponential backoff.
|
|
102
110
|
|
|
@@ -113,10 +121,18 @@ async def call_with_retries_on_transient_errors(
|
|
|
113
121
|
exception (e.g., into a TimeoutError) based on the deadline check.
|
|
114
122
|
The caller is also responsible for propagating the remaining budget
|
|
115
123
|
into `func()` (typically as the per-call gRPC timeout).
|
|
124
|
+
rpc_name: RPC name to include in retry diagnostics.
|
|
125
|
+
exec_id: Execution ID to include in retry diagnostics, for correlating
|
|
126
|
+
the retries of concurrent executions.
|
|
127
|
+
warning_message: Optional warning to emit once after
|
|
128
|
+
RETRY_WARNING_THRESHOLD consecutive non-timeout failures.
|
|
116
129
|
"""
|
|
117
130
|
delay_secs = base_delay_secs
|
|
118
131
|
num_retries = 0
|
|
132
|
+
consecutive_failures = 0
|
|
133
|
+
warned = False
|
|
119
134
|
exclude_status_codes = exclude_status_codes or []
|
|
135
|
+
called_for = f" for exec {exec_id}" if exec_id is not None else ""
|
|
120
136
|
|
|
121
137
|
def is_retryable_status(status: Status) -> bool:
|
|
122
138
|
return status in RETRYABLE_GRPC_STATUS_CODES and status not in exclude_status_codes
|
|
@@ -128,25 +144,41 @@ async def call_with_retries_on_transient_errors(
|
|
|
128
144
|
return False
|
|
129
145
|
return True
|
|
130
146
|
|
|
131
|
-
async def sleep_and_advance(e: Exception):
|
|
132
|
-
nonlocal delay_secs, num_retries
|
|
147
|
+
async def sleep_and_advance(e: Exception, *, count_failure: bool = True):
|
|
148
|
+
nonlocal delay_secs, num_retries, consecutive_failures, warned
|
|
133
149
|
# Clamp the backoff sleep to the remaining deadline so we don't sleep
|
|
134
150
|
# past it just to fail on the next iteration's deadline check.
|
|
135
151
|
sleep_for = delay_secs
|
|
136
152
|
if timeout_deadline is not None:
|
|
137
153
|
sleep_for = min(sleep_for, max(0.0, timeout_deadline - time.monotonic()))
|
|
138
|
-
|
|
154
|
+
elapsed = time.monotonic() - attempt_started_at
|
|
155
|
+
logger.debug(
|
|
156
|
+
f"{rpc_name}{called_for} attempt {num_retries + 1} failed after {elapsed:.3f}s, "
|
|
157
|
+
f"retrying in {sleep_for}s: {e!r}"
|
|
158
|
+
)
|
|
159
|
+
if count_failure:
|
|
160
|
+
consecutive_failures += 1
|
|
161
|
+
if warning_message is not None and consecutive_failures >= RETRY_WARNING_THRESHOLD and not warned:
|
|
162
|
+
logger.warning(
|
|
163
|
+
f"{warning_message} Retrying after {consecutive_failures} consecutive failures. "
|
|
164
|
+
"Set MODAL_LOGLEVEL=DEBUG for details."
|
|
165
|
+
)
|
|
166
|
+
warned = True
|
|
167
|
+
else:
|
|
168
|
+
consecutive_failures = 0
|
|
139
169
|
await asyncio.sleep(sleep_for)
|
|
140
170
|
delay_secs *= delay_factor
|
|
141
171
|
num_retries += 1
|
|
142
172
|
|
|
143
173
|
while True:
|
|
174
|
+
attempt_started_at = time.monotonic()
|
|
144
175
|
try:
|
|
145
176
|
return await func()
|
|
146
177
|
except GRPCError as e:
|
|
147
178
|
if not is_retryable_status(e.status) or not can_retry():
|
|
148
179
|
raise
|
|
149
|
-
|
|
180
|
+
# Long-poll deadlines can expire while the process is still running on a healthy connection.
|
|
181
|
+
await sleep_and_advance(e, count_failure=e.status != Status.DEADLINE_EXCEEDED)
|
|
150
182
|
except AttributeError as e:
|
|
151
183
|
# StreamTerminatedError are not properly raised in grpclib<=0.4.7
|
|
152
184
|
# fixed in https://github.com/vmagamedov/grpclib/issues/185
|
|
@@ -165,7 +197,9 @@ async def call_with_retries_on_transient_errors(
|
|
|
165
197
|
# `timeout_deadline` can further translate this based on
|
|
166
198
|
# whether the deadline has elapsed.
|
|
167
199
|
raise ConnectionError(str(e))
|
|
168
|
-
|
|
200
|
+
# A client-side timeout says nothing about the connection's health, so it
|
|
201
|
+
# doesn't count toward the warning.
|
|
202
|
+
await sleep_and_advance(e, count_failure=not isinstance(e, asyncio.TimeoutError))
|
|
169
203
|
|
|
170
204
|
|
|
171
205
|
_StdioReq = TypeVar("_StdioReq")
|
|
@@ -828,6 +862,12 @@ class TaskCommandRouterClient:
|
|
|
828
862
|
# infinitely retry. For callers without an exec deadline, this
|
|
829
863
|
# could hang indefinitely.
|
|
830
864
|
lambda: self._call_with_auth_retry(self._stub.TaskExecWait, request, timeout=60),
|
|
865
|
+
rpc_name="TaskExecWait",
|
|
866
|
+
exec_id=exec_id,
|
|
867
|
+
warning_message=(
|
|
868
|
+
f"Cannot retrieve the exit status for exec {exec_id} due to connection or service errors. "
|
|
869
|
+
"The command may have already exited."
|
|
870
|
+
),
|
|
831
871
|
base_delay_secs=1, # Retry after 1s since total time is expected to be long.
|
|
832
872
|
delay_factor=1, # Fixed delay.
|
|
833
873
|
max_retries=None, # Retry forever.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
|
-
import
|
|
2
|
+
from json import dumps
|
|
3
3
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
@@ -21,11 +21,12 @@ config_cli = ModalGroup(
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
@config_cli.command("show", help="Show current configuration values
|
|
24
|
+
@config_cli.command("show", help="Show current configuration values.")
|
|
25
25
|
@click.option("--redact/--no-redact", default=True, help="Redact secret credential values.")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
@click.option("--include-internal", is_flag=True, help="Include internal feature flags.", hidden=True)
|
|
27
|
+
@click.option("--json", is_flag=True, default=False)
|
|
28
|
+
def show(redact: bool, include_internal: bool, json: bool):
|
|
29
|
+
config_dict = config.to_dict(include_internal=include_internal)
|
|
29
30
|
if redact and config_dict.get("token_secret"):
|
|
30
31
|
config_dict["token_secret"] = "***"
|
|
31
32
|
if redact and config_dict.get("oauth_refresh_token"):
|
|
@@ -33,7 +34,8 @@ def show(redact: bool):
|
|
|
33
34
|
if redact and config_dict.get("oauth_client_secret"):
|
|
34
35
|
config_dict["oauth_client_secret"] = "***"
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
# Accept --json as a no-op for consistency; we may change the default formatting later
|
|
38
|
+
OutputManager.get().print_json(dumps(config_dict))
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
SET_DEFAULT_ENV_HELP = """Set the default Modal environment for the active profile
|