modal 1.4.4.dev22__tar.gz → 1.5.1__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- {modal-1.4.4.dev22 → modal-1.5.1}/PKG-INFO +6 -3
- {modal-1.4.4.dev22 → modal-1.5.1}/README.md +1 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/__init__.py +2 -0
- modal-1.5.1/modal/_billing.py +134 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_environments.py +85 -2
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_functions.py +13 -9
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_grpc_client.py +3 -3
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_image.py +39 -4
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_partial_function.py +1 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/gpu_memory_snapshot.py +5 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/task_lifecycle_manager.py +9 -4
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_server.py +55 -18
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/blob_utils.py +3 -1
- modal-1.5.1/modal/_utils/curl_utils.py +115 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/grpc_utils.py +85 -8
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/http_utils.py +20 -1
- modal-1.5.1/modal/_utils/proxy_support.py +102 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/task_command_router_client.py +29 -2
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/time_utils.py +23 -0
- modal-1.5.1/modal/_workspace.py +342 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/app.py +37 -23
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/app.pyi +56 -42
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/billing.py +3 -2
- modal-1.5.1/modal/billing.pyi +176 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/_help.py +3 -3
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/app.py +24 -3
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/billing.py +108 -77
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/changelog.py +1 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/config.py +1 -1
- modal-1.5.1/modal/cli/curl.py +137 -0
- modal-1.5.1/modal/cli/endpoint.py +352 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/entry_point.py +8 -2
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/environment.py +140 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/image.py +7 -7
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/shell.py +7 -14
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/utils.py +16 -4
- modal-1.5.1/modal/cli/workspace.py +129 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/client.py +1 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/client.pyi +2 -10
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cls.py +2 -2
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cls.pyi +4 -4
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/config.py +1 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/environments.py +2 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/environments.pyi +91 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/exception.py +1 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/functions.pyi +21 -18
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/partial_function.pyi +1 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/runner.py +16 -12
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/sandbox.py +174 -64
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/sandbox.pyi +122 -38
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/sandbox_fs.py +39 -2
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/schedule.py +1 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/secret.py +39 -9
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/secret.pyi +19 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/server.pyi +78 -15
- modal-1.5.1/modal/workspace.py +25 -0
- modal-1.5.1/modal/workspace.pyi +450 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/PKG-INFO +6 -3
- {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/SOURCES.txt +6 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/requires.txt +5 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/gen_cli_docs.py +48 -2
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/mdmd/mdmd.py +10 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/mdmd/types.py +1 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_grpc.py +195 -3
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_pb2.py +1353 -1093
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_pb2.pyi +522 -21
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_pb2_grpc.py +401 -3
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_pb2_grpc.pyi +126 -2
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/modal_api_grpc.py +12 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_grpc.py +32 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_pb2.py +119 -79
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_pb2.pyi +68 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_pb2_grpc.py +71 -2
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_pb2_grpc.pyi +30 -4
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_version/__init__.py +1 -1
- {modal-1.4.4.dev22 → modal-1.5.1}/pyproject.toml +30 -16
- modal-1.4.4.dev22/modal/_billing.py +0 -94
- modal-1.4.4.dev22/modal/_workspace.py +0 -116
- modal-1.4.4.dev22/modal/workspace.py +0 -12
- modal-1.4.4.dev22/modal/workspace.pyi +0 -105
- {modal-1.4.4.dev22 → modal-1.5.1}/LICENSE +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/__main__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_clustered_functions.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_function_variants.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_ipython.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_load_context.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_location.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_logs.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_object.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/manager.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/pty.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/rich.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/status.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_resolver.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_resources.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_serialization.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_traceback.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_tunnel.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_tunnel.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_type_manager.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/logger.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/version.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/_watcher.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/README.md +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/base-images.json +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/call_graph.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/_download.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/_traceback.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/bootstrap.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/cluster.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/container.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/dashboard.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/dict.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/import_refs.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/launch.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/logo.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/profile.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/queues.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/run.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/secret.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/selector.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/skills.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/token.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/volume.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/container_process.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/container_process.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/dict.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/dict.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/experimental/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/experimental/flash.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/experimental/ipython.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/file_io.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/file_io.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/functions.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/image.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/image.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/io_streams.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/io_streams.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/mount.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/mount.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/network_file_system.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/network_file_system.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/object.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/object.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/output.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/parallel_map.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/parallel_map.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/partial_function.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/proxy.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/proxy.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/py.typed +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/queue.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/queue.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/retries.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/runner.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/running_app.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/scheduler_placement.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/server.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/serving.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/serving.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/snapshot.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/snapshot.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/stream_type.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/token_flow.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/token_flow.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/volume.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal/volume.pyi +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/__init__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/py.typed +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/modal_version/__main__.py +0 -0
- {modal-1.4.4.dev22 → modal-1.5.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,12 +23,15 @@ Requires-Dist: grpclib<0.4.10,>=0.4.7; python_version < "3.14"
|
|
|
23
23
|
Requires-Dist: grpclib<0.4.10,>=0.4.9; python_version >= "3.14"
|
|
24
24
|
Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
|
|
25
25
|
Requires-Dist: rich>=12.0.0
|
|
26
|
-
Requires-Dist: synchronicity~=0.12.
|
|
26
|
+
Requires-Dist: synchronicity~=0.12.5
|
|
27
27
|
Requires-Dist: toml
|
|
28
28
|
Requires-Dist: types-certifi
|
|
29
29
|
Requires-Dist: types-toml
|
|
30
30
|
Requires-Dist: watchfiles
|
|
31
31
|
Requires-Dist: typing_extensions~=4.6
|
|
32
|
+
Provides-Extra: api-proxy-support
|
|
33
|
+
Requires-Dist: python-socks[asyncio]>=2.5; extra == "api-proxy-support"
|
|
34
|
+
Requires-Dist: aiohttp-socks>=0.8; extra == "api-proxy-support"
|
|
32
35
|
Dynamic: license-file
|
|
33
36
|
|
|
34
37
|
# Modal Python SDK
|
|
@@ -45,7 +48,7 @@ access to serverless cloud compute from Python scripts on your local computer.
|
|
|
45
48
|
See the [online documentation](https://modal.com/docs/guide) for many
|
|
46
49
|
[example applications](https://modal.com/docs/examples),
|
|
47
50
|
a [user guide](https://modal.com/docs/guide), and the detailed
|
|
48
|
-
[API reference](https://modal.com/docs/
|
|
51
|
+
[API reference](https://modal.com/docs/sdk/py/latest).
|
|
49
52
|
|
|
50
53
|
## Installation
|
|
51
54
|
|
|
@@ -12,7 +12,7 @@ access to serverless cloud compute from Python scripts on your local computer.
|
|
|
12
12
|
See the [online documentation](https://modal.com/docs/guide) for many
|
|
13
13
|
[example applications](https://modal.com/docs/examples),
|
|
14
14
|
a [user guide](https://modal.com/docs/guide), and the detailed
|
|
15
|
-
[API reference](https://modal.com/docs/
|
|
15
|
+
[API reference](https://modal.com/docs/sdk/py/latest).
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -43,6 +43,7 @@ try:
|
|
|
43
43
|
from .schedule import Cron, Period
|
|
44
44
|
from .scheduler_placement import SchedulerPlacement
|
|
45
45
|
from .secret import Secret
|
|
46
|
+
from .server import Server
|
|
46
47
|
from .snapshot import SandboxSnapshot
|
|
47
48
|
from .volume import Volume
|
|
48
49
|
from .workspace import Workspace
|
|
@@ -80,6 +81,7 @@ __all__ = [
|
|
|
80
81
|
"SandboxSnapshot",
|
|
81
82
|
"SchedulerPlacement",
|
|
82
83
|
"Secret",
|
|
84
|
+
"Server",
|
|
83
85
|
"Tunnel",
|
|
84
86
|
"Volume",
|
|
85
87
|
"Workspace",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright Modal Labs 2025
|
|
2
|
+
from dataclasses import FrozenInstanceError, dataclass
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any, Iterable, TypedDict
|
|
6
|
+
|
|
7
|
+
from modal_proto import api_pb2
|
|
8
|
+
|
|
9
|
+
from ._utils.deprecation import deprecation_warning
|
|
10
|
+
from .client import _Client
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorkspaceBillingReportItem(TypedDict):
|
|
14
|
+
object_id: str
|
|
15
|
+
description: str
|
|
16
|
+
environment_name: str
|
|
17
|
+
interval_start: datetime
|
|
18
|
+
cost: Decimal
|
|
19
|
+
tags: dict[str, str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True, frozen=True)
|
|
23
|
+
class BillingReportItem:
|
|
24
|
+
object_id: str
|
|
25
|
+
description: str
|
|
26
|
+
environment_name: str
|
|
27
|
+
interval_start: datetime
|
|
28
|
+
cost: Decimal
|
|
29
|
+
cost_by_resource: dict[str, Decimal]
|
|
30
|
+
tags: dict[str, str]
|
|
31
|
+
|
|
32
|
+
def __getitem__(self, key: str) -> Any:
|
|
33
|
+
"""mdmd:ignore"""
|
|
34
|
+
if key not in self.__slots__:
|
|
35
|
+
raise KeyError(key)
|
|
36
|
+
|
|
37
|
+
return getattr(self, key)
|
|
38
|
+
|
|
39
|
+
def __setitem__(self, key: str, _: Any):
|
|
40
|
+
"""mdmd:ignore"""
|
|
41
|
+
raise FrozenInstanceError(f"cannot assign to field {key!r}")
|
|
42
|
+
|
|
43
|
+
def keys(self) -> Iterable[str]:
|
|
44
|
+
"""mdmd:ignore"""
|
|
45
|
+
yield from self.__slots__
|
|
46
|
+
|
|
47
|
+
def values(self) -> Iterable[Any]:
|
|
48
|
+
"""mdmd:ignore"""
|
|
49
|
+
for k in self.__slots__:
|
|
50
|
+
yield getattr(self, k)
|
|
51
|
+
|
|
52
|
+
def items(self) -> Iterable[tuple[str, Any]]:
|
|
53
|
+
"""mdmd:ignore"""
|
|
54
|
+
for k in self.__slots__:
|
|
55
|
+
yield k, getattr(self, k)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def _from_proto(cls, pb_item: api_pb2.WorkspaceBillingReportItem) -> "BillingReportItem":
|
|
59
|
+
return cls(
|
|
60
|
+
object_id=pb_item.object_id,
|
|
61
|
+
description=pb_item.description,
|
|
62
|
+
environment_name=pb_item.environment_name,
|
|
63
|
+
interval_start=pb_item.interval.ToDatetime().replace(tzinfo=timezone.utc),
|
|
64
|
+
cost=Decimal(pb_item.cost),
|
|
65
|
+
cost_by_resource={k: Decimal(v) for k, v in pb_item.cost_by_resource.items()},
|
|
66
|
+
tags=dict(pb_item.tags),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _workspace_billing_report(
|
|
71
|
+
*,
|
|
72
|
+
start: datetime, # Start of the report, inclusive
|
|
73
|
+
end: datetime | None = None, # End of the report, exclusive
|
|
74
|
+
resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
|
|
75
|
+
tag_names: list[str] | None = None, # Optional additional metadata to include
|
|
76
|
+
client: _Client | None = None,
|
|
77
|
+
) -> list[WorkspaceBillingReportItem]:
|
|
78
|
+
"""Generate a tabular report of workspace usage by object and time.
|
|
79
|
+
|
|
80
|
+
The result will be a list of dictionaries for each interval (determined by `resolution`)
|
|
81
|
+
between the `start` and `end` limits. The dictionary represents a single Modal object
|
|
82
|
+
that billing can be attributed to (e.g., an App) along with metadata (including user-defined
|
|
83
|
+
tags) for identifying that object. The dictionary also contains a breakdown of the cost value
|
|
84
|
+
attributed to individual resources (for an App, this can be CPU, Memory, specific GPU types,
|
|
85
|
+
etc.). The specific resource types included in the breakdown are subject to change as
|
|
86
|
+
Modal's billing model evolves.
|
|
87
|
+
|
|
88
|
+
The `start` and `end` parameters are required to either have a UTC timezone or to be
|
|
89
|
+
timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
|
|
90
|
+
be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
|
|
91
|
+
parameters are partial: `start` will be rounded to the beginning of its interval, while
|
|
92
|
+
partial `end` intervals will be excluded.
|
|
93
|
+
|
|
94
|
+
Additional user-provided metadata can be included in the report if the objects have tags
|
|
95
|
+
and `tag_names` (i.e., keys) are specified in the request. Alternatively, pass `tag_names=["*"]`
|
|
96
|
+
to include all tags in the report. Note that tags will be attributed to the entire interval even
|
|
97
|
+
if they were added or removed at some point within it. If the tag name was not in use during an
|
|
98
|
+
interval, it will be absent from the tags dictionary in that output row.
|
|
99
|
+
|
|
100
|
+
It's also possible to generate reports using the
|
|
101
|
+
[`modal billing report`](https://modal.com/docs/cli/latest/billing) CLI command. The CLI
|
|
102
|
+
has a few convenience features for generating reports across relative time ranges.
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
deprecation_warning(
|
|
107
|
+
(2026, 6, 18),
|
|
108
|
+
"The `modal.billing.workspace_billing_report()` function is deprecated. "
|
|
109
|
+
"Use the `workspace.billing.report()` method on the `modal.Workspace` object instead.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
from ._workspace import _Workspace
|
|
113
|
+
|
|
114
|
+
data = await _Workspace.from_context(client=client).billing.report(
|
|
115
|
+
start=start,
|
|
116
|
+
end=end,
|
|
117
|
+
resolution=resolution,
|
|
118
|
+
tag_names=tag_names,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
res: list[WorkspaceBillingReportItem] = []
|
|
122
|
+
for datum in data:
|
|
123
|
+
item: WorkspaceBillingReportItem = {
|
|
124
|
+
"object_id": datum.object_id,
|
|
125
|
+
"description": datum.description,
|
|
126
|
+
"environment_name": datum.environment_name,
|
|
127
|
+
"interval_start": datum.interval_start,
|
|
128
|
+
"cost": Decimal(datum.cost),
|
|
129
|
+
"tags": dict(datum.tags),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
res.append(item)
|
|
133
|
+
|
|
134
|
+
return res
|
|
@@ -3,6 +3,7 @@ import asyncio
|
|
|
3
3
|
import builtins
|
|
4
4
|
from collections.abc import Iterable, Mapping
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
6
7
|
from typing import Literal
|
|
7
8
|
|
|
8
9
|
from google.protobuf.empty_pb2 import Empty
|
|
@@ -12,6 +13,7 @@ from synchronicity import classproperty
|
|
|
12
13
|
|
|
13
14
|
from modal_proto import api_pb2
|
|
14
15
|
|
|
16
|
+
from ._billing import BillingReportItem
|
|
15
17
|
from ._load_context import LoadContext
|
|
16
18
|
from ._object import _Object
|
|
17
19
|
from ._resolver import Resolver
|
|
@@ -104,7 +106,7 @@ class _EnvironmentManager:
|
|
|
104
106
|
MemberRole = Literal["viewer", "contributor"]
|
|
105
107
|
|
|
106
108
|
|
|
107
|
-
def _role_to_proto(role: str) -> api_pb2.EnvironmentRole.ValueType:
|
|
109
|
+
def _role_to_proto(role: str) -> "api_pb2.EnvironmentRole.ValueType":
|
|
108
110
|
match role:
|
|
109
111
|
case "viewer":
|
|
110
112
|
return api_pb2.ENVIRONMENT_ROLE_VIEWER
|
|
@@ -318,7 +320,7 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
318
320
|
# def settings(self) -> EnvironmentSettings:
|
|
319
321
|
# return self._settings
|
|
320
322
|
|
|
321
|
-
def _hydrate_metadata(self, metadata: Message):
|
|
323
|
+
def _hydrate_metadata(self, metadata: Message | None):
|
|
322
324
|
# Overridden concrete implementation of base class method
|
|
323
325
|
assert metadata and isinstance(metadata, api_pb2.EnvironmentMetadata)
|
|
324
326
|
self._name = metadata.name or None
|
|
@@ -392,6 +394,87 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
392
394
|
client=client,
|
|
393
395
|
)
|
|
394
396
|
|
|
397
|
+
@property
|
|
398
|
+
def billing(self) -> "_EnvironmentBillingManager":
|
|
399
|
+
return _EnvironmentBillingManager(self)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class _EnvironmentBillingManager:
|
|
403
|
+
"""mdmd:namespace
|
|
404
|
+
Namespace for Environment billing APIs
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
def __init__(self, environment: _Environment):
|
|
408
|
+
"""mdmd:ignore"""
|
|
409
|
+
self._environment = environment
|
|
410
|
+
|
|
411
|
+
async def report(
|
|
412
|
+
self,
|
|
413
|
+
*,
|
|
414
|
+
start: datetime, # Start of the report, inclusive
|
|
415
|
+
end: datetime | None = None, # End of the report, exclusive
|
|
416
|
+
resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
|
|
417
|
+
tag_names: list[str] | None = None, # Optional additional metadata to include
|
|
418
|
+
) -> list[BillingReportItem]:
|
|
419
|
+
"""Return a cost report for Environment usage, broken down by object and time.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
start: Start of the report, inclusive and rounded to the beginning of the interval.
|
|
423
|
+
Must be in UTC or timezone-naive (interpreted as UTC).
|
|
424
|
+
end: End of the report, exclusive. Must be in UTC or timezone-naive. Partial final
|
|
425
|
+
intervals will be excluded from the report.
|
|
426
|
+
resolution: Resolution, e.g. "d" for daily or "h" for hourly.
|
|
427
|
+
tag_names: List of tag names; each row will include the tag name and value in use
|
|
428
|
+
for that object during the relevant time interval. Pass `["*"]` to include all
|
|
429
|
+
tags in the report.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
A list of `BillingReportItem` dataclasses. Each item reports the cost attributed to
|
|
433
|
+
a specific Modal object during a given time interval. Cost is further broken down by
|
|
434
|
+
the resource type that generated it (e.g. CPU, Memory, specific GPU usage).
|
|
435
|
+
Note that the specific resource types included in the breakdown are subject to change
|
|
436
|
+
as Modal's billing model evolves.
|
|
437
|
+
|
|
438
|
+
See also:
|
|
439
|
+
- [`modal environment billing report`](https://modal.com/docs/cli/latest/environment#modal-environment-billing-report):
|
|
440
|
+
An environment report CLI that has convenience features around relative time range queries
|
|
441
|
+
and JSON/CSV output.
|
|
442
|
+
- [`Workspace.billing.report()`](https://modal.com/docs/sdk/py/latest/modal.Workspace#billingreport):
|
|
443
|
+
An analogous report API for the entire Workspace.
|
|
444
|
+
|
|
445
|
+
"""
|
|
446
|
+
if tag_names is None:
|
|
447
|
+
tag_names = []
|
|
448
|
+
|
|
449
|
+
if end is None:
|
|
450
|
+
end = datetime.now(timezone.utc)
|
|
451
|
+
|
|
452
|
+
if start.tzinfo is None:
|
|
453
|
+
start = start.replace(tzinfo=timezone.utc)
|
|
454
|
+
elif start.tzinfo != timezone.utc:
|
|
455
|
+
raise InvalidError("Timezone-aware 'start' parameter must be in UTC.")
|
|
456
|
+
|
|
457
|
+
if end.tzinfo is None:
|
|
458
|
+
end = end.replace(tzinfo=timezone.utc)
|
|
459
|
+
elif end.tzinfo != timezone.utc:
|
|
460
|
+
raise InvalidError("Timezone-aware 'end' parameter must be in UTC.")
|
|
461
|
+
|
|
462
|
+
if not self._environment.is_hydrated:
|
|
463
|
+
await self._environment.hydrate()
|
|
464
|
+
|
|
465
|
+
request = api_pb2.WorkspaceBillingReportRequest(
|
|
466
|
+
resolution=resolution,
|
|
467
|
+
tag_names=tag_names,
|
|
468
|
+
environment_ids=[self._environment.object_id],
|
|
469
|
+
)
|
|
470
|
+
request.start_timestamp.FromDatetime(start)
|
|
471
|
+
request.end_timestamp.FromDatetime(end)
|
|
472
|
+
|
|
473
|
+
return [
|
|
474
|
+
BillingReportItem._from_proto(pb_item)
|
|
475
|
+
async for pb_item in self._environment.client.stub.WorkspaceBillingReport.unary_stream(request)
|
|
476
|
+
]
|
|
477
|
+
|
|
395
478
|
|
|
396
479
|
ENVIRONMENT_CACHE: dict[str, _Environment] = {}
|
|
397
480
|
|
|
@@ -661,6 +661,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
661
661
|
min_containers: int | None = None,
|
|
662
662
|
max_containers: int | None = None,
|
|
663
663
|
buffer_containers: int | None = None,
|
|
664
|
+
scaleup_window: int | None = None,
|
|
664
665
|
scaledown_window: int | None = None,
|
|
665
666
|
max_concurrent_inputs: int | None = None,
|
|
666
667
|
target_concurrent_inputs: int | None = None,
|
|
@@ -766,6 +767,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
766
767
|
min_containers=min_containers,
|
|
767
768
|
max_containers=max_containers,
|
|
768
769
|
buffer_containers=buffer_containers,
|
|
770
|
+
scaleup_window=scaleup_window,
|
|
769
771
|
scaledown_window=scaledown_window,
|
|
770
772
|
)
|
|
771
773
|
|
|
@@ -1157,6 +1159,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1157
1159
|
min_containers: int | None = None,
|
|
1158
1160
|
max_containers: int | None = None,
|
|
1159
1161
|
buffer_containers: int | None = None,
|
|
1162
|
+
scaleup_window: int | None = None,
|
|
1160
1163
|
scaledown_window: int | None = None,
|
|
1161
1164
|
target_concurrency: int | None = None,
|
|
1162
1165
|
) -> None:
|
|
@@ -1164,6 +1167,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1164
1167
|
min_containers=min_containers,
|
|
1165
1168
|
max_containers=max_containers,
|
|
1166
1169
|
buffer_containers=buffer_containers,
|
|
1170
|
+
scaleup_window=scaleup_window,
|
|
1167
1171
|
scaledown_window=scaledown_window,
|
|
1168
1172
|
target_concurrency=target_concurrency,
|
|
1169
1173
|
)
|
|
@@ -1191,10 +1195,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1191
1195
|
its static configuration.
|
|
1192
1196
|
|
|
1193
1197
|
Args:
|
|
1194
|
-
min_containers: Minimum number of containers to keep running
|
|
1195
|
-
max_containers: Maximum concurrent containers
|
|
1196
|
-
buffer_containers: Extra containers to keep warm beyond demand
|
|
1197
|
-
scaledown_window:
|
|
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 current demand.
|
|
1201
|
+
scaledown_window: Maximum duration (in seconds) idle containers wait before scaling down.
|
|
1198
1202
|
|
|
1199
1203
|
Examples:
|
|
1200
1204
|
```python notest
|
|
@@ -1208,7 +1212,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1208
1212
|
|
|
1209
1213
|
# Extend the scaledown window to increase the amount of time that idle containers stay alive
|
|
1210
1214
|
f.update_autoscaler(scaledown_window=300)
|
|
1211
|
-
|
|
1215
|
+
```
|
|
1212
1216
|
|
|
1213
1217
|
"""
|
|
1214
1218
|
# Assert .update_autoscaler() is not called on a method as opposed to the Object. Applicable for Cls only.
|
|
@@ -1908,9 +1912,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1908
1912
|
**kwargs: Keyword arguments forwarded to the remote function.
|
|
1909
1913
|
|
|
1910
1914
|
Returns:
|
|
1911
|
-
A [`modal.FunctionCall`](https://modal.com/docs/
|
|
1915
|
+
A [`modal.FunctionCall`](https://modal.com/docs/sdk/py/latest/modal.FunctionCall) object
|
|
1912
1916
|
that can later be polled or waited for using
|
|
1913
|
-
[`.get(timeout=...)`](https://modal.com/docs/
|
|
1917
|
+
[`.get(timeout=...)`](https://modal.com/docs/sdk/py/latest/modal.FunctionCall#get).
|
|
1914
1918
|
"""
|
|
1915
1919
|
self._check_no_web_url("spawn")
|
|
1916
1920
|
if self._is_generator:
|
|
@@ -2061,7 +2065,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2061
2065
|
"""Returns a structure representing the call graph from a given root
|
|
2062
2066
|
call ID, along with the status of execution for each node.
|
|
2063
2067
|
|
|
2064
|
-
See [`modal.call_graph`](https://modal.com/docs/
|
|
2068
|
+
See [`modal.call_graph`](https://modal.com/docs/sdk/py/latest/modal.call_graph) reference page
|
|
2065
2069
|
for documentation on the structure of the returned `InputInfo` items.
|
|
2066
2070
|
|
|
2067
2071
|
Returns:
|
|
@@ -2079,7 +2083,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2079
2083
|
terminate_containers: bool = False,
|
|
2080
2084
|
):
|
|
2081
2085
|
"""Cancels the function call, which will stop its execution and mark its inputs as
|
|
2082
|
-
[`TERMINATED`](https://modal.com/docs/
|
|
2086
|
+
[`TERMINATED`](https://modal.com/docs/sdk/py/latest/modal.call_graph#modalcall_graphinputstatus).
|
|
2083
2087
|
|
|
2084
2088
|
If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
|
|
2085
2089
|
causing any non-cancelled inputs on those containers to be rescheduled in new containers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
2
|
from collections.abc import Collection, Mapping
|
|
3
|
-
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload
|
|
3
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Generic, Literal, TypeVar, overload
|
|
4
4
|
|
|
5
5
|
import grpclib.client
|
|
6
6
|
from google.protobuf.message import Message
|
|
@@ -178,9 +178,9 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
|
|
|
178
178
|
|
|
179
179
|
async def unary_stream(
|
|
180
180
|
self,
|
|
181
|
-
request,
|
|
181
|
+
request: RequestType,
|
|
182
182
|
metadata: Any | None = None,
|
|
183
|
-
):
|
|
183
|
+
) -> AsyncGenerator[ResponseType, None]:
|
|
184
184
|
from .client import _Client
|
|
185
185
|
|
|
186
186
|
if self.client._snapshotted:
|
|
@@ -9,6 +9,7 @@ import typing
|
|
|
9
9
|
import warnings
|
|
10
10
|
from collections.abc import Callable, Collection, Sequence
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
+
from functools import wraps
|
|
12
13
|
from inspect import isfunction
|
|
13
14
|
from pathlib import Path, PurePosixPath
|
|
14
15
|
from typing import (
|
|
@@ -424,6 +425,20 @@ async def _image_await_build_result(image_id: str, client: _Client) -> api_pb2.I
|
|
|
424
425
|
return result_response
|
|
425
426
|
|
|
426
427
|
|
|
428
|
+
def _requires_image_instance(method):
|
|
429
|
+
@wraps(method)
|
|
430
|
+
def wrapper(self, *args, **kwargs):
|
|
431
|
+
if not isinstance(self, _Image):
|
|
432
|
+
raise InvalidError(
|
|
433
|
+
"Image has not been constructed yet. "
|
|
434
|
+
f"Use one of the static factory methods prior to calling {method.__name__} "
|
|
435
|
+
"like `modal.Image.debian_slim`, `modal.Image.from_registry`, or `modal.Image.micromamba`"
|
|
436
|
+
)
|
|
437
|
+
return method(self, *args, **kwargs)
|
|
438
|
+
|
|
439
|
+
return wrapper
|
|
440
|
+
|
|
441
|
+
|
|
427
442
|
class _Image(_Object, type_prefix="im"):
|
|
428
443
|
"""Base class for container images to run functions in.
|
|
429
444
|
|
|
@@ -755,6 +770,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
755
770
|
context_mount_function=lambda: mount,
|
|
756
771
|
)
|
|
757
772
|
|
|
773
|
+
@_requires_image_instance
|
|
758
774
|
def add_local_file(self, local_path: str | Path, remote_path: str, *, copy: bool = False) -> "_Image":
|
|
759
775
|
"""Adds a local file to the image at `remote_path` within the container.
|
|
760
776
|
|
|
@@ -791,6 +807,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
791
807
|
mount = _Mount._from_local_file(local_path, remote_path)
|
|
792
808
|
return self._add_mount_layer_or_copy(mount, copy=copy)
|
|
793
809
|
|
|
810
|
+
@_requires_image_instance
|
|
794
811
|
def add_local_dir(
|
|
795
812
|
self,
|
|
796
813
|
local_path: str | Path,
|
|
@@ -869,6 +886,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
869
886
|
mount = _Mount._add_local_dir(Path(local_path), PurePosixPath(remote_path), ignore=_ignore_fn(ignore))
|
|
870
887
|
return self._add_mount_layer_or_copy(mount, copy=copy)
|
|
871
888
|
|
|
889
|
+
@_requires_image_instance
|
|
872
890
|
def add_local_python_source(
|
|
873
891
|
self, *modules: str, copy: bool = False, ignore: Sequence[str] | Callable[[Path], bool] = NON_PYTHON_FILES
|
|
874
892
|
) -> "_Image":
|
|
@@ -1012,6 +1030,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1012
1030
|
await resolver.load(self, load_context)
|
|
1013
1031
|
return self
|
|
1014
1032
|
|
|
1033
|
+
@_requires_image_instance
|
|
1015
1034
|
def pip_install(
|
|
1016
1035
|
self,
|
|
1017
1036
|
*packages: str | list[str],
|
|
@@ -1098,6 +1117,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1098
1117
|
secrets=secrets,
|
|
1099
1118
|
)
|
|
1100
1119
|
|
|
1120
|
+
@_requires_image_instance
|
|
1101
1121
|
def pip_install_private_repos(
|
|
1102
1122
|
self,
|
|
1103
1123
|
*repositories: str,
|
|
@@ -1218,6 +1238,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1218
1238
|
force_build=self.force_build or force_build,
|
|
1219
1239
|
)
|
|
1220
1240
|
|
|
1241
|
+
@_requires_image_instance
|
|
1221
1242
|
def pip_install_from_requirements(
|
|
1222
1243
|
self,
|
|
1223
1244
|
requirements_txt: str,
|
|
@@ -1279,6 +1300,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1279
1300
|
secrets=secrets,
|
|
1280
1301
|
)
|
|
1281
1302
|
|
|
1303
|
+
@_requires_image_instance
|
|
1282
1304
|
def pip_install_from_pyproject(
|
|
1283
1305
|
self,
|
|
1284
1306
|
pyproject_toml: str,
|
|
@@ -1362,6 +1384,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1362
1384
|
gpu_config=parse_gpu_config(gpu),
|
|
1363
1385
|
)
|
|
1364
1386
|
|
|
1387
|
+
@_requires_image_instance
|
|
1365
1388
|
def uv_pip_install(
|
|
1366
1389
|
self,
|
|
1367
1390
|
*packages: str | list[str],
|
|
@@ -1493,6 +1516,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1493
1516
|
secrets=secrets,
|
|
1494
1517
|
)
|
|
1495
1518
|
|
|
1519
|
+
@_requires_image_instance
|
|
1496
1520
|
def poetry_install_from_file(
|
|
1497
1521
|
self,
|
|
1498
1522
|
poetry_pyproject_toml: str,
|
|
@@ -1602,6 +1626,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1602
1626
|
gpu_config=parse_gpu_config(gpu),
|
|
1603
1627
|
)
|
|
1604
1628
|
|
|
1629
|
+
@_requires_image_instance
|
|
1605
1630
|
def uv_sync(
|
|
1606
1631
|
self,
|
|
1607
1632
|
uv_project_dir: str = "./",
|
|
@@ -1785,6 +1810,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1785
1810
|
gpu_config=parse_gpu_config(gpu),
|
|
1786
1811
|
)
|
|
1787
1812
|
|
|
1813
|
+
@_requires_image_instance
|
|
1788
1814
|
def dockerfile_commands(
|
|
1789
1815
|
self,
|
|
1790
1816
|
*dockerfile_commands: str | list[str],
|
|
@@ -1873,6 +1899,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1873
1899
|
build_args=build_args,
|
|
1874
1900
|
)
|
|
1875
1901
|
|
|
1902
|
+
@_requires_image_instance
|
|
1876
1903
|
def entrypoint(
|
|
1877
1904
|
self,
|
|
1878
1905
|
entrypoint_commands: list[str],
|
|
@@ -1893,6 +1920,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1893
1920
|
|
|
1894
1921
|
return self.dockerfile_commands(dockerfile_cmd)
|
|
1895
1922
|
|
|
1923
|
+
@_requires_image_instance
|
|
1896
1924
|
def shell(
|
|
1897
1925
|
self,
|
|
1898
1926
|
shell_commands: list[str],
|
|
@@ -1913,6 +1941,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1913
1941
|
|
|
1914
1942
|
return self.dockerfile_commands(dockerfile_cmd)
|
|
1915
1943
|
|
|
1944
|
+
@_requires_image_instance
|
|
1916
1945
|
def run_commands(
|
|
1917
1946
|
self,
|
|
1918
1947
|
*commands: str | list[str],
|
|
@@ -1997,6 +2026,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1997
2026
|
_namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
1998
2027
|
)
|
|
1999
2028
|
|
|
2029
|
+
@_requires_image_instance
|
|
2000
2030
|
def micromamba_install(
|
|
2001
2031
|
self,
|
|
2002
2032
|
*packages: str | list[str],
|
|
@@ -2528,6 +2558,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2528
2558
|
_namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
2529
2559
|
)
|
|
2530
2560
|
|
|
2561
|
+
@_requires_image_instance
|
|
2531
2562
|
def apt_install(
|
|
2532
2563
|
self,
|
|
2533
2564
|
*packages: str | list[str],
|
|
@@ -2579,6 +2610,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2579
2610
|
secrets=secrets,
|
|
2580
2611
|
)
|
|
2581
2612
|
|
|
2613
|
+
@_requires_image_instance
|
|
2582
2614
|
def run_function(
|
|
2583
2615
|
self,
|
|
2584
2616
|
raw_f: Callable[..., Any],
|
|
@@ -2697,6 +2729,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2697
2729
|
force_build=self.force_build or force_build,
|
|
2698
2730
|
)
|
|
2699
2731
|
|
|
2732
|
+
@_requires_image_instance
|
|
2700
2733
|
def env(self, vars: dict[str, str]) -> "_Image":
|
|
2701
2734
|
"""Sets the environment variables in an Image.
|
|
2702
2735
|
|
|
@@ -2727,6 +2760,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2727
2760
|
dockerfile_function=build_dockerfile,
|
|
2728
2761
|
)
|
|
2729
2762
|
|
|
2763
|
+
@_requires_image_instance
|
|
2730
2764
|
def workdir(self, path: str | PurePosixPath) -> "_Image":
|
|
2731
2765
|
"""Set the working directory for subsequent image build steps and function execution.
|
|
2732
2766
|
|
|
@@ -2756,6 +2790,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2756
2790
|
dockerfile_function=build_dockerfile,
|
|
2757
2791
|
)
|
|
2758
2792
|
|
|
2793
|
+
@_requires_image_instance
|
|
2759
2794
|
def cmd(self, cmd: list[str]) -> "_Image":
|
|
2760
2795
|
"""Set the default command (`CMD`) to run when a container is started.
|
|
2761
2796
|
|
|
@@ -2784,6 +2819,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2784
2819
|
|
|
2785
2820
|
return self.dockerfile_commands(dockerfile_cmd)
|
|
2786
2821
|
|
|
2822
|
+
@_requires_image_instance
|
|
2787
2823
|
def pipe(
|
|
2788
2824
|
self,
|
|
2789
2825
|
func: Callable[Concatenate["modal.Image", P], "modal.Image"],
|
|
@@ -2929,17 +2965,16 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2929
2965
|
"""
|
|
2930
2966
|
tag = _parse_named_image_ref(name)
|
|
2931
2967
|
|
|
2932
|
-
if
|
|
2968
|
+
if self._object_id is None:
|
|
2933
2969
|
raise InvalidError("Cannot publish an image that has not been created yet. Call `.build()` first.")
|
|
2934
2970
|
|
|
2935
|
-
_client = client or
|
|
2936
|
-
assert _client is not None
|
|
2971
|
+
_client = client or await _Client.from_env()
|
|
2937
2972
|
|
|
2938
2973
|
resolved_env = environment_name or config.get("environment") or ""
|
|
2939
2974
|
|
|
2940
2975
|
await _client.stub.ImagePublish(
|
|
2941
2976
|
api_pb2.ImagePublishRequest(
|
|
2942
|
-
image_id=self.
|
|
2977
|
+
image_id=self._object_id,
|
|
2943
2978
|
environment_name=resolved_env,
|
|
2944
2979
|
is_public=False,
|
|
2945
2980
|
tag=tag,
|
|
@@ -477,7 +477,7 @@ def _wsgi_app(
|
|
|
477
477
|
Web Server Gateway Interface (WSGI) is a standard for synchronous Python web apps.
|
|
478
478
|
It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
|
|
479
479
|
which is compatible with ASGI and supports additional functionality such as web sockets.
|
|
480
|
-
Modal supports ASGI via [`asgi_app`](https://modal.com/docs/
|
|
480
|
+
Modal supports ASGI via [`asgi_app`](https://modal.com/docs/sdk/py/latest/modal.asgi_app).
|
|
481
481
|
|
|
482
482
|
Examples:
|
|
483
483
|
```python
|
|
@@ -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
|
|