modal 1.6.1.dev3__tar.gz → 1.6.2__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.1.dev3 → modal-1.6.2}/PKG-INFO +4 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/README.md +1 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/__init__.py +3 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_environments.py +2 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_functions.py +148 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_image.py +2 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_logs.py +82 -54
- modal-1.6.2/modal/_metrics.py +212 -0
- modal-1.6.2/modal/_metrics.pyi +312 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_outbound_policy.py +3 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_outbound_policy.pyi +2 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_output/status.py +10 -7
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_server.py +185 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_server.pyi +89 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/async_utils.py +7 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/blob_utils.py +2 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/name_utils.py +9 -7
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/time_utils.py +22 -39
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_workspace.py +183 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/app.py +1 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/app.pyi +2 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/builder/2025.06.txt +8 -4
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/builder/PREVIEW.txt +4 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/builder/base-images.json +4 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/_logs.py +33 -5
- modal-1.6.2/modal/cli/_stats.py +334 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/app.py +5 -5
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/container.py +5 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/dict.py +3 -7
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/endpoint.py +6 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/environment.py +3 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/function.py +477 -34
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/import_refs.py +93 -13
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/network_file_system.py +2 -4
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/queues.py +3 -7
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/run.py +55 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/secret.py +2 -4
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/server.py +493 -27
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/shell.py +8 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/utils.py +29 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/volume.py +3 -7
- modal-1.6.2/modal/cli/workspace.py +641 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/client.pyi +2 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cls.py +1 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cls.pyi +1 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/config.py +4 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/container_process.py +1 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/container_process.pyi +3 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/experimental/__init__.py +66 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/experimental/_flash_container_entry.py +1 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/functions.py +2 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/functions.pyi +161 -6
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/io_streams.py +1 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/io_streams.pyi +9 -9
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/mount.py +14 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/mount.pyi +3 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/queue.py +1 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/queue.pyi +2 -2
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/runner.py +2 -2
- modal-1.6.2/modal/sandbox/__init__.py +9 -0
- modal-1.6.2/modal/sandbox/_common.py +248 -0
- modal-1.6.1.dev3/modal/sandbox_fs.py → modal-1.6.2/modal/sandbox/_filesystem.py +35 -28
- modal-1.6.1.dev3/modal/sandbox.py → modal-1.6.2/modal/sandbox/_sandbox.py +117 -936
- modal-1.6.2/modal/sandbox/_sidecar.py +646 -0
- modal-1.6.1.dev3/modal/snapshot.py → modal-1.6.2/modal/sandbox/_snapshot.py +5 -8
- modal-1.6.1.dev3/modal/_utils/task_command_router_client.py → modal-1.6.2/modal/sandbox/_task_command_router_client.py +132 -54
- modal-1.6.2/modal/sandbox/sandbox.py +11 -0
- {modal-1.6.1.dev3/modal → modal-1.6.2/modal/sandbox}/sandbox.pyi +700 -1240
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/secret.py +13 -4
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/secret.pyi +4 -0
- modal-1.6.2/modal/server.py +6 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/server.pyi +170 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/types.py +90 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/volume.py +10 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/volume.pyi +3 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/workspace.py +3 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/workspace.pyi +239 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal.egg-info/PKG-INFO +4 -3
- {modal-1.6.1.dev3 → modal-1.6.2}/modal.egg-info/SOURCES.txt +11 -7
- {modal-1.6.1.dev3 → modal-1.6.2}/modal.egg-info/requires.txt +3 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/api_grpc.py +224 -16
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/api_pb2.py +1774 -1273
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/api_pb2.pyi +1377 -223
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/api_pb2_grpc.py +466 -35
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/api_pb2_grpc.pyi +150 -16
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/modal_api_grpc.py +14 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/task_command_router_grpc.py +5 -5
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/task_command_router_pb2.py +49 -49
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/task_command_router_pb2.pyi +22 -22
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/task_command_router_pb2_grpc.py +8 -7
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/task_command_router_pb2_grpc.pyi +10 -6
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_version/__init__.py +1 -1
- {modal-1.6.1.dev3 → modal-1.6.2}/pyproject.toml +13 -5
- modal-1.6.1.dev3/modal/cli/_stats.py +0 -141
- modal-1.6.1.dev3/modal/cli/workspace.py +0 -269
- modal-1.6.1.dev3/modal/sandbox_fs.pyi +0 -1087
- modal-1.6.1.dev3/modal/server.py +0 -5
- modal-1.6.1.dev3/modal/snapshot.pyi +0 -85
- {modal-1.6.1.dev3 → modal-1.6.2}/LICENSE +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/__main__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_billing.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_cluster.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_cluster.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_function_variants.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_grpc_client.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_ipython.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_load_context.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_location.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_logs_manager.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_object.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_output/__init__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_output/manager.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_output/pty.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_output/rich.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_partial_function.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_resolver.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_resources.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_serialization.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_supports_logs.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_traceback.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_tunnel.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_tunnel.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_type_manager.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/__init__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/docker_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/logger.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_vendor/version.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/_watcher.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/billing.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/billing.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/builder/README.md +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/call_graph.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/__init__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/_download.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/_help.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/_traceback.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/billing.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/changelog.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/config.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/curl.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/dashboard.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/entry_point.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/image.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/logo.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/profile.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/selector.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/skills.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cli/token.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/client.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cluster.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/cluster.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/dict.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/dict.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/environments.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/environments.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/exception.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/experimental/flash.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/experimental/ipython.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/image.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/image.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/network_file_system.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/network_file_system.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/object.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/object.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/output.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/parallel_map.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/parallel_map.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/partial_function.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/partial_function.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/proxy.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/proxy.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/py.typed +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/retries.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/runner.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/running_app.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/schedule.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/scheduler_placement.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/serving.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/serving.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/stream_type.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/token_flow.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal/token_flow.pyi +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/__init__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/__init__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_proto/py.typed +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/modal_version/__main__.py +0 -0
- {modal-1.6.1.dev3 → modal-1.6.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -12,7 +12,7 @@ Keywords: modal,client,cloud,serverless,infrastructure
|
|
|
12
12
|
Classifier: Topic :: System :: Distributed Computing
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Requires-Python: <3.
|
|
15
|
+
Requires-Python: <3.16,>=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: aiohttp
|
|
@@ -29,6 +29,7 @@ Requires-Dist: types-certifi
|
|
|
29
29
|
Requires-Dist: types-toml
|
|
30
30
|
Requires-Dist: watchfiles
|
|
31
31
|
Requires-Dist: typing_extensions~=4.6
|
|
32
|
+
Requires-Dist: typing_extensions>=4.14; python_version >= "3.15"
|
|
32
33
|
Provides-Extra: api-proxy-support
|
|
33
34
|
Requires-Dist: python-socks[asyncio]>=2.5; extra == "api-proxy-support"
|
|
34
35
|
Requires-Dist: aiohttp-socks>=0.8; extra == "api-proxy-support"
|
|
@@ -52,7 +53,7 @@ a [user guide](https://modal.com/docs/guide), and the detailed
|
|
|
52
53
|
|
|
53
54
|
## Installation
|
|
54
55
|
|
|
55
|
-
**This library requires Python 3.10 – 3.
|
|
56
|
+
**This library requires Python 3.10 – 3.15.**
|
|
56
57
|
|
|
57
58
|
Install the package with `uv` or `pip`:
|
|
58
59
|
|
|
@@ -3,8 +3,8 @@ import sys
|
|
|
3
3
|
|
|
4
4
|
if sys.version_info[:2] < (3, 10):
|
|
5
5
|
raise RuntimeError("This version of Modal requires at least Python 3.10")
|
|
6
|
-
if sys.version_info[:2] >= (3,
|
|
7
|
-
raise RuntimeError("This version of Modal does not support Python 3.
|
|
6
|
+
if sys.version_info[:2] >= (3, 16):
|
|
7
|
+
raise RuntimeError("This version of Modal does not support Python 3.16+")
|
|
8
8
|
|
|
9
9
|
from modal_version import __version__
|
|
10
10
|
|
|
@@ -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:
|
|
@@ -54,7 +54,7 @@ class _EnvironmentManager:
|
|
|
54
54
|
modal.Environment.objects.create("my-environment")
|
|
55
55
|
```
|
|
56
56
|
"""
|
|
57
|
-
check_environment_name(name)
|
|
57
|
+
check_environment_name(name, allow_periods=False)
|
|
58
58
|
|
|
59
59
|
client = await _Client.from_env() if client is None else client
|
|
60
60
|
request = api_pb2.EnvironmentCreateRequest(name=name, is_managed=restricted)
|
|
@@ -464,7 +464,7 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
464
464
|
client: _Client | None = None,
|
|
465
465
|
) -> "_Environment":
|
|
466
466
|
"""Look up an Environment object using its name."""
|
|
467
|
-
check_environment_name(name)
|
|
467
|
+
check_environment_name(name, allow_periods=True)
|
|
468
468
|
return _Environment._get_or_create(
|
|
469
469
|
name=name,
|
|
470
470
|
repr=f"Environment.from_name({name!r})",
|
|
@@ -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
|
|
@@ -50,6 +51,7 @@ from ._utils.async_utils import (
|
|
|
50
51
|
)
|
|
51
52
|
from ._utils.auth_token_manager import _AuthTokenManager
|
|
52
53
|
from ._utils.blob_utils import MAX_ASYNC_OBJECT_SIZE_BYTES, MAX_OBJECT_SIZE_BYTES
|
|
54
|
+
from ._utils.deprecation import deprecation_warning
|
|
53
55
|
from ._utils.function_utils import (
|
|
54
56
|
ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
55
57
|
OUTPUTS_TIMEOUT,
|
|
@@ -661,6 +663,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
661
663
|
source_object_id=self.object_id,
|
|
662
664
|
)
|
|
663
665
|
|
|
666
|
+
@property
|
|
667
|
+
def metrics(self) -> "_FunctionMetricsManager":
|
|
668
|
+
"""Access metric samples with `export()` and series definitions with `schema()`."""
|
|
669
|
+
return _FunctionMetricsManager(self)
|
|
670
|
+
|
|
664
671
|
@property
|
|
665
672
|
def logs(self) -> _FunctionLogsManager:
|
|
666
673
|
"""Access logs for a `Function`.
|
|
@@ -819,8 +826,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
819
826
|
raise InvalidError(
|
|
820
827
|
f"`min_containers` ({min_containers}) cannot be greater than `max_containers` ({max_containers})"
|
|
821
828
|
)
|
|
822
|
-
if scaledown_window is not None and scaledown_window <=
|
|
823
|
-
raise InvalidError("`scaledown_window` must be
|
|
829
|
+
if scaledown_window is not None and not (2 <= scaledown_window <= 3600):
|
|
830
|
+
raise InvalidError("`scaledown_window` must be between 2 and 3600 (inclusive)")
|
|
824
831
|
|
|
825
832
|
autoscaler_settings = api_pb2.AutoscalerSettings(
|
|
826
833
|
min_containers=min_containers,
|
|
@@ -1390,6 +1397,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1390
1397
|
```
|
|
1391
1398
|
|
|
1392
1399
|
"""
|
|
1400
|
+
|
|
1401
|
+
deprecation_warning(
|
|
1402
|
+
(2026, 10, 16),
|
|
1403
|
+
"`Function.update_autoscaler()` is deprecated and will be removed in a future version "
|
|
1404
|
+
"of `modal`. Use `Function.autoscaler.update()` instead.",
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1393
1407
|
# Assert .update_autoscaler() is not called on a method as opposed to the Object. Applicable for Cls only.
|
|
1394
1408
|
if self._is_method:
|
|
1395
1409
|
raise InvalidError("Cannot call .update_autoscaler() on a method. Call it on the class instance instead.")
|
|
@@ -2306,6 +2320,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
2306
2320
|
assert self._metadata
|
|
2307
2321
|
return self._metadata.function_schema
|
|
2308
2322
|
|
|
2323
|
+
@property
|
|
2324
|
+
def autoscaler(self) -> "_FunctionAutoscalerManager":
|
|
2325
|
+
"""Namespace for Function autoscaler APIs."""
|
|
2326
|
+
return _FunctionAutoscalerManager(self)
|
|
2327
|
+
|
|
2309
2328
|
# A bit hacky - but the map-style functions need to not be synchronicity-wrapped
|
|
2310
2329
|
# in order to not execute their input iterators on the synchronicity event loop.
|
|
2311
2330
|
# We still need to wrap them using MethodWithAio to maintain a synchronicity-like
|
|
@@ -2559,3 +2578,130 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2559
2578
|
except Exception as exc:
|
|
2560
2579
|
# TODO: kill all running function calls
|
|
2561
2580
|
raise exc
|
|
2581
|
+
|
|
2582
|
+
|
|
2583
|
+
UNSET_PARAMETER = -1
|
|
2584
|
+
|
|
2585
|
+
|
|
2586
|
+
class _FunctionAutoscalerManager:
|
|
2587
|
+
"""mdmd:namespace"""
|
|
2588
|
+
|
|
2589
|
+
def __init__(self, function: _Function):
|
|
2590
|
+
"""mdmd:hidden"""
|
|
2591
|
+
self._function = function
|
|
2592
|
+
|
|
2593
|
+
async def settings(self) -> FunctionAutoscalerSettings:
|
|
2594
|
+
"""View the current Autoscaler settings for this Function.
|
|
2595
|
+
|
|
2596
|
+
Returns:
|
|
2597
|
+
A `FunctionAutoscalerSettings` dataclass.
|
|
2598
|
+
|
|
2599
|
+
Examples:
|
|
2600
|
+
|
|
2601
|
+
```python notest
|
|
2602
|
+
fn = modal.Function.from_name("my-app", "my_function")
|
|
2603
|
+
settings = fn.autoscaler.settings()
|
|
2604
|
+
```
|
|
2605
|
+
"""
|
|
2606
|
+
|
|
2607
|
+
if not self._function._is_hydrated:
|
|
2608
|
+
await self._function.hydrate()
|
|
2609
|
+
|
|
2610
|
+
response = await self._function.client._stub.FunctionGetSchedulingParams(
|
|
2611
|
+
api_pb2.FunctionGetSchedulingParamsRequest(function_id=self._function.object_id)
|
|
2612
|
+
)
|
|
2613
|
+
|
|
2614
|
+
ret = FunctionAutoscalerSettings._from_proto(response.autoscaler_configuration.settings)
|
|
2615
|
+
if ret.max_containers == 0:
|
|
2616
|
+
ret = dataclasses.replace(ret, max_containers=None)
|
|
2617
|
+
|
|
2618
|
+
return ret
|
|
2619
|
+
|
|
2620
|
+
async def update(
|
|
2621
|
+
self,
|
|
2622
|
+
*,
|
|
2623
|
+
min_containers: int | None = UNSET_PARAMETER,
|
|
2624
|
+
max_containers: int | None = UNSET_PARAMETER,
|
|
2625
|
+
buffer_containers: int | None = UNSET_PARAMETER,
|
|
2626
|
+
scaledown_window: int = UNSET_PARAMETER,
|
|
2627
|
+
) -> FunctionAutoscalerSettings:
|
|
2628
|
+
"""Override the current Autoscaler settings for this Function.
|
|
2629
|
+
|
|
2630
|
+
All parameters to this function are optional; not passing an argument for a given parameter
|
|
2631
|
+
will not update its value.
|
|
2632
|
+
|
|
2633
|
+
Note that re-deploying the App containing this Function will wipe any overrides and revert
|
|
2634
|
+
its Autoscaler settings back to those defined on the Function at creation time.
|
|
2635
|
+
|
|
2636
|
+
Args:
|
|
2637
|
+
min_containers: Minimum number of containers to keep running. Must be a nonnegative
|
|
2638
|
+
integer, or `None`. Passing `None` is equivalent to setting the minimum to 0.
|
|
2639
|
+
max_containers: Maximum concurrent containers. Must be a positive integer, or `None`.
|
|
2640
|
+
Passing `None` is equivalent to disabling the upper bound on container concurrency at
|
|
2641
|
+
the Function level (note that Environment and Workspace level container upper limits
|
|
2642
|
+
still apply.)
|
|
2643
|
+
buffer_containers: Extra containers to keep warm beyond current demand. Must be a
|
|
2644
|
+
nonnegative integer, or `None`. Passing `None` is equivalent to setting the buffer to
|
|
2645
|
+
0.
|
|
2646
|
+
scaledown_window: Maximum duration (in seconds) idle containers wait before scaling down.
|
|
2647
|
+
Must be a positive integer between 2 and 3600, inclusive.
|
|
2648
|
+
|
|
2649
|
+
Returns:
|
|
2650
|
+
A `FunctionAutoscalerSettings` dataclass.
|
|
2651
|
+
|
|
2652
|
+
Examples:
|
|
2653
|
+
|
|
2654
|
+
Update `my_function` so that 2 containers are always active.
|
|
2655
|
+
|
|
2656
|
+
```python notest
|
|
2657
|
+
fn = modal.Function.from_name("my-app", "my_function")
|
|
2658
|
+
fn.autoscaler.update(min_containers=2)
|
|
2659
|
+
```
|
|
2660
|
+
|
|
2661
|
+
Update `my_function` so that there is no Function-level limit on `max_containers`:
|
|
2662
|
+
|
|
2663
|
+
```python notest
|
|
2664
|
+
fn = modal.Function.from_name("my-app", "my_function")
|
|
2665
|
+
fn.autoscaler.update(max_containers=None)
|
|
2666
|
+
```
|
|
2667
|
+
"""
|
|
2668
|
+
|
|
2669
|
+
if not self._function._is_hydrated:
|
|
2670
|
+
await self._function.hydrate()
|
|
2671
|
+
|
|
2672
|
+
settings: api_pb2.AutoscalerSettings = api_pb2.AutoscalerSettings()
|
|
2673
|
+
|
|
2674
|
+
if min_containers != UNSET_PARAMETER:
|
|
2675
|
+
min_containers = min_containers if min_containers is not None else 0
|
|
2676
|
+
if min_containers < 0:
|
|
2677
|
+
raise InvalidError("Invalid argument: `min_containers` must be a nonnegative integer or `None`.")
|
|
2678
|
+
|
|
2679
|
+
settings.min_containers = min_containers
|
|
2680
|
+
|
|
2681
|
+
if buffer_containers != UNSET_PARAMETER:
|
|
2682
|
+
buffer_containers = buffer_containers if buffer_containers is not None else 0
|
|
2683
|
+
if buffer_containers < 0:
|
|
2684
|
+
raise InvalidError("Invalid argument: `buffer_containers` must be a nonnegative integer or `None`.")
|
|
2685
|
+
|
|
2686
|
+
settings.buffer_containers = buffer_containers
|
|
2687
|
+
|
|
2688
|
+
if max_containers != UNSET_PARAMETER:
|
|
2689
|
+
if max_containers is not None and max_containers <= 0:
|
|
2690
|
+
raise InvalidError("Invalid argument: `max_containers` must be a postive integer or `None`.")
|
|
2691
|
+
|
|
2692
|
+
settings.max_containers = max_containers if max_containers is not None else 0
|
|
2693
|
+
|
|
2694
|
+
if scaledown_window != UNSET_PARAMETER:
|
|
2695
|
+
if not (2 <= scaledown_window <= 3600):
|
|
2696
|
+
raise InvalidError("Invalid argument: `scaledown_window` must be between 2 and 3600 (inclusive)")
|
|
2697
|
+
|
|
2698
|
+
settings.scaledown_window = scaledown_window
|
|
2699
|
+
|
|
2700
|
+
request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self._function.object_id, settings=settings)
|
|
2701
|
+
response = await self._function.client._stub.FunctionUpdateSchedulingParams(request)
|
|
2702
|
+
|
|
2703
|
+
ret = FunctionAutoscalerSettings._from_proto(response.current_settings)
|
|
2704
|
+
if ret.max_containers == 0:
|
|
2705
|
+
ret = dataclasses.replace(ret, max_containers=None)
|
|
2706
|
+
|
|
2707
|
+
return ret
|
|
@@ -76,8 +76,8 @@ ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVI
|
|
|
76
76
|
# Python versions in mount.py where we specify the "standalone Python versions" we create mounts for.
|
|
77
77
|
# Consider consolidating these multiple sources of truth?
|
|
78
78
|
SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
|
|
79
|
-
"PREVIEW": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"],
|
|
80
|
-
"2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"],
|
|
79
|
+
"PREVIEW": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
|
|
80
|
+
"2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
|
|
81
81
|
"2024.10": ["3.10", "3.11", "3.12", "3.13"],
|
|
82
82
|
"2024.04": ["3.10", "3.11", "3.12"],
|
|
83
83
|
"2023.12": ["3.10", "3.11", "3.12"],
|
|
@@ -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)
|