modal 1.4.3.dev25__tar.gz → 1.4.3.dev27__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.4.3.dev25 → modal-1.4.3.dev27}/PKG-INFO +1 -1
- modal-1.4.3.dev27/modal/_function_variants.py +257 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_functions.py +150 -151
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_output/status.py +1 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_partial_function.py +14 -16
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_resources.py +1 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/container_io_manager.py +1 -3
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/container_io_manager.pyi +3 -9
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/user_code_imports.py +2 -2
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_serialization.py +1 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/function_utils.py +23 -2
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/app.py +6 -6
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/app.pyi +10 -10
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/cluster.py +13 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/container.py +13 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/import_refs.py +1 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/run.py +1 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/shell.py +4 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/client.pyi +2 -2
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cls.py +117 -171
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cls.pyi +44 -77
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/config.py +1 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/experimental/flash.py +6 -6
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/experimental/flash.pyi +2 -2
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/functions.pyi +88 -15
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/partial_function.pyi +14 -16
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/sandbox.py +22 -12
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/sandbox.pyi +11 -8
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal.egg-info/SOURCES.txt +1 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/api_pb2.py +380 -380
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/api_pb2.pyi +16 -4
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_version/__init__.py +1 -1
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/LICENSE +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/README.md +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/__main__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_billing.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_clustered_functions.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_environments.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_grpc_client.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_ipython.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_load_context.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_location.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_logs.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_object.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_output/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_output/manager.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_output/pty.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_output/rich.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_resolver.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_server.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_traceback.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_tunnel.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_tunnel.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_type_manager.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/logger.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_vendor/version.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/_watcher.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/billing.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/builder/README.md +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/builder/base-images.json +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/call_graph.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/_download.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/_help.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/_traceback.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/app.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/billing.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/bootstrap.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/changelog.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/config.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/dashboard.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/dict.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/entry_point.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/environment.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/launch.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/logo.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/profile.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/queues.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/secret.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/selector.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/token.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/utils.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cli/volume.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/client.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/container_process.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/container_process.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/dict.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/dict.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/environments.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/environments.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/exception.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/experimental/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/experimental/ipython.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/file_io.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/file_io.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/functions.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/image.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/image.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/io_streams.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/io_streams.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/mount.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/mount.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/network_file_system.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/network_file_system.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/object.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/object.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/output.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/parallel_map.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/parallel_map.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/partial_function.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/proxy.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/proxy.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/py.typed +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/queue.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/queue.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/retries.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/runner.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/runner.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/running_app.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/sandbox_fs.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/schedule.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/scheduler_placement.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/secret.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/secret.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/server.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/server.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/serving.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/serving.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/snapshot.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/snapshot.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/stream_type.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/token_flow.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/token_flow.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/volume.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal/volume.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_docs/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/__init__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/api_grpc.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/py.typed +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/modal_version/__main__.py +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/pyproject.toml +0 -0
- {modal-1.4.3.dev25 → modal-1.4.3.dev27}/setup.cfg +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
import dataclasses
|
|
3
|
+
from collections.abc import Collection, Sequence, Sized
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
6
|
+
|
|
7
|
+
from modal_proto import api_pb2
|
|
8
|
+
|
|
9
|
+
from ._resources import convert_fn_config_to_resources_config
|
|
10
|
+
from ._serialization import (
|
|
11
|
+
apply_defaults,
|
|
12
|
+
serialize,
|
|
13
|
+
serialize_proto_params,
|
|
14
|
+
validate_parameter_values,
|
|
15
|
+
)
|
|
16
|
+
from ._utils.function_utils import _parse_retries
|
|
17
|
+
from ._utils.mount_utils import validate_volumes, validate_volumes_by_object_id
|
|
18
|
+
from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
19
|
+
from .retries import Retries
|
|
20
|
+
from .secret import _Secret
|
|
21
|
+
from .volume import _Volume
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ._functions import _Function
|
|
25
|
+
from ._load_context import LoadContext
|
|
26
|
+
from ._object import _Object
|
|
27
|
+
from ._resolver import Resolver
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass()
|
|
31
|
+
class _FunctionOptions:
|
|
32
|
+
"""Data class that holds local state for a dynamically configured Function / Cls.
|
|
33
|
+
|
|
34
|
+
Not a public interface. Dataclass fields represent post-validation parameter values.
|
|
35
|
+
Use the `.new()` constructor to transform from the public interface types.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Note that default values must be "untruthy" so we can that detect when they are not set.
|
|
39
|
+
secrets: Collection[_Secret] = ()
|
|
40
|
+
validated_volumes: Sequence[tuple[str, _Volume]] = ()
|
|
41
|
+
cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]] = ()
|
|
42
|
+
resources: Optional[api_pb2.Resources] = None
|
|
43
|
+
retry_policy: Optional[api_pb2.FunctionRetryPolicy] = None
|
|
44
|
+
max_containers: Optional[int] = None
|
|
45
|
+
buffer_containers: Optional[int] = None
|
|
46
|
+
scaledown_window: Optional[int] = None
|
|
47
|
+
timeout_secs: Optional[int] = None
|
|
48
|
+
scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
|
|
49
|
+
cloud: Optional[str] = None
|
|
50
|
+
max_concurrent_inputs: Optional[int] = None
|
|
51
|
+
target_concurrent_inputs: Optional[int] = None
|
|
52
|
+
batch_max_size: Optional[int] = None
|
|
53
|
+
batch_wait_ms: Optional[int] = None
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def new(
|
|
57
|
+
cls,
|
|
58
|
+
*,
|
|
59
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
|
60
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
|
61
|
+
gpu: Optional[str] = None,
|
|
62
|
+
env: Optional[dict[str, Optional[str]]] = None,
|
|
63
|
+
secrets: Optional[Collection[_Secret]] = None,
|
|
64
|
+
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
|
|
65
|
+
retries: Optional[Union[int, Retries]] = None,
|
|
66
|
+
max_containers: Optional[int] = None,
|
|
67
|
+
buffer_containers: Optional[int] = None,
|
|
68
|
+
scaledown_window: Optional[int] = None,
|
|
69
|
+
timeout: Optional[int] = None,
|
|
70
|
+
region: Optional[Union[str, Sequence[str]]] = None,
|
|
71
|
+
cloud: Optional[str] = None,
|
|
72
|
+
max_concurrent_inputs: Optional[int] = None,
|
|
73
|
+
target_concurrent_inputs: Optional[int] = None,
|
|
74
|
+
batch_max_size: Optional[int] = None,
|
|
75
|
+
batch_wait_ms: Optional[int] = None,
|
|
76
|
+
) -> "_FunctionOptions":
|
|
77
|
+
"""Internal constructor that validates and normalizes public parameters."""
|
|
78
|
+
retry_policy = _parse_retries(retries)
|
|
79
|
+
if gpu or cpu or memory:
|
|
80
|
+
resources = convert_fn_config_to_resources_config(cpu=cpu, memory=memory, gpu=gpu)
|
|
81
|
+
else:
|
|
82
|
+
resources = None
|
|
83
|
+
|
|
84
|
+
validated_volumes = validate_volumes(volumes)
|
|
85
|
+
cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
|
|
86
|
+
validated_volumes_no_cloud_buckets = [(k, v) for k, v in validated_volumes if isinstance(v, _Volume)]
|
|
87
|
+
|
|
88
|
+
secrets = secrets or []
|
|
89
|
+
if env:
|
|
90
|
+
secrets = [*secrets, _Secret.from_dict(env)]
|
|
91
|
+
|
|
92
|
+
scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
|
|
93
|
+
if region:
|
|
94
|
+
regions = [region] if isinstance(region, str) else list(region)
|
|
95
|
+
scheduler_placement = api_pb2.SchedulerPlacement(regions=regions)
|
|
96
|
+
|
|
97
|
+
# Use batched and concurrent decorators to apply consistent validation logic
|
|
98
|
+
from .partial_function import batched, concurrent
|
|
99
|
+
|
|
100
|
+
if batch_max_size is not None and batch_wait_ms is not None:
|
|
101
|
+
batched(max_batch_size=batch_max_size, wait_ms=batch_wait_ms)
|
|
102
|
+
|
|
103
|
+
if max_concurrent_inputs:
|
|
104
|
+
concurrent(max_inputs=max_concurrent_inputs, target_inputs=target_concurrent_inputs)
|
|
105
|
+
|
|
106
|
+
return cls(
|
|
107
|
+
secrets=secrets,
|
|
108
|
+
validated_volumes=validated_volumes_no_cloud_buckets,
|
|
109
|
+
cloud_bucket_mounts=cloud_bucket_mounts,
|
|
110
|
+
resources=resources,
|
|
111
|
+
retry_policy=retry_policy,
|
|
112
|
+
max_containers=max_containers,
|
|
113
|
+
buffer_containers=buffer_containers,
|
|
114
|
+
scaledown_window=scaledown_window,
|
|
115
|
+
timeout_secs=timeout,
|
|
116
|
+
scheduler_placement=scheduler_placement,
|
|
117
|
+
cloud=cloud,
|
|
118
|
+
max_concurrent_inputs=max_concurrent_inputs,
|
|
119
|
+
target_concurrent_inputs=target_concurrent_inputs,
|
|
120
|
+
batch_max_size=batch_max_size,
|
|
121
|
+
batch_wait_ms=batch_wait_ms,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def merge_options(self, new_options: "_FunctionOptions") -> "_FunctionOptions":
|
|
125
|
+
"""Implement protobuf-like MergeFrom semantics for this dataclass.
|
|
126
|
+
|
|
127
|
+
This mostly exists to support "stacking" of `.with_options()` calls.
|
|
128
|
+
Returns a new _FunctionOptions instance without modifying self.
|
|
129
|
+
"""
|
|
130
|
+
# Create a shallow copy of self to start with.
|
|
131
|
+
merged = dataclasses.replace(self)
|
|
132
|
+
|
|
133
|
+
# Don't use dataclasses.asdict() because it does a deepcopy(), which chokes on a hydrated object.
|
|
134
|
+
new_options_dict = {k.name: getattr(new_options, k.name) for k in dataclasses.fields(new_options)}
|
|
135
|
+
|
|
136
|
+
# Resources needs special merge handling because individual fields are parameters in the public API.
|
|
137
|
+
merged_resources = api_pb2.Resources()
|
|
138
|
+
if merged.resources:
|
|
139
|
+
merged_resources.MergeFrom(merged.resources)
|
|
140
|
+
if new_resources := new_options_dict.pop("resources"):
|
|
141
|
+
merged_resources.MergeFrom(new_resources)
|
|
142
|
+
merged.resources = merged_resources
|
|
143
|
+
|
|
144
|
+
for key, value in new_options_dict.items():
|
|
145
|
+
if value: # Only overwrite data when the value was set in the new options.
|
|
146
|
+
setattr(merged, key, value)
|
|
147
|
+
|
|
148
|
+
return merged
|
|
149
|
+
|
|
150
|
+
def _unhydrated_object_deps(self) -> list["_Object"]:
|
|
151
|
+
"""Return unhydrated `modal.Object` instances that are part of the configuration payload."""
|
|
152
|
+
all_deps = (
|
|
153
|
+
[volume for _, volume in self.validated_volumes]
|
|
154
|
+
+ list(self.secrets)
|
|
155
|
+
+ [mount.secret for _, mount in self.cloud_bucket_mounts if mount.secret]
|
|
156
|
+
)
|
|
157
|
+
return [dep for dep in all_deps if not dep.is_hydrated]
|
|
158
|
+
|
|
159
|
+
def to_proto(self) -> api_pb2.FunctionOptions:
|
|
160
|
+
"""Convert the dataclass to a FunctionOptions protobuf message."""
|
|
161
|
+
# Validate that the same volume (by object_id) isn't mounted at multiple paths.
|
|
162
|
+
# Needs to be called late so that volumes are hydrated
|
|
163
|
+
validate_volumes_by_object_id(self.validated_volumes)
|
|
164
|
+
|
|
165
|
+
volume_mounts = [
|
|
166
|
+
api_pb2.VolumeMount(
|
|
167
|
+
mount_path=path,
|
|
168
|
+
volume_id=volume.object_id,
|
|
169
|
+
allow_background_commits=True,
|
|
170
|
+
read_only=volume._read_only,
|
|
171
|
+
)
|
|
172
|
+
for path, volume in self.validated_volumes
|
|
173
|
+
]
|
|
174
|
+
return api_pb2.FunctionOptions(
|
|
175
|
+
secret_ids=[secret.object_id for secret in self.secrets],
|
|
176
|
+
replace_secret_ids=bool(self.secrets),
|
|
177
|
+
replace_volume_mounts=len(volume_mounts) > 0,
|
|
178
|
+
volume_mounts=volume_mounts,
|
|
179
|
+
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(self.cloud_bucket_mounts),
|
|
180
|
+
replace_cloud_bucket_mounts=bool(self.cloud_bucket_mounts),
|
|
181
|
+
resources=self.resources,
|
|
182
|
+
retry_policy=self.retry_policy,
|
|
183
|
+
concurrency_limit=self.max_containers,
|
|
184
|
+
buffer_containers=self.buffer_containers,
|
|
185
|
+
task_idle_timeout_secs=self.scaledown_window,
|
|
186
|
+
timeout_secs=self.timeout_secs,
|
|
187
|
+
max_concurrent_inputs=self.max_concurrent_inputs,
|
|
188
|
+
target_concurrent_inputs=self.target_concurrent_inputs,
|
|
189
|
+
batch_max_size=self.batch_max_size,
|
|
190
|
+
batch_linger_ms=self.batch_wait_ms,
|
|
191
|
+
scheduler_placement=self.scheduler_placement,
|
|
192
|
+
cloud_provider_str=self.cloud,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _make_function_variant(
|
|
197
|
+
base_function: "_Function",
|
|
198
|
+
options: Optional[_FunctionOptions],
|
|
199
|
+
parameter_schema: Optional[Sequence[api_pb2.ClassParameterSpec]],
|
|
200
|
+
args: Sized,
|
|
201
|
+
kwargs: dict[str, Any],
|
|
202
|
+
) -> "_Function":
|
|
203
|
+
"""Extend a base Function with parameter values or dynamic configuration options."""
|
|
204
|
+
|
|
205
|
+
async def _load(
|
|
206
|
+
function_variant: "_Function",
|
|
207
|
+
resolver: "Resolver",
|
|
208
|
+
load_context: "LoadContext",
|
|
209
|
+
existing_object_id: Optional[str],
|
|
210
|
+
):
|
|
211
|
+
if not base_function.is_hydrated:
|
|
212
|
+
await base_function.hydrate(load_context.client)
|
|
213
|
+
assert base_function._client and base_function._client.stub
|
|
214
|
+
|
|
215
|
+
if parameter_schema is None:
|
|
216
|
+
# This branch is about backwards compatibility.
|
|
217
|
+
# For Cls, we have `parameter_schema = None` for both old-style classes that
|
|
218
|
+
# use a custom constructor (and hence use pickle serialization) and for
|
|
219
|
+
# un-parameterized classes of any vintage, because such classes historically
|
|
220
|
+
# sent serialized empty args/kwargs rather than a null `serialized_params` bytestring.
|
|
221
|
+
serialized_params = serialize((args, kwargs))
|
|
222
|
+
else:
|
|
223
|
+
# New-style modal.parameter() based parameterization with protobuf serialization,
|
|
224
|
+
# including true Function variants with no parameters defined
|
|
225
|
+
# (in which case, serialized_params is a null bytestring).
|
|
226
|
+
kwargs_with_defaults = apply_defaults(kwargs, parameter_schema)
|
|
227
|
+
validate_parameter_values(kwargs_with_defaults, parameter_schema)
|
|
228
|
+
serialized_params = serialize_proto_params(kwargs_with_defaults)
|
|
229
|
+
|
|
230
|
+
options_pb = options.to_proto() if options else None
|
|
231
|
+
|
|
232
|
+
req = api_pb2.FunctionBindParamsRequest(
|
|
233
|
+
function_id=base_function.object_id,
|
|
234
|
+
serialized_params=serialized_params,
|
|
235
|
+
function_options=options_pb,
|
|
236
|
+
environment_name=load_context.environment_name
|
|
237
|
+
or "", # TODO: investigate shouldn't environment name always be specified here?
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
response = await base_function._client.stub.FunctionBindParams(req)
|
|
241
|
+
function_variant._hydrate(response.bound_function_id, base_function._client, response.handle_metadata)
|
|
242
|
+
|
|
243
|
+
def _deps():
|
|
244
|
+
if options:
|
|
245
|
+
return options._unhydrated_object_deps()
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
fun = base_function._from_loader(
|
|
249
|
+
_load,
|
|
250
|
+
base_function._rep,
|
|
251
|
+
hydrate_lazily=True,
|
|
252
|
+
deps=_deps,
|
|
253
|
+
load_context_overrides=base_function._load_context_overrides,
|
|
254
|
+
)
|
|
255
|
+
fun._info = base_function._info
|
|
256
|
+
fun._spec = base_function._spec # TODO (elias): fix - this is incorrect when using with_options
|
|
257
|
+
return fun
|
|
@@ -5,7 +5,7 @@ import inspect
|
|
|
5
5
|
import time
|
|
6
6
|
import typing
|
|
7
7
|
import warnings
|
|
8
|
-
from collections.abc import AsyncGenerator, Collection, Sequence
|
|
8
|
+
from collections.abc import AsyncGenerator, Collection, Sequence
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from pathlib import PurePosixPath
|
|
11
11
|
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Optional, Union
|
|
@@ -18,6 +18,7 @@ from synchronicity.combined_types import MethodWithAio
|
|
|
18
18
|
from modal_proto import api_pb2
|
|
19
19
|
from modal_proto.modal_api_grpc import ModalClientModal
|
|
20
20
|
|
|
21
|
+
from ._function_variants import _FunctionOptions, _make_function_variant
|
|
21
22
|
from ._load_context import LoadContext
|
|
22
23
|
from ._object import _Object, live_method, live_method_gen
|
|
23
24
|
from ._output.pty import get_pty_info
|
|
@@ -26,11 +27,8 @@ from ._resolver import Resolver
|
|
|
26
27
|
from ._resources import convert_fn_config_to_resources_config
|
|
27
28
|
from ._runtime.execution_context import current_input_id, is_local
|
|
28
29
|
from ._serialization import (
|
|
29
|
-
apply_defaults,
|
|
30
30
|
get_callable_schema,
|
|
31
31
|
serialize,
|
|
32
|
-
serialize_proto_params,
|
|
33
|
-
validate_parameter_values,
|
|
34
32
|
)
|
|
35
33
|
from ._traceback import print_server_warnings
|
|
36
34
|
from ._utils.async_utils import (
|
|
@@ -48,6 +46,7 @@ from ._utils.function_utils import (
|
|
|
48
46
|
OUTPUTS_TIMEOUT,
|
|
49
47
|
FunctionInfo,
|
|
50
48
|
_create_input,
|
|
49
|
+
_parse_retries,
|
|
51
50
|
_process_result,
|
|
52
51
|
_stream_function_call_data,
|
|
53
52
|
get_function_type,
|
|
@@ -552,26 +551,6 @@ class FunctionStats:
|
|
|
552
551
|
input_headroom: int
|
|
553
552
|
|
|
554
553
|
|
|
555
|
-
def _parse_retries(
|
|
556
|
-
retries: Optional[Union[int, Retries]],
|
|
557
|
-
source: str = "",
|
|
558
|
-
) -> Optional[api_pb2.FunctionRetryPolicy]:
|
|
559
|
-
if isinstance(retries, int):
|
|
560
|
-
return Retries(
|
|
561
|
-
max_retries=retries,
|
|
562
|
-
initial_delay=1.0,
|
|
563
|
-
backoff_coefficient=1.0,
|
|
564
|
-
)._to_proto()
|
|
565
|
-
elif isinstance(retries, Retries):
|
|
566
|
-
return retries._to_proto()
|
|
567
|
-
elif retries is None:
|
|
568
|
-
return None
|
|
569
|
-
else:
|
|
570
|
-
extra = f" on {source}" if source else ""
|
|
571
|
-
msg = f"Retries parameter must be an integer or instance of modal.Retries. Found: {type(retries)}{extra}."
|
|
572
|
-
raise InvalidError(msg)
|
|
573
|
-
|
|
574
|
-
|
|
575
554
|
@dataclass
|
|
576
555
|
class _FunctionSpec:
|
|
577
556
|
"""
|
|
@@ -655,6 +634,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
655
634
|
None # set for 0.67+ class service functions
|
|
656
635
|
)
|
|
657
636
|
_metadata: Optional[api_pb2.FunctionHandleMetadata] = None
|
|
637
|
+
_options: _FunctionOptions
|
|
638
|
+
_base_function: Optional["_Function"] = None
|
|
658
639
|
|
|
659
640
|
@staticmethod
|
|
660
641
|
def from_local(
|
|
@@ -738,9 +719,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
738
719
|
|
|
739
720
|
if retry_policy is not None:
|
|
740
721
|
if webhook_config is not None:
|
|
741
|
-
raise InvalidError("Web
|
|
722
|
+
raise InvalidError("Web Functions do not support retries.")
|
|
742
723
|
if is_generator:
|
|
743
|
-
raise InvalidError("Generator
|
|
724
|
+
raise InvalidError("Generator Functions do not support retries.")
|
|
744
725
|
|
|
745
726
|
if timeout is None: # type: ignore[unreachable] # Help users who aren't using type checkers
|
|
746
727
|
raise InvalidError("The `timeout` parameter cannot be set to None: https://modal.com/docs/guide/timeouts")
|
|
@@ -1172,130 +1153,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1172
1153
|
|
|
1173
1154
|
return obj
|
|
1174
1155
|
|
|
1175
|
-
def _bind_parameters(
|
|
1176
|
-
self,
|
|
1177
|
-
obj: "modal.cls._Obj",
|
|
1178
|
-
options: Optional["modal.cls._ServiceOptions"],
|
|
1179
|
-
args: Sized,
|
|
1180
|
-
kwargs: dict[str, Any],
|
|
1181
|
-
) -> "_Function":
|
|
1182
|
-
"""mdmd:hidden
|
|
1183
|
-
|
|
1184
|
-
Binds a class-function to a specific instance of (init params, options) or a new workspace
|
|
1185
|
-
"""
|
|
1186
|
-
|
|
1187
|
-
parent = self
|
|
1188
|
-
|
|
1189
|
-
async def _load(
|
|
1190
|
-
param_bound_func: _Function,
|
|
1191
|
-
resolver: Resolver,
|
|
1192
|
-
load_context: LoadContext,
|
|
1193
|
-
existing_object_id: Optional[str],
|
|
1194
|
-
):
|
|
1195
|
-
if not parent.is_hydrated:
|
|
1196
|
-
# While the base Object.hydrate() method appears to be idempotent, it's not always safe
|
|
1197
|
-
await parent.hydrate()
|
|
1198
|
-
|
|
1199
|
-
assert parent._client and parent._client.stub
|
|
1200
|
-
|
|
1201
|
-
if (
|
|
1202
|
-
parent._class_parameter_info
|
|
1203
|
-
and parent._class_parameter_info.format == api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO
|
|
1204
|
-
):
|
|
1205
|
-
if args:
|
|
1206
|
-
# TODO(elias) - We could potentially support positional args as well, if we want to?
|
|
1207
|
-
raise InvalidError(
|
|
1208
|
-
"Can't use positional arguments with modal.parameter-based synthetic constructors.\n"
|
|
1209
|
-
"Use (<parameter_name>=value) keyword arguments when constructing classes instead."
|
|
1210
|
-
)
|
|
1211
|
-
schema = parent._class_parameter_info.schema
|
|
1212
|
-
kwargs_with_defaults = apply_defaults(kwargs, schema)
|
|
1213
|
-
validate_parameter_values(kwargs_with_defaults, schema)
|
|
1214
|
-
serialized_params = serialize_proto_params(kwargs_with_defaults)
|
|
1215
|
-
can_use_parent = len(parent._class_parameter_info.schema) == 0 # no parameters
|
|
1216
|
-
else:
|
|
1217
|
-
from modal.cls import _ServiceOptions # Should probably define this dataclass here?
|
|
1218
|
-
|
|
1219
|
-
can_use_parent = len(args) + len(kwargs) == 0 and (options == _ServiceOptions())
|
|
1220
|
-
serialized_params = serialize((args, kwargs))
|
|
1221
|
-
|
|
1222
|
-
if can_use_parent:
|
|
1223
|
-
# We can end up here if parent wasn't hydrated when class was instantiated, but has been since.
|
|
1224
|
-
param_bound_func._hydrate_from_other(parent)
|
|
1225
|
-
return
|
|
1226
|
-
|
|
1227
|
-
assert parent is not None and parent.is_hydrated
|
|
1228
|
-
|
|
1229
|
-
if options:
|
|
1230
|
-
# Validate that the same volume (by object_id) isn't mounted at multiple paths
|
|
1231
|
-
validate_volumes_by_object_id(options.validated_volumes)
|
|
1232
|
-
|
|
1233
|
-
volume_mounts = [
|
|
1234
|
-
api_pb2.VolumeMount(
|
|
1235
|
-
mount_path=path,
|
|
1236
|
-
volume_id=volume.object_id,
|
|
1237
|
-
allow_background_commits=True,
|
|
1238
|
-
read_only=volume._read_only,
|
|
1239
|
-
)
|
|
1240
|
-
for path, volume in options.validated_volumes
|
|
1241
|
-
]
|
|
1242
|
-
options_pb = api_pb2.FunctionOptions(
|
|
1243
|
-
secret_ids=[s.object_id for s in options.secrets],
|
|
1244
|
-
replace_secret_ids=bool(options.secrets),
|
|
1245
|
-
replace_volume_mounts=len(volume_mounts) > 0,
|
|
1246
|
-
volume_mounts=volume_mounts,
|
|
1247
|
-
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(options.cloud_bucket_mounts),
|
|
1248
|
-
replace_cloud_bucket_mounts=bool(options.cloud_bucket_mounts),
|
|
1249
|
-
resources=options.resources,
|
|
1250
|
-
retry_policy=options.retry_policy,
|
|
1251
|
-
concurrency_limit=options.max_containers,
|
|
1252
|
-
buffer_containers=options.buffer_containers,
|
|
1253
|
-
task_idle_timeout_secs=options.scaledown_window,
|
|
1254
|
-
timeout_secs=options.timeout_secs,
|
|
1255
|
-
max_concurrent_inputs=options.max_concurrent_inputs,
|
|
1256
|
-
target_concurrent_inputs=options.target_concurrent_inputs,
|
|
1257
|
-
batch_max_size=options.batch_max_size,
|
|
1258
|
-
batch_linger_ms=options.batch_wait_ms,
|
|
1259
|
-
scheduler_placement=options.scheduler_placement,
|
|
1260
|
-
cloud_provider_str=options.cloud,
|
|
1261
|
-
)
|
|
1262
|
-
else:
|
|
1263
|
-
options_pb = None
|
|
1264
|
-
|
|
1265
|
-
req = api_pb2.FunctionBindParamsRequest(
|
|
1266
|
-
function_id=parent.object_id,
|
|
1267
|
-
serialized_params=serialized_params,
|
|
1268
|
-
function_options=options_pb,
|
|
1269
|
-
environment_name=load_context.environment_name
|
|
1270
|
-
or "", # TODO: investigate shouldn't environment name always be specified here?
|
|
1271
|
-
)
|
|
1272
|
-
|
|
1273
|
-
response = await parent._client.stub.FunctionBindParams(req)
|
|
1274
|
-
param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
|
|
1275
|
-
|
|
1276
|
-
def _deps():
|
|
1277
|
-
if options:
|
|
1278
|
-
all_deps = (
|
|
1279
|
-
[v for _, v in options.validated_volumes]
|
|
1280
|
-
+ list(options.secrets)
|
|
1281
|
-
+ [mount.secret for _, mount in options.cloud_bucket_mounts if mount.secret]
|
|
1282
|
-
)
|
|
1283
|
-
return [dep for dep in all_deps if not dep.is_hydrated]
|
|
1284
|
-
return []
|
|
1285
|
-
|
|
1286
|
-
fun: _Function = _Function._from_loader(
|
|
1287
|
-
_load,
|
|
1288
|
-
"Function(parametrized)",
|
|
1289
|
-
hydrate_lazily=True,
|
|
1290
|
-
deps=_deps,
|
|
1291
|
-
load_context_overrides=self._load_context_overrides,
|
|
1292
|
-
)
|
|
1293
|
-
|
|
1294
|
-
fun._info = self._info
|
|
1295
|
-
fun._obj = obj
|
|
1296
|
-
fun._spec = self._spec # TODO (elias): fix - this is incorrect when using with_options
|
|
1297
|
-
return fun
|
|
1298
|
-
|
|
1299
1156
|
@live_method
|
|
1300
1157
|
async def update_autoscaler(
|
|
1301
1158
|
self,
|
|
@@ -1472,6 +1329,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1472
1329
|
self._serve_mounts = frozenset()
|
|
1473
1330
|
self._metadata = None
|
|
1474
1331
|
self._experimental_flash_urls = None
|
|
1332
|
+
self._options = _FunctionOptions()
|
|
1333
|
+
self._base_function = None
|
|
1475
1334
|
|
|
1476
1335
|
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
1477
1336
|
# Overridden concrete implementation of base class method
|
|
@@ -1532,7 +1391,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1532
1391
|
|
|
1533
1392
|
@live_method
|
|
1534
1393
|
async def get_web_url(self) -> Optional[str]:
|
|
1535
|
-
"""URL
|
|
1394
|
+
"""URL for addressing a Web Function via HTTP.
|
|
1395
|
+
|
|
1396
|
+
Returns None when this is not a Web Function.
|
|
1397
|
+
"""
|
|
1536
1398
|
return self._web_url
|
|
1537
1399
|
|
|
1538
1400
|
@live_method
|
|
@@ -1540,6 +1402,143 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1540
1402
|
"""URL of the flash service for the function."""
|
|
1541
1403
|
return list(self._experimental_flash_urls) if self._experimental_flash_urls else None
|
|
1542
1404
|
|
|
1405
|
+
def _apply_dynamic_config(
|
|
1406
|
+
self,
|
|
1407
|
+
new_options: _FunctionOptions,
|
|
1408
|
+
config_method_name: str,
|
|
1409
|
+
) -> "_Function[P, ReturnType, OriginalReturnType]":
|
|
1410
|
+
base_function = self._base_function or self
|
|
1411
|
+
combined_options = self._options.merge_options(new_options)
|
|
1412
|
+
|
|
1413
|
+
# For "True Functions" (i.e. not the Function inside a Cls), we will use
|
|
1414
|
+
# `parameter_schema=[]` when the Function is un-parameterized. Note that
|
|
1415
|
+
# `parameter_schema=Null` has different semantics that preserve Cls backwards compatibility.
|
|
1416
|
+
parameter_schema: Sequence[api_pb2.ClassParameterSpec] = []
|
|
1417
|
+
args = ()
|
|
1418
|
+
kwargs: dict[str, Any] = {}
|
|
1419
|
+
|
|
1420
|
+
# The main reason to have two layers here (_apply_dynamic_config -> _make_function_variant)
|
|
1421
|
+
# is because the _make_function_variant logic is shared between a true Function and a
|
|
1422
|
+
# "class service function", but we need to propagate different instance metadata to the
|
|
1423
|
+
# new object in each case. There's likely a cleaner way to do this, and then we could just
|
|
1424
|
+
# collapse the dynamic configuration into a single layer.
|
|
1425
|
+
new_function = _make_function_variant(base_function, combined_options, parameter_schema, args, kwargs)
|
|
1426
|
+
|
|
1427
|
+
new_function._options = combined_options
|
|
1428
|
+
new_function._base_function = base_function
|
|
1429
|
+
new_function._rep = f"{self._rep}.{config_method_name}(...)"
|
|
1430
|
+
|
|
1431
|
+
new_function._app = self._app
|
|
1432
|
+
new_function._obj = self._obj
|
|
1433
|
+
|
|
1434
|
+
new_function._is_generator = self._is_generator
|
|
1435
|
+
new_function._webhook_config = self._webhook_config
|
|
1436
|
+
|
|
1437
|
+
# Other fields not necessarily initialized
|
|
1438
|
+
if self._info is not None:
|
|
1439
|
+
new_function._build_args = self._build_args
|
|
1440
|
+
new_function._is_method = self._is_method
|
|
1441
|
+
new_function._raw_f = self._raw_f
|
|
1442
|
+
new_function._tag = self._tag
|
|
1443
|
+
|
|
1444
|
+
return new_function
|
|
1445
|
+
|
|
1446
|
+
def with_options(
|
|
1447
|
+
self,
|
|
1448
|
+
*,
|
|
1449
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
|
1450
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
|
1451
|
+
gpu: Optional[str] = None,
|
|
1452
|
+
env: Optional[dict[str, Optional[str]]] = None,
|
|
1453
|
+
secrets: Optional[Collection[_Secret]] = None,
|
|
1454
|
+
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
|
|
1455
|
+
retries: Optional[Union[int, Retries]] = None,
|
|
1456
|
+
max_containers: Optional[int] = None,
|
|
1457
|
+
buffer_containers: Optional[int] = None,
|
|
1458
|
+
scaledown_window: Optional[int] = None,
|
|
1459
|
+
timeout: Optional[int] = None,
|
|
1460
|
+
region: Optional[Union[str, Sequence[str]]] = None,
|
|
1461
|
+
cloud: Optional[str] = None,
|
|
1462
|
+
) -> "_Function[P, ReturnType, OriginalReturnType]":
|
|
1463
|
+
"""Dynamically override the static Function configuration with invocation-specific values.
|
|
1464
|
+
|
|
1465
|
+
This method returns a new Function instance with the dynamic configuration. Invocations of
|
|
1466
|
+
the new Function will run in a distinct container pool and autoscale independently from the
|
|
1467
|
+
base Function (and from other dynamic configurations).
|
|
1468
|
+
|
|
1469
|
+
Note that options cannot be "unset" with this method (i.e., if a GPU is configured in the
|
|
1470
|
+
`@app.cls()` decorator, passing `gpu=None` here will not create a CPU-only instance).
|
|
1471
|
+
Additionally, container arguments like `volumes` and `secrets` will _replace_ the base
|
|
1472
|
+
configuration or any previous use of this method rather than extending it.
|
|
1473
|
+
|
|
1474
|
+
**Usage:**
|
|
1475
|
+
|
|
1476
|
+
You can use this method after looking up a deployed Function:
|
|
1477
|
+
|
|
1478
|
+
```python notest
|
|
1479
|
+
fn = modal.Function.from_name("my_app", "fn").with_options(gpu="H100")
|
|
1480
|
+
fn.remote() # will run on a H100 GPU
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
Or by referencing another Function defined in the same App:
|
|
1484
|
+
|
|
1485
|
+
```python notest
|
|
1486
|
+
@app.function()
|
|
1487
|
+
def fn():
|
|
1488
|
+
...
|
|
1489
|
+
|
|
1490
|
+
# From a local entrypoint or another Function
|
|
1491
|
+
fn.with_options(gpu="H100").remote() # Uses an H100 GPU
|
|
1492
|
+
fn.remote() # Uses the static configuration with no GPU
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
"""
|
|
1496
|
+
options = _FunctionOptions.new(
|
|
1497
|
+
cpu=cpu,
|
|
1498
|
+
memory=memory,
|
|
1499
|
+
gpu=gpu,
|
|
1500
|
+
env=env,
|
|
1501
|
+
secrets=secrets,
|
|
1502
|
+
volumes=volumes,
|
|
1503
|
+
retries=retries,
|
|
1504
|
+
max_containers=max_containers,
|
|
1505
|
+
buffer_containers=buffer_containers,
|
|
1506
|
+
scaledown_window=scaledown_window,
|
|
1507
|
+
timeout=timeout,
|
|
1508
|
+
region=region,
|
|
1509
|
+
cloud=cloud,
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
return self._apply_dynamic_config(options, "with_options")
|
|
1513
|
+
|
|
1514
|
+
def with_concurrency(
|
|
1515
|
+
self,
|
|
1516
|
+
*,
|
|
1517
|
+
max_inputs: int,
|
|
1518
|
+
target_inputs: Optional[int] = None,
|
|
1519
|
+
) -> "_Function[P, ReturnType, OriginalReturnType]":
|
|
1520
|
+
"""Override the static Function configuration with invocation-specific input concurrency.
|
|
1521
|
+
|
|
1522
|
+
Returns a new Function instance that is dynamically configured to behave like a Function using
|
|
1523
|
+
the `@modal.concurrent` decorator. This instance will autoscale independently from the base Function.
|
|
1524
|
+
"""
|
|
1525
|
+
options = _FunctionOptions.new(max_concurrent_inputs=max_inputs, target_concurrent_inputs=target_inputs)
|
|
1526
|
+
return self._apply_dynamic_config(options, "with_concurrency")
|
|
1527
|
+
|
|
1528
|
+
def with_batching(
|
|
1529
|
+
self,
|
|
1530
|
+
*,
|
|
1531
|
+
max_batch_size: int,
|
|
1532
|
+
wait_ms: int,
|
|
1533
|
+
) -> "_Function[P, ReturnType, OriginalReturnType]":
|
|
1534
|
+
"""Override the static Function configuration with invocation-specific dynamic batching.
|
|
1535
|
+
|
|
1536
|
+
Returns a new Function instance that is dynamically configured to behave like a Function using
|
|
1537
|
+
the `@modal.batched` decorator. This instance will autoscale independently from the base Function.
|
|
1538
|
+
"""
|
|
1539
|
+
options = _FunctionOptions.new(batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
|
|
1540
|
+
return self._apply_dynamic_config(options, "with_batching")
|
|
1541
|
+
|
|
1543
1542
|
@property
|
|
1544
1543
|
async def is_generator(self) -> bool:
|
|
1545
1544
|
"""mdmd:hidden"""
|
|
@@ -90,7 +90,7 @@ class FunctionCreationStatus:
|
|
|
90
90
|
suffix = _get_suffix_from_web_url_info(url_info)
|
|
91
91
|
class_web_endpoint_method_status_row = self._output_mgr.add_status_row()
|
|
92
92
|
class_web_endpoint_method_status_row.finish(
|
|
93
|
-
f"Created
|
|
93
|
+
f"Created Web Function URL for {method_definition.function_name} => [magenta underline]"
|
|
94
94
|
f"{method_definition.web_url}[/magenta underline]{suffix}"
|
|
95
95
|
)
|
|
96
96
|
for custom_domain in method_definition.custom_domain_info:
|