modal 1.6.2.dev4__tar.gz → 1.6.2.dev6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/PKG-INFO +1 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/__init__.py +1 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_functions.py +6 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_logs.py +82 -54
- modal-1.6.2.dev6/modal/_metrics.py +212 -0
- modal-1.6.2.dev6/modal/_metrics.pyi +312 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_outbound_policy.py +3 -3
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_outbound_policy.pyi +2 -2
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_server.py +6 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_server.pyi +6 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/time_utils.py +22 -39
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/_logs.py +31 -3
- modal-1.6.2.dev6/modal/cli/_stats.py +334 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/container.py +2 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/function.py +199 -19
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/server.py +185 -14
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/shell.py +2 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/utils.py +1 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/client.pyi +2 -2
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/container_process.py +1 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/container_process.pyi +3 -3
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/functions.pyi +12 -6
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/io_streams.py +1 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/io_streams.pyi +9 -9
- modal-1.6.2.dev6/modal/sandbox/_common.py +248 -0
- modal-1.6.2.dev4/modal/sandbox_fs.py → modal-1.6.2.dev6/modal/sandbox/_filesystem.py +32 -27
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/sandbox/_sandbox.py +64 -852
- modal-1.6.2.dev6/modal/sandbox/_sidecar.py +646 -0
- modal-1.6.2.dev4/modal/snapshot.py → modal-1.6.2.dev6/modal/sandbox/_snapshot.py +5 -8
- modal-1.6.2.dev4/modal/_utils/task_command_router_client.py → modal-1.6.2.dev6/modal/sandbox/_task_command_router_client.py +103 -46
- modal-1.6.2.dev6/modal/sandbox/sandbox.py +11 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/sandbox/sandbox.pyi +811 -30
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/server.pyi +6 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/types.py +53 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal.egg-info/SOURCES.txt +7 -5
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/api_grpc.py +192 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/api_pb2.py +1496 -1000
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/api_pb2.pyi +1227 -116
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/api_pb2_grpc.py +400 -2
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/api_pb2_grpc.pyi +130 -6
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/modal_api_grpc.py +12 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/task_command_router_grpc.py +5 -5
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/task_command_router_pb2.py +49 -49
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/task_command_router_pb2.pyi +22 -22
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/task_command_router_pb2_grpc.py +8 -7
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/task_command_router_pb2_grpc.pyi +10 -6
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_version/__init__.py +1 -1
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/pyproject.toml +2 -0
- modal-1.6.2.dev4/modal/cli/_stats.py +0 -141
- modal-1.6.2.dev4/modal/sandbox/sandbox.py +0 -9
- modal-1.6.2.dev4/modal/sandbox_fs.pyi +0 -1092
- modal-1.6.2.dev4/modal/snapshot.pyi +0 -85
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/LICENSE +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/README.md +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/__main__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_billing.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_cluster.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_cluster.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_environments.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_function_variants.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_grpc_client.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_image.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_ipython.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_load_context.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_location.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_logs_manager.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_object.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_output/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_output/manager.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_output/pty.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_output/rich.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_output/status.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_partial_function.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_resolver.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_resources.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_serialization.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_supports_logs.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_traceback.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_tunnel.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_tunnel.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_type_manager.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/async_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/blob_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/docker_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/logger.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/name_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_vendor/version.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_watcher.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/_workspace.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/app.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/app.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/billing.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/billing.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/builder/2025.06.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/builder/README.md +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/builder/base-images.json +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/call_graph.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/_download.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/_help.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/_traceback.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/app.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/billing.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/changelog.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/config.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/curl.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/dashboard.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/dict.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/endpoint.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/entry_point.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/environment.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/image.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/import_refs.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/logo.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/network_file_system.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/profile.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/queues.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/run.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/secret.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/selector.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/skills.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/token.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/volume.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cli/workspace.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/client.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cls.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cls.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cluster.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/cluster.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/config.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/dict.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/dict.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/environments.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/environments.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/exception.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/experimental/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/experimental/flash.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/experimental/ipython.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/functions.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/image.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/image.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/mount.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/mount.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/network_file_system.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/network_file_system.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/object.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/object.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/output.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/parallel_map.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/parallel_map.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/partial_function.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/partial_function.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/proxy.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/proxy.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/py.typed +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/queue.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/queue.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/retries.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/runner.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/runner.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/running_app.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/sandbox/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/schedule.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/scheduler_placement.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/secret.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/secret.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/server.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/serving.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/serving.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/stream_type.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/token_flow.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/token_flow.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/volume.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/volume.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/workspace.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal/workspace.pyi +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal.egg-info/requires.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/__init__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_proto/py.typed +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/modal_version/__main__.py +0 -0
- {modal-1.6.2.dev4 → modal-1.6.2.dev6}/setup.cfg +0 -0
|
@@ -43,11 +43,11 @@ try:
|
|
|
43
43
|
from .queue import Queue
|
|
44
44
|
from .retries import Retries
|
|
45
45
|
from .sandbox import Probe, Sandbox
|
|
46
|
+
from .sandbox.sandbox import SandboxSnapshot
|
|
46
47
|
from .schedule import Cron, Period
|
|
47
48
|
from .scheduler_placement import SchedulerPlacement
|
|
48
49
|
from .secret import Secret
|
|
49
50
|
from .server import Server
|
|
50
|
-
from .snapshot import SandboxSnapshot
|
|
51
51
|
from .volume import Volume
|
|
52
52
|
from .workspace import Workspace
|
|
53
53
|
except Exception:
|
|
@@ -27,6 +27,7 @@ from ._image import _Image
|
|
|
27
27
|
from ._load_context import LoadContext
|
|
28
28
|
from ._logs import LogsFilters
|
|
29
29
|
from ._logs_manager import _FunctionCallLogsManager, _FunctionLogsManager
|
|
30
|
+
from ._metrics import _FunctionMetricsManager
|
|
30
31
|
from ._object import _Object, live_method, live_method_gen
|
|
31
32
|
from ._output.pty import get_pty_info
|
|
32
33
|
from ._output.status import FunctionCreationStatus
|
|
@@ -661,6 +662,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
661
662
|
source_object_id=self.object_id,
|
|
662
663
|
)
|
|
663
664
|
|
|
665
|
+
@property
|
|
666
|
+
def metrics(self) -> "_FunctionMetricsManager":
|
|
667
|
+
"""Access metric samples with `export()` and series definitions with `schema()`."""
|
|
668
|
+
return _FunctionMetricsManager(self)
|
|
669
|
+
|
|
664
670
|
@property
|
|
665
671
|
def logs(self) -> _FunctionLogsManager:
|
|
666
672
|
"""Access logs for a `Function`.
|
|
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import dataclasses
|
|
6
|
-
from collections.abc import AsyncGenerator
|
|
6
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
7
7
|
from datetime import datetime, timedelta, timezone
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING, Sequence, TypeVar
|
|
9
9
|
|
|
10
10
|
from google.protobuf.timestamp_pb2 import Timestamp
|
|
11
11
|
|
|
@@ -13,6 +13,9 @@ from modal._utils.async_utils import TaskContext
|
|
|
13
13
|
from modal.exception import LogsFetchError
|
|
14
14
|
from modal_proto import api_pb2
|
|
15
15
|
|
|
16
|
+
_T = TypeVar("_T")
|
|
17
|
+
_CountRanges = Callable[[float, float, int], Awaitable[list[tuple[float, float, int]]]]
|
|
18
|
+
|
|
16
19
|
if TYPE_CHECKING:
|
|
17
20
|
from modal.client import _Client
|
|
18
21
|
|
|
@@ -194,6 +197,32 @@ def _next_smaller_bucket_secs(current_bucket_secs: int) -> int | None:
|
|
|
194
197
|
return None
|
|
195
198
|
|
|
196
199
|
|
|
200
|
+
async def _count_log_ranges(
|
|
201
|
+
client: _Client,
|
|
202
|
+
app_id: str,
|
|
203
|
+
since: float,
|
|
204
|
+
until: float,
|
|
205
|
+
bucket_secs: int,
|
|
206
|
+
filters: LogsFilters,
|
|
207
|
+
) -> list[tuple[float, float, int]]:
|
|
208
|
+
count_req = api_pb2.AppCountLogsRequest(
|
|
209
|
+
app_id=app_id,
|
|
210
|
+
since=_seconds_to_timestamp(since),
|
|
211
|
+
until=_seconds_to_timestamp(until),
|
|
212
|
+
bucket_secs=bucket_secs,
|
|
213
|
+
source=filters.source,
|
|
214
|
+
function_id=filters.function_id,
|
|
215
|
+
parametrized_function_id=filters.parametrized_function_id,
|
|
216
|
+
function_call_id=filters.function_call_id,
|
|
217
|
+
task_id=filters.task_id,
|
|
218
|
+
sandbox_id=filters.sandbox_id,
|
|
219
|
+
search_text=filters.search_text,
|
|
220
|
+
)
|
|
221
|
+
count_resp: api_pb2.AppCountLogsResponse = await client._stub.AppCountLogs(count_req)
|
|
222
|
+
|
|
223
|
+
return _buckets_to_ranges(list(count_resp.buckets), bucket_secs)
|
|
224
|
+
|
|
225
|
+
|
|
197
226
|
async def _refine_dense_ranges(
|
|
198
227
|
client: _Client,
|
|
199
228
|
app_id: str,
|
|
@@ -201,6 +230,19 @@ async def _refine_dense_ranges(
|
|
|
201
230
|
filters: LogsFilters,
|
|
202
231
|
max_ranges: int,
|
|
203
232
|
max_iterations: int,
|
|
233
|
+
) -> list[tuple[float, float, int]]:
|
|
234
|
+
async def count_ranges(start: float, end: float, bucket_secs: int) -> list[tuple[float, float, int]]:
|
|
235
|
+
return await _count_log_ranges(client, app_id, start, end, bucket_secs, filters)
|
|
236
|
+
|
|
237
|
+
return await _refine_ranges(ranges, count_ranges, max_ranges, max_iterations)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def _refine_ranges(
|
|
241
|
+
ranges: list[tuple[float, float, int]],
|
|
242
|
+
count_ranges: _CountRanges,
|
|
243
|
+
max_ranges: int,
|
|
244
|
+
max_iterations: int,
|
|
245
|
+
max_concurrent_counts: int = _MAX_CONCURRENT_COUNTS,
|
|
204
246
|
) -> list[tuple[float, float, int]]:
|
|
205
247
|
"""Refine ranges exceeding _FETCH_LIMIT by subdividing with smaller buckets.
|
|
206
248
|
|
|
@@ -239,25 +281,11 @@ async def _refine_dense_ranges(
|
|
|
239
281
|
break
|
|
240
282
|
|
|
241
283
|
# Fire off all re-count RPCs in parallel with bounded concurrency
|
|
242
|
-
semaphore = asyncio.Semaphore(
|
|
284
|
+
semaphore = asyncio.Semaphore(max_concurrent_counts)
|
|
243
285
|
|
|
244
286
|
async def _recount(start: float, end: float, smaller_secs: int) -> list[tuple[float, float, int]]:
|
|
245
287
|
async with semaphore:
|
|
246
|
-
|
|
247
|
-
app_id=app_id,
|
|
248
|
-
since=_seconds_to_timestamp(start),
|
|
249
|
-
until=_seconds_to_timestamp(end),
|
|
250
|
-
bucket_secs=smaller_secs,
|
|
251
|
-
source=filters.source,
|
|
252
|
-
function_id=filters.function_id,
|
|
253
|
-
parametrized_function_id=filters.parametrized_function_id,
|
|
254
|
-
function_call_id=filters.function_call_id,
|
|
255
|
-
task_id=filters.task_id,
|
|
256
|
-
sandbox_id=filters.sandbox_id,
|
|
257
|
-
search_text=filters.search_text,
|
|
258
|
-
)
|
|
259
|
-
sub_resp = await client._stub.AppCountLogs(sub_req)
|
|
260
|
-
sub_ranges = _buckets_to_ranges(list(sub_resp.buckets), smaller_secs)
|
|
288
|
+
sub_ranges = await count_ranges(start, end, smaller_secs)
|
|
261
289
|
# Clamp the edge sub-ranges to the parent boundaries. Bucket
|
|
262
290
|
# alignment can make the first bucket begin before the parent
|
|
263
291
|
# (e.g. parent (6,12) with 4s buckets produces (4,8)) or make
|
|
@@ -418,26 +446,34 @@ async def fetch_logs(
|
|
|
418
446
|
|
|
419
447
|
if until - since > _MAX_FETCH_RANGE:
|
|
420
448
|
raise LogsFetchError(f"Time range cannot exceed {_MAX_FETCH_RANGE.days} days.")
|
|
421
|
-
# Phase 1: Count logs per bucket
|
|
422
|
-
bucket_secs = _pick_bucket_secs(since, until)
|
|
423
|
-
count_req = api_pb2.AppCountLogsRequest(
|
|
424
|
-
app_id=app_id,
|
|
425
|
-
since=_datetime_to_timestamp(since),
|
|
426
|
-
until=_datetime_to_timestamp(until),
|
|
427
|
-
bucket_secs=bucket_secs,
|
|
428
|
-
source=filters.source,
|
|
429
|
-
function_id=filters.function_id,
|
|
430
|
-
parametrized_function_id=filters.parametrized_function_id,
|
|
431
|
-
function_call_id=filters.function_call_id,
|
|
432
|
-
task_id=filters.task_id,
|
|
433
|
-
sandbox_id=filters.sandbox_id,
|
|
434
|
-
search_text=filters.search_text,
|
|
435
|
-
)
|
|
436
|
-
count_resp: api_pb2.AppCountLogsResponse = await client._stub.AppCountLogs(count_req)
|
|
437
449
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
450
|
+
async def count_ranges(start: float, end: float, bucket_secs: int) -> list[tuple[float, float, int]]:
|
|
451
|
+
return await _count_log_ranges(client, app_id, start, end, bucket_secs, filters)
|
|
452
|
+
|
|
453
|
+
async def fetch_interval(start: float, end: float, limit: int) -> list[api_pb2.TaskLogsBatch]:
|
|
454
|
+
return await _fetch_interval(client, app_id, start, end, limit, filters)
|
|
455
|
+
|
|
456
|
+
async for batch in _fetch_time_range(
|
|
457
|
+
since,
|
|
458
|
+
until,
|
|
459
|
+
count_ranges,
|
|
460
|
+
fetch_interval,
|
|
461
|
+
):
|
|
462
|
+
yield batch
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
async def _fetch_time_range(
|
|
466
|
+
since: datetime,
|
|
467
|
+
until: datetime,
|
|
468
|
+
count_ranges: _CountRanges,
|
|
469
|
+
fetch_interval: Callable[[float, float, int], Awaitable[Sequence[_T]]],
|
|
470
|
+
*,
|
|
471
|
+
max_concurrent_counts: int = _MAX_CONCURRENT_COUNTS,
|
|
472
|
+
) -> AsyncGenerator[_T]:
|
|
473
|
+
"""Count, refine, and fetch time intervals with bounded concurrency and ordered results."""
|
|
474
|
+
ranges = await count_ranges(since.timestamp(), until.timestamp(), _pick_bucket_secs(since, until))
|
|
475
|
+
total_records = sum(count for _, _, count in ranges)
|
|
476
|
+
if total_records == 0:
|
|
441
477
|
return
|
|
442
478
|
|
|
443
479
|
# Trim leading/trailing empty buckets so they don't consume refinement
|
|
@@ -449,19 +485,17 @@ async def fetch_logs(
|
|
|
449
485
|
|
|
450
486
|
# Phase 1b: Refine any ranges that exceed the per-fetch limit.
|
|
451
487
|
# Budget: at most _MAX_FETCHES intervals, each fetching up to _FETCH_LIMIT entries.
|
|
452
|
-
ranges = await
|
|
453
|
-
client,
|
|
454
|
-
app_id,
|
|
488
|
+
ranges = await _refine_ranges(
|
|
455
489
|
ranges,
|
|
456
|
-
|
|
490
|
+
count_ranges,
|
|
457
491
|
max_ranges=_MAX_FETCHES,
|
|
458
492
|
max_iterations=_MAX_REFINE_ITERATIONS,
|
|
493
|
+
max_concurrent_counts=max_concurrent_counts,
|
|
459
494
|
)
|
|
460
|
-
fetch_error_message = "Too many logs to fetch in time range. Consider narrowing the range or adding filters."
|
|
461
495
|
# Check that all ranges fit within the per-fetch limit.
|
|
462
496
|
over_limit = [r for r in ranges if r[2] > _FETCH_LIMIT]
|
|
463
497
|
if over_limit:
|
|
464
|
-
raise LogsFetchError(
|
|
498
|
+
raise LogsFetchError("Too many logs to fetch in time range. Consider narrowing the range or adding filters.")
|
|
465
499
|
|
|
466
500
|
# Phase 2: Build intervals from non-empty ranges and fetch in parallel
|
|
467
501
|
since_secs = since.timestamp()
|
|
@@ -476,24 +510,17 @@ async def fetch_logs(
|
|
|
476
510
|
return
|
|
477
511
|
|
|
478
512
|
if len(intervals) > _MAX_FETCHES:
|
|
479
|
-
raise LogsFetchError(
|
|
513
|
+
raise LogsFetchError("Too many logs to fetch in time range. Consider narrowing the range or adding filters.")
|
|
480
514
|
|
|
481
515
|
# Fetch all intervals concurrently (bounded by semaphore), but yield
|
|
482
|
-
# results in interval order so
|
|
516
|
+
# results in interval order so records stay chronological. Awaiting each
|
|
483
517
|
# task in sequence lets us stream batches to the caller as soon as all
|
|
484
518
|
# preceding intervals have completed.
|
|
485
519
|
semaphore = asyncio.Semaphore(_MAX_CONCURRENT_FETCHES)
|
|
486
520
|
|
|
487
|
-
async def _bounded_fetch(since_s: float, until_s: float) ->
|
|
521
|
+
async def _bounded_fetch(since_s: float, until_s: float) -> Sequence[_T]:
|
|
488
522
|
async with semaphore:
|
|
489
|
-
return await
|
|
490
|
-
client,
|
|
491
|
-
app_id,
|
|
492
|
-
since_s,
|
|
493
|
-
until_s,
|
|
494
|
-
_FETCH_LIMIT,
|
|
495
|
-
filters,
|
|
496
|
-
)
|
|
523
|
+
return await fetch_interval(since_s, until_s, _FETCH_LIMIT)
|
|
497
524
|
|
|
498
525
|
tasks = [asyncio.create_task(_bounded_fetch(s, u)) for s, u in intervals]
|
|
499
526
|
|
|
@@ -504,3 +531,4 @@ async def fetch_logs(
|
|
|
504
531
|
finally:
|
|
505
532
|
for task in tasks:
|
|
506
533
|
task.cancel()
|
|
534
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
|
|
6
|
+
from modal_proto import api_pb2
|
|
7
|
+
|
|
8
|
+
from ._object import _Object
|
|
9
|
+
from ._utils.async_utils import synchronize_api
|
|
10
|
+
from .exception import InvalidError
|
|
11
|
+
from .types import MetricDefinition, MetricSeries, MetricsSchema, ObjectMetrics, TimeSeriesPoint
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _metrics_request(
|
|
15
|
+
since: datetime | None, until: datetime | None, bucket_size: timedelta | None, groups: list[str] | None
|
|
16
|
+
) -> api_pb2.FunctionGetMetricsRequest:
|
|
17
|
+
until = (until or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
|
18
|
+
since = (since or until - timedelta(hours=1)).astimezone(timezone.utc)
|
|
19
|
+
if since >= until:
|
|
20
|
+
raise InvalidError("`since` must be before `until`.")
|
|
21
|
+
if since.timestamp() < 0:
|
|
22
|
+
raise InvalidError("`since` must be at or after the Unix epoch.")
|
|
23
|
+
if until - since > timedelta(days=31):
|
|
24
|
+
raise InvalidError("Metrics time range cannot exceed 31 days.")
|
|
25
|
+
if groups is not None and (not isinstance(groups, list) or any(not isinstance(g, str) or not g for g in groups)):
|
|
26
|
+
raise InvalidError("`groups` must be a list of nonempty group names.")
|
|
27
|
+
request = api_pb2.FunctionGetMetricsRequest(groups=groups or [])
|
|
28
|
+
request.since.FromDatetime(since)
|
|
29
|
+
request.until.FromDatetime(until)
|
|
30
|
+
if bucket_size is not None:
|
|
31
|
+
if not isinstance(bucket_size, timedelta):
|
|
32
|
+
raise InvalidError("`bucket_size` must be a timedelta.")
|
|
33
|
+
seconds = bucket_size.total_seconds()
|
|
34
|
+
if not seconds.is_integer() or not 60 <= seconds <= 2**32 - 1:
|
|
35
|
+
raise InvalidError("`bucket_size` must be a whole number of seconds, at least one minute.")
|
|
36
|
+
request.bucket_secs = int(seconds)
|
|
37
|
+
if int(until.timestamp()) // request.bucket_secs - int(since.timestamp()) // request.bucket_secs > 500:
|
|
38
|
+
raise InvalidError("Metrics are limited to 500 points per series; increase `bucket_size`.")
|
|
39
|
+
return request
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _object_metrics(
|
|
43
|
+
object_id: str, response: api_pb2.FunctionGetMetricsResponse | api_pb2.ServerGetMetricsResponse
|
|
44
|
+
) -> ObjectMetrics:
|
|
45
|
+
series = {
|
|
46
|
+
item.name: MetricSeries(
|
|
47
|
+
name=item.name,
|
|
48
|
+
unit=item.unit,
|
|
49
|
+
aggregation=item.name.rsplit("_", 1)[-1],
|
|
50
|
+
description=item.description,
|
|
51
|
+
points=[
|
|
52
|
+
TimeSeriesPoint(
|
|
53
|
+
time=point.timestamp.ToDatetime(tzinfo=timezone.utc),
|
|
54
|
+
value=point.value if point.HasField("value") else None,
|
|
55
|
+
)
|
|
56
|
+
for point in item.points
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
for item in response.series
|
|
60
|
+
}
|
|
61
|
+
return ObjectMetrics(
|
|
62
|
+
object_id=object_id,
|
|
63
|
+
since=response.since.ToDatetime(tzinfo=timezone.utc),
|
|
64
|
+
until=response.until.ToDatetime(tzinfo=timezone.utc),
|
|
65
|
+
bucket_size=timedelta(seconds=response.bucket_secs),
|
|
66
|
+
point_count=sum(len(item.points) for item in series.values()),
|
|
67
|
+
series=series,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _MetricsManager:
|
|
72
|
+
"""mdmd:namespace"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, function: _Object, *, is_server: bool):
|
|
75
|
+
"""mdmd:hidden"""
|
|
76
|
+
self._function = function
|
|
77
|
+
self._is_server = is_server
|
|
78
|
+
|
|
79
|
+
async def schema(self, groups: list[str] | None = None) -> dict[str, MetricsSchema]:
|
|
80
|
+
"""Return metric definitions keyed by group name without fetching samples.
|
|
81
|
+
|
|
82
|
+
Omit `groups` to list all groups. Each group contains its exact individual
|
|
83
|
+
series definitions.
|
|
84
|
+
"""
|
|
85
|
+
response = await self._schema(groups=groups)
|
|
86
|
+
schema: dict[str, MetricsSchema] = {}
|
|
87
|
+
for row in response.groups:
|
|
88
|
+
if row.name not in schema:
|
|
89
|
+
schema[row.name] = MetricsSchema(name=row.name, definitions=[])
|
|
90
|
+
schema[row.name].definitions.extend(
|
|
91
|
+
MetricDefinition(name=m.name, unit=m.unit, description=m.description) for m in row.metrics
|
|
92
|
+
)
|
|
93
|
+
return schema
|
|
94
|
+
|
|
95
|
+
async def _schema(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
since: datetime | None = None,
|
|
99
|
+
until: datetime | None = None,
|
|
100
|
+
bucket_size: timedelta | None = None,
|
|
101
|
+
groups: list[str] | None = None,
|
|
102
|
+
) -> api_pb2.MetricsGetInfoResponse:
|
|
103
|
+
"""Fetch schema rows and resolved bucket metadata for a preview window."""
|
|
104
|
+
window = _metrics_request(since, until, bucket_size, groups)
|
|
105
|
+
await self._function.hydrate()
|
|
106
|
+
target = api_pb2.MetricsGetInfoRequest
|
|
107
|
+
request = target(
|
|
108
|
+
target_type=target.METRICS_TARGET_TYPE_SERVER if self._is_server else target.METRICS_TARGET_TYPE_FUNCTION,
|
|
109
|
+
since=window.since,
|
|
110
|
+
until=window.until,
|
|
111
|
+
groups=groups,
|
|
112
|
+
)
|
|
113
|
+
if window.HasField("bucket_secs"):
|
|
114
|
+
request.bucket_secs = window.bucket_secs
|
|
115
|
+
return await self._function.client._stub.MetricsGetInfo(request)
|
|
116
|
+
|
|
117
|
+
async def _export(
|
|
118
|
+
self,
|
|
119
|
+
since: datetime | None,
|
|
120
|
+
until: datetime | None,
|
|
121
|
+
bucket_size: timedelta | None,
|
|
122
|
+
groups: list[str] | None,
|
|
123
|
+
all_variants: bool = False,
|
|
124
|
+
) -> ObjectMetrics:
|
|
125
|
+
request = _metrics_request(since, until, bucket_size, groups)
|
|
126
|
+
await self._function.hydrate()
|
|
127
|
+
request.function_id = self._function.object_id
|
|
128
|
+
if self._is_server:
|
|
129
|
+
server_request = api_pb2.ServerGetMetricsRequest(
|
|
130
|
+
function_id=request.function_id,
|
|
131
|
+
since=request.since,
|
|
132
|
+
until=request.until,
|
|
133
|
+
groups=request.groups,
|
|
134
|
+
)
|
|
135
|
+
if request.HasField("bucket_secs"):
|
|
136
|
+
server_request.bucket_secs = request.bucket_secs
|
|
137
|
+
response = await self._function.client._stub.ServerGetMetrics(server_request)
|
|
138
|
+
else:
|
|
139
|
+
request.rollup = all_variants
|
|
140
|
+
response = await self._function.client._stub.FunctionGetMetrics(request)
|
|
141
|
+
return _object_metrics(self._function.object_id, response)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class _FunctionMetricsManager(_MetricsManager):
|
|
145
|
+
"""mdmd:namespace"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, function: _Object):
|
|
148
|
+
"""mdmd:hidden"""
|
|
149
|
+
super().__init__(function, is_server=False)
|
|
150
|
+
|
|
151
|
+
async def export(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
since: datetime | None = None,
|
|
155
|
+
until: datetime | None = None,
|
|
156
|
+
bucket_size: timedelta | None = None,
|
|
157
|
+
groups: list[str] | None = None,
|
|
158
|
+
all_variants: bool = False,
|
|
159
|
+
) -> ObjectMetrics:
|
|
160
|
+
"""Fetch metric samples for a Function.
|
|
161
|
+
|
|
162
|
+
Defaults to all groups over the last hour. `since` defaults to one hour
|
|
163
|
+
before `until`; naive datetimes use local time. The maximum range is 31
|
|
164
|
+
days, with at most 500 points per series. If `bucket_size` is omitted,
|
|
165
|
+
Modal selects a supported width targeting approximately 100 buckets.
|
|
166
|
+
Bounds round down to UTC bucket boundaries, so data before `since` may
|
|
167
|
+
be included and the partial bucket at `until` is excluded.
|
|
168
|
+
|
|
169
|
+
Set `all_variants=True` to include the root Function and its variants.
|
|
170
|
+
Missing observations have value None. Use `schema()` to discover
|
|
171
|
+
group names and series definitions without fetching samples.
|
|
172
|
+
|
|
173
|
+
```python notest
|
|
174
|
+
metrics = function.metrics.export(bucket_size=timedelta(minutes=5), groups=["calls"])
|
|
175
|
+
for point in metrics["successful_calls_count"].points:
|
|
176
|
+
print(point.time, point.value)
|
|
177
|
+
```
|
|
178
|
+
"""
|
|
179
|
+
return await self._export(since, until, bucket_size, groups, all_variants)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _ServerMetricsManager(_MetricsManager):
|
|
183
|
+
"""mdmd:namespace"""
|
|
184
|
+
|
|
185
|
+
def __init__(self, function: _Object):
|
|
186
|
+
"""mdmd:hidden"""
|
|
187
|
+
super().__init__(function, is_server=True)
|
|
188
|
+
|
|
189
|
+
async def export(
|
|
190
|
+
self,
|
|
191
|
+
*,
|
|
192
|
+
since: datetime | None = None,
|
|
193
|
+
until: datetime | None = None,
|
|
194
|
+
bucket_size: timedelta | None = None,
|
|
195
|
+
groups: list[str] | None = None,
|
|
196
|
+
) -> ObjectMetrics:
|
|
197
|
+
"""Fetch metric samples for a Server.
|
|
198
|
+
|
|
199
|
+
Defaults to all groups over the last hour. `since` defaults to one hour
|
|
200
|
+
before `until`; naive datetimes use local time. The maximum range is 31
|
|
201
|
+
days, with at most 500 points per series. Omit `bucket_size` for automatic
|
|
202
|
+
sizing. Bounds round down to UTC bucket boundaries; the partial bucket
|
|
203
|
+
at `until` is excluded. Missing observations have value None.
|
|
204
|
+
|
|
205
|
+
Use `schema()` to discover groups and series without fetching samples.
|
|
206
|
+
"""
|
|
207
|
+
return await self._export(since, until, bucket_size, groups)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
MetricsManager = synchronize_api(_MetricsManager, target_module=__name__)
|
|
211
|
+
FunctionMetricsManager = synchronize_api(_FunctionMetricsManager, target_module=__name__)
|
|
212
|
+
ServerMetricsManager = synchronize_api(_ServerMetricsManager, target_module=__name__)
|