modal 1.5.1.dev14__tar.gz → 1.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.5.1.dev14 → modal-1.5.2}/PKG-INFO +1 -1
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/__init__.py +2 -1
- modal-1.5.2/modal/_billing.py +83 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_environments.py +33 -29
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_function_variants.py +4 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_functions.py +8 -16
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_image.py +41 -13
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/container_io_manager.py +1 -1
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/gpu_memory_snapshot.py +5 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/task_lifecycle_manager.py +9 -4
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_server.py +31 -4
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/blob_utils.py +7 -0
- modal-1.5.2/modal/_utils/curl_utils.py +115 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/grpc_utils.py +80 -17
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/name_utils.py +8 -2
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/task_command_router_client.py +24 -8
- modal-1.5.2/modal/_workspace.py +388 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/app.py +15 -16
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/app.pyi +30 -28
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/billing.py +2 -1
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/billing.pyi +27 -36
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/call_graph.py +1 -30
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/_download.py +2 -1
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/_help.py +7 -1
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/app.py +24 -3
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/billing.py +9 -9
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/changelog.py +7 -7
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/cluster.py +1 -1
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/config.py +2 -2
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/container.py +33 -14
- modal-1.5.2/modal/cli/curl.py +144 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/dict.py +5 -5
- modal-1.5.2/modal/cli/endpoint.py +352 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/entry_point.py +8 -2
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/environment.py +17 -18
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/image.py +104 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/network_file_system.py +12 -6
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/profile.py +21 -6
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/queues.py +5 -5
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/secret.py +2 -2
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/shell.py +6 -2
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/utils.py +8 -3
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/volume.py +17 -9
- modal-1.5.2/modal/cli/workspace.py +175 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/client.pyi +2 -10
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cls.py +5 -4
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cls.pyi +4 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/config.py +4 -2
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/dict.py +1 -13
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/dict.pyi +4 -24
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/environments.pyi +64 -17
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/file_io.py +1 -1
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/file_io.pyi +9 -9
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/functions.pyi +16 -15
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/network_file_system.pyi +7 -7
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/queue.py +1 -13
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/queue.pyi +4 -24
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/sandbox.py +228 -73
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/sandbox.pyi +322 -69
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/sandbox_fs.py +1 -64
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/sandbox_fs.pyi +14 -117
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/secret.py +40 -22
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/secret.pyi +23 -24
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/server.pyi +65 -12
- modal-1.5.2/modal/types.py +289 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/volume.py +22 -39
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/volume.pyi +34 -73
- modal-1.5.2/modal/workspace.py +30 -0
- modal-1.5.2/modal/workspace.pyi +578 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.1.dev14 → modal-1.5.2}/modal.egg-info/SOURCES.txt +5 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_docs/gen_cli_docs.py +22 -7
- modal-1.5.2/modal_docs/gen_reference_docs.py +361 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_docs/mdmd/mdmd.py +139 -23
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_docs/mdmd/signatures.py +28 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_docs/mdmd/types.py +1 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/api_grpc.py +176 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/api_pb2.py +1149 -966
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/api_pb2.pyi +430 -44
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/api_pb2_grpc.py +367 -3
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/api_pb2_grpc.pyi +120 -8
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/modal_api_grpc.py +11 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/task_command_router_grpc.py +16 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/task_command_router_pb2.py +71 -41
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/task_command_router_pb2.pyi +40 -3
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/task_command_router_pb2_grpc.py +34 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/task_command_router_pb2_grpc.pyi +12 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_version/__init__.py +1 -1
- modal-1.5.1.dev14/modal/_billing.py +0 -155
- modal-1.5.1.dev14/modal/_workspace.py +0 -191
- modal-1.5.1.dev14/modal/workspace.py +0 -13
- modal-1.5.1.dev14/modal/workspace.pyi +0 -139
- modal-1.5.1.dev14/modal_docs/gen_reference_docs.py +0 -177
- {modal-1.5.1.dev14 → modal-1.5.2}/LICENSE +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/README.md +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/__main__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_clustered_functions.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_grpc_client.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_ipython.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_load_context.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_location.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_logs.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_object.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_output/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_output/manager.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_output/pty.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_output/rich.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_output/status.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_partial_function.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_resolver.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_resources.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_serialization.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_traceback.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_tunnel.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_tunnel.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_type_manager.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/logger.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_vendor/version.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/_watcher.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/builder/README.md +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/builder/base-images.json +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/_traceback.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/dashboard.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/import_refs.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/launch.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/logo.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/run.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/selector.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/skills.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cli/token.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/client.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/container_process.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/container_process.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/environments.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/exception.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/experimental/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/experimental/flash.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/experimental/ipython.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/functions.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/image.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/image.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/io_streams.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/io_streams.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/mount.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/mount.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/network_file_system.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/object.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/object.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/output.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/parallel_map.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/parallel_map.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/partial_function.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/partial_function.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/proxy.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/proxy.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/py.typed +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/retries.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/runner.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/runner.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/running_app.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/schedule.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/scheduler_placement.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/server.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/serving.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/serving.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/snapshot.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/snapshot.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/stream_type.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/token_flow.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal/token_flow.pyi +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_docs/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/__init__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_proto/py.typed +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/modal_version/__main__.py +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/pyproject.toml +0 -0
- {modal-1.5.1.dev14 → modal-1.5.2}/setup.cfg +0 -0
|
@@ -9,7 +9,7 @@ if sys.version_info[:2] >= (3, 15):
|
|
|
9
9
|
from modal_version import __version__
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
|
-
from . import billing
|
|
12
|
+
from . import billing, types
|
|
13
13
|
from ._runtime.execution_context import current_function_call_id, current_input_id, interact, is_local
|
|
14
14
|
from ._tunnel import Tunnel, forward
|
|
15
15
|
from .app import App
|
|
@@ -100,6 +100,7 @@ __all__ = [
|
|
|
100
100
|
"interact",
|
|
101
101
|
"method",
|
|
102
102
|
"parameter",
|
|
103
|
+
"types",
|
|
103
104
|
"web_endpoint",
|
|
104
105
|
"web_server",
|
|
105
106
|
"wsgi_app",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright Modal Labs 2025
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
from ._utils.deprecation import deprecation_warning
|
|
7
|
+
from .client import _Client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkspaceBillingReportItem(TypedDict):
|
|
11
|
+
object_id: str
|
|
12
|
+
description: str
|
|
13
|
+
environment_name: str
|
|
14
|
+
interval_start: datetime
|
|
15
|
+
cost: Decimal
|
|
16
|
+
tags: dict[str, str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _workspace_billing_report(
|
|
20
|
+
*,
|
|
21
|
+
start: datetime, # Start of the report, inclusive
|
|
22
|
+
end: datetime | None = None, # End of the report, exclusive
|
|
23
|
+
resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
|
|
24
|
+
tag_names: list[str] | None = None, # Optional additional metadata to include
|
|
25
|
+
client: _Client | None = None,
|
|
26
|
+
) -> list[WorkspaceBillingReportItem]:
|
|
27
|
+
"""Generate a tabular report of workspace usage by object and time.
|
|
28
|
+
|
|
29
|
+
The result will be a list of dictionaries for each interval (determined by `resolution`)
|
|
30
|
+
between the `start` and `end` limits. The dictionary represents a single Modal object
|
|
31
|
+
that billing can be attributed to (e.g., an App) along with metadata (including user-defined
|
|
32
|
+
tags) for identifying that object. The dictionary also contains a breakdown of the cost value
|
|
33
|
+
attributed to individual resources (for an App, this can be CPU, Memory, specific GPU types,
|
|
34
|
+
etc.). The specific resource types included in the breakdown are subject to change as
|
|
35
|
+
Modal's billing model evolves.
|
|
36
|
+
|
|
37
|
+
The `start` and `end` parameters are required to either have a UTC timezone or to be
|
|
38
|
+
timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
|
|
39
|
+
be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
|
|
40
|
+
parameters are partial: `start` will be rounded to the beginning of its interval, while
|
|
41
|
+
partial `end` intervals will be excluded.
|
|
42
|
+
|
|
43
|
+
Additional user-provided metadata can be included in the report if the objects have tags
|
|
44
|
+
and `tag_names` (i.e., keys) are specified in the request. Alternatively, pass `tag_names=["*"]`
|
|
45
|
+
to include all tags in the report. Note that tags will be attributed to the entire interval even
|
|
46
|
+
if they were added or removed at some point within it. If the tag name was not in use during an
|
|
47
|
+
interval, it will be absent from the tags dictionary in that output row.
|
|
48
|
+
|
|
49
|
+
It's also possible to generate reports using the
|
|
50
|
+
[`modal billing report`](https://modal.com/docs/cli/latest/billing) CLI command. The CLI
|
|
51
|
+
has a few convenience features for generating reports across relative time ranges.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
deprecation_warning(
|
|
56
|
+
(2026, 6, 18),
|
|
57
|
+
"The `modal.billing.workspace_billing_report()` function is deprecated. "
|
|
58
|
+
"Use the `workspace.billing.report()` method on the `modal.Workspace` object instead.",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
from ._workspace import _Workspace
|
|
62
|
+
|
|
63
|
+
data = await _Workspace.from_context(client=client).billing.report(
|
|
64
|
+
start=start,
|
|
65
|
+
end=end,
|
|
66
|
+
resolution=resolution,
|
|
67
|
+
tag_names=tag_names,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
res: list[WorkspaceBillingReportItem] = []
|
|
71
|
+
for datum in data:
|
|
72
|
+
item: WorkspaceBillingReportItem = {
|
|
73
|
+
"object_id": datum.object_id,
|
|
74
|
+
"description": datum.description,
|
|
75
|
+
"environment_name": datum.environment_name,
|
|
76
|
+
"interval_start": datum.interval_start,
|
|
77
|
+
"cost": Decimal(datum.cost),
|
|
78
|
+
"tags": dict(datum.tags),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
res.append(item)
|
|
82
|
+
|
|
83
|
+
return res
|
|
@@ -13,7 +13,6 @@ from synchronicity import classproperty
|
|
|
13
13
|
|
|
14
14
|
from modal_proto import api_pb2
|
|
15
15
|
|
|
16
|
-
from ._billing import BILLING_DOCSTRING, BillingReportItem
|
|
17
16
|
from ._load_context import LoadContext
|
|
18
17
|
from ._object import _Object
|
|
19
18
|
from ._resolver import Resolver
|
|
@@ -21,6 +20,7 @@ from ._utils.name_utils import check_environment_name
|
|
|
21
20
|
from .client import _Client
|
|
22
21
|
from .config import config, logger
|
|
23
22
|
from .exception import InvalidError, WorkspaceManagementError
|
|
23
|
+
from .types import BillingReportItem
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
@dataclass(frozen=True)
|
|
@@ -103,9 +103,6 @@ class _EnvironmentManager:
|
|
|
103
103
|
await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
MemberRole = Literal["viewer", "contributor"]
|
|
107
|
-
|
|
108
|
-
|
|
109
106
|
def _role_to_proto(role: str) -> "api_pb2.EnvironmentRole.ValueType":
|
|
110
107
|
match role:
|
|
111
108
|
case "viewer":
|
|
@@ -116,7 +113,7 @@ def _role_to_proto(role: str) -> "api_pb2.EnvironmentRole.ValueType":
|
|
|
116
113
|
raise InvalidError(f"Invalid Environment role: {role!r} (expected 'viewer' or 'contributor')")
|
|
117
114
|
|
|
118
115
|
|
|
119
|
-
def _role_from_proto(proto_value: int) ->
|
|
116
|
+
def _role_from_proto(proto_value: int) -> str:
|
|
120
117
|
match proto_value:
|
|
121
118
|
case api_pb2.ENVIRONMENT_ROLE_VIEWER:
|
|
122
119
|
return "viewer"
|
|
@@ -137,7 +134,7 @@ class _EnvironmentMembersManager:
|
|
|
137
134
|
"""mdmd:hidden"""
|
|
138
135
|
self._environment = environment
|
|
139
136
|
|
|
140
|
-
async def list(self) -> dict[Literal["users", "service_users"], dict[str,
|
|
137
|
+
async def list(self) -> dict[Literal["users", "service_users"], dict[str, str]]:
|
|
141
138
|
"""Return the members of a restricted Environment with their roles.
|
|
142
139
|
|
|
143
140
|
**Examples:**
|
|
@@ -155,8 +152,8 @@ class _EnvironmentMembersManager:
|
|
|
155
152
|
req = api_pb2.EnvironmentGetManagedRequest(environment_id=self._environment.object_id)
|
|
156
153
|
resp = await self._environment.client.stub.EnvironmentGetManaged(req)
|
|
157
154
|
|
|
158
|
-
users: dict[str,
|
|
159
|
-
service_users: dict[str,
|
|
155
|
+
users: dict[str, str] = {}
|
|
156
|
+
service_users: dict[str, str] = {}
|
|
160
157
|
for principal in resp.principal_roles:
|
|
161
158
|
role = _role_from_proto(principal.role)
|
|
162
159
|
if principal.user_id:
|
|
@@ -169,8 +166,8 @@ class _EnvironmentMembersManager:
|
|
|
169
166
|
async def update(
|
|
170
167
|
self,
|
|
171
168
|
*,
|
|
172
|
-
users: Mapping[str,
|
|
173
|
-
service_users: Mapping[str,
|
|
169
|
+
users: Mapping[str, str] | None = None,
|
|
170
|
+
service_users: Mapping[str, str] | None = None,
|
|
174
171
|
) -> None:
|
|
175
172
|
"""Add or modify roles for members of a restricted Environment.
|
|
176
173
|
|
|
@@ -416,26 +413,33 @@ class _EnvironmentBillingManager:
|
|
|
416
413
|
resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
|
|
417
414
|
tag_names: list[str] | None = None, # Optional additional metadata to include
|
|
418
415
|
) -> list[BillingReportItem]:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
416
|
+
"""Return a cost report for Environment usage, broken down by object and time.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
start: Start of the report, inclusive and rounded to the beginning of the interval.
|
|
420
|
+
Must be in UTC or timezone-naive (interpreted as UTC).
|
|
421
|
+
end: End of the report, exclusive. Must be in UTC or timezone-naive. Partial final
|
|
422
|
+
intervals will be excluded from the report.
|
|
423
|
+
resolution: Resolution, e.g. "d" for daily or "h" for hourly.
|
|
424
|
+
tag_names: List of tag names; each row will include the tag name and value in use
|
|
425
|
+
for that object during the relevant time interval. Pass `["*"]` to include all
|
|
426
|
+
tags in the report.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
A list of `BillingReportItem` dataclasses. Each item reports the cost attributed to
|
|
430
|
+
a specific Modal object during a given time interval. Cost is further broken down by
|
|
431
|
+
the resource type that generated it (e.g. CPU, Memory, specific GPU usage).
|
|
432
|
+
Note that the specific resource types included in the breakdown are subject to change
|
|
433
|
+
as Modal's billing model evolves.
|
|
434
|
+
|
|
435
|
+
See also:
|
|
436
|
+
- [`modal environment billing report`](https://modal.com/docs/cli/latest/environment#modal-environment-billing-report):
|
|
437
|
+
An environment report CLI that has convenience features around relative time range queries
|
|
438
|
+
and JSON/CSV output.
|
|
439
|
+
- [`Workspace.billing.report()`](https://modal.com/docs/sdk/py/latest/modal.Workspace#billingreport):
|
|
440
|
+
An analogous report API for the entire Workspace.
|
|
438
441
|
|
|
442
|
+
"""
|
|
439
443
|
if tag_names is None:
|
|
440
444
|
tag_names = []
|
|
441
445
|
|
|
@@ -51,6 +51,7 @@ class _FunctionOptions:
|
|
|
51
51
|
target_concurrent_inputs: int | None = None
|
|
52
52
|
batch_max_size: int | None = None
|
|
53
53
|
batch_wait_ms: int | None = None
|
|
54
|
+
routing_region: str | None = None
|
|
54
55
|
|
|
55
56
|
@classmethod
|
|
56
57
|
def new(
|
|
@@ -73,6 +74,7 @@ class _FunctionOptions:
|
|
|
73
74
|
target_concurrent_inputs: int | None = None,
|
|
74
75
|
batch_max_size: int | None = None,
|
|
75
76
|
batch_wait_ms: int | None = None,
|
|
77
|
+
routing_region: str | None = None,
|
|
76
78
|
) -> "_FunctionOptions":
|
|
77
79
|
"""Internal constructor that validates and normalizes public parameters."""
|
|
78
80
|
retry_policy = _parse_retries(retries)
|
|
@@ -119,6 +121,7 @@ class _FunctionOptions:
|
|
|
119
121
|
target_concurrent_inputs=target_concurrent_inputs,
|
|
120
122
|
batch_max_size=batch_max_size,
|
|
121
123
|
batch_wait_ms=batch_wait_ms,
|
|
124
|
+
routing_region=routing_region,
|
|
122
125
|
)
|
|
123
126
|
|
|
124
127
|
def merge_options(self, new_options: "_FunctionOptions") -> "_FunctionOptions":
|
|
@@ -182,6 +185,7 @@ class _FunctionOptions:
|
|
|
182
185
|
batch_linger_ms=self.batch_wait_ms,
|
|
183
186
|
scheduler_placement=self.scheduler_placement,
|
|
184
187
|
cloud_provider_str=self.cloud,
|
|
188
|
+
routing_region=self.routing_region,
|
|
185
189
|
)
|
|
186
190
|
|
|
187
191
|
|
|
@@ -90,6 +90,7 @@ from .proxy import _Proxy
|
|
|
90
90
|
from .retries import Retries, RetryManager
|
|
91
91
|
from .schedule import Schedule
|
|
92
92
|
from .secret import _Secret
|
|
93
|
+
from .types import FunctionStats
|
|
93
94
|
from .volume import _Volume, _volume_to_mount_proto
|
|
94
95
|
|
|
95
96
|
if TYPE_CHECKING:
|
|
@@ -541,17 +542,6 @@ class _InputPlaneInvocation:
|
|
|
541
542
|
return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
|
|
542
543
|
|
|
543
544
|
|
|
544
|
-
# Wrapper type for api_pb2.FunctionStats
|
|
545
|
-
@dataclass(frozen=True)
|
|
546
|
-
class FunctionStats:
|
|
547
|
-
"""Simple data structure storing stats for a running function."""
|
|
548
|
-
|
|
549
|
-
backlog: int
|
|
550
|
-
num_total_runners: int
|
|
551
|
-
num_running_inputs: int
|
|
552
|
-
input_headroom: int
|
|
553
|
-
|
|
554
|
-
|
|
555
545
|
@dataclass
|
|
556
546
|
class _FunctionSpec:
|
|
557
547
|
"""
|
|
@@ -1195,10 +1185,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1195
1185
|
its static configuration.
|
|
1196
1186
|
|
|
1197
1187
|
Args:
|
|
1198
|
-
min_containers: Minimum number of containers to keep running
|
|
1199
|
-
max_containers: Maximum concurrent containers
|
|
1200
|
-
buffer_containers: Extra containers to keep warm beyond demand
|
|
1201
|
-
scaledown_window:
|
|
1188
|
+
min_containers: Minimum number of containers to keep running.
|
|
1189
|
+
max_containers: Maximum concurrent containers.
|
|
1190
|
+
buffer_containers: Extra containers to keep warm beyond current demand.
|
|
1191
|
+
scaledown_window: Maximum duration (in seconds) idle containers wait before scaling down.
|
|
1202
1192
|
|
|
1203
1193
|
Examples:
|
|
1204
1194
|
```python notest
|
|
@@ -1212,7 +1202,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1212
1202
|
|
|
1213
1203
|
# Extend the scaledown window to increase the amount of time that idle containers stay alive
|
|
1214
1204
|
f.update_autoscaler(scaledown_window=300)
|
|
1215
|
-
|
|
1205
|
+
```
|
|
1216
1206
|
|
|
1217
1207
|
"""
|
|
1218
1208
|
# Assert .update_autoscaler() is not called on a method as opposed to the Object. Applicable for Cls only.
|
|
@@ -1519,6 +1509,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1519
1509
|
timeout: int | None = None,
|
|
1520
1510
|
region: str | Sequence[str] | None = None,
|
|
1521
1511
|
cloud: str | None = None,
|
|
1512
|
+
routing_region: str | None = None,
|
|
1522
1513
|
) -> "_Function[P, ReturnType, OriginalReturnType]":
|
|
1523
1514
|
"""Dynamically override the static Function configuration with invocation-specific values.
|
|
1524
1515
|
|
|
@@ -1567,6 +1558,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1567
1558
|
timeout=timeout,
|
|
1568
1559
|
region=region,
|
|
1569
1560
|
cloud=cloud,
|
|
1561
|
+
routing_region=routing_region,
|
|
1570
1562
|
)
|
|
1571
1563
|
|
|
1572
1564
|
return self._apply_dynamic_config(options, "with_options")
|
|
@@ -117,13 +117,34 @@ def _validate_image_tag(tag: str) -> None:
|
|
|
117
117
|
check_object_name(tag, "Image tag")
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
def _parse_named_image_ref(name: str) -> str:
|
|
120
|
+
def _parse_named_image_ref(name: str) -> tuple[str, str]:
|
|
121
|
+
"""Parse an image reference, returning (namespace_prefix, name_tag).
|
|
122
|
+
|
|
123
|
+
If the name contains a '/', the part before the last '/' is extracted as
|
|
124
|
+
a namespace prefix (intended for environment/name or workspace/env/name
|
|
125
|
+
syntax). The actual image name (after the last '/') is validated as a
|
|
126
|
+
standard object name.
|
|
127
|
+
|
|
128
|
+
Returns a tuple of (prefix, "full_name:tag") where prefix is empty string
|
|
129
|
+
if no '/' is present.
|
|
130
|
+
"""
|
|
121
131
|
image_name, sep, tag = name.partition(":")
|
|
122
132
|
if not sep:
|
|
123
133
|
tag = "latest"
|
|
134
|
+
|
|
135
|
+
prefix = ""
|
|
136
|
+
if "/" in image_name:
|
|
137
|
+
prefix, image_name = image_name.rsplit("/", 1)
|
|
138
|
+
if not prefix:
|
|
139
|
+
raise InvalidError("Invalid Image name: '/' prefix must be non-empty.")
|
|
140
|
+
if not image_name:
|
|
141
|
+
raise InvalidError("Invalid Image name: name after '/' must be non-empty.")
|
|
142
|
+
|
|
124
143
|
_validate_image_name(image_name)
|
|
125
144
|
_validate_image_tag(tag)
|
|
126
|
-
|
|
145
|
+
|
|
146
|
+
full_name = f"{prefix}/{image_name}" if prefix else image_name
|
|
147
|
+
return prefix, f"{full_name}:{tag}"
|
|
127
148
|
|
|
128
149
|
|
|
129
150
|
def _validate_python_version(
|
|
@@ -1022,7 +1043,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1022
1043
|
```
|
|
1023
1044
|
"""
|
|
1024
1045
|
if app.app_id is None:
|
|
1025
|
-
raise InvalidError("App has not been initialized yet. Use the
|
|
1046
|
+
raise InvalidError("App has not been initialized yet. Use the context manager `app.run()` or `App.lookup`")
|
|
1026
1047
|
|
|
1027
1048
|
resolver = Resolver()
|
|
1028
1049
|
async with TaskContext() as tc:
|
|
@@ -2923,12 +2944,15 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2923
2944
|
...
|
|
2924
2945
|
```
|
|
2925
2946
|
"""
|
|
2926
|
-
tag = _parse_named_image_ref(name)
|
|
2947
|
+
namespace_prefix, tag = _parse_named_image_ref(name)
|
|
2948
|
+
|
|
2949
|
+
if namespace_prefix and environment_name:
|
|
2950
|
+
raise InvalidError("Cannot specify 'environment_name' when the image name contains a '/'.")
|
|
2927
2951
|
|
|
2928
2952
|
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
2929
2953
|
req = api_pb2.ImageGetByTagRequest(
|
|
2930
2954
|
tag=tag,
|
|
2931
|
-
environment_name=load_context.environment_name,
|
|
2955
|
+
environment_name="" if namespace_prefix else load_context.environment_name,
|
|
2932
2956
|
)
|
|
2933
2957
|
response = await load_context.client.stub.ImageGetByTag(req)
|
|
2934
2958
|
self._hydrate(response.image_id, load_context.client, None)
|
|
@@ -2963,21 +2987,25 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2963
2987
|
image.publish("my-image-with-numpy:v1")
|
|
2964
2988
|
```
|
|
2965
2989
|
"""
|
|
2966
|
-
tag = _parse_named_image_ref(name)
|
|
2990
|
+
namespace_prefix, tag = _parse_named_image_ref(name)
|
|
2967
2991
|
|
|
2968
|
-
if
|
|
2969
|
-
|
|
2992
|
+
if namespace_prefix:
|
|
2993
|
+
if environment_name:
|
|
2994
|
+
raise InvalidError("Cannot specify 'environment_name' when the image name contains a '/'.")
|
|
2995
|
+
resolved_env = ""
|
|
2996
|
+
else:
|
|
2997
|
+
resolved_env = environment_name or config.get("environment") or ""
|
|
2970
2998
|
|
|
2971
|
-
|
|
2972
|
-
|
|
2999
|
+
if self._object_id is None:
|
|
3000
|
+
raise InvalidError("Cannot publish an image that has not been created yet. Call `.build()` first.")
|
|
2973
3001
|
|
|
2974
|
-
|
|
3002
|
+
_client = client or await _Client.from_env()
|
|
2975
3003
|
|
|
2976
3004
|
await _client.stub.ImagePublish(
|
|
2977
3005
|
api_pb2.ImagePublishRequest(
|
|
2978
|
-
image_id=self.
|
|
3006
|
+
image_id=self._object_id,
|
|
2979
3007
|
environment_name=resolved_env,
|
|
2980
|
-
|
|
3008
|
+
allow_public=False,
|
|
2981
3009
|
tag=tag,
|
|
2982
3010
|
)
|
|
2983
3011
|
)
|
|
@@ -264,7 +264,7 @@ class IOContext:
|
|
|
264
264
|
if not inspect.isasyncgen(expected_async_gen):
|
|
265
265
|
raise InvalidError(f"Async generator function returned value of type {type(expected_async_gen)}")
|
|
266
266
|
|
|
267
|
-
async with aclosing(expected_async_gen) as gen:
|
|
267
|
+
async with aclosing(cast(AsyncGenerator[Any, None], expected_async_gen)) as gen:
|
|
268
268
|
async for result in gen:
|
|
269
269
|
yield result
|
|
270
270
|
logger.debug(f"Finished generator input {self.input_ids}")
|
|
@@ -17,6 +17,11 @@ from modal.config import config, logger
|
|
|
17
17
|
|
|
18
18
|
CUDA_CHECKPOINT_PATH: str = config.get("cuda_checkpoint_path")
|
|
19
19
|
|
|
20
|
+
# Exit code used to signal to modal-runtime that a GPU memory snapshot restore failed
|
|
21
|
+
# and the task should be retried without a snapshot. This is a sentinel value that must
|
|
22
|
+
# stay in sync with `GPU_SNAPSHOT_RESTORE_FAILED_EXIT_CODE` in runner.rs.
|
|
23
|
+
SNAPSHOT_RESTORE_FAILED_EXIT_CODE: int = 222
|
|
24
|
+
|
|
20
25
|
# Maximum total duration for each individual `cuda-checkpoint` invocation.
|
|
21
26
|
CUDA_CHECKPOINT_TIMEOUT: float = 3 * 60.0
|
|
22
27
|
|
|
@@ -180,10 +180,15 @@ class _TaskLifecycleManager:
|
|
|
180
180
|
if self.function_def._experimental_enable_gpu_snapshot and self.function_def.resources.gpu_config.gpu_type:
|
|
181
181
|
logger.debug("GPU memory snapshot enabled. Attempting to restore GPU memory.")
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
try:
|
|
184
|
+
if self._cuda_checkpoint_session is None:
|
|
185
|
+
raise gpu_memory_snapshot.CudaCheckpointException("CudaCheckpointSession not found")
|
|
186
|
+
self._cuda_checkpoint_session.restore()
|
|
187
|
+
except gpu_memory_snapshot.CudaCheckpointException as exc:
|
|
188
|
+
logger.warning(f"GPU memory snapshot restore failed; retrying task without snapshot. Error: {exc}")
|
|
189
|
+
sys.stderr.flush()
|
|
190
|
+
# Exit with a sentinel code that the runtime will use to retry the task without a snapshot.
|
|
191
|
+
os._exit(gpu_memory_snapshot.SNAPSHOT_RESTORE_FAILED_EXIT_CODE)
|
|
187
192
|
|
|
188
193
|
self._client = await _Client.from_env()
|
|
189
194
|
|
|
@@ -46,7 +46,7 @@ def validate_http_server_config(
|
|
|
46
46
|
class _Server:
|
|
47
47
|
"""Server runs an HTTP server started in an `@modal.enter` method.
|
|
48
48
|
|
|
49
|
-
See [
|
|
49
|
+
See the [guide](https://modal.com/docs/guide/servers) for more information.
|
|
50
50
|
|
|
51
51
|
Generally, you will not construct a Server directly.
|
|
52
52
|
Instead, use the [`@app.server()`](https://modal.com/docs/sdk/py/latest/modal.App#server) decorator.
|
|
@@ -77,6 +77,7 @@ class _Server:
|
|
|
77
77
|
|
|
78
78
|
@property
|
|
79
79
|
def object_id(self) -> str:
|
|
80
|
+
"""Modal's internal ID for this Server instance."""
|
|
80
81
|
return self._service_function.object_id
|
|
81
82
|
|
|
82
83
|
@staticmethod
|
|
@@ -91,6 +92,7 @@ class _Server:
|
|
|
91
92
|
|
|
92
93
|
@live_method
|
|
93
94
|
async def get_url(self) -> str | None:
|
|
95
|
+
"""The URL for making requests to this Server."""
|
|
94
96
|
urls = await self._get_service_function()._experimental_get_flash_urls()
|
|
95
97
|
# Return the first URL if it exists. Servers should only have one URL
|
|
96
98
|
# since they only have one region.
|
|
@@ -101,16 +103,28 @@ class _Server:
|
|
|
101
103
|
async def update_autoscaler(
|
|
102
104
|
self,
|
|
103
105
|
*,
|
|
106
|
+
target_concurrency: int | None = None,
|
|
104
107
|
min_containers: int | None = None,
|
|
105
108
|
max_containers: int | None = None,
|
|
106
109
|
buffer_containers: int | None = None,
|
|
107
110
|
scaleup_window: int | None = None,
|
|
108
111
|
scaledown_window: int | None = None,
|
|
109
|
-
target_concurrency: int | None = None,
|
|
110
112
|
) -> None:
|
|
111
113
|
"""Override the current autoscaler behavior for this Server.
|
|
112
114
|
|
|
113
|
-
Unspecified parameters will retain their current value.
|
|
115
|
+
Unspecified parameters will retain their current value, i.e. either the static value
|
|
116
|
+
from the `@app.server()` decorator, or an override value from a previous call to this method.
|
|
117
|
+
|
|
118
|
+
Subsequent deployments of the App containing this Server will reset the autoscaler back to
|
|
119
|
+
its static configuration.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
target_concurrency: Target number of concurrent requests per container.
|
|
123
|
+
min_containers: Minimum number of containers to keep running regardless of demand.
|
|
124
|
+
max_containers: Limit on the number of containers that can be concurrently running.
|
|
125
|
+
buffer_containers: Extra containers to scale up beyond current demand.
|
|
126
|
+
scaleup_window: Seconds of sustained demand required before scaling up new containers.
|
|
127
|
+
scaledown_window: Maximum duration (in seconds) idle containers wait before scaling down.
|
|
114
128
|
|
|
115
129
|
Examples:
|
|
116
130
|
```python notest
|
|
@@ -130,7 +144,7 @@ class _Server:
|
|
|
130
144
|
|
|
131
145
|
# Disable the Server autoscaling by setting target_concurrency to 0
|
|
132
146
|
server.update_autoscaler(target_concurrency=0)
|
|
133
|
-
|
|
147
|
+
```
|
|
134
148
|
|
|
135
149
|
"""
|
|
136
150
|
return await self._get_service_function()._update_autoscaler(
|
|
@@ -144,6 +158,13 @@ class _Server:
|
|
|
144
158
|
|
|
145
159
|
# ============ Hydration ============
|
|
146
160
|
async def hydrate(self, client: _Client | None = None) -> "_Server":
|
|
161
|
+
"""Synchronize the local object with its identity on the Modal server.
|
|
162
|
+
|
|
163
|
+
It is rarely necessary to call this method explicitly, as most operations will
|
|
164
|
+
lazily hydrate when needed. The main use case is when you need to access object
|
|
165
|
+
metadata, such as its ID.
|
|
166
|
+
|
|
167
|
+
"""
|
|
147
168
|
# This is required since we want to support @livemethod() decorated methods
|
|
148
169
|
service_function = self._get_service_function()
|
|
149
170
|
await service_function.hydrate(client)
|
|
@@ -183,6 +204,12 @@ class _Server:
|
|
|
183
204
|
object with metadata from Modal servers until the first
|
|
184
205
|
time it is actually used.
|
|
185
206
|
|
|
207
|
+
Args:
|
|
208
|
+
app_name: Name of the App containing the Server.
|
|
209
|
+
name: Name of the Server within the App.
|
|
210
|
+
environment_name: Name of the Environment where the App is deployed.
|
|
211
|
+
client: Modal client instance for this session.
|
|
212
|
+
|
|
186
213
|
```python notest
|
|
187
214
|
server = modal.Server.from_name("other-app", "Server")
|
|
188
215
|
```
|
|
@@ -138,6 +138,13 @@ async def _upload_to_s3_url(
|
|
|
138
138
|
text = await resp.text()
|
|
139
139
|
except Exception:
|
|
140
140
|
text = "<no body>"
|
|
141
|
+
|
|
142
|
+
if "The Content-MD5 you specified did not match what we received." in text:
|
|
143
|
+
raise ExecutionError(
|
|
144
|
+
"Hash mismatch during upload. This may be caused by files changing while an upload is in "
|
|
145
|
+
"progress. Please try again."
|
|
146
|
+
)
|
|
147
|
+
|
|
141
148
|
raise ExecutionError(f"Put to url {upload_url} failed with status {resp.status}: {text}")
|
|
142
149
|
|
|
143
150
|
# client side ETag checksum verification
|