modal 1.5.4.dev13__tar.gz → 1.5.4.dev15__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.4.dev13 → modal-1.5.4.dev15}/PKG-INFO +1 -1
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_image.py +71 -11
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_logs.py +9 -7
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_logs_manager.py +192 -71
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_logs_manager.pyi +82 -14
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_supports_logs.py +20 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/image.py +37 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/run.py +6 -3
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/workspace.py +21 -4
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/client.pyi +2 -2
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/exception.py +8 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/functions.pyi +6 -6
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/image.pyi +30 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/sandbox.py +1 -8
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/api_grpc.py +16 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/api_pb2.py +715 -685
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/api_pb2.pyi +58 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/api_pb2_grpc.py +34 -1
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/api_pb2_grpc.pyi +13 -3
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_version/__init__.py +1 -1
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/LICENSE +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/README.md +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/__main__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_billing.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_clustered_functions.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_environments.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_function_variants.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_functions.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_grpc_client.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_ipython.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_load_context.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_location.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_object.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_output/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_output/manager.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_output/pty.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_output/rich.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_output/status.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_partial_function.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_resolver.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_resources.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_serialization.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_server.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_traceback.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_tunnel.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_tunnel.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_type_manager.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/logger.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_vendor/version.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_watcher.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/_workspace.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/app.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/app.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/billing.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/billing.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/builder/README.md +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/builder/base-images.json +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/call_graph.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/_download.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/_help.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/_traceback.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/app.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/billing.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/changelog.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/cluster.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/config.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/container.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/curl.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/dashboard.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/dict.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/endpoint.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/entry_point.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/environment.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/import_refs.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/launch.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/logo.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/profile.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/queues.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/secret.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/selector.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/shell.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/skills.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/token.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/utils.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cli/volume.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/client.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cls.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/cls.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/config.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/container_process.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/container_process.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/dict.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/dict.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/environments.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/environments.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/experimental/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/experimental/flash.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/experimental/ipython.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/file_io.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/file_io.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/functions.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/image.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/io_streams.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/io_streams.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/mount.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/mount.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/network_file_system.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/network_file_system.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/object.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/object.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/output.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/parallel_map.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/parallel_map.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/partial_function.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/partial_function.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/proxy.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/proxy.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/py.typed +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/queue.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/queue.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/retries.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/runner.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/runner.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/running_app.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/sandbox.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/sandbox_fs.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/schedule.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/scheduler_placement.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/secret.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/secret.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/server.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/server.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/serving.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/serving.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/snapshot.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/snapshot.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/stream_type.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/token_flow.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/token_flow.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/types.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/volume.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/volume.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/workspace.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal/workspace.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/__init__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/py.typed +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/modal_version/__main__.py +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/pyproject.toml +0 -0
- {modal-1.5.4.dev13 → modal-1.5.4.dev15}/setup.cfg +0 -0
|
@@ -30,9 +30,11 @@ from modal_proto import api_pb2
|
|
|
30
30
|
|
|
31
31
|
from ._environments import _get_environment_cached
|
|
32
32
|
from ._load_context import LoadContext
|
|
33
|
+
from ._logs_manager import _ImageLogsManager
|
|
33
34
|
from ._object import _Object, live_method_gen
|
|
34
35
|
from ._resolver import Resolver
|
|
35
36
|
from ._serialization import get_preferred_payload_format, serialize
|
|
37
|
+
from ._supports_logs import _ImageLogQueryData
|
|
36
38
|
from ._utils.async_utils import TaskContext, deprecate_aio_usage, synchronizer
|
|
37
39
|
from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
|
38
40
|
from ._utils.docker_utils import (
|
|
@@ -47,10 +49,10 @@ from .cloud_bucket_mount import _CloudBucketMount
|
|
|
47
49
|
from .config import config, logger, user_config_path
|
|
48
50
|
from .exception import (
|
|
49
51
|
ExecutionError,
|
|
52
|
+
ImageBuildError,
|
|
50
53
|
InternalError,
|
|
51
54
|
InvalidError,
|
|
52
55
|
NotFoundError,
|
|
53
|
-
RemoteError,
|
|
54
56
|
ServiceError,
|
|
55
57
|
VersionError,
|
|
56
58
|
)
|
|
@@ -477,6 +479,12 @@ def _requires_image_instance(method):
|
|
|
477
479
|
return wrapper
|
|
478
480
|
|
|
479
481
|
|
|
482
|
+
@dataclass(frozen=True)
|
|
483
|
+
class _FailedBuildAttempt:
|
|
484
|
+
image_id: str
|
|
485
|
+
client: _Client
|
|
486
|
+
|
|
487
|
+
|
|
480
488
|
class _Image(_Object, type_prefix="im"):
|
|
481
489
|
"""Base class for container images to run functions in.
|
|
482
490
|
|
|
@@ -493,6 +501,9 @@ class _Image(_Object, type_prefix="im"):
|
|
|
493
501
|
_added_python_source_set: frozenset[str] # used to warn about missing mounts during auto-mount deprecation
|
|
494
502
|
_metadata: api_pb2.ImageMetadata | None = None # set on hydration, private for now
|
|
495
503
|
_is_empty: bool
|
|
504
|
+
_build_steps: list[api_pb2.ImageBuildStep] | None = None
|
|
505
|
+
# A failed image ID is only queryable through the client that performed the build.
|
|
506
|
+
_failed_build_attempt: _FailedBuildAttempt | None = None
|
|
496
507
|
|
|
497
508
|
def _initialize_from_empty(self):
|
|
498
509
|
self.inside_exceptions = []
|
|
@@ -524,6 +535,9 @@ class _Image(_Object, type_prefix="im"):
|
|
|
524
535
|
if metadata:
|
|
525
536
|
assert isinstance(metadata, api_pb2.ImageMetadata)
|
|
526
537
|
self._metadata = metadata
|
|
538
|
+
# Successful hydration supersedes diagnostic state retained from a failed build.
|
|
539
|
+
self._failed_build_attempt = None
|
|
540
|
+
self._build_steps = None
|
|
527
541
|
|
|
528
542
|
def _add_mount_layer_or_copy(self, mount: _Mount, copy: bool = False):
|
|
529
543
|
if copy:
|
|
@@ -752,12 +766,12 @@ class _Image(_Object, type_prefix="im"):
|
|
|
752
766
|
|
|
753
767
|
if result.status == api_pb2.GenericResult.GENERIC_STATUS_FAILURE:
|
|
754
768
|
if result.exception:
|
|
755
|
-
raise
|
|
769
|
+
raise ImageBuildError(
|
|
770
|
+
f"Image build for {image_id} failed with the exception:\n{result.exception}", image_id
|
|
771
|
+
)
|
|
756
772
|
else:
|
|
757
|
-
msg = f"Image build for {image_id} failed
|
|
758
|
-
|
|
759
|
-
msg += " (Hint: Use `modal.enable_output()` to see logs from the process building the Image.)"
|
|
760
|
-
raise RemoteError(msg)
|
|
773
|
+
msg = f"Image build for {image_id} failed.\nView the build logs:\n modal image logs {image_id}"
|
|
774
|
+
raise ImageBuildError(msg, image_id)
|
|
761
775
|
elif result.status == api_pb2.GenericResult.GENERIC_STATUS_TERMINATED:
|
|
762
776
|
msg = f"Image build for {image_id} terminated due to external shut-down. Please try again."
|
|
763
777
|
if result.exception:
|
|
@@ -765,15 +779,16 @@ class _Image(_Object, type_prefix="im"):
|
|
|
765
779
|
f"Image build for {image_id} terminated due to external shut-down with the exception:\n"
|
|
766
780
|
f"{result.exception}"
|
|
767
781
|
)
|
|
768
|
-
raise
|
|
782
|
+
raise ImageBuildError(msg, image_id)
|
|
769
783
|
elif result.status == api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT:
|
|
770
|
-
raise
|
|
771
|
-
f"Image build for {image_id} timed out. Please try again with a larger `timeout` parameter."
|
|
784
|
+
raise ImageBuildError(
|
|
785
|
+
f"Image build for {image_id} timed out. Please try again with a larger `timeout` parameter.",
|
|
786
|
+
image_id,
|
|
772
787
|
)
|
|
773
788
|
elif result.status == api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
|
|
774
789
|
pass
|
|
775
790
|
else:
|
|
776
|
-
raise
|
|
791
|
+
raise ImageBuildError("Unknown status %s!" % result.status, image_id)
|
|
777
792
|
|
|
778
793
|
self._hydrate(image_id, load_context.client, metadata)
|
|
779
794
|
local_mounts: set[_Mount] = set()
|
|
@@ -1064,8 +1079,18 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1064
1079
|
|
|
1065
1080
|
resolver = Resolver()
|
|
1066
1081
|
async with TaskContext() as tc:
|
|
1082
|
+
self._build_steps = None
|
|
1083
|
+
self._failed_build_attempt = None
|
|
1067
1084
|
load_context = LoadContext(task_context=tc).merged_with(app._root_load_context)
|
|
1068
|
-
|
|
1085
|
+
try:
|
|
1086
|
+
await resolver.load(self, load_context)
|
|
1087
|
+
except ImageBuildError as e:
|
|
1088
|
+
self._failed_build_attempt = _FailedBuildAttempt(
|
|
1089
|
+
image_id=e.image_id,
|
|
1090
|
+
client=load_context.client,
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
raise
|
|
1069
1094
|
return self
|
|
1070
1095
|
|
|
1071
1096
|
@_requires_image_instance
|
|
@@ -3028,3 +3053,38 @@ class _Image(_Object, type_prefix="im"):
|
|
|
3028
3053
|
"Images cannot currently be hydrated on demand; you can build an Image by running an App that uses it."
|
|
3029
3054
|
)
|
|
3030
3055
|
return self
|
|
3056
|
+
|
|
3057
|
+
async def _resolve_image_id_client_for_logs(self) -> tuple[str, _Client]:
|
|
3058
|
+
if self._failed_build_attempt is not None:
|
|
3059
|
+
return self._failed_build_attempt.image_id, self._failed_build_attempt.client
|
|
3060
|
+
if self._object_id is None:
|
|
3061
|
+
raise InvalidError("Cannot fetch logs for an image that has not been built yet.")
|
|
3062
|
+
if self._is_hydrated:
|
|
3063
|
+
return self._object_id, self.client
|
|
3064
|
+
# Use self._load_context_overrides to handle case of explicitly passed
|
|
3065
|
+
# client in from_id
|
|
3066
|
+
client = (await self._load_context_overrides.apply_defaults()).client
|
|
3067
|
+
return self._object_id, client
|
|
3068
|
+
|
|
3069
|
+
async def _get_log_query_data(self) -> _ImageLogQueryData:
|
|
3070
|
+
image_id, client = await self._resolve_image_id_client_for_logs()
|
|
3071
|
+
|
|
3072
|
+
if self._build_steps is None:
|
|
3073
|
+
request = api_pb2.ImageBuildChainGetRequest(image_id=image_id)
|
|
3074
|
+
response = await client.stub.ImageBuildChainGet(request)
|
|
3075
|
+
self._build_steps = list(response.build_steps)
|
|
3076
|
+
return _ImageLogQueryData(client, self._build_steps or [], image_id)
|
|
3077
|
+
|
|
3078
|
+
@property
|
|
3079
|
+
def logs(self) -> _ImageLogsManager:
|
|
3080
|
+
"""Access logs for an `Image`.
|
|
3081
|
+
|
|
3082
|
+
Use [`fetch()`](#logsfetch)
|
|
3083
|
+
to read logs for individual build layers and [`tail()`](#logstail)
|
|
3084
|
+
to read the most recent logs.
|
|
3085
|
+
|
|
3086
|
+
See also:
|
|
3087
|
+
- [`modal app logs`](https://modal.com/docs/cli/latest/app#modal-app-logs):
|
|
3088
|
+
CLI access to logs for an App.
|
|
3089
|
+
"""
|
|
3090
|
+
return _ImageLogsManager(self)
|
|
@@ -256,13 +256,16 @@ async def _refine_dense_ranges(
|
|
|
256
256
|
)
|
|
257
257
|
sub_resp = await client.stub.AppCountLogs(sub_req)
|
|
258
258
|
sub_ranges = _buckets_to_ranges(list(sub_resp.buckets), smaller_secs)
|
|
259
|
-
# Clamp the
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
#
|
|
259
|
+
# Clamp the edge sub-ranges to the parent boundaries. Bucket
|
|
260
|
+
# alignment can make the first bucket begin before the parent
|
|
261
|
+
# (e.g. parent (6,12) with 4s buckets produces (4,8)) or make
|
|
262
|
+
# the last bucket end after it (e.g. parent (0,30) with 20s
|
|
263
|
+
# buckets produces (20,40)), causing overlaps with siblings.
|
|
263
264
|
if sub_ranges:
|
|
264
|
-
|
|
265
|
-
sub_ranges[
|
|
265
|
+
first_start, first_end, first_count = sub_ranges[0]
|
|
266
|
+
sub_ranges[0] = (max(first_start, start), first_end, first_count)
|
|
267
|
+
last_start, last_end, last_count = sub_ranges[-1]
|
|
268
|
+
sub_ranges[-1] = (last_start, min(last_end, end), last_count)
|
|
266
269
|
return sub_ranges
|
|
267
270
|
|
|
268
271
|
refine_set = {i for i, _ in to_refine}
|
|
@@ -410,7 +413,6 @@ async def fetch_logs(
|
|
|
410
413
|
|
|
411
414
|
if until - since > _MAX_FETCH_RANGE:
|
|
412
415
|
raise LogsFetchError(f"Time range cannot exceed {_MAX_FETCH_RANGE.days} days.")
|
|
413
|
-
|
|
414
416
|
# Phase 1: Count logs per bucket
|
|
415
417
|
bucket_secs = _pick_bucket_secs(since, until)
|
|
416
418
|
count_req = api_pb2.AppCountLogsRequest(
|
|
@@ -11,8 +11,8 @@ from datetime import datetime, timezone
|
|
|
11
11
|
|
|
12
12
|
from grpclib.exceptions import StreamTerminatedError
|
|
13
13
|
|
|
14
|
-
from modal._logs import fetch_logs, tail_logs
|
|
15
|
-
from modal._supports_logs import _LogQueryData, _SupportsLogs
|
|
14
|
+
from modal._logs import LogsFilters, fetch_logs, tail_logs
|
|
15
|
+
from modal._supports_logs import _LogQueryData, _SupportsImageLogs, _SupportsLogs
|
|
16
16
|
from modal._utils.async_utils import synchronize_api
|
|
17
17
|
from modal._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES as _RETRYABLE_STREAM_STATUSES
|
|
18
18
|
from modal._utils.time_utils import locale_tz
|
|
@@ -38,6 +38,70 @@ class _StreamStopReason(enum.Enum):
|
|
|
38
38
|
STOP_STREAM = "stop_stream"
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def _normalize_utc_datetime(value: datetime, name: str) -> datetime:
|
|
42
|
+
if not isinstance(value, datetime):
|
|
43
|
+
raise TypeError(f"{name} must be a datetime")
|
|
44
|
+
if value.tzinfo is None:
|
|
45
|
+
return value.replace(tzinfo=locale_tz()).astimezone(timezone.utc)
|
|
46
|
+
return value.astimezone(timezone.utc)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _entry_source(file_descriptor: api_pb2.FileDescriptor.ValueType) -> LogSource:
|
|
50
|
+
match file_descriptor:
|
|
51
|
+
case api_pb2.FILE_DESCRIPTOR_STDOUT:
|
|
52
|
+
return "stdout"
|
|
53
|
+
case api_pb2.FILE_DESCRIPTOR_STDERR:
|
|
54
|
+
return "stderr"
|
|
55
|
+
case _:
|
|
56
|
+
return "system"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _entry_timestamp(item: api_pb2.TaskLogs) -> datetime:
|
|
60
|
+
if item.timestamp_ns:
|
|
61
|
+
return datetime.fromtimestamp(item.timestamp_ns / 1_000_000_000, timezone.utc)
|
|
62
|
+
return datetime.fromtimestamp(item.timestamp, timezone.utc)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve_source(source: LogSource | None) -> api_pb2.FileDescriptor.ValueType:
|
|
66
|
+
match source:
|
|
67
|
+
case "stderr":
|
|
68
|
+
return api_pb2.FILE_DESCRIPTOR_STDERR
|
|
69
|
+
case "stdout":
|
|
70
|
+
return api_pb2.FILE_DESCRIPTOR_STDOUT
|
|
71
|
+
case "system":
|
|
72
|
+
return api_pb2.FILE_DESCRIPTOR_INFO
|
|
73
|
+
case None:
|
|
74
|
+
return api_pb2.FILE_DESCRIPTOR_UNSPECIFIED
|
|
75
|
+
case _:
|
|
76
|
+
raise ValueError("source must be one of 'stdout', 'stderr', 'system', or None")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _entry_context_ids(object_id: str, item: api_pb2.TaskLogs, batch: api_pb2.TaskLogsBatch) -> list[str]:
|
|
80
|
+
match object_id[:2]:
|
|
81
|
+
case "fu":
|
|
82
|
+
context_ids = [
|
|
83
|
+
item.function_call_id,
|
|
84
|
+
item.input_id or batch.input_id,
|
|
85
|
+
item.container_id or batch.task_id,
|
|
86
|
+
]
|
|
87
|
+
case "fc":
|
|
88
|
+
context_ids = [item.input_id or batch.input_id, item.container_id or batch.task_id]
|
|
89
|
+
case _:
|
|
90
|
+
context_ids = []
|
|
91
|
+
# filter for empty strings, can happen for Server which have function_ids but no input/container
|
|
92
|
+
return [c for c in context_ids if c]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _entry_from_item(object_id: str, item: api_pb2.TaskLogs, batch: api_pb2.TaskLogsBatch) -> LogEntry:
|
|
96
|
+
return LogEntry(
|
|
97
|
+
message=item.data,
|
|
98
|
+
timestamp=_entry_timestamp(item),
|
|
99
|
+
source=_entry_source(item.file_descriptor),
|
|
100
|
+
object_id=object_id,
|
|
101
|
+
context_ids=_entry_context_ids(object_id, item, batch),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
41
105
|
class _LogsManager:
|
|
42
106
|
"""mdmd:namespace"""
|
|
43
107
|
|
|
@@ -52,68 +116,6 @@ class _LogsManager:
|
|
|
52
116
|
self._query_params = await self._source._get_log_query_data()
|
|
53
117
|
return self._query_params
|
|
54
118
|
|
|
55
|
-
@staticmethod
|
|
56
|
-
def _resolve_source(source: LogSource | None) -> api_pb2.FileDescriptor.ValueType:
|
|
57
|
-
match source:
|
|
58
|
-
case "stderr":
|
|
59
|
-
return api_pb2.FILE_DESCRIPTOR_STDERR
|
|
60
|
-
case "stdout":
|
|
61
|
-
return api_pb2.FILE_DESCRIPTOR_STDOUT
|
|
62
|
-
case "system":
|
|
63
|
-
return api_pb2.FILE_DESCRIPTOR_INFO
|
|
64
|
-
case None:
|
|
65
|
-
return api_pb2.FILE_DESCRIPTOR_UNSPECIFIED
|
|
66
|
-
case _:
|
|
67
|
-
raise ValueError("source must be one of 'stdout', 'stderr', 'system', or None")
|
|
68
|
-
|
|
69
|
-
@staticmethod
|
|
70
|
-
def _normalize_utc_datetime(value: datetime, name: str) -> datetime:
|
|
71
|
-
if not isinstance(value, datetime):
|
|
72
|
-
raise TypeError(f"{name} must be a datetime")
|
|
73
|
-
if value.tzinfo is None:
|
|
74
|
-
return value.replace(tzinfo=locale_tz()).astimezone(timezone.utc)
|
|
75
|
-
return value.astimezone(timezone.utc)
|
|
76
|
-
|
|
77
|
-
@staticmethod
|
|
78
|
-
def _entry_source(file_descriptor: api_pb2.FileDescriptor.ValueType) -> LogSource:
|
|
79
|
-
match file_descriptor:
|
|
80
|
-
case api_pb2.FILE_DESCRIPTOR_STDOUT:
|
|
81
|
-
return "stdout"
|
|
82
|
-
case api_pb2.FILE_DESCRIPTOR_STDERR:
|
|
83
|
-
return "stderr"
|
|
84
|
-
case _:
|
|
85
|
-
return "system"
|
|
86
|
-
|
|
87
|
-
@staticmethod
|
|
88
|
-
def _entry_timestamp(item: api_pb2.TaskLogs) -> datetime:
|
|
89
|
-
if item.timestamp_ns:
|
|
90
|
-
return datetime.fromtimestamp(item.timestamp_ns / 1_000_000_000, timezone.utc)
|
|
91
|
-
return datetime.fromtimestamp(item.timestamp, timezone.utc)
|
|
92
|
-
|
|
93
|
-
def _entry_context_ids(self, item: api_pb2.TaskLogs, batch: api_pb2.TaskLogsBatch) -> list[str]:
|
|
94
|
-
match self._source.object_id[:2]:
|
|
95
|
-
case "fu":
|
|
96
|
-
context_ids = [
|
|
97
|
-
item.function_call_id,
|
|
98
|
-
item.input_id or batch.input_id,
|
|
99
|
-
item.container_id or batch.task_id,
|
|
100
|
-
]
|
|
101
|
-
case "fc":
|
|
102
|
-
context_ids = [item.input_id or batch.input_id, item.container_id or batch.task_id]
|
|
103
|
-
case _:
|
|
104
|
-
context_ids = []
|
|
105
|
-
# filter for empty strings, can happen for Server which have function_ids but no input/container
|
|
106
|
-
return [c for c in context_ids if c]
|
|
107
|
-
|
|
108
|
-
def _entry_from_item(self, item: api_pb2.TaskLogs, batch: api_pb2.TaskLogsBatch) -> LogEntry:
|
|
109
|
-
return LogEntry(
|
|
110
|
-
message=item.data,
|
|
111
|
-
timestamp=self._entry_timestamp(item),
|
|
112
|
-
source=self._entry_source(item.file_descriptor),
|
|
113
|
-
object_id=self._source.object_id,
|
|
114
|
-
context_ids=self._entry_context_ids(item, batch),
|
|
115
|
-
)
|
|
116
|
-
|
|
117
119
|
async def _watch_stream_stop(self, deadline: _Deadline) -> _StreamStopReason | None:
|
|
118
120
|
while True:
|
|
119
121
|
if deadline.value is not None and time.monotonic() >= deadline.value:
|
|
@@ -135,13 +137,13 @@ class _LogsManager:
|
|
|
135
137
|
search_text: str = "",
|
|
136
138
|
) -> AsyncGenerator[LogEntry, None]:
|
|
137
139
|
"""Fetch all associated logs corresponding to the date range and filters."""
|
|
138
|
-
since =
|
|
139
|
-
until =
|
|
140
|
+
since = _normalize_utc_datetime(since, "since")
|
|
141
|
+
until = _normalize_utc_datetime(until or datetime.now(timezone.utc), "until")
|
|
140
142
|
target = await self._params()
|
|
141
|
-
filters = dataclasses.replace(target.filters, source=
|
|
143
|
+
filters = dataclasses.replace(target.filters, source=_resolve_source(source), search_text=search_text)
|
|
142
144
|
async for batch in fetch_logs(target.client, target.app_id, since=since, until=until, filters=filters):
|
|
143
145
|
for item in batch.items:
|
|
144
|
-
yield self.
|
|
146
|
+
yield _entry_from_item(self._source.object_id, item, batch)
|
|
145
147
|
|
|
146
148
|
async def tail(
|
|
147
149
|
self,
|
|
@@ -151,7 +153,7 @@ class _LogsManager:
|
|
|
151
153
|
) -> AsyncGenerator[LogEntry, None]:
|
|
152
154
|
"""Fetch the most recent logs."""
|
|
153
155
|
params = await self._params()
|
|
154
|
-
filters = dataclasses.replace(params.filters, source=
|
|
156
|
+
filters = dataclasses.replace(params.filters, source=_resolve_source(source))
|
|
155
157
|
async for batch in tail_logs(
|
|
156
158
|
params.client,
|
|
157
159
|
params.app_id,
|
|
@@ -159,7 +161,7 @@ class _LogsManager:
|
|
|
159
161
|
filters=filters,
|
|
160
162
|
):
|
|
161
163
|
for item in batch.items:
|
|
162
|
-
yield self.
|
|
164
|
+
yield _entry_from_item(self._source.object_id, item, batch)
|
|
163
165
|
|
|
164
166
|
def _create_log_stream(
|
|
165
167
|
self, params: _LogQueryData, last_entry_id: str, timeout: float
|
|
@@ -180,7 +182,7 @@ class _LogsManager:
|
|
|
180
182
|
def _stream_entries(self, batch: api_pb2.TaskLogsBatch):
|
|
181
183
|
for item in batch.items:
|
|
182
184
|
if item.data:
|
|
183
|
-
yield self.
|
|
185
|
+
yield _entry_from_item(self._source.object_id, item, batch)
|
|
184
186
|
|
|
185
187
|
@staticmethod
|
|
186
188
|
def _advance_batch(batch: api_pb2.TaskLogsBatch, last_entry_id: str) -> tuple[str, bool]:
|
|
@@ -668,3 +670,122 @@ class _FunctionCallLogsManager:
|
|
|
668
670
|
|
|
669
671
|
|
|
670
672
|
FunctionCallLogsManager = synchronize_api(_FunctionCallLogsManager, target_module=__name__)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class _ImageLogsManager:
|
|
676
|
+
"""mdmd:namespace"""
|
|
677
|
+
|
|
678
|
+
_MAX_CONCURRENT_FETCHES = 10
|
|
679
|
+
|
|
680
|
+
def __init__(self, source: _SupportsImageLogs):
|
|
681
|
+
"""mdmd:hidden"""
|
|
682
|
+
self._source = source
|
|
683
|
+
|
|
684
|
+
async def fetch(
|
|
685
|
+
self,
|
|
686
|
+
layers: int | None = 1,
|
|
687
|
+
) -> AsyncGenerator[LogEntry, None]:
|
|
688
|
+
"""Fetch logs for the most recent Image build steps.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
layers: The number of build layers to fetch, counting backward
|
|
692
|
+
from the final Image. If None, logs are fetched for all build steps.
|
|
693
|
+
"""
|
|
694
|
+
params = await self._source._get_log_query_data()
|
|
695
|
+
if layers is None:
|
|
696
|
+
layer_count = len(params.build_steps)
|
|
697
|
+
elif layers < 1:
|
|
698
|
+
raise ValueError("layers must be at least 1")
|
|
699
|
+
else:
|
|
700
|
+
layer_count = min(layers, len(params.build_steps))
|
|
701
|
+
|
|
702
|
+
selected_layers = params.build_steps[-layer_count:]
|
|
703
|
+
|
|
704
|
+
semaphore = asyncio.Semaphore(self._MAX_CONCURRENT_FETCHES)
|
|
705
|
+
queues = [asyncio.Queue(maxsize=100) for _ in selected_layers]
|
|
706
|
+
DONE = object()
|
|
707
|
+
|
|
708
|
+
async def _bounded_fetch(step: api_pb2.ImageBuildStep, queue: asyncio.Queue[LogEntry | object]) -> None:
|
|
709
|
+
cancelled = False
|
|
710
|
+
# Awkward but intentional double try/except to track whether the task was cancelled.
|
|
711
|
+
# In Python 3.11+ we can use Task.cancelling().
|
|
712
|
+
try:
|
|
713
|
+
try:
|
|
714
|
+
async with semaphore:
|
|
715
|
+
async for batch in fetch_logs(
|
|
716
|
+
params.client,
|
|
717
|
+
step.builder_app_id,
|
|
718
|
+
# Protobuf timestamps represent UTC time.
|
|
719
|
+
since=step.started_at.ToDatetime(tzinfo=timezone.utc),
|
|
720
|
+
until=step.finished_at.ToDatetime(tzinfo=timezone.utc),
|
|
721
|
+
filters=LogsFilters(
|
|
722
|
+
task_id=step.builder_task_id,
|
|
723
|
+
),
|
|
724
|
+
):
|
|
725
|
+
for item in batch.items:
|
|
726
|
+
await queue.put(_entry_from_item(params.object_id, item, batch))
|
|
727
|
+
except Exception as e:
|
|
728
|
+
await queue.put(e)
|
|
729
|
+
except asyncio.CancelledError:
|
|
730
|
+
cancelled = True
|
|
731
|
+
raise
|
|
732
|
+
finally:
|
|
733
|
+
if not cancelled:
|
|
734
|
+
await queue.put(DONE)
|
|
735
|
+
|
|
736
|
+
tasks = [asyncio.create_task(_bounded_fetch(s, q)) for s, q in zip(selected_layers, queues)]
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
for q in queues:
|
|
740
|
+
while (item := await q.get()) is not DONE:
|
|
741
|
+
if isinstance(item, Exception):
|
|
742
|
+
raise item
|
|
743
|
+
yield item
|
|
744
|
+
finally:
|
|
745
|
+
for task in tasks:
|
|
746
|
+
task.cancel()
|
|
747
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
748
|
+
|
|
749
|
+
async def tail(
|
|
750
|
+
self,
|
|
751
|
+
entries: int = 100,
|
|
752
|
+
) -> AsyncGenerator[LogEntry, None]:
|
|
753
|
+
"""Fetch the most recent Image logs.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
entries: The number of log entries to return.
|
|
757
|
+
"""
|
|
758
|
+
params = await self._source._get_log_query_data()
|
|
759
|
+
remaining_entries = entries
|
|
760
|
+
entries_by_step: list[list[LogEntry]] = []
|
|
761
|
+
|
|
762
|
+
for step in reversed(params.build_steps):
|
|
763
|
+
if remaining_entries <= 0:
|
|
764
|
+
break
|
|
765
|
+
|
|
766
|
+
step_entries: list[LogEntry] = []
|
|
767
|
+
async for batch in tail_logs(
|
|
768
|
+
params.client,
|
|
769
|
+
step.builder_app_id,
|
|
770
|
+
remaining_entries,
|
|
771
|
+
since=step.started_at.ToDatetime(tzinfo=timezone.utc),
|
|
772
|
+
until=step.finished_at.ToDatetime(tzinfo=timezone.utc),
|
|
773
|
+
filters=LogsFilters(task_id=step.builder_task_id),
|
|
774
|
+
):
|
|
775
|
+
for item in batch.items:
|
|
776
|
+
if len(step_entries) >= remaining_entries:
|
|
777
|
+
break
|
|
778
|
+
step_entries.append(_entry_from_item(params.object_id, item, batch))
|
|
779
|
+
|
|
780
|
+
if step_entries:
|
|
781
|
+
entries_by_step.append(step_entries)
|
|
782
|
+
remaining_entries -= len(step_entries)
|
|
783
|
+
|
|
784
|
+
# Steps are queried newest-to-oldest so we can stop as soon as we have
|
|
785
|
+
# enough entries, but results are exposed in chronological build order.
|
|
786
|
+
for step_entries in reversed(entries_by_step):
|
|
787
|
+
for entry in step_entries:
|
|
788
|
+
yield entry
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
ImageLogsManager = synchronize_api(_ImageLogsManager, target_module=__name__)
|
|
@@ -32,6 +32,17 @@ class _StreamStopReason(enum.Enum):
|
|
|
32
32
|
IDLE_TIMEOUT = "idle_timeout"
|
|
33
33
|
STOP_STREAM = "stop_stream"
|
|
34
34
|
|
|
35
|
+
def _normalize_utc_datetime(value: datetime.datetime, name: str) -> datetime.datetime: ...
|
|
36
|
+
def _entry_source(file_descriptor: int) -> typing.Literal["stdout", "stderr", "system"]: ...
|
|
37
|
+
def _entry_timestamp(item: modal_proto.api_pb2.TaskLogs) -> datetime.datetime: ...
|
|
38
|
+
def _resolve_source(source: typing.Optional[typing.Literal["stdout", "stderr", "system"]]) -> int: ...
|
|
39
|
+
def _entry_context_ids(
|
|
40
|
+
object_id: str, item: modal_proto.api_pb2.TaskLogs, batch: modal_proto.api_pb2.TaskLogsBatch
|
|
41
|
+
) -> list[str]: ...
|
|
42
|
+
def _entry_from_item(
|
|
43
|
+
object_id: str, item: modal_proto.api_pb2.TaskLogs, batch: modal_proto.api_pb2.TaskLogsBatch
|
|
44
|
+
) -> modal.types.LogEntry: ...
|
|
45
|
+
|
|
35
46
|
class _LogsManager:
|
|
36
47
|
"""mdmd:namespace"""
|
|
37
48
|
def __init__(
|
|
@@ -43,20 +54,6 @@ class _LogsManager:
|
|
|
43
54
|
...
|
|
44
55
|
|
|
45
56
|
async def _params(self) -> modal._supports_logs._LogQueryData: ...
|
|
46
|
-
@staticmethod
|
|
47
|
-
def _resolve_source(source: typing.Optional[typing.Literal["stdout", "stderr", "system"]]) -> int: ...
|
|
48
|
-
@staticmethod
|
|
49
|
-
def _normalize_utc_datetime(value: datetime.datetime, name: str) -> datetime.datetime: ...
|
|
50
|
-
@staticmethod
|
|
51
|
-
def _entry_source(file_descriptor: int) -> typing.Literal["stdout", "stderr", "system"]: ...
|
|
52
|
-
@staticmethod
|
|
53
|
-
def _entry_timestamp(item: modal_proto.api_pb2.TaskLogs) -> datetime.datetime: ...
|
|
54
|
-
def _entry_context_ids(
|
|
55
|
-
self, item: modal_proto.api_pb2.TaskLogs, batch: modal_proto.api_pb2.TaskLogsBatch
|
|
56
|
-
) -> list[str]: ...
|
|
57
|
-
def _entry_from_item(
|
|
58
|
-
self, item: modal_proto.api_pb2.TaskLogs, batch: modal_proto.api_pb2.TaskLogsBatch
|
|
59
|
-
) -> modal.types.LogEntry: ...
|
|
60
57
|
async def _watch_stream_stop(self, deadline: _Deadline) -> typing.Optional[_StreamStopReason]: ...
|
|
61
58
|
def fetch(
|
|
62
59
|
self,
|
|
@@ -927,3 +924,74 @@ class FunctionCallLogsManager:
|
|
|
927
924
|
...
|
|
928
925
|
|
|
929
926
|
fetch: __fetch_spec
|
|
927
|
+
|
|
928
|
+
class _ImageLogsManager:
|
|
929
|
+
"""mdmd:namespace"""
|
|
930
|
+
def __init__(self, source: modal._supports_logs._SupportsImageLogs):
|
|
931
|
+
"""mdmd:hidden"""
|
|
932
|
+
...
|
|
933
|
+
|
|
934
|
+
def fetch(self, layers: typing.Optional[int] = 1) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
|
|
935
|
+
"""Fetch logs for the most recent Image build steps.
|
|
936
|
+
|
|
937
|
+
Args:
|
|
938
|
+
layers: The number of build layers to fetch, counting backward
|
|
939
|
+
from the final Image. If None, logs are fetched for all build steps.
|
|
940
|
+
"""
|
|
941
|
+
...
|
|
942
|
+
|
|
943
|
+
def tail(self, entries: int = 100) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
|
|
944
|
+
"""Fetch the most recent Image logs.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
entries: The number of log entries to return.
|
|
948
|
+
"""
|
|
949
|
+
...
|
|
950
|
+
|
|
951
|
+
class ImageLogsManager:
|
|
952
|
+
"""mdmd:namespace"""
|
|
953
|
+
def __init__(self, source: modal._supports_logs._SupportsImageLogs):
|
|
954
|
+
"""mdmd:hidden"""
|
|
955
|
+
...
|
|
956
|
+
|
|
957
|
+
class __fetch_spec(typing_extensions.Protocol):
|
|
958
|
+
def __call__(self, /, layers: typing.Optional[int] = 1) -> typing.Generator[modal.types.LogEntry, None, None]:
|
|
959
|
+
"""Fetch logs for the most recent Image build steps.
|
|
960
|
+
|
|
961
|
+
Args:
|
|
962
|
+
layers: The number of build layers to fetch, counting backward
|
|
963
|
+
from the final Image. If None, logs are fetched for all build steps.
|
|
964
|
+
"""
|
|
965
|
+
...
|
|
966
|
+
|
|
967
|
+
def aio(
|
|
968
|
+
self, /, layers: typing.Optional[int] = 1
|
|
969
|
+
) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
|
|
970
|
+
"""Fetch logs for the most recent Image build steps.
|
|
971
|
+
|
|
972
|
+
Args:
|
|
973
|
+
layers: The number of build layers to fetch, counting backward
|
|
974
|
+
from the final Image. If None, logs are fetched for all build steps.
|
|
975
|
+
"""
|
|
976
|
+
...
|
|
977
|
+
|
|
978
|
+
fetch: __fetch_spec
|
|
979
|
+
|
|
980
|
+
class __tail_spec(typing_extensions.Protocol):
|
|
981
|
+
def __call__(self, /, entries: int = 100) -> typing.Generator[modal.types.LogEntry, None, None]:
|
|
982
|
+
"""Fetch the most recent Image logs.
|
|
983
|
+
|
|
984
|
+
Args:
|
|
985
|
+
entries: The number of log entries to return.
|
|
986
|
+
"""
|
|
987
|
+
...
|
|
988
|
+
|
|
989
|
+
def aio(self, /, entries: int = 100) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
|
|
990
|
+
"""Fetch the most recent Image logs.
|
|
991
|
+
|
|
992
|
+
Args:
|
|
993
|
+
entries: The number of log entries to return.
|
|
994
|
+
"""
|
|
995
|
+
...
|
|
996
|
+
|
|
997
|
+
tail: __tail_spec
|
|
@@ -5,6 +5,7 @@ from typing import Protocol
|
|
|
5
5
|
|
|
6
6
|
from modal._logs import LogsFilters
|
|
7
7
|
from modal.client import _Client
|
|
8
|
+
from modal_proto import api_pb2
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -30,3 +31,22 @@ class _SupportsLogs(Protocol):
|
|
|
30
31
|
|
|
31
32
|
@property
|
|
32
33
|
def object_id(self) -> str: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclasses.dataclass(frozen=True)
|
|
37
|
+
class _ImageLogQueryData:
|
|
38
|
+
"""Resolved data needed to query logs for a Modal object.
|
|
39
|
+
|
|
40
|
+
Log managers lazily ask their source object for this data before the first
|
|
41
|
+
query.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
client: _Client
|
|
45
|
+
build_steps: list[api_pb2.ImageBuildStep]
|
|
46
|
+
object_id: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _SupportsImageLogs(Protocol):
|
|
50
|
+
"""Protocol for objects that support log streaming and fetching and have build steps."""
|
|
51
|
+
|
|
52
|
+
async def _get_log_query_data(self) -> _ImageLogQueryData: ...
|