modal 1.2.5.dev10__tar.gz → 1.2.7.dev11__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.2.5.dev10 → modal-1.2.7.dev11}/PKG-INFO +3 -3
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/README.md +1 -1
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/__init__.py +2 -2
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/__main__.py +4 -29
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_container_entrypoint.py +0 -1
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_functions.py +8 -17
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_grpc_client.py +48 -28
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_output.py +10 -11
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_partial_function.py +1 -2
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/container_io_manager.py +5 -6
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/gpu_memory_snapshot.py +9 -7
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/user_code_imports.py +10 -2
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_traceback.py +1 -1
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_tunnel.py +5 -9
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/blob_utils.py +23 -9
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/function_utils.py +15 -19
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/grpc_utils.py +2 -7
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/package_utils.py +0 -1
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/task_command_router_client.py +125 -126
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/app.py +43 -8
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/app.pyi +8 -4
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/entry_point.py +23 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/environment.py +2 -16
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/launch.py +0 -74
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/network_file_system.py +4 -16
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/run.py +1 -1
- modal-1.2.7.dev11/modal/cli/shell.py +375 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/utils.py +1 -13
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/volume.py +4 -16
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/client.pyi +2 -2
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cls.py +0 -6
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/container_process.py +2 -2
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/dict.py +44 -15
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/dict.pyi +2 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/exception.py +146 -16
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/experimental/__init__.py +2 -1
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/experimental/flash.py +46 -4
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/experimental/flash.pyi +22 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/file_io.py +39 -67
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/file_io.pyi +12 -27
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/functions.pyi +1 -1
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/image.py +17 -14
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/io_streams.py +23 -26
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/mount.py +23 -19
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/mount.pyi +3 -3
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/queue.py +12 -14
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/runner.py +1 -8
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/sandbox.py +50 -27
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/sandbox.pyi +39 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/secret.py +4 -20
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/volume.py +19 -18
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal.egg-info/PKG-INFO +3 -3
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal.egg-info/SOURCES.txt +0 -8
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/api.proto +24 -4
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/api_pb2.py +662 -656
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/api_pb2.pyi +37 -8
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/task_command_router.proto +20 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/task_command_router_grpc.py +33 -0
- modal-1.2.7.dev11/modal_proto/task_command_router_pb2.py +180 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/task_command_router_pb2.pyi +51 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/task_command_router_pb2_grpc.py +69 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/task_command_router_pb2_grpc.pyi +25 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_version/__init__.py +1 -1
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_version/__main__.py +1 -1
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/pyproject.toml +13 -6
- modal-1.2.5.dev10/modal/cli/programs/launch_instance_ssh.py +0 -94
- modal-1.2.5.dev10/modal/cli/programs/run_marimo.py +0 -95
- modal-1.2.5.dev10/modal/cli/shell.py +0 -237
- modal-1.2.5.dev10/modal_proto/sandbox_router.proto +0 -145
- modal-1.2.5.dev10/modal_proto/sandbox_router_grpc.py +0 -105
- modal-1.2.5.dev10/modal_proto/sandbox_router_pb2.py +0 -149
- modal-1.2.5.dev10/modal_proto/sandbox_router_pb2.pyi +0 -333
- modal-1.2.5.dev10/modal_proto/sandbox_router_pb2_grpc.py +0 -203
- modal-1.2.5.dev10/modal_proto/sandbox_router_pb2_grpc.pyi +0 -75
- modal-1.2.5.dev10/modal_proto/task_command_router_pb2.py +0 -149
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/LICENSE +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_billing.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_clustered_functions.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_clustered_functions.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_ipython.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_load_context.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_location.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_object.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_pty.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_resolver.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_resources.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/__init__.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/asgi.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/execution_context.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/telemetry.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_serialization.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_tunnel.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_type_manager.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/__init__.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/app_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/async_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/deprecation.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/docker_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/git_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/hash_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/http_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/logger.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/mount_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/name_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/shell_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_utils/time_utils.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_vendor/__init__.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_vendor/tblib.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/_watcher.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/billing.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/builder/2023.12.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/builder/2024.04.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/builder/2024.10.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/builder/2025.06.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/builder/README.md +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/builder/base-images.json +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/call_graph.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/__init__.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/_download.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/_traceback.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/app.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/cluster.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/config.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/container.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/dict.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/import_refs.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/profile.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/programs/__init__.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/programs/vscode.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/queues.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/secret.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cli/token.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/client.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/cls.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/config.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/container_process.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/environments.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/environments.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/experimental/ipython.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/file_pattern_matcher.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/functions.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/gpu.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/image.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/io_streams.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/network_file_system.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/network_file_system.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/object.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/object.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/output.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/parallel_map.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/parallel_map.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/partial_function.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/partial_function.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/proxy.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/proxy.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/py.typed +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/queue.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/retries.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/runner.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/running_app.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/schedule.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/scheduler_placement.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/secret.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/serving.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/serving.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/snapshot.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/snapshot.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/stream_type.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/token_flow.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/token_flow.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal/volume.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal.egg-info/requires.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal.egg-info/top_level.txt +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_docs/__init__.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/__init__.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/api_grpc.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/modal_proto/py.typed +0 -0
- {modal-1.2.5.dev10 → modal-1.2.7.dev11}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.7.dev11
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -13,7 +13,7 @@ Classifier: Topic :: System :: Distributed Computing
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Requires-Python: <3.14,>=3.
|
|
16
|
+
Requires-Python: <3.14,>=3.10
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: aiohttp
|
|
@@ -51,7 +51,7 @@ a [user guide](https://modal.com/docs/guide), and the detailed
|
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
53
|
|
|
54
|
-
**This library requires Python 3.
|
|
54
|
+
**This library requires Python 3.10 – 3.13.**
|
|
55
55
|
|
|
56
56
|
Install the package with `uv` or `pip`:
|
|
57
57
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
|
-
if sys.version_info[:2] < (3,
|
|
5
|
-
raise RuntimeError("This version of Modal requires at least Python 3.
|
|
4
|
+
if sys.version_info[:2] < (3, 10):
|
|
5
|
+
raise RuntimeError("This version of Modal requires at least Python 3.10")
|
|
6
6
|
if sys.version_info[:2] >= (3, 14):
|
|
7
7
|
raise RuntimeError("This version of Modal does not support Python 3.14+")
|
|
8
8
|
|
|
@@ -35,37 +35,12 @@ def main():
|
|
|
35
35
|
):
|
|
36
36
|
raise
|
|
37
37
|
|
|
38
|
-
from grpclib import GRPCError, Status
|
|
39
38
|
from rich.panel import Panel
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Status.CANCELLED: "Cancelled",
|
|
46
|
-
Status.DATA_LOSS: "Data loss",
|
|
47
|
-
Status.DEADLINE_EXCEEDED: "Deadline exceeded",
|
|
48
|
-
Status.FAILED_PRECONDITION: "Failed precondition",
|
|
49
|
-
Status.INTERNAL: "Internal",
|
|
50
|
-
Status.INVALID_ARGUMENT: "Invalid",
|
|
51
|
-
Status.NOT_FOUND: "Not found",
|
|
52
|
-
Status.OUT_OF_RANGE: "Out of range",
|
|
53
|
-
Status.PERMISSION_DENIED: "Permission denied",
|
|
54
|
-
Status.RESOURCE_EXHAUSTED: "Resource exhausted",
|
|
55
|
-
Status.UNAUTHENTICATED: "Unauthenticaed",
|
|
56
|
-
Status.UNAVAILABLE: "Unavailable",
|
|
57
|
-
Status.UNIMPLEMENTED: "Unimplemented",
|
|
58
|
-
Status.UNKNOWN: "Unknown",
|
|
59
|
-
}
|
|
60
|
-
title = f"Error: {status_map.get(exc.status, 'Unknown')}"
|
|
61
|
-
content = str(exc.message)
|
|
62
|
-
if exc.details:
|
|
63
|
-
content += f"\n\nDetails: {exc.details}"
|
|
64
|
-
else:
|
|
65
|
-
title = "Error"
|
|
66
|
-
content = str(exc)
|
|
67
|
-
if notes := getattr(exc, "__notes__", []):
|
|
68
|
-
content = f"{content}\n\nNote: {' '.join(notes)}"
|
|
40
|
+
title = "Error"
|
|
41
|
+
content = str(exc)
|
|
42
|
+
if notes := getattr(exc, "__notes__", []):
|
|
43
|
+
content = f"{content}\n\nNote: {' '.join(notes)}"
|
|
69
44
|
|
|
70
45
|
console = make_console(stderr=True)
|
|
71
46
|
panel = Panel(content, title=title, title_align="left", border_style="red")
|
|
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Optional, Union
|
|
|
13
13
|
|
|
14
14
|
import typing_extensions
|
|
15
15
|
from google.protobuf.message import Message
|
|
16
|
-
from grpclib import
|
|
16
|
+
from grpclib import Status
|
|
17
17
|
from synchronicity.combined_types import MethodWithAio
|
|
18
18
|
|
|
19
19
|
from modal_proto import api_pb2
|
|
@@ -694,7 +694,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
694
694
|
# Experimental: Clustered functions
|
|
695
695
|
cluster_size: Optional[int] = None,
|
|
696
696
|
rdma: Optional[bool] = None,
|
|
697
|
-
|
|
697
|
+
single_use_containers: bool = False,
|
|
698
698
|
ephemeral_disk: Optional[int] = None,
|
|
699
699
|
include_source: bool = True,
|
|
700
700
|
experimental_options: Optional[dict[str, str]] = None,
|
|
@@ -810,14 +810,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
810
810
|
if arg.default is not inspect.Parameter.empty:
|
|
811
811
|
raise InvalidError(f"Modal batched function {func_name} does not accept default arguments.")
|
|
812
812
|
|
|
813
|
-
if max_inputs is not None:
|
|
814
|
-
if not isinstance(max_inputs, int):
|
|
815
|
-
raise InvalidError(f"`max_inputs` must be an int, not {type(max_inputs).__name__}")
|
|
816
|
-
if max_inputs <= 0:
|
|
817
|
-
raise InvalidError("`max_inputs` must be positive")
|
|
818
|
-
if max_inputs > 1:
|
|
819
|
-
raise InvalidError("Only `max_inputs=1` is currently supported")
|
|
820
|
-
|
|
821
813
|
# Validate volumes
|
|
822
814
|
validated_volumes = validate_volumes(volumes)
|
|
823
815
|
cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
|
|
@@ -988,6 +980,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
988
980
|
function_definition = api_pb2.Function(
|
|
989
981
|
module_name=info.module_name or "",
|
|
990
982
|
function_name=info.function_name,
|
|
983
|
+
implementation_name=info.implementation_name,
|
|
991
984
|
mount_ids=loaded_mount_ids,
|
|
992
985
|
secret_ids=[secret.object_id for secret in secrets],
|
|
993
986
|
image_id=(image.object_id if image else ""),
|
|
@@ -1023,7 +1016,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1023
1016
|
object_dependencies=object_dependencies,
|
|
1024
1017
|
block_network=block_network,
|
|
1025
1018
|
untrusted=restrict_modal_access,
|
|
1026
|
-
|
|
1019
|
+
single_use_containers=single_use_containers,
|
|
1020
|
+
max_inputs=int(single_use_containers), # TODO(michael) remove after worker rollover
|
|
1027
1021
|
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
|
|
1028
1022
|
scheduler_placement=scheduler_placement,
|
|
1029
1023
|
is_class=info.is_service_class(),
|
|
@@ -1054,6 +1048,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1054
1048
|
function_data = api_pb2.FunctionData(
|
|
1055
1049
|
module_name=function_definition.module_name,
|
|
1056
1050
|
function_name=function_definition.function_name,
|
|
1051
|
+
implementation_name=function_definition.implementation_name,
|
|
1057
1052
|
function_type=function_definition.function_type,
|
|
1058
1053
|
warm_pool_size=function_definition.warm_pool_size,
|
|
1059
1054
|
concurrency_limit=function_definition.concurrency_limit,
|
|
@@ -1124,12 +1119,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1124
1119
|
)
|
|
1125
1120
|
try:
|
|
1126
1121
|
response: api_pb2.FunctionCreateResponse = await load_context.client.stub.FunctionCreate(request)
|
|
1127
|
-
except
|
|
1128
|
-
if
|
|
1129
|
-
raise InvalidError(exc.message)
|
|
1130
|
-
if exc.status == Status.FAILED_PRECONDITION:
|
|
1131
|
-
raise InvalidError(exc.message)
|
|
1132
|
-
if exc.message and "Received :status = '413'" in exc.message:
|
|
1122
|
+
except Exception as exc:
|
|
1123
|
+
if "Received :status = '413'" in str(exc):
|
|
1133
1124
|
raise InvalidError(f"Function {info.function_name} is too large to deploy.")
|
|
1134
1125
|
raise
|
|
1135
1126
|
function_creation_status.set_response(response)
|
|
@@ -5,10 +5,10 @@ import grpclib.client
|
|
|
5
5
|
from google.protobuf.message import Message
|
|
6
6
|
from grpclib import GRPCError, Status
|
|
7
7
|
|
|
8
|
+
from . import exception
|
|
8
9
|
from ._traceback import suppress_tb_frames
|
|
9
10
|
from ._utils.grpc_utils import Retry, _retry_transient_errors
|
|
10
11
|
from .config import config, logger
|
|
11
|
-
from .exception import InvalidError, NotFoundError
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from .client import _Client
|
|
@@ -20,6 +20,29 @@ RequestType = TypeVar("RequestType", bound=Message)
|
|
|
20
20
|
ResponseType = TypeVar("ResponseType", bound=Message)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
class WrappedGRPCError(exception.Error, exception._GRPCErrorWrapper): ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_STATUS_TO_EXCEPTION: dict[Status, type[exception._GRPCErrorWrapper]] = {
|
|
27
|
+
Status.CANCELLED: exception.ServiceError,
|
|
28
|
+
Status.UNKNOWN: exception.ServiceError,
|
|
29
|
+
Status.INVALID_ARGUMENT: exception.InvalidError,
|
|
30
|
+
Status.DEADLINE_EXCEEDED: exception.ServiceError,
|
|
31
|
+
Status.NOT_FOUND: exception.NotFoundError,
|
|
32
|
+
Status.ALREADY_EXISTS: exception.AlreadyExistsError,
|
|
33
|
+
Status.PERMISSION_DENIED: exception.PermissionDeniedError,
|
|
34
|
+
Status.RESOURCE_EXHAUSTED: exception.ResourceExhaustedError,
|
|
35
|
+
Status.FAILED_PRECONDITION: exception.ConflictError,
|
|
36
|
+
Status.ABORTED: exception.ConflictError,
|
|
37
|
+
Status.OUT_OF_RANGE: exception.InvalidError,
|
|
38
|
+
Status.UNIMPLEMENTED: exception.UnimplementedError,
|
|
39
|
+
Status.INTERNAL: exception.InternalError,
|
|
40
|
+
Status.UNAVAILABLE: exception.ServiceError,
|
|
41
|
+
Status.DATA_LOSS: exception.DataLossError,
|
|
42
|
+
Status.UNAUTHENTICATED: exception.AuthError,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
23
46
|
class grpc_error_converter:
|
|
24
47
|
def __enter__(self):
|
|
25
48
|
pass
|
|
@@ -29,20 +52,14 @@ class grpc_error_converter:
|
|
|
29
52
|
use_full_traceback = config.get("traceback")
|
|
30
53
|
with suppress_tb_frames(1):
|
|
31
54
|
if isinstance(exc, GRPCError):
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
tb = exc.__traceback__
|
|
41
|
-
while tb.tb_next:
|
|
42
|
-
tb = tb.tb_next
|
|
43
|
-
exc.with_traceback(tb)
|
|
44
|
-
raise exc from None # from None to skip the grpc-internal cause
|
|
45
|
-
raise exc
|
|
55
|
+
modal_exc = _STATUS_TO_EXCEPTION[exc.status](exc.message)
|
|
56
|
+
modal_exc._grpc_message = exc.message
|
|
57
|
+
modal_exc._grpc_status = exc.status
|
|
58
|
+
modal_exc._grpc_details = exc.details
|
|
59
|
+
if use_full_traceback:
|
|
60
|
+
raise modal_exc
|
|
61
|
+
else:
|
|
62
|
+
raise modal_exc from None # from None to skip the grpc-internal cause
|
|
46
63
|
|
|
47
64
|
return False
|
|
48
65
|
|
|
@@ -100,17 +117,20 @@ class UnaryUnaryWrapper(Generic[RequestType, ResponseType]):
|
|
|
100
117
|
) -> ResponseType:
|
|
101
118
|
with suppress_tb_frames(1):
|
|
102
119
|
if timeout is not None and retry is not None:
|
|
103
|
-
raise InvalidError("Retry must be None when timeout is set")
|
|
120
|
+
raise exception.InvalidError("Retry must be None when timeout is set")
|
|
104
121
|
|
|
105
122
|
if retry is None:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
with grpc_error_converter():
|
|
124
|
+
return await self.direct(req, timeout=timeout, metadata=metadata)
|
|
125
|
+
|
|
126
|
+
# TODO do we need suppress_error_frames(1) here too?
|
|
127
|
+
with grpc_error_converter():
|
|
128
|
+
return await _retry_transient_errors(
|
|
129
|
+
self, # type: ignore
|
|
130
|
+
req,
|
|
131
|
+
retry=retry,
|
|
132
|
+
metadata=metadata,
|
|
133
|
+
)
|
|
114
134
|
|
|
115
135
|
async def direct(
|
|
116
136
|
self,
|
|
@@ -135,8 +155,7 @@ class UnaryUnaryWrapper(Generic[RequestType, ResponseType]):
|
|
|
135
155
|
#
|
|
136
156
|
# [1]: https://github.com/vmagamedov/grpclib/blob/62f968a4c84e3f64e6966097574ff0a59969ea9b/grpclib/client.py#L844
|
|
137
157
|
self.wrapped_method.channel = await self.client._get_channel(self.server_url)
|
|
138
|
-
|
|
139
|
-
return await self.client._call_unary(self.wrapped_method, req, timeout=timeout, metadata=metadata)
|
|
158
|
+
return await self.client._call_unary(self.wrapped_method, req, timeout=timeout, metadata=metadata)
|
|
140
159
|
|
|
141
160
|
|
|
142
161
|
class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
|
|
@@ -167,5 +186,6 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
|
|
|
167
186
|
logger.debug(f"refreshing client after snapshot for {self.name.rsplit('/', 1)[1]}")
|
|
168
187
|
self.client = await _Client.from_env()
|
|
169
188
|
self.wrapped_method.channel = await self.client._get_channel(self.server_url)
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
with grpc_error_converter():
|
|
190
|
+
async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
|
|
191
|
+
yield response
|
|
@@ -12,7 +12,7 @@ from collections.abc import Generator
|
|
|
12
12
|
from datetime import timedelta
|
|
13
13
|
from typing import Callable, ClassVar
|
|
14
14
|
|
|
15
|
-
from grpclib.exceptions import
|
|
15
|
+
from grpclib.exceptions import StreamTerminatedError
|
|
16
16
|
from rich.console import Console, Group, RenderableType
|
|
17
17
|
from rich.live import Live
|
|
18
18
|
from rich.panel import Panel
|
|
@@ -34,10 +34,11 @@ from rich.text import Text
|
|
|
34
34
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
35
35
|
from modal_proto import api_pb2
|
|
36
36
|
|
|
37
|
-
from ._utils.grpc_utils import
|
|
37
|
+
from ._utils.grpc_utils import Retry
|
|
38
38
|
from ._utils.shell_utils import stream_from_stdin, write_to_fd
|
|
39
39
|
from .client import _Client
|
|
40
40
|
from .config import logger
|
|
41
|
+
from .exception import InternalError, ServiceError
|
|
41
42
|
|
|
42
43
|
if platform.system() == "Windows":
|
|
43
44
|
default_spinner = "line"
|
|
@@ -556,7 +557,7 @@ async def get_app_logs_loop(
|
|
|
556
557
|
async def stop_pty_shell():
|
|
557
558
|
nonlocal pty_shell_finish_event, pty_shell_input_task
|
|
558
559
|
if pty_shell_finish_event:
|
|
559
|
-
print("\r", end="") # move cursor to beginning of line
|
|
560
|
+
print("\r", end="") # move cursor to beginning of line # noqa: T201
|
|
560
561
|
pty_shell_finish_event.set()
|
|
561
562
|
pty_shell_finish_event = None
|
|
562
563
|
|
|
@@ -623,7 +624,7 @@ async def get_app_logs_loop(
|
|
|
623
624
|
# This corresponds to the `modal run -i` use case where a breakpoint
|
|
624
625
|
# triggers and the task drops into an interactive PTY mode
|
|
625
626
|
if pty_shell_finish_event:
|
|
626
|
-
print("ERROR: concurrent PTY shells are not supported.")
|
|
627
|
+
print("ERROR: concurrent PTY shells are not supported.") # noqa: T201
|
|
627
628
|
else:
|
|
628
629
|
pty_shell_stdout = output_mgr._stdout
|
|
629
630
|
pty_shell_finish_event = asyncio.Event()
|
|
@@ -644,13 +645,11 @@ async def get_app_logs_loop(
|
|
|
644
645
|
while True:
|
|
645
646
|
try:
|
|
646
647
|
await _get_logs()
|
|
647
|
-
except (
|
|
648
|
-
if isinstance(exc,
|
|
649
|
-
if
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
logger.debug("Log fetching timed out. Retrying ...")
|
|
653
|
-
continue
|
|
648
|
+
except (ServiceError, InternalError, StreamTerminatedError, socket.gaierror, AttributeError) as exc:
|
|
649
|
+
if isinstance(exc, (ServiceError, InternalError)):
|
|
650
|
+
# Try again if we had a temporary connection drop, for example if computer went to sleep.
|
|
651
|
+
logger.debug("Log fetching timed out. Retrying ...")
|
|
652
|
+
continue
|
|
654
653
|
elif isinstance(exc, StreamTerminatedError):
|
|
655
654
|
logger.debug("Stream closed. Retrying ...")
|
|
656
655
|
continue
|
|
@@ -160,9 +160,8 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
160
160
|
raise InvalidError("Interface decorators cannot be combined with lifecycle decorators.")
|
|
161
161
|
|
|
162
162
|
has_web_interface = self.flags & _PartialFunctionFlags.WEB_INTERFACE
|
|
163
|
-
has_http_web_interface = self.flags & _PartialFunctionFlags.HTTP_WEB_INTERFACE
|
|
164
163
|
has_callable_interface = self.flags & _PartialFunctionFlags.CALLABLE_INTERFACE
|
|
165
|
-
if
|
|
164
|
+
if has_web_interface and has_callable_interface:
|
|
166
165
|
self.registered = True # Hacky, avoid false-positive warning
|
|
167
166
|
raise InvalidError("Callable decorators cannot be combined with web interface decorators.")
|
|
168
167
|
|
|
@@ -845,8 +845,9 @@ class _ContainerIOManager:
|
|
|
845
845
|
yield inputs
|
|
846
846
|
yielded = True
|
|
847
847
|
|
|
848
|
-
#
|
|
849
|
-
|
|
848
|
+
# TODO(michael): Remove use of max_inputs after worker rollover
|
|
849
|
+
single_use_container = self.function_def.single_use_containers or self.function_def.max_inputs == 1
|
|
850
|
+
if final_input_received or single_use_container:
|
|
850
851
|
return
|
|
851
852
|
finally:
|
|
852
853
|
if not yielded:
|
|
@@ -991,12 +992,10 @@ class _ContainerIOManager:
|
|
|
991
992
|
# Busy-wait for restore. `/__modal/restore-state.json` is created
|
|
992
993
|
# by the worker process with updates to the container config.
|
|
993
994
|
restored_path = Path(config.get("restore_state_path"))
|
|
994
|
-
|
|
995
|
+
logger.debug("Waiting for restore")
|
|
995
996
|
while not restored_path.exists():
|
|
996
|
-
logger.debug(f"Waiting for restore (elapsed={time.perf_counter() - start:.3f}s)")
|
|
997
997
|
await asyncio.sleep(0.01)
|
|
998
998
|
continue
|
|
999
|
-
|
|
1000
999
|
logger.debug("Container: restored")
|
|
1001
1000
|
|
|
1002
1001
|
# Look for state file and create new client with updated credentials.
|
|
@@ -1007,7 +1006,7 @@ class _ContainerIOManager:
|
|
|
1007
1006
|
# Start a debugger if the worker tells us to
|
|
1008
1007
|
if int(restored_state.get("snapshot_debug", 0)):
|
|
1009
1008
|
logger.debug("Entering snapshot debugger")
|
|
1010
|
-
breakpoint()
|
|
1009
|
+
breakpoint() # noqa: T100
|
|
1011
1010
|
|
|
1012
1011
|
# Local ContainerIOManager state.
|
|
1013
1012
|
for key in ["task_id", "function_id"]:
|
|
@@ -18,11 +18,14 @@ from modal.config import config, logger
|
|
|
18
18
|
|
|
19
19
|
CUDA_CHECKPOINT_PATH: str = config.get("cuda_checkpoint_path")
|
|
20
20
|
|
|
21
|
-
# Maximum total duration for an entire toggle operation.
|
|
22
|
-
CUDA_CHECKPOINT_TOGGLE_TIMEOUT: float = 5 * 60.0
|
|
23
|
-
|
|
24
21
|
# Maximum total duration for each individual `cuda-checkpoint` invocation.
|
|
25
|
-
CUDA_CHECKPOINT_TIMEOUT: float =
|
|
22
|
+
CUDA_CHECKPOINT_TIMEOUT: float = 3 * 60.0
|
|
23
|
+
|
|
24
|
+
# Number of retries for each individual `cuda-checkpoint --toggle` invocation.
|
|
25
|
+
CUDA_CHECKPOINT_TOGGLE_NUM_RETRIES: int = 3
|
|
26
|
+
|
|
27
|
+
# Maximum total duration for an entire toggle operation.
|
|
28
|
+
CUDA_CHECKPOINT_TOGGLE_TIMEOUT: float = CUDA_CHECKPOINT_TOGGLE_NUM_RETRIES * CUDA_CHECKPOINT_TIMEOUT
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
class CudaCheckpointState(Enum):
|
|
@@ -58,7 +61,7 @@ class CudaCheckpointProcess:
|
|
|
58
61
|
|
|
59
62
|
start_time = time.monotonic()
|
|
60
63
|
retry_count = 0
|
|
61
|
-
max_retries =
|
|
64
|
+
max_retries = CUDA_CHECKPOINT_TOGGLE_NUM_RETRIES
|
|
62
65
|
|
|
63
66
|
attempts = 0
|
|
64
67
|
while self._should_continue_toggle(
|
|
@@ -201,8 +204,7 @@ class CudaCheckpointSession:
|
|
|
201
204
|
[CUDA_CHECKPOINT_PATH, "--get-state", "--pid", str(pid)],
|
|
202
205
|
capture_output=True,
|
|
203
206
|
text=True,
|
|
204
|
-
|
|
205
|
-
timeout=5,
|
|
207
|
+
timeout=CUDA_CHECKPOINT_TIMEOUT,
|
|
206
208
|
)
|
|
207
209
|
|
|
208
210
|
# If the command succeeds (return code 0), this PID has a CUDA session
|
|
@@ -30,6 +30,7 @@ from modal._utils.function_utils import (
|
|
|
30
30
|
from modal.app import _App
|
|
31
31
|
from modal.config import logger
|
|
32
32
|
from modal.exception import ExecutionError, InvalidError
|
|
33
|
+
from modal.experimental.flash import _FlashContainerEntry
|
|
33
34
|
from modal_proto import api_pb2
|
|
34
35
|
|
|
35
36
|
if typing.TYPE_CHECKING:
|
|
@@ -242,7 +243,7 @@ def create_breakpoint_wrapper(container_io_manager: "modal._runtime.container_io
|
|
|
242
243
|
def breakpoint_wrapper():
|
|
243
244
|
# note: it would be nice to not have breakpoint_wrapper() included in the backtrace
|
|
244
245
|
container_io_manager.interact(from_breakpoint=True)
|
|
245
|
-
import pdb
|
|
246
|
+
import pdb # noqa: T100
|
|
246
247
|
|
|
247
248
|
current_frame = inspect.currentframe()
|
|
248
249
|
if current_frame is not None:
|
|
@@ -392,18 +393,22 @@ class ImportedClass(Service):
|
|
|
392
393
|
event_loop: UserCodeEventLoop,
|
|
393
394
|
container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
|
|
394
395
|
):
|
|
396
|
+
flash_entry = _FlashContainerEntry(self.function_def.http_config)
|
|
395
397
|
# Identify the "enter" methods to run after resuming from a snapshot.
|
|
396
398
|
if not self.function_def.is_auto_snapshot:
|
|
397
399
|
post_snapshot_methods = _find_callables_for_obj(
|
|
398
400
|
self.user_cls_instance, _PartialFunctionFlags.ENTER_POST_SNAPSHOT
|
|
399
401
|
)
|
|
400
402
|
call_lifecycle_functions(event_loop, container_io_manager, list(post_snapshot_methods.values()))
|
|
403
|
+
flash_entry.enter()
|
|
401
404
|
try:
|
|
402
405
|
yield
|
|
403
406
|
finally:
|
|
404
407
|
if not self.function_def.is_auto_snapshot:
|
|
408
|
+
flash_entry.stop()
|
|
405
409
|
exit_methods = _find_callables_for_obj(self.user_cls_instance, _PartialFunctionFlags.EXIT)
|
|
406
410
|
call_lifecycle_functions(event_loop, container_io_manager, list(exit_methods.values()))
|
|
411
|
+
flash_entry.close()
|
|
407
412
|
|
|
408
413
|
|
|
409
414
|
def get_user_class_instance(_cls: modal.cls._Cls, args: tuple[Any, ...], kwargs: dict[str, Any]) -> typing.Any:
|
|
@@ -460,7 +465,10 @@ def import_single_function_service(
|
|
|
460
465
|
else:
|
|
461
466
|
# Load the module dynamically
|
|
462
467
|
module = importlib.import_module(function_def.module_name)
|
|
463
|
-
|
|
468
|
+
|
|
469
|
+
# Fall back to function_name just to be safe around the migration
|
|
470
|
+
# Going forward, implementation_name should always be set
|
|
471
|
+
qual_name: str = function_def.implementation_name or function_def.function_name
|
|
464
472
|
|
|
465
473
|
if not is_global_object(qual_name):
|
|
466
474
|
raise LocalFunctionError("Attempted to load a function defined in a function scope")
|
|
@@ -119,7 +119,7 @@ def print_exception(exc: Optional[type[BaseException]], value: Optional[BaseExce
|
|
|
119
119
|
traceback.print_exception(exc, value, tb)
|
|
120
120
|
if sys.version_info < (3, 11) and value is not None: # type: ignore
|
|
121
121
|
notes = getattr(value, "__notes__", [])
|
|
122
|
-
print(*notes, sep="\n", file=sys.stderr)
|
|
122
|
+
print(*notes, sep="\n", file=sys.stderr) # noqa: T201
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
|
|
@@ -5,14 +5,13 @@ from collections.abc import AsyncIterator
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
|
-
from grpclib import GRPCError, Status
|
|
9
8
|
from synchronicity.async_wrap import asynccontextmanager
|
|
10
9
|
|
|
11
10
|
from modal_proto import api_pb2
|
|
12
11
|
|
|
13
12
|
from ._utils.async_utils import synchronize_api
|
|
14
13
|
from .client import _Client
|
|
15
|
-
from .exception import InvalidError, RemoteError
|
|
14
|
+
from .exception import AlreadyExistsError, InvalidError, RemoteError, ServiceError
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
@dataclass(frozen=True)
|
|
@@ -186,13 +185,10 @@ async def _forward(
|
|
|
186
185
|
response = await client.stub.TunnelStart(
|
|
187
186
|
api_pb2.TunnelStartRequest(port=port, unencrypted=unencrypted, tunnel_type=tunnel_type)
|
|
188
187
|
)
|
|
189
|
-
except
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
raise RemoteError("Relay server is unavailable") from exc
|
|
194
|
-
else:
|
|
195
|
-
raise
|
|
188
|
+
except AlreadyExistsError as exc:
|
|
189
|
+
raise InvalidError(f"Port {port} is already forwarded")
|
|
190
|
+
except ServiceError as exc:
|
|
191
|
+
raise RemoteError("Relay server is unavailable") from exc
|
|
196
192
|
|
|
197
193
|
try:
|
|
198
194
|
yield Tunnel(response.host, response.port, response.unencrypted_host, response.unencrypted_port)
|
|
@@ -152,12 +152,13 @@ async def perform_multipart_upload(
|
|
|
152
152
|
part_etags = await TaskContext.gather(*upload_coros)
|
|
153
153
|
|
|
154
154
|
# The body of the complete_multipart_upload command needs some data in xml format:
|
|
155
|
-
|
|
155
|
+
completion_parts = ["<CompleteMultipartUpload>"]
|
|
156
156
|
for part_number, etag in enumerate(part_etags, 1):
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
completion_parts.append(f"""<Part>\n<PartNumber>{part_number}</PartNumber>\n<ETag>"{etag}"</ETag>\n</Part>""")
|
|
158
|
+
completion_parts.append("</CompleteMultipartUpload>")
|
|
159
|
+
completion_body = "\n".join(completion_parts)
|
|
159
160
|
|
|
160
|
-
# etag of combined object should be md5 hex of
|
|
161
|
+
# etag of combined object should be md5 hex of concatenated md5 *bytes* from parts + `-{num_parts}`
|
|
161
162
|
bin_hash_parts = [bytes.fromhex(etag) for etag in part_etags]
|
|
162
163
|
|
|
163
164
|
expected_multipart_etag = hashlib.md5(b"".join(bin_hash_parts)).hexdigest() + f"-{len(part_etags)}"
|
|
@@ -371,11 +372,17 @@ class FileUploadSpec:
|
|
|
371
372
|
mount_filename: str
|
|
372
373
|
|
|
373
374
|
use_blob: bool
|
|
374
|
-
content: Optional[bytes] # typically None if using blob, required otherwise
|
|
375
375
|
sha256_hex: str
|
|
376
376
|
md5_hex: str
|
|
377
377
|
mode: int # file permission bits (last 12 bits of st_mode)
|
|
378
378
|
size: int
|
|
379
|
+
content: Optional[bytes] = None # Set for very small files to avoid double-read
|
|
380
|
+
|
|
381
|
+
def read_content(self) -> bytes:
|
|
382
|
+
"""Read content from source."""
|
|
383
|
+
with self.source() as fp:
|
|
384
|
+
fp.seek(0)
|
|
385
|
+
return fp.read()
|
|
379
386
|
|
|
380
387
|
|
|
381
388
|
def _get_file_upload_spec(
|
|
@@ -384,6 +391,7 @@ def _get_file_upload_spec(
|
|
|
384
391
|
mount_filename: PurePosixPath,
|
|
385
392
|
mode: int,
|
|
386
393
|
) -> FileUploadSpec:
|
|
394
|
+
content = None
|
|
387
395
|
with source() as fp:
|
|
388
396
|
# Current position is ignored - we always upload from position 0
|
|
389
397
|
fp.seek(0, os.SEEK_END)
|
|
@@ -394,12 +402,18 @@ def _get_file_upload_spec(
|
|
|
394
402
|
# TODO(dano): remove the placeholder md5 once we stop requiring md5 for blobs
|
|
395
403
|
md5_hex = "baadbaadbaadbaadbaadbaadbaadbaad" if size > MULTIPART_UPLOAD_THRESHOLD else None
|
|
396
404
|
use_blob = True
|
|
397
|
-
content = None
|
|
398
405
|
hashes = get_upload_hashes(fp, md5_hex=md5_hex)
|
|
399
406
|
else:
|
|
400
407
|
use_blob = False
|
|
401
|
-
|
|
402
|
-
|
|
408
|
+
# For very small files (< 256 KiB), read content once and cache it
|
|
409
|
+
# This avoids double-read penalty while limiting memory usage
|
|
410
|
+
if size < 256 * 1024: # 256 KiB threshold
|
|
411
|
+
fp.seek(0)
|
|
412
|
+
content = fp.read()
|
|
413
|
+
hashes = get_upload_hashes(content)
|
|
414
|
+
else:
|
|
415
|
+
# For medium files (256 KiB - 4 MiB), compute hashes without caching content
|
|
416
|
+
hashes = get_upload_hashes(fp)
|
|
403
417
|
|
|
404
418
|
return FileUploadSpec(
|
|
405
419
|
source=source,
|
|
@@ -407,11 +421,11 @@ def _get_file_upload_spec(
|
|
|
407
421
|
source_is_path=isinstance(source_description, Path),
|
|
408
422
|
mount_filename=mount_filename.as_posix(),
|
|
409
423
|
use_blob=use_blob,
|
|
410
|
-
content=content,
|
|
411
424
|
sha256_hex=hashes.sha256_hex(),
|
|
412
425
|
md5_hex=hashes.md5_hex(),
|
|
413
426
|
mode=mode & 0o7777,
|
|
414
427
|
size=size,
|
|
428
|
+
content=content,
|
|
415
429
|
)
|
|
416
430
|
|
|
417
431
|
|