modAL 1.6.2__tar.gz → 1.6.2.dev0__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 → modal-1.6.2.dev0}/PKG-INFO +3 -4
- {modal-1.6.2 → modal-1.6.2.dev0}/README.md +1 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/__init__.py +3 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_environments.py +2 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_functions.py +2 -148
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_image.py +2 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_logs.py +54 -82
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_outbound_policy.py +3 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_outbound_policy.pyi +2 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_output/status.py +7 -10
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_server.py +0 -185
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_server.pyi +0 -89
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/async_utils.py +1 -7
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/blob_utils.py +0 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/name_utils.py +7 -9
- modal-1.6.2/modal/sandbox/_task_command_router_client.py → modal-1.6.2.dev0/modal/_utils/task_command_router_client.py +46 -103
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/time_utils.py +39 -22
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_workspace.py +3 -183
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/app.py +1 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/app.pyi +2 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/builder/2025.06.txt +4 -8
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/builder/PREVIEW.txt +2 -4
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/builder/base-images.json +2 -4
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/_logs.py +5 -33
- modal-1.6.2.dev0/modal/cli/_stats.py +141 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/app.py +5 -5
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/container.py +3 -5
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/dict.py +7 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/endpoint.py +1 -6
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/environment.py +3 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/function.py +34 -477
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/import_refs.py +13 -93
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/network_file_system.py +4 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/queues.py +7 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/run.py +2 -55
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/secret.py +4 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/server.py +27 -493
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/shell.py +2 -8
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/utils.py +1 -29
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/volume.py +7 -3
- modal-1.6.2.dev0/modal/cli/workspace.py +269 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/client.pyi +2 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cls.py +1 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cls.pyi +1 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/config.py +0 -4
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/container_process.py +1 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/container_process.pyi +3 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/experimental/__init__.py +0 -66
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/experimental/_flash_container_entry.py +0 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/functions.py +1 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/functions.pyi +0 -155
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/io_streams.py +1 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/io_streams.pyi +9 -9
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/mount.py +1 -14
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/mount.pyi +3 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/queue.py +1 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/queue.pyi +2 -2
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/runner.py +2 -2
- modal-1.6.2/modal/sandbox/_sandbox.py → modal-1.6.2.dev0/modal/sandbox.py +897 -94
- {modal-1.6.2/modal/sandbox → modal-1.6.2.dev0/modal}/sandbox.pyi +1238 -691
- modal-1.6.2/modal/sandbox/_filesystem.py → modal-1.6.2.dev0/modal/sandbox_fs.py +28 -35
- modal-1.6.2.dev0/modal/sandbox_fs.pyi +1087 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/secret.py +4 -13
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/secret.pyi +0 -4
- modal-1.6.2.dev0/modal/server.py +5 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/server.pyi +0 -170
- modal-1.6.2/modal/sandbox/_snapshot.py → modal-1.6.2.dev0/modal/snapshot.py +8 -5
- modal-1.6.2.dev0/modal/snapshot.pyi +85 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/types.py +1 -90
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/volume.py +0 -10
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/volume.pyi +0 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/workspace.py +0 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/workspace.pyi +1 -239
- {modal-1.6.2 → modal-1.6.2.dev0}/modal.egg-info/PKG-INFO +3 -4
- {modal-1.6.2 → modal-1.6.2.dev0}/modal.egg-info/SOURCES.txt +7 -11
- {modal-1.6.2 → modal-1.6.2.dev0}/modal.egg-info/requires.txt +0 -3
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/api_grpc.py +16 -224
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/api_pb2.py +1273 -1774
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/api_pb2.pyi +231 -1385
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/api_pb2_grpc.py +35 -466
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/api_pb2_grpc.pyi +16 -150
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/modal_api_grpc.py +1 -14
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/task_command_router_grpc.py +5 -5
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/task_command_router_pb2.py +49 -49
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/task_command_router_pb2.pyi +22 -22
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/task_command_router_pb2_grpc.py +7 -8
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/task_command_router_pb2_grpc.pyi +6 -10
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_version/__init__.py +1 -1
- {modal-1.6.2 → modal-1.6.2.dev0}/pyproject.toml +5 -13
- modal-1.6.2/modal/_metrics.py +0 -212
- modal-1.6.2/modal/_metrics.pyi +0 -312
- modal-1.6.2/modal/cli/_stats.py +0 -334
- modal-1.6.2/modal/cli/workspace.py +0 -641
- modal-1.6.2/modal/sandbox/__init__.py +0 -9
- modal-1.6.2/modal/sandbox/_common.py +0 -248
- modal-1.6.2/modal/sandbox/_sidecar.py +0 -646
- modal-1.6.2/modal/sandbox/sandbox.py +0 -11
- modal-1.6.2/modal/server.py +0 -6
- {modal-1.6.2 → modal-1.6.2.dev0}/LICENSE +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/__main__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_billing.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_cluster.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_cluster.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_function_variants.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_grpc_client.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_ipython.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_load_context.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_location.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_logs_manager.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_object.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_output/__init__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_output/manager.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_output/pty.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_output/rich.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_partial_function.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_resolver.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_resources.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_serialization.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_supports_logs.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_traceback.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_tunnel.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_tunnel.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_type_manager.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/__init__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/docker_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/logger.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_vendor/version.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/_watcher.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/billing.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/billing.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/builder/README.md +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/call_graph.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/__init__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/_download.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/_help.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/_traceback.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/billing.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/changelog.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/config.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/curl.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/dashboard.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/entry_point.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/image.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/logo.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/profile.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/selector.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/skills.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cli/token.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/client.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cluster.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/cluster.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/dict.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/dict.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/environments.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/environments.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/exception.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/experimental/flash.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/experimental/ipython.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/image.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/image.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/network_file_system.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/network_file_system.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/object.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/object.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/output.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/parallel_map.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/parallel_map.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/partial_function.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/partial_function.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/proxy.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/proxy.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/py.typed +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/retries.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/runner.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/running_app.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/schedule.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/scheduler_placement.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/serving.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/serving.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/stream_type.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/token_flow.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal/token_flow.pyi +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/__init__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/__init__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_proto/py.typed +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/modal_version/__main__.py +0 -0
- {modal-1.6.2 → modal-1.6.2.dev0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.6.2
|
|
3
|
+
Version: 1.6.2.dev0
|
|
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.15,>=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: aiohttp
|
|
@@ -29,7 +29,6 @@ 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"
|
|
33
32
|
Provides-Extra: api-proxy-support
|
|
34
33
|
Requires-Dist: python-socks[asyncio]>=2.5; extra == "api-proxy-support"
|
|
35
34
|
Requires-Dist: aiohttp-socks>=0.8; extra == "api-proxy-support"
|
|
@@ -53,7 +52,7 @@ a [user guide](https://modal.com/docs/guide), and the detailed
|
|
|
53
52
|
|
|
54
53
|
## Installation
|
|
55
54
|
|
|
56
|
-
**This library requires Python 3.10 – 3.
|
|
55
|
+
**This library requires Python 3.10 – 3.14.**
|
|
57
56
|
|
|
58
57
|
Install the package with `uv` or `pip`:
|
|
59
58
|
|
|
@@ -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, 15):
|
|
7
|
+
raise RuntimeError("This version of Modal does not support Python 3.15+")
|
|
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
|
|
47
46
|
from .schedule import Cron, Period
|
|
48
47
|
from .scheduler_placement import SchedulerPlacement
|
|
49
48
|
from .secret import Secret
|
|
50
49
|
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)
|
|
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)
|
|
468
468
|
return _Environment._get_or_create(
|
|
469
469
|
name=name,
|
|
470
470
|
repr=f"Environment.from_name({name!r})",
|
|
@@ -27,7 +27,6 @@ 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
|
|
31
30
|
from ._object import _Object, live_method, live_method_gen
|
|
32
31
|
from ._output.pty import get_pty_info
|
|
33
32
|
from ._output.status import FunctionCreationStatus
|
|
@@ -51,7 +50,6 @@ from ._utils.async_utils import (
|
|
|
51
50
|
)
|
|
52
51
|
from ._utils.auth_token_manager import _AuthTokenManager
|
|
53
52
|
from ._utils.blob_utils import MAX_ASYNC_OBJECT_SIZE_BYTES, MAX_OBJECT_SIZE_BYTES
|
|
54
|
-
from ._utils.deprecation import deprecation_warning
|
|
55
53
|
from ._utils.function_utils import (
|
|
56
54
|
ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
57
55
|
OUTPUTS_TIMEOUT,
|
|
@@ -663,11 +661,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
663
661
|
source_object_id=self.object_id,
|
|
664
662
|
)
|
|
665
663
|
|
|
666
|
-
@property
|
|
667
|
-
def metrics(self) -> "_FunctionMetricsManager":
|
|
668
|
-
"""Access metric samples with `export()` and series definitions with `schema()`."""
|
|
669
|
-
return _FunctionMetricsManager(self)
|
|
670
|
-
|
|
671
664
|
@property
|
|
672
665
|
def logs(self) -> _FunctionLogsManager:
|
|
673
666
|
"""Access logs for a `Function`.
|
|
@@ -826,8 +819,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
826
819
|
raise InvalidError(
|
|
827
820
|
f"`min_containers` ({min_containers}) cannot be greater than `max_containers` ({max_containers})"
|
|
828
821
|
)
|
|
829
|
-
if scaledown_window is not None and
|
|
830
|
-
raise InvalidError("`scaledown_window` must be
|
|
822
|
+
if scaledown_window is not None and scaledown_window <= 0:
|
|
823
|
+
raise InvalidError("`scaledown_window` must be > 0")
|
|
831
824
|
|
|
832
825
|
autoscaler_settings = api_pb2.AutoscalerSettings(
|
|
833
826
|
min_containers=min_containers,
|
|
@@ -1397,13 +1390,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1397
1390
|
```
|
|
1398
1391
|
|
|
1399
1392
|
"""
|
|
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
|
-
|
|
1407
1393
|
# Assert .update_autoscaler() is not called on a method as opposed to the Object. Applicable for Cls only.
|
|
1408
1394
|
if self._is_method:
|
|
1409
1395
|
raise InvalidError("Cannot call .update_autoscaler() on a method. Call it on the class instance instead.")
|
|
@@ -2320,11 +2306,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
2320
2306
|
assert self._metadata
|
|
2321
2307
|
return self._metadata.function_schema
|
|
2322
2308
|
|
|
2323
|
-
@property
|
|
2324
|
-
def autoscaler(self) -> "_FunctionAutoscalerManager":
|
|
2325
|
-
"""Namespace for Function autoscaler APIs."""
|
|
2326
|
-
return _FunctionAutoscalerManager(self)
|
|
2327
|
-
|
|
2328
2309
|
# A bit hacky - but the map-style functions need to not be synchronicity-wrapped
|
|
2329
2310
|
# in order to not execute their input iterators on the synchronicity event loop.
|
|
2330
2311
|
# We still need to wrap them using MethodWithAio to maintain a synchronicity-like
|
|
@@ -2578,130 +2559,3 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2578
2559
|
except Exception as exc:
|
|
2579
2560
|
# TODO: kill all running function calls
|
|
2580
2561
|
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"],
|
|
80
|
+
"2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"],
|
|
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
|
|
7
7
|
from datetime import datetime, timedelta, timezone
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
from google.protobuf.timestamp_pb2 import Timestamp
|
|
11
11
|
|
|
@@ -13,9 +13,6 @@ 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
|
-
|
|
19
16
|
if TYPE_CHECKING:
|
|
20
17
|
from modal.client import _Client
|
|
21
18
|
|
|
@@ -197,32 +194,6 @@ def _next_smaller_bucket_secs(current_bucket_secs: int) -> int | None:
|
|
|
197
194
|
return None
|
|
198
195
|
|
|
199
196
|
|
|
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
|
-
|
|
226
197
|
async def _refine_dense_ranges(
|
|
227
198
|
client: _Client,
|
|
228
199
|
app_id: str,
|
|
@@ -230,19 +201,6 @@ async def _refine_dense_ranges(
|
|
|
230
201
|
filters: LogsFilters,
|
|
231
202
|
max_ranges: int,
|
|
232
203
|
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,
|
|
246
204
|
) -> list[tuple[float, float, int]]:
|
|
247
205
|
"""Refine ranges exceeding _FETCH_LIMIT by subdividing with smaller buckets.
|
|
248
206
|
|
|
@@ -281,11 +239,25 @@ async def _refine_ranges(
|
|
|
281
239
|
break
|
|
282
240
|
|
|
283
241
|
# Fire off all re-count RPCs in parallel with bounded concurrency
|
|
284
|
-
semaphore = asyncio.Semaphore(
|
|
242
|
+
semaphore = asyncio.Semaphore(_MAX_CONCURRENT_COUNTS)
|
|
285
243
|
|
|
286
244
|
async def _recount(start: float, end: float, smaller_secs: int) -> list[tuple[float, float, int]]:
|
|
287
245
|
async with semaphore:
|
|
288
|
-
|
|
246
|
+
sub_req = api_pb2.AppCountLogsRequest(
|
|
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)
|
|
289
261
|
# Clamp the edge sub-ranges to the parent boundaries. Bucket
|
|
290
262
|
# alignment can make the first bucket begin before the parent
|
|
291
263
|
# (e.g. parent (6,12) with 4s buckets produces (4,8)) or make
|
|
@@ -446,34 +418,26 @@ async def fetch_logs(
|
|
|
446
418
|
|
|
447
419
|
if until - since > _MAX_FETCH_RANGE:
|
|
448
420
|
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)
|
|
449
437
|
|
|
450
|
-
|
|
451
|
-
|
|
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:
|
|
438
|
+
ranges = _buckets_to_ranges(list(count_resp.buckets), bucket_secs)
|
|
439
|
+
total_logs = sum(count for _, _, count in ranges)
|
|
440
|
+
if total_logs == 0:
|
|
477
441
|
return
|
|
478
442
|
|
|
479
443
|
# Trim leading/trailing empty buckets so they don't consume refinement
|
|
@@ -485,17 +449,19 @@ async def _fetch_time_range(
|
|
|
485
449
|
|
|
486
450
|
# Phase 1b: Refine any ranges that exceed the per-fetch limit.
|
|
487
451
|
# Budget: at most _MAX_FETCHES intervals, each fetching up to _FETCH_LIMIT entries.
|
|
488
|
-
ranges = await
|
|
452
|
+
ranges = await _refine_dense_ranges(
|
|
453
|
+
client,
|
|
454
|
+
app_id,
|
|
489
455
|
ranges,
|
|
490
|
-
|
|
456
|
+
filters,
|
|
491
457
|
max_ranges=_MAX_FETCHES,
|
|
492
458
|
max_iterations=_MAX_REFINE_ITERATIONS,
|
|
493
|
-
max_concurrent_counts=max_concurrent_counts,
|
|
494
459
|
)
|
|
460
|
+
fetch_error_message = "Too many logs to fetch in time range. Consider narrowing the range or adding filters."
|
|
495
461
|
# Check that all ranges fit within the per-fetch limit.
|
|
496
462
|
over_limit = [r for r in ranges if r[2] > _FETCH_LIMIT]
|
|
497
463
|
if over_limit:
|
|
498
|
-
raise LogsFetchError(
|
|
464
|
+
raise LogsFetchError(fetch_error_message)
|
|
499
465
|
|
|
500
466
|
# Phase 2: Build intervals from non-empty ranges and fetch in parallel
|
|
501
467
|
since_secs = since.timestamp()
|
|
@@ -510,17 +476,24 @@ async def _fetch_time_range(
|
|
|
510
476
|
return
|
|
511
477
|
|
|
512
478
|
if len(intervals) > _MAX_FETCHES:
|
|
513
|
-
raise LogsFetchError(
|
|
479
|
+
raise LogsFetchError(fetch_error_message)
|
|
514
480
|
|
|
515
481
|
# Fetch all intervals concurrently (bounded by semaphore), but yield
|
|
516
|
-
# results in interval order so
|
|
482
|
+
# results in interval order so logs stay chronological. Awaiting each
|
|
517
483
|
# task in sequence lets us stream batches to the caller as soon as all
|
|
518
484
|
# preceding intervals have completed.
|
|
519
485
|
semaphore = asyncio.Semaphore(_MAX_CONCURRENT_FETCHES)
|
|
520
486
|
|
|
521
|
-
async def _bounded_fetch(since_s: float, until_s: float) ->
|
|
487
|
+
async def _bounded_fetch(since_s: float, until_s: float) -> list[api_pb2.TaskLogsBatch]:
|
|
522
488
|
async with semaphore:
|
|
523
|
-
return await
|
|
489
|
+
return await _fetch_interval(
|
|
490
|
+
client,
|
|
491
|
+
app_id,
|
|
492
|
+
since_s,
|
|
493
|
+
until_s,
|
|
494
|
+
_FETCH_LIMIT,
|
|
495
|
+
filters,
|
|
496
|
+
)
|
|
524
497
|
|
|
525
498
|
tasks = [asyncio.create_task(_bounded_fetch(s, u)) for s, u in intervals]
|
|
526
499
|
|
|
@@ -531,4 +504,3 @@ async def _fetch_time_range(
|
|
|
531
504
|
finally:
|
|
532
505
|
for task in tasks:
|
|
533
506
|
task.cancel()
|
|
534
|
-
await asyncio.gather(*tasks, return_exceptions=True)
|
|
@@ -139,11 +139,11 @@ class _OutboundPolicy:
|
|
|
139
139
|
"""Deduplicated list of secrets referenced by the policy's replacements."""
|
|
140
140
|
return list(dict.fromkeys(r.secret for r in self._replacements if r.secret is not None))
|
|
141
141
|
|
|
142
|
-
def _to_proto(self) -> api_pb2.
|
|
142
|
+
def _to_proto(self) -> api_pb2.OutboundPolicy:
|
|
143
143
|
"""Convert to the wire format. Referenced secrets must be hydrated first."""
|
|
144
|
-
return api_pb2.
|
|
144
|
+
return api_pb2.OutboundPolicy(
|
|
145
145
|
header_replacements=[
|
|
146
|
-
api_pb2.
|
|
146
|
+
api_pb2.OutboundPolicy.HeaderReplacement(
|
|
147
147
|
domain=replacement.domain,
|
|
148
148
|
secret_id=(replacement.secret.object_id if replacement.secret is not None else ""),
|
|
149
149
|
headers=replacement.headers,
|
|
@@ -107,7 +107,7 @@ class _OutboundPolicy:
|
|
|
107
107
|
"""Deduplicated list of secrets referenced by the policy's replacements."""
|
|
108
108
|
...
|
|
109
109
|
|
|
110
|
-
def _to_proto(self) -> modal_proto.api_pb2.
|
|
110
|
+
def _to_proto(self) -> modal_proto.api_pb2.OutboundPolicy:
|
|
111
111
|
"""Convert to the wire format. Referenced secrets must be hydrated first."""
|
|
112
112
|
...
|
|
113
113
|
|
|
@@ -185,6 +185,6 @@ class OutboundPolicy:
|
|
|
185
185
|
"""Deduplicated list of secrets referenced by the policy's replacements."""
|
|
186
186
|
...
|
|
187
187
|
|
|
188
|
-
def _to_proto(self) -> modal_proto.api_pb2.
|
|
188
|
+
def _to_proto(self) -> modal_proto.api_pb2.OutboundPolicy:
|
|
189
189
|
"""Convert to the wire format. Referenced secrets must be hydrated first."""
|
|
190
190
|
...
|
|
@@ -20,11 +20,6 @@ def _get_annotation_for_web_url(url_info: api_pb2.WebUrlInfo) -> str:
|
|
|
20
20
|
return suffix
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def _format_url(url: str) -> str:
|
|
24
|
-
# OSC 8 hyperlinks stay clickable when the URL wraps across lines
|
|
25
|
-
return f"[dim underline][link={url}]{url}[/link][/dim underline]"
|
|
26
|
-
|
|
27
|
-
|
|
28
23
|
class FunctionCreationStatus:
|
|
29
24
|
"""Context manager for tracking and displaying function creation progress."""
|
|
30
25
|
|
|
@@ -63,9 +58,9 @@ class FunctionCreationStatus:
|
|
|
63
58
|
suffix = _get_annotation_for_web_url(url_info)
|
|
64
59
|
|
|
65
60
|
self.status_row.finish(f"Created {self.type} [green]{self.name}[/green]")
|
|
66
|
-
self.status_row.details(f"{
|
|
61
|
+
self.status_row.details(f"[dim underline]{web_url}[/dim underline]{auth_suffix}{suffix}")
|
|
67
62
|
for custom_domain in self.response.function.custom_domain_info:
|
|
68
|
-
self.status_row.details(f"Custom domain: {
|
|
63
|
+
self.status_row.details(f"Custom domain: [dim underline]{custom_domain.url}[/dim underline]")
|
|
69
64
|
|
|
70
65
|
elif self.response.function.flash_service_urls:
|
|
71
66
|
# Despite the gRPC API types, Servers only have one URL
|
|
@@ -73,7 +68,7 @@ class FunctionCreationStatus:
|
|
|
73
68
|
auth_suffix = " 🔑" if authenticated else " [yellow](unauthenticated)[/yellow]"
|
|
74
69
|
self.status_row.finish(f"Created {self.type} [green]{self.name}[/green]")
|
|
75
70
|
url = self.response.function.flash_service_urls[0]
|
|
76
|
-
self.status_row.details(f"{
|
|
71
|
+
self.status_row.details(f"[dim underline]{url}[/dim underline]{auth_suffix}")
|
|
77
72
|
|
|
78
73
|
else:
|
|
79
74
|
self.status_row.finish(f"Created {self.type} [green]{self.name}[/green].")
|
|
@@ -86,8 +81,10 @@ class FunctionCreationStatus:
|
|
|
86
81
|
auth_suffix = " 🔑" if authenticated else " [yellow](unauthenticated)[/yellow]"
|
|
87
82
|
self.status_row.details(
|
|
88
83
|
f"{method_definition.function_name} -> "
|
|
89
|
-
f"{
|
|
84
|
+
f"[dim underline]{method_definition.web_url}[/dim underline]{auth_suffix}{suffix}"
|
|
90
85
|
)
|
|
91
86
|
for custom_domain in method_definition.custom_domain_info:
|
|
92
87
|
indent = len(method_definition.function_name) * " "
|
|
93
|
-
self.status_row.details(
|
|
88
|
+
self.status_row.details(
|
|
89
|
+
f"{indent} -> [dim underline]{custom_domain.url}[/dim underline]{auth_suffix}"
|
|
90
|
+
)
|