modal 1.5.4.dev20__tar.gz → 1.5.4.dev22__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.dev20 → modal-1.5.4.dev22}/PKG-INFO +1 -1
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_clustered_functions.py +8 -5
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_functions.py +19 -4
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/execution_context.py +6 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/execution_context.pyi +1 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_server.py +7 -2
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/function_utils.py +23 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/task_command_router_client.py +1 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/time_utils.py +4 -1
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_workspace.py +83 -1
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/app.py +6 -4
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/app.pyi +8 -4
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/billing.py +30 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/client.py +1 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/client.pyi +4 -2
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/config.py +1 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/experimental/flash.py +15 -9
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/experimental/flash.pyi +6 -2
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/functions.pyi +3 -3
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/runner.py +6 -6
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/runner.pyi +5 -3
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/sandbox.py +101 -67
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/sandbox.pyi +21 -9
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/server.pyi +14 -4
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/token_flow.py +7 -5
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/types.py +9 -2
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/workspace.pyi +20 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_pb2.py +146 -120
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_pb2.pyi +42 -1
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_pb2.py +92 -92
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_pb2.pyi +10 -2
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_version/__init__.py +1 -1
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/pyproject.toml +4 -10
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/LICENSE +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/README.md +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/__main__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_billing.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_environments.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_function_variants.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_grpc_client.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_image.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_ipython.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_load_context.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_location.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_logs.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_logs_manager.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_object.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/manager.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/pty.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/rich.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/status.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_partial_function.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_resolver.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_resources.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_serialization.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_supports_logs.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_traceback.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_tunnel.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_tunnel.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_type_manager.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/logger.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/version.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_watcher.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/billing.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/billing.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/README.md +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/base-images.json +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/call_graph.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/_download.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/_help.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/_traceback.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/app.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/changelog.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/cluster.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/config.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/container.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/curl.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/dashboard.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/dict.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/endpoint.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/entry_point.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/environment.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/image.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/import_refs.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/launch.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/logo.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/profile.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/queues.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/run.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/secret.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/selector.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/shell.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/skills.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/token.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/utils.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/volume.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/workspace.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cls.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cls.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/container_process.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/container_process.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/dict.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/dict.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/environments.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/environments.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/exception.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/experimental/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/experimental/ipython.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/file_io.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/file_io.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/functions.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/image.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/image.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/io_streams.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/io_streams.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/mount.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/mount.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/network_file_system.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/network_file_system.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/object.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/object.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/output.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/parallel_map.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/parallel_map.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/partial_function.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/partial_function.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/proxy.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/proxy.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/py.typed +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/queue.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/queue.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/retries.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/running_app.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/sandbox_fs.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/schedule.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/scheduler_placement.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/secret.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/secret.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/server.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/serving.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/serving.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/snapshot.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/snapshot.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/stream_type.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/token_flow.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/volume.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/volume.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/workspace.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/__init__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_grpc.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/py.typed +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_version/__main__.py +0 -0
- {modal-1.5.4.dev20 → modal-1.5.4.dev22}/setup.cfg +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import socket
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from typing import cast
|
|
5
6
|
|
|
6
7
|
from modal._utils.async_utils import synchronize_api
|
|
7
8
|
from modal.client import _Client
|
|
@@ -41,9 +42,11 @@ def get_fabric_peers() -> list[int]:
|
|
|
41
42
|
async def _initialize_clustered_function(client: _Client, task_id: str, world_size: int):
|
|
42
43
|
global cluster_info
|
|
43
44
|
|
|
44
|
-
def get_i6pn():
|
|
45
|
+
def get_i6pn() -> str:
|
|
45
46
|
"""Returns the ipv6 address assigned to this container."""
|
|
46
|
-
|
|
47
|
+
# An AF_INET6 result always carries a (host, port, flowinfo, scope_id) sockaddr,
|
|
48
|
+
# but getaddrinfo is typed for every address family, some of which lead with an int
|
|
49
|
+
return cast(str, socket.getaddrinfo("i6pn.modal.local", None, socket.AF_INET6)[0][4][0])
|
|
47
50
|
|
|
48
51
|
hostname = socket.gethostname()
|
|
49
52
|
container_ip = get_i6pn()
|
|
@@ -77,9 +80,9 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
|
|
|
77
80
|
cluster_info = ClusterInfo(
|
|
78
81
|
rank=resp.cluster_rank,
|
|
79
82
|
cluster_id=resp.cluster_id,
|
|
80
|
-
container_ips=resp.container_ips,
|
|
81
|
-
container_ipv4_ips=resp.container_ipv4_ips,
|
|
82
|
-
fabric_ids=resp.fabric_ids,
|
|
83
|
+
container_ips=list(resp.container_ips),
|
|
84
|
+
container_ipv4_ips=list(resp.container_ipv4_ips),
|
|
85
|
+
fabric_ids=list(resp.fabric_ids),
|
|
83
86
|
)
|
|
84
87
|
else:
|
|
85
88
|
cluster_info = ClusterInfo(
|
|
@@ -56,7 +56,9 @@ from ._utils.function_utils import (
|
|
|
56
56
|
_stream_function_call_data,
|
|
57
57
|
get_function_type,
|
|
58
58
|
is_async,
|
|
59
|
+
normalize_fractional_target_concurrency,
|
|
59
60
|
parse_gpu_config,
|
|
61
|
+
validate_target_concurrency,
|
|
60
62
|
)
|
|
61
63
|
from ._utils.grpc_utils import Retry, RetryWarningMessage
|
|
62
64
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes, validate_volumes_by_object_id
|
|
@@ -681,7 +683,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
681
683
|
scaleup_window: int | None = None,
|
|
682
684
|
scaledown_window: int | None = None,
|
|
683
685
|
max_concurrent_inputs: int | None = None,
|
|
684
|
-
target_concurrent_inputs:
|
|
686
|
+
target_concurrent_inputs: float | None = None,
|
|
685
687
|
batch_max_size: int | None = None,
|
|
686
688
|
batch_wait_ms: int | None = None,
|
|
687
689
|
cloud: str | None = None,
|
|
@@ -789,6 +791,17 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
789
791
|
scaledown_window=scaledown_window,
|
|
790
792
|
)
|
|
791
793
|
|
|
794
|
+
target_concurrent_inputs_int = 0
|
|
795
|
+
if target_concurrent_inputs is not None:
|
|
796
|
+
if is_server:
|
|
797
|
+
validate_target_concurrency(target_concurrent_inputs, "target_inputs", allow_fractional=True)
|
|
798
|
+
autoscaler_settings.target_concurrency_float = normalize_fractional_target_concurrency(
|
|
799
|
+
target_concurrent_inputs
|
|
800
|
+
)
|
|
801
|
+
else:
|
|
802
|
+
validate_target_concurrency(target_concurrent_inputs, "target_inputs", allow_fractional=False)
|
|
803
|
+
target_concurrent_inputs_int = int(target_concurrent_inputs)
|
|
804
|
+
|
|
792
805
|
# For clustered functions, container settings must be multiples of cluster_size
|
|
793
806
|
if cluster_size is not None and cluster_size > 1:
|
|
794
807
|
for field in ["min_containers", "max_containers", "buffer_containers"]:
|
|
@@ -1014,7 +1027,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1014
1027
|
app_name=app_name,
|
|
1015
1028
|
is_builder_function=is_builder_function,
|
|
1016
1029
|
max_concurrent_inputs=max_concurrent_inputs or 0,
|
|
1017
|
-
target_concurrent_inputs=
|
|
1030
|
+
target_concurrent_inputs=target_concurrent_inputs_int,
|
|
1018
1031
|
batch_max_size=batch_max_size or 0,
|
|
1019
1032
|
batch_linger_ms=batch_wait_ms or 0,
|
|
1020
1033
|
worker_id=config.get("worker_id"),
|
|
@@ -1200,7 +1213,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1200
1213
|
buffer_containers: int | None = None,
|
|
1201
1214
|
scaleup_window: int | None = None,
|
|
1202
1215
|
scaledown_window: int | None = None,
|
|
1203
|
-
target_concurrency:
|
|
1216
|
+
target_concurrency: float | None = None,
|
|
1204
1217
|
) -> ServerAutoscalerSettings:
|
|
1205
1218
|
settings = api_pb2.AutoscalerSettings(
|
|
1206
1219
|
min_containers=min_containers,
|
|
@@ -1208,8 +1221,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1208
1221
|
buffer_containers=buffer_containers,
|
|
1209
1222
|
scaleup_window=scaleup_window,
|
|
1210
1223
|
scaledown_window=scaledown_window,
|
|
1211
|
-
target_concurrency=target_concurrency,
|
|
1212
1224
|
)
|
|
1225
|
+
if target_concurrency is not None:
|
|
1226
|
+
validate_target_concurrency(target_concurrency, "target_concurrency", allow_fractional=True)
|
|
1227
|
+
settings.target_concurrency_float = normalize_fractional_target_concurrency(target_concurrency)
|
|
1213
1228
|
request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
|
|
1214
1229
|
response = await self.client.stub.FunctionUpdateSchedulingParams(request)
|
|
1215
1230
|
|
|
@@ -120,3 +120,9 @@ def _import_context():
|
|
|
120
120
|
yield
|
|
121
121
|
finally:
|
|
122
122
|
_is_currently_importing = False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _in_import_context() -> bool:
|
|
126
|
+
# Accessor rather than direct attribute reads, since module-level variables
|
|
127
|
+
# are not exposed in the generated .pyi stub for this module
|
|
128
|
+
return _is_currently_importing
|
|
@@ -75,6 +75,7 @@ def _set_current_context_ids(
|
|
|
75
75
|
input_ids: list[str], function_call_ids: list[str], attempt_tokens: list[str]
|
|
76
76
|
) -> collections.abc.Callable[[], None]: ...
|
|
77
77
|
def _import_context(): ...
|
|
78
|
+
def _in_import_context() -> bool: ...
|
|
78
79
|
|
|
79
80
|
_current_input_id: contextvars.ContextVar
|
|
80
81
|
|
|
@@ -124,7 +124,7 @@ class _Server:
|
|
|
124
124
|
async def update_autoscaler(
|
|
125
125
|
self,
|
|
126
126
|
*,
|
|
127
|
-
target_concurrency:
|
|
127
|
+
target_concurrency: float | None = None,
|
|
128
128
|
min_containers: int | None = None,
|
|
129
129
|
max_containers: int | None = None,
|
|
130
130
|
buffer_containers: int | None = None,
|
|
@@ -140,7 +140,9 @@ class _Server:
|
|
|
140
140
|
its static configuration.
|
|
141
141
|
|
|
142
142
|
Args:
|
|
143
|
-
target_concurrency:
|
|
143
|
+
target_concurrency:
|
|
144
|
+
Target number of concurrent requests per container. May be fractional, e.g. 1.5 to
|
|
145
|
+
target three concurrent requests per two containers.
|
|
144
146
|
min_containers: Minimum number of containers to keep running regardless of demand.
|
|
145
147
|
max_containers: Limit on the number of containers that can be concurrently running.
|
|
146
148
|
buffer_containers: Extra containers to scale up beyond current demand.
|
|
@@ -167,6 +169,9 @@ class _Server:
|
|
|
167
169
|
# Adjust Server autoscaling to target 20 concurrent requests per replica
|
|
168
170
|
server.update_autoscaler(target_concurrency=20)
|
|
169
171
|
|
|
172
|
+
# Target three concurrent requests for every two containers
|
|
173
|
+
server.update_autoscaler(target_concurrency=1.5)
|
|
174
|
+
|
|
170
175
|
# Disable the Server autoscaling by setting target_concurrency to 0
|
|
171
176
|
server.update_autoscaler(target_concurrency=0)
|
|
172
177
|
```
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import asyncio
|
|
3
3
|
import inspect
|
|
4
|
+
import math
|
|
4
5
|
import os
|
|
5
6
|
import typing
|
|
6
7
|
from collections.abc import AsyncGenerator, Callable
|
|
@@ -81,6 +82,28 @@ def is_flash_object(experimental_options: dict[str, Any] | None, http_config: ap
|
|
|
81
82
|
return bool(experimental_options and experimental_options.get("flash", False)) or http_config is not None
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
def validate_target_concurrency(value: float, param_name: str, allow_fractional: bool) -> None:
|
|
86
|
+
"""Validate a user-supplied target concurrency.
|
|
87
|
+
|
|
88
|
+
Only `@app.server` may use a fractional Flash autoscaling target.
|
|
89
|
+
"""
|
|
90
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
91
|
+
raise InvalidError(f"The `{param_name}` argument must be a number, not {type(value).__name__}.")
|
|
92
|
+
if not math.isfinite(value):
|
|
93
|
+
raise InvalidError(f"The `{param_name}` argument must be finite, not {value}.")
|
|
94
|
+
if value < 0:
|
|
95
|
+
raise InvalidError(f"The `{param_name}` argument must be non-negative.")
|
|
96
|
+
if not allow_fractional and not isinstance(value, int):
|
|
97
|
+
raise InvalidError(
|
|
98
|
+
f"The `{param_name}` argument must be an integer; fractional values are only supported for Servers."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def normalize_fractional_target_concurrency(value: float) -> float:
|
|
103
|
+
"""Clamp (0, 1) up to 1 and round to the nearest hundredth for Flash targets."""
|
|
104
|
+
return 1.0 if 0 < value < 1 else round(float(value), 2)
|
|
105
|
+
|
|
106
|
+
|
|
84
107
|
def is_method_fn(object_qual_name: str):
|
|
85
108
|
# methods have names like Cls.foo.
|
|
86
109
|
if "<locals>" in object_qual_name:
|
|
@@ -8,7 +8,7 @@ def is_leap(year: int):
|
|
|
8
8
|
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def month_length(month: int, year: int):
|
|
11
|
+
def month_length(month: int, year: int) -> int:
|
|
12
12
|
match month:
|
|
13
13
|
# January, March, May, July, August, October, December
|
|
14
14
|
case 1 | 3 | 5 | 7 | 8 | 10 | 12:
|
|
@@ -22,6 +22,9 @@ def month_length(month: int, year: int):
|
|
|
22
22
|
case 2:
|
|
23
23
|
return 29 if is_leap(year) else 28
|
|
24
24
|
|
|
25
|
+
case _:
|
|
26
|
+
raise ValueError(f"Invalid month: {month}")
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
# also accepts negative inputs for n_months
|
|
27
30
|
def add_months(s: datetime, n_months: int):
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
2
|
import builtins
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Mapping
|
|
3
5
|
from datetime import datetime, timezone
|
|
6
|
+
from decimal import Decimal
|
|
4
7
|
from typing import Optional
|
|
5
8
|
|
|
6
9
|
from google.protobuf.empty_pb2 import Empty
|
|
7
10
|
|
|
8
|
-
from modal.exception import InvalidError
|
|
11
|
+
from modal.exception import DeprecationError, InvalidError
|
|
9
12
|
from modal_proto import api_pb2
|
|
10
13
|
|
|
11
14
|
from ._load_context import LoadContext
|
|
@@ -246,6 +249,64 @@ class _WorkspaceProxyTokenManager:
|
|
|
246
249
|
return environment.object_id
|
|
247
250
|
|
|
248
251
|
|
|
252
|
+
class _WorkspaceBillingRates(Mapping[str, Decimal]):
|
|
253
|
+
def __init__(
|
|
254
|
+
self, rates: Mapping[str, str], warnings: Mapping[str, str], errors: Mapping[str, str], formatted: str
|
|
255
|
+
):
|
|
256
|
+
self._rates = {k: Decimal(v) for k, v in rates.items()}
|
|
257
|
+
self._warnings = warnings
|
|
258
|
+
self._errors = errors
|
|
259
|
+
self._formatted = formatted
|
|
260
|
+
|
|
261
|
+
def __getitem__(self, key: str) -> Decimal:
|
|
262
|
+
if key in self._errors:
|
|
263
|
+
raise DeprecationError(self._errors[key])
|
|
264
|
+
elif key in self._warnings:
|
|
265
|
+
warnings.warn(self._warnings[key], DeprecationError)
|
|
266
|
+
|
|
267
|
+
return self._rates[key]
|
|
268
|
+
|
|
269
|
+
def __iter__(self):
|
|
270
|
+
for k in self._rates:
|
|
271
|
+
# We don't want iteration over the dict to surface any deprecated keys
|
|
272
|
+
if k in self._warnings:
|
|
273
|
+
continue
|
|
274
|
+
if k in self._errors:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
yield k
|
|
278
|
+
|
|
279
|
+
def __len__(self):
|
|
280
|
+
return len(set(self._rates.keys()) - set(self._warnings.keys()) - set(self._errors.keys()))
|
|
281
|
+
|
|
282
|
+
# Ensures that `rates.get()` doesn't throw `DeprecationError`s
|
|
283
|
+
# fixme(ayush): pyright seems to assume that the return type of `.get` for a `Mapping[K, V]` is
|
|
284
|
+
# always `V` regardless of the type of `default`. Fix this somehow?
|
|
285
|
+
def get(self, key, default=None): # pyright: ignore [reportIncompatibleMethodOverride]
|
|
286
|
+
try:
|
|
287
|
+
return self[key]
|
|
288
|
+
except KeyError:
|
|
289
|
+
return default
|
|
290
|
+
except DeprecationError as e:
|
|
291
|
+
warnings.warn(str(e), DeprecationError)
|
|
292
|
+
return default
|
|
293
|
+
|
|
294
|
+
# Ensures that `'k' in rates` doesn't throw `DeprecationError`s
|
|
295
|
+
def __contains__(self, key):
|
|
296
|
+
try:
|
|
297
|
+
self[key]
|
|
298
|
+
except (KeyError, DeprecationError):
|
|
299
|
+
return False
|
|
300
|
+
else:
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
def __str__(self) -> str:
|
|
304
|
+
return self._formatted
|
|
305
|
+
|
|
306
|
+
def __repr__(self) -> str:
|
|
307
|
+
return repr({k: v for k, v in self.items()})
|
|
308
|
+
|
|
309
|
+
|
|
249
310
|
class _WorkspaceBillingManager:
|
|
250
311
|
"""mdmd:namespace"""
|
|
251
312
|
|
|
@@ -253,6 +314,27 @@ class _WorkspaceBillingManager:
|
|
|
253
314
|
"""mdmd:hidden"""
|
|
254
315
|
self._workspace = workspace
|
|
255
316
|
|
|
317
|
+
async def rates(self) -> _WorkspaceBillingRates:
|
|
318
|
+
"""Return current pricing rates for the given workspace.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
A single mapping containing cost values. All values are reported as `decimal.Decimal`s.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
if not self._workspace.is_hydrated:
|
|
325
|
+
await self._workspace.hydrate()
|
|
326
|
+
|
|
327
|
+
response = await self._workspace.client.stub.WorkspaceBillingRates(
|
|
328
|
+
api_pb2.WorkspaceBillingRatesRequest(),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return _WorkspaceBillingRates(
|
|
332
|
+
response.rates,
|
|
333
|
+
response.deprecation_warnings,
|
|
334
|
+
response.deprecation_errors,
|
|
335
|
+
response.formatted,
|
|
336
|
+
)
|
|
337
|
+
|
|
256
338
|
async def report(
|
|
257
339
|
self,
|
|
258
340
|
*,
|
|
@@ -39,6 +39,7 @@ from ._utils.function_utils import (
|
|
|
39
39
|
is_flash_object,
|
|
40
40
|
is_global_object,
|
|
41
41
|
is_method_fn,
|
|
42
|
+
validate_target_concurrency,
|
|
42
43
|
)
|
|
43
44
|
from ._utils.mount_utils import validate_volumes
|
|
44
45
|
from ._utils.name_utils import check_object_name, check_tag_dict
|
|
@@ -1288,7 +1289,7 @@ class _App:
|
|
|
1288
1289
|
cpu: float | tuple[float, float] | None = None, # CPU cores to request
|
|
1289
1290
|
memory: int | tuple[int, int] | None = None, # Memory in MiB to request
|
|
1290
1291
|
ephemeral_disk: int | None = None, # Ephemeral disk size in MiB
|
|
1291
|
-
target_concurrency:
|
|
1292
|
+
target_concurrency: float | None = None, # Target concurrency for the server; 0 disables autoscaling
|
|
1292
1293
|
min_containers: int | None = None, # Minimum number of containers to keep warm
|
|
1293
1294
|
max_containers: int | None = None, # Maximum number of containers
|
|
1294
1295
|
buffer_containers: int | None = None, # Additional idle containers under active load
|
|
@@ -1335,7 +1336,9 @@ class _App:
|
|
|
1335
1336
|
Specify, in MiB, a memory request which is the minimum memory required. Or, pass (request, limit) to
|
|
1336
1337
|
additionally specify a hard limit in MiB.
|
|
1337
1338
|
ephemeral_disk: Specify, in MiB, the ephemeral disk size for the server.
|
|
1338
|
-
target_concurrency:
|
|
1339
|
+
target_concurrency:
|
|
1340
|
+
Target number of concurrent requests per container; 0 disables autoscaling. May be
|
|
1341
|
+
fractional, e.g. 1.5 to target three concurrent requests per two containers.
|
|
1339
1342
|
min_containers: Minimum number of containers to keep running regardless of demand.
|
|
1340
1343
|
max_containers: Limit on the number of containers that can be concurrently running.
|
|
1341
1344
|
buffer_containers: Extra containers to scale up beyond current demand.
|
|
@@ -1383,8 +1386,7 @@ class _App:
|
|
|
1383
1386
|
)
|
|
1384
1387
|
|
|
1385
1388
|
if target_concurrency is not None:
|
|
1386
|
-
|
|
1387
|
-
raise InvalidError("The `target_concurrency` argument must be a non-negative integer.")
|
|
1389
|
+
validate_target_concurrency(target_concurrency, "target_concurrency", allow_fractional=True)
|
|
1388
1390
|
|
|
1389
1391
|
if scaleup_window is not None and scaleup_window <= 0:
|
|
1390
1392
|
raise InvalidError("`scaleup_window` must be > 0")
|
|
@@ -744,7 +744,7 @@ class _App:
|
|
|
744
744
|
cpu: typing.Union[float, tuple[float, float], None] = None,
|
|
745
745
|
memory: typing.Union[int, tuple[int, int], None] = None,
|
|
746
746
|
ephemeral_disk: typing.Optional[int] = None,
|
|
747
|
-
target_concurrency: typing.Optional[
|
|
747
|
+
target_concurrency: typing.Optional[float] = None,
|
|
748
748
|
min_containers: typing.Optional[int] = None,
|
|
749
749
|
max_containers: typing.Optional[int] = None,
|
|
750
750
|
buffer_containers: typing.Optional[int] = None,
|
|
@@ -791,7 +791,9 @@ class _App:
|
|
|
791
791
|
Specify, in MiB, a memory request which is the minimum memory required. Or, pass (request, limit) to
|
|
792
792
|
additionally specify a hard limit in MiB.
|
|
793
793
|
ephemeral_disk: Specify, in MiB, the ephemeral disk size for the server.
|
|
794
|
-
target_concurrency:
|
|
794
|
+
target_concurrency:
|
|
795
|
+
Target number of concurrent requests per container; 0 disables autoscaling. May be
|
|
796
|
+
fractional, e.g. 1.5 to target three concurrent requests per two containers.
|
|
795
797
|
min_containers: Minimum number of containers to keep running regardless of demand.
|
|
796
798
|
max_containers: Limit on the number of containers that can be concurrently running.
|
|
797
799
|
buffer_containers: Extra containers to scale up beyond current demand.
|
|
@@ -1750,7 +1752,7 @@ class App:
|
|
|
1750
1752
|
cpu: typing.Union[float, tuple[float, float], None] = None,
|
|
1751
1753
|
memory: typing.Union[int, tuple[int, int], None] = None,
|
|
1752
1754
|
ephemeral_disk: typing.Optional[int] = None,
|
|
1753
|
-
target_concurrency: typing.Optional[
|
|
1755
|
+
target_concurrency: typing.Optional[float] = None,
|
|
1754
1756
|
min_containers: typing.Optional[int] = None,
|
|
1755
1757
|
max_containers: typing.Optional[int] = None,
|
|
1756
1758
|
buffer_containers: typing.Optional[int] = None,
|
|
@@ -1797,7 +1799,9 @@ class App:
|
|
|
1797
1799
|
Specify, in MiB, a memory request which is the minimum memory required. Or, pass (request, limit) to
|
|
1798
1800
|
additionally specify a hard limit in MiB.
|
|
1799
1801
|
ephemeral_disk: Specify, in MiB, the ephemeral disk size for the server.
|
|
1800
|
-
target_concurrency:
|
|
1802
|
+
target_concurrency:
|
|
1803
|
+
Target number of concurrent requests per container; 0 disables autoscaling. May be
|
|
1804
|
+
fractional, e.g. 1.5 to target three concurrent requests per two containers.
|
|
1801
1805
|
min_containers: Minimum number of containers to keep running regardless of demand.
|
|
1802
1806
|
max_containers: Limit on the number of containers that can be concurrently running.
|
|
1803
1807
|
buffer_containers: Extra containers to scale up beyond current demand.
|
|
@@ -5,6 +5,7 @@ from json import dumps
|
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
from rich.box import SIMPLE_HEAD
|
|
8
|
+
from rich.padding import Padding
|
|
8
9
|
from rich.table import Table
|
|
9
10
|
from rich.text import Text
|
|
10
11
|
|
|
@@ -296,3 +297,32 @@ async def summary(for_: str | None, json: bool):
|
|
|
296
297
|
)
|
|
297
298
|
|
|
298
299
|
output.print(t)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@billing_cli.command("rates")
|
|
303
|
+
@click.option("--json", "json", is_flag=True, default=False, help="Output as JSON.")
|
|
304
|
+
@synchronizer.create_blocking
|
|
305
|
+
async def rates(json: bool = False):
|
|
306
|
+
"""Return current pricing rates.
|
|
307
|
+
|
|
308
|
+
Examples:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
modal billing rates
|
|
312
|
+
```
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
rates = await _Workspace.from_context().billing.rates()
|
|
316
|
+
|
|
317
|
+
output = OutputManager.get()
|
|
318
|
+
|
|
319
|
+
if json:
|
|
320
|
+
output.print_json(
|
|
321
|
+
dumps(
|
|
322
|
+
{k: str(v) for k, v in rates.items()},
|
|
323
|
+
sort_keys=True,
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
output.print(Padding(Text(rates._formatted), (1, 2)))
|
|
@@ -31,13 +31,14 @@ class _Client:
|
|
|
31
31
|
_snapshotted: bool
|
|
32
32
|
_connection_manager: typing.Optional[modal._utils.grpc_utils.ConnectionManager]
|
|
33
33
|
client_type: int
|
|
34
|
+
server_url: str
|
|
34
35
|
|
|
35
36
|
def __init__(
|
|
36
37
|
self,
|
|
37
38
|
server_url: str,
|
|
38
39
|
client_type: int,
|
|
39
40
|
credentials: typing.Optional[tuple[str, str]],
|
|
40
|
-
version: str = "1.5.4.
|
|
41
|
+
version: str = "1.5.4.dev22",
|
|
41
42
|
):
|
|
42
43
|
"""mdmd:hidden
|
|
43
44
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -201,13 +202,14 @@ class Client:
|
|
|
201
202
|
_snapshotted: bool
|
|
202
203
|
_connection_manager: typing.Optional[modal._utils.grpc_utils.ConnectionManager]
|
|
203
204
|
client_type: int
|
|
205
|
+
server_url: str
|
|
204
206
|
|
|
205
207
|
def __init__(
|
|
206
208
|
self,
|
|
207
209
|
server_url: str,
|
|
208
210
|
client_type: int,
|
|
209
211
|
credentials: typing.Optional[tuple[str, str]],
|
|
210
|
-
version: str = "1.5.4.
|
|
212
|
+
version: str = "1.5.4.dev22",
|
|
211
213
|
):
|
|
212
214
|
"""mdmd:hidden
|
|
213
215
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -321,6 +321,7 @@ _SETTINGS = {
|
|
|
321
321
|
"traceback": _Setting(False, transform=_to_boolean),
|
|
322
322
|
"image_builder_version": _Setting(),
|
|
323
323
|
"strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
|
|
324
|
+
"sandbox_v2": _Setting(None, transform=_to_boolean),
|
|
324
325
|
"snapshot_debug": _Setting(False, transform=_to_boolean),
|
|
325
326
|
"cuda_checkpoint_path": _Setting("/__modal/.bin/cuda-checkpoint"), # Used for snapshotting GPU memory.
|
|
326
327
|
"build_validation": _Setting("error", transform=_check_value(["error", "warn", "ignore"])),
|
|
@@ -17,7 +17,7 @@ from modal.cls import _Cls
|
|
|
17
17
|
from modal.dict import _Dict
|
|
18
18
|
from modal_proto import api_pb2
|
|
19
19
|
|
|
20
|
-
from .._runtime.
|
|
20
|
+
from .._runtime.task_lifecycle_manager import UserException
|
|
21
21
|
from .._server import validate_http_server_config
|
|
22
22
|
from .._tunnel import _forward as _forward_tunnel
|
|
23
23
|
from .._utils.async_utils import synchronize_api, synchronizer
|
|
@@ -86,6 +86,7 @@ class _FlashManager:
|
|
|
86
86
|
self.tunnel = await self.tunnel_manager.__aenter__()
|
|
87
87
|
parsed_url = urlparse(self.tunnel.url)
|
|
88
88
|
host = parsed_url.hostname
|
|
89
|
+
assert host is not None, f"Tunnel URL has no host: {self.tunnel.url}"
|
|
89
90
|
port = parsed_url.port or 443
|
|
90
91
|
|
|
91
92
|
if self.is_server:
|
|
@@ -326,7 +327,7 @@ class _FlashPrometheusAutoscaler:
|
|
|
326
327
|
self.autoscaling_interval_seconds = autoscaling_interval_seconds
|
|
327
328
|
|
|
328
329
|
FlashClass = _Cls.from_name(app_name, cls_name)
|
|
329
|
-
self.fn = FlashClass.
|
|
330
|
+
self.fn = FlashClass._get_class_service_function()
|
|
330
331
|
self.cls = FlashClass()
|
|
331
332
|
|
|
332
333
|
self.http_client = aiohttp.ClientSession()
|
|
@@ -335,7 +336,7 @@ class _FlashPrometheusAutoscaler:
|
|
|
335
336
|
create_if_missing=True,
|
|
336
337
|
)
|
|
337
338
|
|
|
338
|
-
self.autoscaler_thread = None
|
|
339
|
+
self.autoscaler_thread: asyncio.Task | None = None
|
|
339
340
|
|
|
340
341
|
async def start(self):
|
|
341
342
|
await self.fn.hydrate(client=self.client)
|
|
@@ -519,11 +520,13 @@ class _FlashPrometheusAutoscaler:
|
|
|
519
520
|
return sum_metric, n_containers_with_metrics
|
|
520
521
|
|
|
521
522
|
async def _get_metrics(self, url: str) -> dict[str, list[Any]] | None: # technically any should be Sample
|
|
522
|
-
|
|
523
|
+
import aiohttp
|
|
524
|
+
from prometheus_client.parser import text_string_to_metric_families
|
|
525
|
+
from prometheus_client.samples import Sample
|
|
523
526
|
|
|
524
527
|
# Fetch the metrics from the endpoint
|
|
525
528
|
try:
|
|
526
|
-
response = await self.http_client.get(url, timeout=3)
|
|
529
|
+
response = await self.http_client.get(url, timeout=aiohttp.ClientTimeout(total=3))
|
|
527
530
|
response.raise_for_status()
|
|
528
531
|
except asyncio.TimeoutError:
|
|
529
532
|
logger.warning(f"[Modal Flash] Timeout getting metrics from {url}")
|
|
@@ -627,6 +630,8 @@ class _FlashPrometheusAutoscaler:
|
|
|
627
630
|
return new_replicas
|
|
628
631
|
|
|
629
632
|
async def stop(self):
|
|
633
|
+
if self.autoscaler_thread is None:
|
|
634
|
+
return
|
|
630
635
|
self.autoscaler_thread.cancel()
|
|
631
636
|
await self.autoscaler_thread
|
|
632
637
|
|
|
@@ -696,20 +701,21 @@ async def flash_prometheus_autoscaler(
|
|
|
696
701
|
|
|
697
702
|
|
|
698
703
|
@synchronizer.create_blocking
|
|
699
|
-
async def flash_get_containers(app_name: str, cls_name: str) -> list[
|
|
704
|
+
async def flash_get_containers(app_name: str, cls_name: str) -> list[Any]:
|
|
700
705
|
"""
|
|
701
706
|
Return a list of flash containers for a deployed Flash service.
|
|
702
707
|
|
|
708
|
+
Each entry exposes `task_id`, `host`, and `port` attributes.
|
|
709
|
+
|
|
703
710
|
This is a highly experimental method that can break or be removed at any time without warning.
|
|
704
711
|
Do not use this method unless explicitly instructed to do so by Modal support.
|
|
705
712
|
"""
|
|
706
713
|
client = await _Client.from_env()
|
|
707
|
-
fn = _Cls.from_name(app_name, cls_name).
|
|
708
|
-
assert fn is not None
|
|
714
|
+
fn = _Cls.from_name(app_name, cls_name)._get_class_service_function()
|
|
709
715
|
await fn.hydrate(client=client)
|
|
710
716
|
req = api_pb2.FlashContainerListRequest(function_id=fn.object_id)
|
|
711
717
|
resp = await client.stub.FlashContainerList(req)
|
|
712
|
-
return resp.containers
|
|
718
|
+
return list(resp.containers)
|
|
713
719
|
|
|
714
720
|
|
|
715
721
|
def _http_server(
|
|
@@ -404,17 +404,21 @@ class __flash_prometheus_autoscaler_spec(typing_extensions.Protocol):
|
|
|
404
404
|
flash_prometheus_autoscaler: __flash_prometheus_autoscaler_spec
|
|
405
405
|
|
|
406
406
|
class __flash_get_containers_spec(typing_extensions.Protocol):
|
|
407
|
-
def __call__(self, /, app_name: str, cls_name: str) -> list[
|
|
407
|
+
def __call__(self, /, app_name: str, cls_name: str) -> list[typing.Any]:
|
|
408
408
|
"""Return a list of flash containers for a deployed Flash service.
|
|
409
409
|
|
|
410
|
+
Each entry exposes `task_id`, `host`, and `port` attributes.
|
|
411
|
+
|
|
410
412
|
This is a highly experimental method that can break or be removed at any time without warning.
|
|
411
413
|
Do not use this method unless explicitly instructed to do so by Modal support.
|
|
412
414
|
"""
|
|
413
415
|
...
|
|
414
416
|
|
|
415
|
-
async def aio(self, /, app_name: str, cls_name: str) -> list[
|
|
417
|
+
async def aio(self, /, app_name: str, cls_name: str) -> list[typing.Any]:
|
|
416
418
|
"""Return a list of flash containers for a deployed Flash service.
|
|
417
419
|
|
|
420
|
+
Each entry exposes `task_id`, `host`, and `port` attributes.
|
|
421
|
+
|
|
418
422
|
This is a highly experimental method that can break or be removed at any time without warning.
|
|
419
423
|
Do not use this method unless explicitly instructed to do so by Modal support.
|
|
420
424
|
"""
|
|
@@ -117,7 +117,7 @@ class Function(
|
|
|
117
117
|
scaleup_window: typing.Optional[int] = None,
|
|
118
118
|
scaledown_window: typing.Optional[int] = None,
|
|
119
119
|
max_concurrent_inputs: typing.Optional[int] = None,
|
|
120
|
-
target_concurrent_inputs: typing.Optional[
|
|
120
|
+
target_concurrent_inputs: typing.Optional[float] = None,
|
|
121
121
|
batch_max_size: typing.Optional[int] = None,
|
|
122
122
|
batch_wait_ms: typing.Optional[int] = None,
|
|
123
123
|
cloud: typing.Optional[str] = None,
|
|
@@ -179,7 +179,7 @@ class Function(
|
|
|
179
179
|
buffer_containers: typing.Optional[int] = None,
|
|
180
180
|
scaleup_window: typing.Optional[int] = None,
|
|
181
181
|
scaledown_window: typing.Optional[int] = None,
|
|
182
|
-
target_concurrency: typing.Optional[
|
|
182
|
+
target_concurrency: typing.Optional[float] = None,
|
|
183
183
|
) -> modal.types.ServerAutoscalerSettings: ...
|
|
184
184
|
async def aio(
|
|
185
185
|
self,
|
|
@@ -190,7 +190,7 @@ class Function(
|
|
|
190
190
|
buffer_containers: typing.Optional[int] = None,
|
|
191
191
|
scaleup_window: typing.Optional[int] = None,
|
|
192
192
|
scaledown_window: typing.Optional[int] = None,
|
|
193
|
-
target_concurrency: typing.Optional[
|
|
193
|
+
target_concurrency: typing.Optional[float] = None,
|
|
194
194
|
) -> modal.types.ServerAutoscalerSettings: ...
|
|
195
195
|
|
|
196
196
|
_update_autoscaler_server: ___update_autoscaler_server_spec
|