modal 1.5.6.dev10__tar.gz → 1.5.6.dev12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/PKG-INFO +1 -1
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_functions.py +8 -1
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/execution_context.py +1 -1
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_server.py +19 -1
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/task_command_router_client.py +4 -1
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/time_utils.py +11 -0
- modal-1.5.6.dev12/modal/cli/_stats.py +50 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/app.py +8 -9
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/entry_point.py +2 -0
- modal-1.5.6.dev12/modal/cli/function.py +402 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/image.py +4 -2
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/import_refs.py +5 -2
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/profile.py +21 -6
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/token.py +9 -4
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/client.pyi +2 -2
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/config.py +46 -6
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/functions.pyi +10 -8
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/runner.py +13 -3
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/server.pyi +37 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/token_flow.py +8 -1
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/types.py +51 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/volume.py +138 -8
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/volume.pyi +28 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/SOURCES.txt +2 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_pb2.py +1033 -976
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_pb2.pyi +186 -63
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_grpc.py +16 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2.py +45 -25
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2.pyi +29 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2_grpc.py +34 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2_grpc.pyi +12 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_version/__init__.py +1 -1
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/LICENSE +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/README.md +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/__main__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_billing.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_clustered_functions.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_environments.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_function_variants.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_image.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_location.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_logs.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_logs_manager.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_object.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/manager.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/pty.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/rich.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/status.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_partial_function.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_resources.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_traceback.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_tunnel.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_workspace.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/app.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/app.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/billing.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/cluster.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/config.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/container.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/curl.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/dashboard.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/endpoint.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/launch.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/queues.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/secret.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/shell.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/utils.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/workspace.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/client.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cls.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cls.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/container_process.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/dict.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/dict.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/environments.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/environments.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/exception.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/experimental/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/experimental/flash.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/file_io.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/file_io.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/functions.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/image.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/image.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/io_streams.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/io_streams.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/mount.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/network_file_system.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/network_file_system.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/object.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/object.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/output.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/parallel_map.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/parallel_map.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/partial_function.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/partial_function.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/proxy.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/py.typed +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/queue.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/retries.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/running_app.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/sandbox.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/sandbox.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/sandbox_fs.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/schedule.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/secret.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/secret.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/server.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/serving.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/snapshot.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/token_flow.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/workspace.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/workspace.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_grpc.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/pyproject.toml +0 -0
- {modal-1.5.6.dev10 → modal-1.5.6.dev12}/setup.cfg +0 -0
|
@@ -16,6 +16,7 @@ from google.protobuf.message import Message
|
|
|
16
16
|
from grpclib import Status
|
|
17
17
|
from synchronicity.combined_types import MethodWithAio
|
|
18
18
|
|
|
19
|
+
from modal._utils.logger import logger
|
|
19
20
|
from modal.types import CloudBucketMountInfo, FunctionInfo, HttpInfo, VolumeMountInfo
|
|
20
21
|
from modal_proto import api_pb2
|
|
21
22
|
from modal_proto.modal_api_grpc import ModalClientModal
|
|
@@ -2306,7 +2307,8 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2306
2307
|
"""Fetch information about the graph of Inputs this FunctionCall is part of.
|
|
2307
2308
|
|
|
2308
2309
|
Note: the call graph data is not populated in real-time, and its capture is best-effort.
|
|
2309
|
-
We do not recommend relying on this method
|
|
2310
|
+
Large call graphs may be truncated. We do not recommend relying on this method
|
|
2311
|
+
for critical use cases.
|
|
2310
2312
|
|
|
2311
2313
|
See the [`modal.types`](/docs/sdk/py/latest/types) reference for information
|
|
2312
2314
|
on the return values.
|
|
@@ -2317,6 +2319,11 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2317
2319
|
assert self._client and self._client.stub
|
|
2318
2320
|
request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
|
|
2319
2321
|
response = await self._client.stub.FunctionGetCallGraph(request)
|
|
2322
|
+
if response.truncated:
|
|
2323
|
+
logger.warning(
|
|
2324
|
+
f"Call graph for {self.object_id} was truncated; "
|
|
2325
|
+
f"returning the first {len(response.inputs)} inputs. The graph may be incomplete."
|
|
2326
|
+
)
|
|
2320
2327
|
return _reconstruct_call_graph(response)
|
|
2321
2328
|
|
|
2322
2329
|
@live_method
|
|
@@ -109,7 +109,7 @@ _current_input_id: ContextVar = ContextVar("_current_input_id")
|
|
|
109
109
|
_current_function_call_id: ContextVar = ContextVar("_current_function_call_id")
|
|
110
110
|
_current_attempt_token: ContextVar = ContextVar("_current_attempt_token")
|
|
111
111
|
|
|
112
|
-
_is_currently_importing = False
|
|
112
|
+
_is_currently_importing = False
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
@contextmanager
|
|
@@ -15,7 +15,7 @@ from ._supports_logs import _LogQueryData
|
|
|
15
15
|
from .client import _Client
|
|
16
16
|
from .cls import is_parameter
|
|
17
17
|
from .exception import InvalidError
|
|
18
|
-
from .types import ServerAutoscalerSettings
|
|
18
|
+
from .types import ServerAutoscalerSettings, ServerInfo
|
|
19
19
|
|
|
20
20
|
if typing.TYPE_CHECKING:
|
|
21
21
|
import modal.app
|
|
@@ -101,6 +101,24 @@ class _Server:
|
|
|
101
101
|
"""
|
|
102
102
|
return _ServerLogsManager(self)
|
|
103
103
|
|
|
104
|
+
async def info(self, *, refresh: bool = False) -> ServerInfo:
|
|
105
|
+
"""Get an overview of a Server's resource requests, associated mounts, http config, etc.
|
|
106
|
+
|
|
107
|
+
This method performs a network request to populate this information if the Server handle is
|
|
108
|
+
a remote lookup whose information has not yet been fetched (e.g. from `Server.from_name(...)`),
|
|
109
|
+
or if `refresh=True`.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
refresh: Always perform a network request. Pass `refresh=True` to ensure that this method
|
|
113
|
+
returns the most up to date information.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
This returns a [`modal.types.ServerInfo`](https://modal.com/docs/sdk/py/latest/types#ServerInfo)
|
|
117
|
+
dataclass.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
return ServerInfo._from_function_info(await self._get_service_function().info(refresh=refresh))
|
|
121
|
+
|
|
104
122
|
@staticmethod
|
|
105
123
|
def _extract_user_cls(wrapped_user_cls: "type | _PartialFunction") -> type:
|
|
106
124
|
if isinstance(wrapped_user_cls, _PartialFunction):
|
|
@@ -840,8 +840,11 @@ class TaskCommandRouterClient:
|
|
|
840
840
|
# We successfully authenticated after a JWT refresh, reset the auth retry flag.
|
|
841
841
|
if did_auth_retry:
|
|
842
842
|
did_auth_retry = False
|
|
843
|
-
#
|
|
843
|
+
# Any received chunk is progress: reset the backoff and refill the
|
|
844
|
+
# retry budget so it bounds consecutive failures, not failures over
|
|
845
|
+
# the stream's lifetime.
|
|
844
846
|
delay_secs = self.stream_stdio_retry_delay_secs
|
|
847
|
+
num_retries_remaining = self.stream_stdio_max_retries
|
|
845
848
|
# Track it so transient reconnects request the
|
|
846
849
|
# correct next byte.
|
|
847
850
|
if is_first_chunk_of_attempt and isinstance(item, sr_pb2.SandboxStdioReadV2Response):
|
|
@@ -3,6 +3,17 @@ import re
|
|
|
3
3
|
from datetime import datetime, timedelta, timezone, tzinfo
|
|
4
4
|
from zoneinfo import ZoneInfo
|
|
5
5
|
|
|
6
|
+
_DURATION_UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
|
7
|
+
_DURATION_EXPR = re.compile(r"^(?P<n>\d+)(?P<unit>s|m|h|d)$")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_duration(value: str) -> timedelta:
|
|
11
|
+
"""Parse a compact duration such as ``30m``, ``2h``, or ``1d``."""
|
|
12
|
+
match = _DURATION_EXPR.match(value)
|
|
13
|
+
if not match:
|
|
14
|
+
raise ValueError(f"Invalid duration: '{value}'")
|
|
15
|
+
return timedelta(seconds=int(match.group("n")) * _DURATION_UNITS[match.group("unit")])
|
|
16
|
+
|
|
6
17
|
|
|
7
18
|
def is_leap(year: int):
|
|
8
19
|
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
"""Shared color styling for stats CLI commands."""
|
|
3
|
+
|
|
4
|
+
from rich.style import Style
|
|
5
|
+
|
|
6
|
+
STATS_HEADING_STYLE = "white"
|
|
7
|
+
STATS_METADATA_STYLE = "bright_black"
|
|
8
|
+
STATS_SECTION_STYLE = "bold white"
|
|
9
|
+
STATS_SUCCESS_STYLE = "green"
|
|
10
|
+
STATS_PROGRESS_STYLE = "cyan"
|
|
11
|
+
STATS_WARNING_STYLE = "yellow"
|
|
12
|
+
STATS_PROBLEM_STYLE = "red"
|
|
13
|
+
|
|
14
|
+
_HIGH_PROBLEM_RATE = 0.05
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def stats_style(style: str, use_color: bool) -> str | Style:
|
|
18
|
+
if use_color:
|
|
19
|
+
return style
|
|
20
|
+
if style == STATS_SECTION_STYLE:
|
|
21
|
+
return "bold"
|
|
22
|
+
return Style()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def success_style(count: int, total: int, use_color: bool) -> str | None:
|
|
26
|
+
if use_color and total and count / total > 0.5:
|
|
27
|
+
return STATS_SUCCESS_STYLE
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def progress_style(count: int, use_color: bool) -> str | None:
|
|
32
|
+
if use_color and count:
|
|
33
|
+
return STATS_PROGRESS_STYLE
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def problem_style(count: int, total: int, use_color: bool) -> str | None:
|
|
38
|
+
if not use_color or count == 0:
|
|
39
|
+
return None
|
|
40
|
+
if not total or count / total >= _HIGH_PROBLEM_RATE:
|
|
41
|
+
return STATS_PROBLEM_STYLE
|
|
42
|
+
return STATS_WARNING_STYLE
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def percentile_style(p50: float | None, p99: float | None, use_color: bool) -> str | Style:
|
|
46
|
+
if not use_color or p50 is None or p99 is None or p99 <= 0:
|
|
47
|
+
return Style()
|
|
48
|
+
if p50 == 0 or p99 >= 2 * p50:
|
|
49
|
+
return STATS_WARNING_STYLE
|
|
50
|
+
return Style()
|
|
@@ -3,7 +3,7 @@ import re
|
|
|
3
3
|
import sys
|
|
4
4
|
import time
|
|
5
5
|
import warnings
|
|
6
|
-
from datetime import datetime,
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
7
|
from json import dumps
|
|
8
8
|
from typing import Literal, get_args
|
|
9
9
|
|
|
@@ -25,7 +25,7 @@ from modal.runner import DEPLOYMENT_STRATEGY_TYPE, _stop_and_wait_for_containers
|
|
|
25
25
|
from modal_proto import api_pb2
|
|
26
26
|
|
|
27
27
|
from .._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
|
|
28
|
-
from .._utils.time_utils import locale_tz, timestamp_to_localized_str
|
|
28
|
+
from .._utils.time_utils import locale_tz, parse_duration, timestamp_to_localized_str
|
|
29
29
|
from ._help import ModalGroup
|
|
30
30
|
from .utils import (
|
|
31
31
|
_fetch_app_logs,
|
|
@@ -136,9 +136,6 @@ async def list_(env: str | None = None, json: bool = False):
|
|
|
136
136
|
display_table(columns, rows, json, title=f"Apps{env_part}")
|
|
137
137
|
|
|
138
138
|
|
|
139
|
-
_RELATIVE_TIME_UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
|
140
|
-
|
|
141
|
-
|
|
142
139
|
def _parse_time_arg(value: str | None, default: datetime) -> datetime:
|
|
143
140
|
"""Parse a time argument that can be a relative duration (e.g. '2h', '30m') or ISO 8601 datetime.
|
|
144
141
|
|
|
@@ -148,10 +145,12 @@ def _parse_time_arg(value: str | None, default: datetime) -> datetime:
|
|
|
148
145
|
if value is None:
|
|
149
146
|
return default
|
|
150
147
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
try:
|
|
149
|
+
duration = parse_duration(value)
|
|
150
|
+
except ValueError:
|
|
151
|
+
pass
|
|
152
|
+
else:
|
|
153
|
+
return datetime.now(timezone.utc) - duration
|
|
155
154
|
|
|
156
155
|
# Try ISO 8601 datetime
|
|
157
156
|
try:
|
|
@@ -21,6 +21,7 @@ from .dashboard import dashboard
|
|
|
21
21
|
from .dict import dict_cli
|
|
22
22
|
from .endpoint import endpoint_cli
|
|
23
23
|
from .environment import environment_cli
|
|
24
|
+
from .function import function_cli
|
|
24
25
|
from .image import image_cli
|
|
25
26
|
from .launch import launch_cli
|
|
26
27
|
from .logo import print_logo
|
|
@@ -117,6 +118,7 @@ entrypoint_cli.add_command(launch_cli, hidden=True)
|
|
|
117
118
|
entrypoint_cli.add_command(app_cli, panel="Deployments")
|
|
118
119
|
entrypoint_cli.add_command(container_cli, panel="Deployments")
|
|
119
120
|
entrypoint_cli.add_command(endpoint_cli, panel="Deployments")
|
|
121
|
+
entrypoint_cli.add_command(function_cli, panel="Deployments")
|
|
120
122
|
# cluster is hidden while multi-node is in beta/experimental
|
|
121
123
|
entrypoint_cli.add_command(cluster_cli, panel="Deployments", hidden=True)
|
|
122
124
|
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
import json as json_lib
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from click import UsageError
|
|
8
|
+
from google.protobuf.timestamp_pb2 import Timestamp
|
|
9
|
+
from rich.style import Style
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from modal._environments import ensure_env
|
|
14
|
+
from modal._object import _get_environment_name
|
|
15
|
+
from modal._utils.async_utils import synchronizer
|
|
16
|
+
from modal._utils.time_utils import parse_duration
|
|
17
|
+
from modal.client import _Client
|
|
18
|
+
from modal.output import OutputManager
|
|
19
|
+
from modal_proto import api_pb2
|
|
20
|
+
|
|
21
|
+
from ._help import ModalGroup
|
|
22
|
+
from ._stats import (
|
|
23
|
+
STATS_HEADING_STYLE,
|
|
24
|
+
STATS_METADATA_STYLE,
|
|
25
|
+
STATS_SECTION_STYLE,
|
|
26
|
+
percentile_style,
|
|
27
|
+
problem_style,
|
|
28
|
+
progress_style,
|
|
29
|
+
stats_style,
|
|
30
|
+
success_style,
|
|
31
|
+
)
|
|
32
|
+
from .app import _parse_time_arg
|
|
33
|
+
from .utils import env_option
|
|
34
|
+
|
|
35
|
+
function_cli = ModalGroup(name="function", help="Inspect deployed Modal Functions.")
|
|
36
|
+
|
|
37
|
+
_DEFAULT_STATS_WINDOW = timedelta(hours=1)
|
|
38
|
+
_DEFAULT_METRIC_PRECISION = 2
|
|
39
|
+
_DISPLAYED_PERCENTILES = ((5000, "p50"), (9000, "p90"), (9900, "p99"))
|
|
40
|
+
_INPUT_METRIC_ORDER = ("Execution time (s)", "End-to-end latency (s)")
|
|
41
|
+
_CONTAINER_METRIC_ORDER = (
|
|
42
|
+
"Startup time (s)",
|
|
43
|
+
"CPU Usage (cores)",
|
|
44
|
+
"Memory Usage (GiB)",
|
|
45
|
+
"GPU Utilization (%)",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _resolve_function_id(client: _Client, function_identifier: str, environment_name: str) -> str:
|
|
50
|
+
if function_identifier == "":
|
|
51
|
+
raise UsageError("FUNCTION must be a Function ID (fu-…) or a deployed Function name (APP_NAME/FUNCTION_NAME).")
|
|
52
|
+
|
|
53
|
+
if "/" in function_identifier:
|
|
54
|
+
app_name, function_name = function_identifier.split("/", 1)
|
|
55
|
+
|
|
56
|
+
if not (app_name and function_name):
|
|
57
|
+
raise UsageError(
|
|
58
|
+
"FUNCTION must be a Function ID (fu-…) or a deployed Function name (APP_NAME/FUNCTION_NAME)."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
response = await client.stub.FunctionGet(
|
|
62
|
+
api_pb2.FunctionGetRequest(
|
|
63
|
+
app_name=app_name,
|
|
64
|
+
object_tag=function_name,
|
|
65
|
+
environment_name=environment_name,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
return response.function_id
|
|
69
|
+
|
|
70
|
+
if function_identifier.startswith("fu-"):
|
|
71
|
+
return function_identifier
|
|
72
|
+
|
|
73
|
+
raise UsageError("FUNCTION must be a Function ID (fu-…) or a deployed Function name (APP_NAME/FUNCTION_NAME).")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _timestamp(value: datetime) -> Timestamp:
|
|
77
|
+
timestamp = Timestamp()
|
|
78
|
+
timestamp.FromDatetime(value.astimezone(timezone.utc))
|
|
79
|
+
return timestamp
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _percentile_table(rows: list[tuple[Text, Text, Text, Text]], use_color: bool) -> Table:
|
|
83
|
+
table = Table(box=None, pad_edge=False, padding=(0, 2), header_style="")
|
|
84
|
+
table.add_column("", min_width=30)
|
|
85
|
+
percentile_header_style = stats_style(STATS_METADATA_STYLE, use_color)
|
|
86
|
+
table.add_column(Text("p50", style=percentile_header_style), justify="right", min_width=8)
|
|
87
|
+
table.add_column(Text("p90", style=percentile_header_style), justify="right", min_width=8)
|
|
88
|
+
table.add_column(Text("p99", style=percentile_header_style), justify="right", min_width=8)
|
|
89
|
+
for row in rows:
|
|
90
|
+
table.add_row(*row)
|
|
91
|
+
return table
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _count_with_percentage(count: int, total: int, label: str) -> str:
|
|
95
|
+
percentage = count / total if total else 0
|
|
96
|
+
return f"{count:,} {label} ({percentage:.1%})"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _percentile_name(percentile_basis_points: int) -> str:
|
|
100
|
+
return f"p{percentile_basis_points / 100:g}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _distribution_json(distribution: api_pb2.StatsPercentileDistribution) -> dict[str, object]:
|
|
104
|
+
return {
|
|
105
|
+
"unit": distribution.unit,
|
|
106
|
+
"percentiles": {
|
|
107
|
+
_percentile_name(percentile.percentile_basis_points): percentile.value
|
|
108
|
+
for percentile in distribution.percentiles
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _distribution_map_json(
|
|
114
|
+
distributions: dict[str, api_pb2.StatsPercentileDistribution],
|
|
115
|
+
) -> dict[str, object]:
|
|
116
|
+
return {name: _distribution_json(distribution) for name, distribution in distributions.items()}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _time_range_stats_json(response: api_pb2.FunctionGetTimeRangeStatsResponse) -> dict[str, object]:
|
|
120
|
+
return {
|
|
121
|
+
"since": response.since.ToDatetime(tzinfo=timezone.utc).isoformat(),
|
|
122
|
+
"until": response.until.ToDatetime(tzinfo=timezone.utc).isoformat(),
|
|
123
|
+
"input_success_count": response.input_success_count,
|
|
124
|
+
"input_failure_count": response.input_failure_count,
|
|
125
|
+
"input_timeout_count": response.input_timeout_count,
|
|
126
|
+
"input_running_at_end_count": response.input_running_at_end_count,
|
|
127
|
+
"input_percentile_stats": _distribution_map_json(
|
|
128
|
+
cast(dict[str, api_pb2.StatsPercentileDistribution], response.input_percentile_stats)
|
|
129
|
+
),
|
|
130
|
+
"container_started_count": response.container_started_count,
|
|
131
|
+
"container_error_count": response.container_error_count,
|
|
132
|
+
"container_creating_at_end_count": response.container_creating_at_end_count,
|
|
133
|
+
"container_percentile_stats": _distribution_map_json(
|
|
134
|
+
cast(dict[str, api_pb2.StatsPercentileDistribution], response.container_percentile_stats)
|
|
135
|
+
),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _stats_json(
|
|
140
|
+
function_id: str,
|
|
141
|
+
history: api_pb2.FunctionGetTimeRangeStatsResponse,
|
|
142
|
+
all_variants: bool,
|
|
143
|
+
) -> dict[str, object]:
|
|
144
|
+
return {
|
|
145
|
+
"function_id": function_id,
|
|
146
|
+
"all_variants": all_variants,
|
|
147
|
+
"variant_count": history.variant_count,
|
|
148
|
+
**_time_range_stats_json(history),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _metric_rows(
|
|
153
|
+
distributions: dict[str, api_pb2.StatsPercentileDistribution],
|
|
154
|
+
expected_order: tuple[str, ...],
|
|
155
|
+
use_color: bool,
|
|
156
|
+
) -> list[tuple[Text, Text, Text, Text]]:
|
|
157
|
+
rows: list[tuple[Text, Text, Text, Text]] = []
|
|
158
|
+
ordered_names = [name for name in expected_order if name in distributions]
|
|
159
|
+
ordered_names.extend(sorted(set(distributions) - set(expected_order)))
|
|
160
|
+
for name in ordered_names:
|
|
161
|
+
distribution = distributions[name]
|
|
162
|
+
values_by_percentile = {
|
|
163
|
+
percentile.percentile_basis_points: percentile.value for percentile in distribution.percentiles
|
|
164
|
+
}
|
|
165
|
+
if not any(basis_points in values_by_percentile for basis_points, _ in _DISPLAYED_PERCENTILES):
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
p50 = values_by_percentile.get(5000)
|
|
169
|
+
p99 = values_by_percentile.get(9900)
|
|
170
|
+
values = []
|
|
171
|
+
for basis_points, _ in _DISPLAYED_PERCENTILES:
|
|
172
|
+
value = values_by_percentile.get(basis_points)
|
|
173
|
+
value_text = "—" if value is None else f"{value:.{_DEFAULT_METRIC_PRECISION}f}"
|
|
174
|
+
style = percentile_style(p50, p99, use_color) if basis_points == 9900 else Style()
|
|
175
|
+
values.append(Text(value_text, style=style))
|
|
176
|
+
rows.append(
|
|
177
|
+
(
|
|
178
|
+
Text(f" {name}", style=stats_style(STATS_METADATA_STYLE, use_color)),
|
|
179
|
+
values[0],
|
|
180
|
+
values[1],
|
|
181
|
+
values[2],
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
return rows
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@function_cli.command("stats", no_args_is_help=True)
|
|
188
|
+
@click.argument("function_identifier", metavar="FUNCTION")
|
|
189
|
+
@click.option(
|
|
190
|
+
"--since",
|
|
191
|
+
default=None,
|
|
192
|
+
help="Start of time range. Treated as UTC if timezone not supplied."
|
|
193
|
+
"Accepts an ISO 8601 datetime or relative time such as '2h' or '30m'.",
|
|
194
|
+
)
|
|
195
|
+
@click.option(
|
|
196
|
+
"--until",
|
|
197
|
+
default=None,
|
|
198
|
+
help="End of time range. Treated as UTC if timezone not supplied. Accepts the same argument types as --since.",
|
|
199
|
+
)
|
|
200
|
+
@click.option(
|
|
201
|
+
"--all-variants",
|
|
202
|
+
is_flag=True,
|
|
203
|
+
default=False,
|
|
204
|
+
help="Aggregate the base Function and its variants.",
|
|
205
|
+
)
|
|
206
|
+
@click.option(
|
|
207
|
+
"--container",
|
|
208
|
+
type=str,
|
|
209
|
+
default=None,
|
|
210
|
+
metavar="CONTAINER",
|
|
211
|
+
help="Compute the stats only for this container. Takes precedence over --all-variants.",
|
|
212
|
+
)
|
|
213
|
+
@click.option("--no-color", is_flag=True, default=False, help="Disable colors in the output.")
|
|
214
|
+
@click.option("--json", "json_output", is_flag=True, default=False, help="Output stats as JSON.")
|
|
215
|
+
@env_option
|
|
216
|
+
@synchronizer.create_blocking
|
|
217
|
+
async def stats(
|
|
218
|
+
function_identifier: str,
|
|
219
|
+
since: str | None = None,
|
|
220
|
+
until: str | None = None,
|
|
221
|
+
all_variants: bool = False,
|
|
222
|
+
container: str | None = None,
|
|
223
|
+
no_color: bool = False,
|
|
224
|
+
json_output: bool = False,
|
|
225
|
+
*,
|
|
226
|
+
env: str | None = None,
|
|
227
|
+
):
|
|
228
|
+
"""Show aggregate statistics for a modal Function.
|
|
229
|
+
|
|
230
|
+
FUNCTION may be a Function ID or a deployed Function name in the form
|
|
231
|
+
``APP_NAME/FUNCTION_NAME``. By default, stats describe the specified Function ID.
|
|
232
|
+
Pass ``--all-variants`` to aggregate across all its direct variants.
|
|
233
|
+
|
|
234
|
+
If --since and --until are omitted, the stats are returned for the last hour.
|
|
235
|
+
|
|
236
|
+
The available metrics and their definitions are subject to change and are provided
|
|
237
|
+
on a best-effort basis. They may be delayed or incomplete. Do not rely on this command
|
|
238
|
+
for autoscaler management.
|
|
239
|
+
|
|
240
|
+
Examples:
|
|
241
|
+
|
|
242
|
+
Show stats from the last hour using a Function ID:
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
modal function stats fu-abc123
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Show stats for a deployed Function by name:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
modal function stats my-app/my-function
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Show stats across all parameterized instances of a deployed `modal.Cls`:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
modal function stats 'my-app/MyClass.*' --all-variants
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Show stats from a specific relative window:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
modal function stats my-app/my-function --since 5h --until 2h
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Show stats for an explicit time range as JSON:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
modal function stats my-app/my-function \
|
|
270
|
+
--since 2026-08-28T14:00:00Z \
|
|
271
|
+
--until 2026-08-28T16:00:00Z \
|
|
272
|
+
--json
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Show stats for a specific container:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
modal function stats my-app/my-function --container ta-12345
|
|
279
|
+
```
|
|
280
|
+
"""
|
|
281
|
+
now = datetime.now(timezone.utc)
|
|
282
|
+
until_dt = _parse_time_arg(until, default=now)
|
|
283
|
+
since_dt = _parse_time_arg(since, default=until_dt - _DEFAULT_STATS_WINDOW)
|
|
284
|
+
if since_dt >= until_dt:
|
|
285
|
+
raise UsageError("--since must be before --until.")
|
|
286
|
+
|
|
287
|
+
history_duration: timedelta | None = _DEFAULT_STATS_WINDOW if since is None else None
|
|
288
|
+
if since is not None and until is None:
|
|
289
|
+
try:
|
|
290
|
+
history_duration = parse_duration(since)
|
|
291
|
+
except ValueError:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
environment_name = _get_environment_name(ensure_env(env))
|
|
295
|
+
client = await _Client.from_env()
|
|
296
|
+
function_id = await _resolve_function_id(client, function_identifier, environment_name)
|
|
297
|
+
req = api_pb2.FunctionGetTimeRangeStatsRequest(
|
|
298
|
+
function_id=function_id,
|
|
299
|
+
since=_timestamp(since_dt),
|
|
300
|
+
until=_timestamp(until_dt),
|
|
301
|
+
rollup=all_variants,
|
|
302
|
+
)
|
|
303
|
+
if container:
|
|
304
|
+
req.container_id = container
|
|
305
|
+
|
|
306
|
+
resp = await client.stub.FunctionGetTimeRangeStats(req)
|
|
307
|
+
|
|
308
|
+
if json_output:
|
|
309
|
+
OutputManager.get().print_json(json_lib.dumps(_stats_json(function_id, resp, all_variants)))
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
output = OutputManager.get()
|
|
313
|
+
output.print("")
|
|
314
|
+
heading = f"Function stats for {function_id}"
|
|
315
|
+
if all_variants:
|
|
316
|
+
variant_label = "variant" if resp.variant_count == 1 else "variants"
|
|
317
|
+
heading += f" · all variants ({resp.variant_count:,} {variant_label})"
|
|
318
|
+
use_color = not no_color
|
|
319
|
+
output.print(Text(heading, style=stats_style(STATS_HEADING_STYLE, use_color)))
|
|
320
|
+
output.print("")
|
|
321
|
+
since_label = resp.since.ToDatetime(tzinfo=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
322
|
+
until_label = resp.until.ToDatetime(tzinfo=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
323
|
+
range_heading = ""
|
|
324
|
+
if history_duration is not None:
|
|
325
|
+
range_heading = f"Duration {str(history_duration)} · "
|
|
326
|
+
output.print(
|
|
327
|
+
Text(
|
|
328
|
+
f"{range_heading}{since_label} to {until_label} UTC",
|
|
329
|
+
style=stats_style(STATS_METADATA_STYLE, use_color),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
output.print("")
|
|
333
|
+
|
|
334
|
+
completed_input_count = resp.input_success_count + resp.input_failure_count + resp.input_timeout_count
|
|
335
|
+
input_count = completed_input_count + resp.input_running_at_end_count
|
|
336
|
+
input_heading = Text("Inputs", style=stats_style(STATS_SECTION_STYLE, use_color))
|
|
337
|
+
input_heading.append(" " * (46 - len("Inputs")))
|
|
338
|
+
input_heading.append(f"{input_count:,} total (")
|
|
339
|
+
input_heading.append(
|
|
340
|
+
f"{resp.input_running_at_end_count:,} running",
|
|
341
|
+
style=progress_style(resp.input_running_at_end_count, use_color),
|
|
342
|
+
)
|
|
343
|
+
input_heading.append(")")
|
|
344
|
+
output.print(input_heading)
|
|
345
|
+
|
|
346
|
+
input_counts = Text(" ")
|
|
347
|
+
input_counts.append(
|
|
348
|
+
_count_with_percentage(resp.input_success_count, completed_input_count, "succeeded"),
|
|
349
|
+
style=success_style(resp.input_success_count, completed_input_count, use_color),
|
|
350
|
+
)
|
|
351
|
+
input_counts.append(" · ")
|
|
352
|
+
input_counts.append(
|
|
353
|
+
_count_with_percentage(resp.input_failure_count, completed_input_count, "failed"),
|
|
354
|
+
style=problem_style(resp.input_failure_count, completed_input_count, use_color),
|
|
355
|
+
)
|
|
356
|
+
input_counts.append(" · ")
|
|
357
|
+
input_counts.append(
|
|
358
|
+
_count_with_percentage(resp.input_timeout_count, completed_input_count, "timed out"),
|
|
359
|
+
style=problem_style(resp.input_timeout_count, completed_input_count, use_color),
|
|
360
|
+
)
|
|
361
|
+
output.print(input_counts)
|
|
362
|
+
input_rows = _metric_rows(
|
|
363
|
+
cast(dict[str, api_pb2.StatsPercentileDistribution], resp.input_percentile_stats),
|
|
364
|
+
_INPUT_METRIC_ORDER,
|
|
365
|
+
use_color,
|
|
366
|
+
)
|
|
367
|
+
if input_rows:
|
|
368
|
+
output.print("")
|
|
369
|
+
output.print(_percentile_table(input_rows, use_color))
|
|
370
|
+
|
|
371
|
+
output.print("")
|
|
372
|
+
container_count = resp.container_started_count + resp.container_error_count
|
|
373
|
+
container_heading = Text("Containers", style=stats_style(STATS_SECTION_STYLE, use_color))
|
|
374
|
+
container_heading.append(" " * (46 - len("Containers")))
|
|
375
|
+
container_heading.append(f"{container_count:,} total (")
|
|
376
|
+
container_heading.append(
|
|
377
|
+
f"{resp.container_creating_at_end_count:,} creating",
|
|
378
|
+
style=progress_style(resp.container_creating_at_end_count, use_color),
|
|
379
|
+
)
|
|
380
|
+
container_heading.append(")")
|
|
381
|
+
output.print(container_heading)
|
|
382
|
+
|
|
383
|
+
container_counts = Text(" ")
|
|
384
|
+
container_counts.append(
|
|
385
|
+
_count_with_percentage(resp.container_started_count, container_count, "started"),
|
|
386
|
+
style=success_style(resp.container_started_count, container_count, use_color),
|
|
387
|
+
)
|
|
388
|
+
container_counts.append(" · ")
|
|
389
|
+
container_counts.append(
|
|
390
|
+
_count_with_percentage(resp.container_error_count, container_count, "errored"),
|
|
391
|
+
style=problem_style(resp.container_error_count, container_count, use_color),
|
|
392
|
+
)
|
|
393
|
+
output.print(container_counts)
|
|
394
|
+
|
|
395
|
+
container_rows = _metric_rows(
|
|
396
|
+
cast(dict[str, api_pb2.StatsPercentileDistribution], resp.container_percentile_stats),
|
|
397
|
+
_CONTAINER_METRIC_ORDER,
|
|
398
|
+
use_color,
|
|
399
|
+
)
|
|
400
|
+
if container_rows:
|
|
401
|
+
output.print("")
|
|
402
|
+
output.print(_percentile_table(container_rows, use_color))
|
|
@@ -58,7 +58,7 @@ async def logs(image_id: str, layers: int | None, all_layers: bool) -> None:
|
|
|
58
58
|
effective_layers = None if all_layers else layers or 1
|
|
59
59
|
image = Image.from_id(image_id)
|
|
60
60
|
output_mgr = OutputManager.get()
|
|
61
|
-
last_message =
|
|
61
|
+
last_message: str | None = None
|
|
62
62
|
async for entry in image.logs.fetch.aio(layers=effective_layers):
|
|
63
63
|
last_message = entry.message
|
|
64
64
|
await output_mgr.put_fetched_log(
|
|
@@ -69,7 +69,9 @@ async def logs(image_id: str, layers: int | None, all_layers: bool) -> None:
|
|
|
69
69
|
)
|
|
70
70
|
)
|
|
71
71
|
output_mgr.flush_lines()
|
|
72
|
-
if last_message
|
|
72
|
+
if last_message is None:
|
|
73
|
+
output_mgr.print(f"No build logs found for Image {image_id}.")
|
|
74
|
+
elif not last_message.endswith("\n"):
|
|
73
75
|
output_mgr.print("")
|
|
74
76
|
|
|
75
77
|
|
|
@@ -27,6 +27,7 @@ from modal.exception import InvalidError, _CliUserExecutionError
|
|
|
27
27
|
from modal.functions import Function
|
|
28
28
|
from modal.output import OutputManager
|
|
29
29
|
|
|
30
|
+
from .._runtime import execution_context
|
|
30
31
|
from .._utils.async_utils import synchronizer
|
|
31
32
|
|
|
32
33
|
|
|
@@ -74,7 +75,8 @@ def import_file_or_module(import_ref: ImportRef, base_cmd: str = ""):
|
|
|
74
75
|
f"Use `{base_cmd} -m {import_ref.file_or_module}` instead.",
|
|
75
76
|
)
|
|
76
77
|
try:
|
|
77
|
-
|
|
78
|
+
with execution_context._import_context():
|
|
79
|
+
module = importlib.import_module(import_ref.file_or_module)
|
|
78
80
|
except Exception as exc:
|
|
79
81
|
raise _CliUserExecutionError(import_ref.file_or_module) from exc
|
|
80
82
|
else:
|
|
@@ -97,7 +99,8 @@ def import_file_or_module(import_ref: ImportRef, base_cmd: str = ""):
|
|
|
97
99
|
sys.modules[module_name] = module
|
|
98
100
|
try:
|
|
99
101
|
assert spec.loader
|
|
100
|
-
|
|
102
|
+
with execution_context._import_context():
|
|
103
|
+
spec.loader.exec_module(module)
|
|
101
104
|
except Exception as exc:
|
|
102
105
|
if isinstance(exc, ImportError) and "attempted relative import with no known parent package" in str(exc):
|
|
103
106
|
if import_ref.file_or_module and import_ref.command and not import_ref.use_module_mode:
|