modal 1.5.6.dev11__tar.gz → 1.5.6.dev12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/PKG-INFO +1 -1
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_functions.py +8 -1
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/execution_context.py +1 -1
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/image.py +4 -2
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/import_refs.py +5 -2
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/profile.py +21 -6
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/token.py +9 -4
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/client.pyi +2 -2
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/config.py +46 -6
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/functions.pyi +4 -2
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/runner.py +13 -3
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/token_flow.py +8 -1
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/volume.py +138 -8
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/volume.pyi +28 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/api_pb2.py +624 -603
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/api_pb2.pyi +67 -3
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/task_command_router_grpc.py +16 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2.py +45 -25
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2.pyi +29 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2_grpc.py +34 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2_grpc.pyi +12 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_version/__init__.py +1 -1
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/LICENSE +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/README.md +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/__main__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_billing.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_clustered_functions.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_environments.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_function_variants.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_image.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_location.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_logs.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_logs_manager.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_object.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_output/manager.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_output/pty.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_output/rich.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_output/status.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_partial_function.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_resources.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_server.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_traceback.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_tunnel.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/_workspace.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/app.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/app.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/billing.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/_stats.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/app.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/cluster.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/config.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/container.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/curl.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/dashboard.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/endpoint.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/entry_point.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/function.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/launch.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/queues.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/secret.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/shell.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/utils.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cli/workspace.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/client.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cls.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/cls.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/container_process.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/dict.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/dict.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/environments.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/environments.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/exception.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/experimental/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/experimental/flash.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/file_io.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/file_io.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/functions.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/image.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/image.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/io_streams.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/io_streams.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/mount.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/network_file_system.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/network_file_system.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/object.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/object.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/output.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/parallel_map.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/parallel_map.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/partial_function.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/partial_function.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/proxy.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/py.typed +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/queue.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/retries.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/running_app.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/sandbox.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/sandbox.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/sandbox_fs.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/schedule.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/secret.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/secret.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/server.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/server.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/serving.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/snapshot.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/token_flow.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/types.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/workspace.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal/workspace.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/api_grpc.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/pyproject.toml +0 -0
- {modal-1.5.6.dev11 → modal-1.5.6.dev12}/setup.cfg +0 -0
|
@@ -16,6 +16,7 @@ from google.protobuf.message import Message
|
|
|
16
16
|
from grpclib import Status
|
|
17
17
|
from synchronicity.combined_types import MethodWithAio
|
|
18
18
|
|
|
19
|
+
from modal._utils.logger import logger
|
|
19
20
|
from modal.types import CloudBucketMountInfo, FunctionInfo, HttpInfo, VolumeMountInfo
|
|
20
21
|
from modal_proto import api_pb2
|
|
21
22
|
from modal_proto.modal_api_grpc import ModalClientModal
|
|
@@ -2306,7 +2307,8 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2306
2307
|
"""Fetch information about the graph of Inputs this FunctionCall is part of.
|
|
2307
2308
|
|
|
2308
2309
|
Note: the call graph data is not populated in real-time, and its capture is best-effort.
|
|
2309
|
-
We do not recommend relying on this method
|
|
2310
|
+
Large call graphs may be truncated. We do not recommend relying on this method
|
|
2311
|
+
for critical use cases.
|
|
2310
2312
|
|
|
2311
2313
|
See the [`modal.types`](/docs/sdk/py/latest/types) reference for information
|
|
2312
2314
|
on the return values.
|
|
@@ -2317,6 +2319,11 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2317
2319
|
assert self._client and self._client.stub
|
|
2318
2320
|
request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
|
|
2319
2321
|
response = await self._client.stub.FunctionGetCallGraph(request)
|
|
2322
|
+
if response.truncated:
|
|
2323
|
+
logger.warning(
|
|
2324
|
+
f"Call graph for {self.object_id} was truncated; "
|
|
2325
|
+
f"returning the first {len(response.inputs)} inputs. The graph may be incomplete."
|
|
2326
|
+
)
|
|
2320
2327
|
return _reconstruct_call_graph(response)
|
|
2321
2328
|
|
|
2322
2329
|
@live_method
|
|
@@ -109,7 +109,7 @@ _current_input_id: ContextVar = ContextVar("_current_input_id")
|
|
|
109
109
|
_current_function_call_id: ContextVar = ContextVar("_current_function_call_id")
|
|
110
110
|
_current_attempt_token: ContextVar = ContextVar("_current_attempt_token")
|
|
111
111
|
|
|
112
|
-
_is_currently_importing = False
|
|
112
|
+
_is_currently_importing = False
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
@contextmanager
|
|
@@ -58,7 +58,7 @@ async def logs(image_id: str, layers: int | None, all_layers: bool) -> None:
|
|
|
58
58
|
effective_layers = None if all_layers else layers or 1
|
|
59
59
|
image = Image.from_id(image_id)
|
|
60
60
|
output_mgr = OutputManager.get()
|
|
61
|
-
last_message =
|
|
61
|
+
last_message: str | None = None
|
|
62
62
|
async for entry in image.logs.fetch.aio(layers=effective_layers):
|
|
63
63
|
last_message = entry.message
|
|
64
64
|
await output_mgr.put_fetched_log(
|
|
@@ -69,7 +69,9 @@ async def logs(image_id: str, layers: int | None, all_layers: bool) -> None:
|
|
|
69
69
|
)
|
|
70
70
|
)
|
|
71
71
|
output_mgr.flush_lines()
|
|
72
|
-
if last_message
|
|
72
|
+
if last_message is None:
|
|
73
|
+
output_mgr.print(f"No build logs found for Image {image_id}.")
|
|
74
|
+
elif not last_message.endswith("\n"):
|
|
73
75
|
output_mgr.print("")
|
|
74
76
|
|
|
75
77
|
|
|
@@ -27,6 +27,7 @@ from modal.exception import InvalidError, _CliUserExecutionError
|
|
|
27
27
|
from modal.functions import Function
|
|
28
28
|
from modal.output import OutputManager
|
|
29
29
|
|
|
30
|
+
from .._runtime import execution_context
|
|
30
31
|
from .._utils.async_utils import synchronizer
|
|
31
32
|
|
|
32
33
|
|
|
@@ -74,7 +75,8 @@ def import_file_or_module(import_ref: ImportRef, base_cmd: str = ""):
|
|
|
74
75
|
f"Use `{base_cmd} -m {import_ref.file_or_module}` instead.",
|
|
75
76
|
)
|
|
76
77
|
try:
|
|
77
|
-
|
|
78
|
+
with execution_context._import_context():
|
|
79
|
+
module = importlib.import_module(import_ref.file_or_module)
|
|
78
80
|
except Exception as exc:
|
|
79
81
|
raise _CliUserExecutionError(import_ref.file_or_module) from exc
|
|
80
82
|
else:
|
|
@@ -97,7 +99,8 @@ def import_file_or_module(import_ref: ImportRef, base_cmd: str = ""):
|
|
|
97
99
|
sys.modules[module_name] = module
|
|
98
100
|
try:
|
|
99
101
|
assert spec.loader
|
|
100
|
-
|
|
102
|
+
with execution_context._import_context():
|
|
103
|
+
spec.loader.exec_module(module)
|
|
101
104
|
except Exception as exc:
|
|
102
105
|
if isinstance(exc, ImportError) and "attempted relative import with no known parent package" in str(exc):
|
|
103
106
|
if import_ref.file_or_module and import_ref.command and not import_ref.use_module_mode:
|
|
@@ -17,7 +17,7 @@ from modal.config import (
|
|
|
17
17
|
config_set_active_profile,
|
|
18
18
|
)
|
|
19
19
|
from modal.environments import Environment
|
|
20
|
-
from modal.exception import AuthError
|
|
20
|
+
from modal.exception import AuthError, InvalidError
|
|
21
21
|
from modal.output import OutputManager
|
|
22
22
|
|
|
23
23
|
from ._help import ModalGroup
|
|
@@ -53,8 +53,11 @@ async def list_(json: bool | None = False):
|
|
|
53
53
|
lookup_coros = [
|
|
54
54
|
_lookup_workspace(
|
|
55
55
|
config.get("server_url", profile=profile),
|
|
56
|
-
config.get("token_id", profile=profile, use_env=False),
|
|
57
|
-
config.get("token_secret", profile=profile, use_env=False),
|
|
56
|
+
token_id=config.get("token_id", profile=profile, use_env=False),
|
|
57
|
+
token_secret=config.get("token_secret", profile=profile, use_env=False),
|
|
58
|
+
oauth_refresh_token=config.get("oauth_refresh_token", profile=profile, use_env=False),
|
|
59
|
+
oauth_client_id=config.get("oauth_client_id", profile=profile, use_env=False),
|
|
60
|
+
oauth_client_secret=config.get("oauth_client_secret", profile=profile, use_env=False),
|
|
58
61
|
)
|
|
59
62
|
for profile in profiles
|
|
60
63
|
]
|
|
@@ -77,16 +80,28 @@ async def list_(json: bool | None = False):
|
|
|
77
80
|
rows.append((active, content))
|
|
78
81
|
|
|
79
82
|
env_based_workspace: str | None = None
|
|
80
|
-
|
|
83
|
+
credential_env_vars = (
|
|
84
|
+
"MODAL_TOKEN_ID",
|
|
85
|
+
"MODAL_TOKEN_SECRET",
|
|
86
|
+
"MODAL_OAUTH_REFRESH_TOKEN",
|
|
87
|
+
"MODAL_OAUTH_CLIENT_ID",
|
|
88
|
+
"MODAL_OAUTH_CLIENT_SECRET",
|
|
89
|
+
)
|
|
90
|
+
if any(env_var in os.environ for env_var in credential_env_vars):
|
|
81
91
|
try:
|
|
82
92
|
env_based_resp = await _lookup_workspace(
|
|
83
93
|
config.get("server_url", profile=config_module._profile),
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
token_id=config.get("token_id", profile=config_module._profile),
|
|
95
|
+
token_secret=config.get("token_secret", profile=config_module._profile),
|
|
96
|
+
oauth_refresh_token=config.get("oauth_refresh_token", profile=config_module._profile),
|
|
97
|
+
oauth_client_id=config.get("oauth_client_id", profile=config_module._profile),
|
|
98
|
+
oauth_client_secret=config.get("oauth_client_secret", profile=config_module._profile),
|
|
86
99
|
)
|
|
87
100
|
env_based_workspace = env_based_resp.username
|
|
88
101
|
except AuthError:
|
|
89
102
|
env_based_workspace = "Unknown (authentication failure)"
|
|
103
|
+
except InvalidError:
|
|
104
|
+
env_based_workspace = "Unknown (invalid credential configuration)"
|
|
90
105
|
|
|
91
106
|
output = OutputManager.get()
|
|
92
107
|
highlight = "bold green" if env_based_workspace is None else "yellow"
|
|
@@ -62,10 +62,15 @@ async def info():
|
|
|
62
62
|
|
|
63
63
|
output = OutputManager.get()
|
|
64
64
|
env_vars = []
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
for env_var in (
|
|
66
|
+
"MODAL_TOKEN_ID",
|
|
67
|
+
"MODAL_TOKEN_SECRET",
|
|
68
|
+
"MODAL_OAUTH_REFRESH_TOKEN",
|
|
69
|
+
"MODAL_OAUTH_CLIENT_ID",
|
|
70
|
+
"MODAL_OAUTH_CLIENT_SECRET",
|
|
71
|
+
):
|
|
72
|
+
if os.environ.get(env_var):
|
|
73
|
+
env_vars.append(env_var)
|
|
69
74
|
|
|
70
75
|
if env_vars:
|
|
71
76
|
env_vars_str = " and ".join(env_vars)
|
|
@@ -77,7 +77,7 @@ class _Client:
|
|
|
77
77
|
server_url: str,
|
|
78
78
|
client_type: int,
|
|
79
79
|
credentials: typing.Optional[tuple[str, str]],
|
|
80
|
-
version: str = "1.5.6.
|
|
80
|
+
version: str = "1.5.6.dev12",
|
|
81
81
|
*,
|
|
82
82
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
83
83
|
):
|
|
@@ -277,7 +277,7 @@ class Client:
|
|
|
277
277
|
server_url: str,
|
|
278
278
|
client_type: int,
|
|
279
279
|
credentials: typing.Optional[tuple[str, str]],
|
|
280
|
-
version: str = "1.5.6.
|
|
280
|
+
version: str = "1.5.6.dev12",
|
|
281
281
|
*,
|
|
282
282
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
283
283
|
):
|
|
@@ -109,7 +109,7 @@ from google.protobuf.empty_pb2 import Empty
|
|
|
109
109
|
from modal_proto import api_pb2
|
|
110
110
|
|
|
111
111
|
from ._utils.logger import configure_logger
|
|
112
|
-
from .exception import InvalidError, NotFoundError
|
|
112
|
+
from .exception import AuthError, InvalidError, NotFoundError
|
|
113
113
|
|
|
114
114
|
# By default, try api.modal.com and fail over to api.modal2.com
|
|
115
115
|
DEFAULT_SERVER_URL = "https://api.modal.com,https://api.modal2.com"
|
|
@@ -184,11 +184,51 @@ def _read_user_config():
|
|
|
184
184
|
_user_config = _read_user_config()
|
|
185
185
|
|
|
186
186
|
|
|
187
|
-
async def _lookup_workspace(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
async def _lookup_workspace(
|
|
188
|
+
server_url: str,
|
|
189
|
+
token_id: str | None = None,
|
|
190
|
+
token_secret: str | None = None,
|
|
191
|
+
*,
|
|
192
|
+
oauth_refresh_token: str | None = None,
|
|
193
|
+
oauth_client_id: str | None = None,
|
|
194
|
+
oauth_client_secret: str | None = None,
|
|
195
|
+
) -> api_pb2.WorkspaceNameLookupResponse:
|
|
196
|
+
from .client import _Client, _OAuthCredentials
|
|
197
|
+
|
|
198
|
+
has_token_config = bool(token_id or token_secret)
|
|
199
|
+
has_oauth_config = bool(oauth_refresh_token or oauth_client_id or oauth_client_secret)
|
|
200
|
+
if has_token_config and has_oauth_config:
|
|
201
|
+
raise InvalidError("Modal token credentials and OAuth credentials cannot both be configured.")
|
|
202
|
+
if has_oauth_config:
|
|
203
|
+
missing_oauth_credentials = [
|
|
204
|
+
name
|
|
205
|
+
for name, value in (
|
|
206
|
+
("refresh token", oauth_refresh_token),
|
|
207
|
+
("client ID", oauth_client_id),
|
|
208
|
+
("client secret", oauth_client_secret),
|
|
209
|
+
)
|
|
210
|
+
if not value
|
|
211
|
+
]
|
|
212
|
+
if missing_oauth_credentials:
|
|
213
|
+
raise InvalidError(f"OAuth credentials are incomplete; missing {', '.join(missing_oauth_credentials)}.")
|
|
214
|
+
assert oauth_refresh_token and oauth_client_id and oauth_client_secret
|
|
215
|
+
credentials = None
|
|
216
|
+
oauth_credentials = _OAuthCredentials(
|
|
217
|
+
refresh_token=oauth_refresh_token,
|
|
218
|
+
client_id=oauth_client_id,
|
|
219
|
+
client_secret=oauth_client_secret,
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
if not token_id or not token_secret:
|
|
223
|
+
raise AuthError("Token ID and secret must both be configured.")
|
|
224
|
+
credentials = (token_id, token_secret)
|
|
225
|
+
oauth_credentials = None
|
|
226
|
+
async with _Client(
|
|
227
|
+
server_url,
|
|
228
|
+
api_pb2.CLIENT_TYPE_CLIENT,
|
|
229
|
+
credentials,
|
|
230
|
+
oauth_credentials=oauth_credentials,
|
|
231
|
+
) as client:
|
|
192
232
|
return await client.stub.WorkspaceNameLookup(Empty(), retry=None, timeout=3)
|
|
193
233
|
|
|
194
234
|
|
|
@@ -1231,7 +1231,8 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
|
|
|
1231
1231
|
"""Fetch information about the graph of Inputs this FunctionCall is part of.
|
|
1232
1232
|
|
|
1233
1233
|
Note: the call graph data is not populated in real-time, and its capture is best-effort.
|
|
1234
|
-
We do not recommend relying on this method
|
|
1234
|
+
Large call graphs may be truncated. We do not recommend relying on this method
|
|
1235
|
+
for critical use cases.
|
|
1235
1236
|
|
|
1236
1237
|
See the [`modal.types`](/docs/sdk/py/latest/types) reference for information
|
|
1237
1238
|
on the return values.
|
|
@@ -1245,7 +1246,8 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
|
|
|
1245
1246
|
"""Fetch information about the graph of Inputs this FunctionCall is part of.
|
|
1246
1247
|
|
|
1247
1248
|
Note: the call graph data is not populated in real-time, and its capture is best-effort.
|
|
1248
|
-
We do not recommend relying on this method
|
|
1249
|
+
Large call graphs may be truncated. We do not recommend relying on this method
|
|
1250
|
+
for critical use cases.
|
|
1249
1251
|
|
|
1250
1252
|
See the [`modal.types`](/docs/sdk/py/latest/types) reference for information
|
|
1251
1253
|
on the return values.
|
|
@@ -377,13 +377,17 @@ async def _run_app(
|
|
|
377
377
|
deployment_strategy: str = "rolling",
|
|
378
378
|
) -> AsyncGenerator["modal.app._App", None]:
|
|
379
379
|
"""mdmd:hidden"""
|
|
380
|
+
if modal._runtime.execution_context._in_import_context():
|
|
381
|
+
raise InvalidError(
|
|
382
|
+
"`App.run()` cannot be called in global scope. "
|
|
383
|
+
'Use `if __name__ == "__main__"` to run Apps in a script, '
|
|
384
|
+
"or convert the block to a local entrypoint and use the `modal run` CLI."
|
|
385
|
+
)
|
|
386
|
+
|
|
380
387
|
load_context = await app._root_load_context.reset().in_place_upgrade(
|
|
381
388
|
client=client, environment_name=environment_name
|
|
382
389
|
)
|
|
383
390
|
|
|
384
|
-
if modal._runtime.execution_context._in_import_context():
|
|
385
|
-
raise InvalidError("Can not run an app in global scope within a container")
|
|
386
|
-
|
|
387
391
|
if app._running_app:
|
|
388
392
|
raise InvalidError(
|
|
389
393
|
"App is already running and can't be started again.\n"
|
|
@@ -601,6 +605,12 @@ async def _deploy_app(
|
|
|
601
605
|
|
|
602
606
|
Users should prefer the `modal deploy` CLI or the `App.deploy` method.
|
|
603
607
|
"""
|
|
608
|
+
if modal._runtime.execution_context._in_import_context():
|
|
609
|
+
raise InvalidError(
|
|
610
|
+
"`App.deploy()` should not be called in global scope. "
|
|
611
|
+
'Use `if __name__ == "__main__"` to deploy Apps in a script, '
|
|
612
|
+
"or use the `modal deploy` CLI."
|
|
613
|
+
)
|
|
604
614
|
|
|
605
615
|
name = name or app.name or ""
|
|
606
616
|
if not name:
|
|
@@ -166,7 +166,14 @@ async def _set_token(
|
|
|
166
166
|
)
|
|
167
167
|
|
|
168
168
|
# Warn the user if their token will be ignored
|
|
169
|
-
|
|
169
|
+
credential_env_vars = [
|
|
170
|
+
"MODAL_TOKEN_ID",
|
|
171
|
+
"MODAL_TOKEN_SECRET",
|
|
172
|
+
"MODAL_OAUTH_REFRESH_TOKEN",
|
|
173
|
+
"MODAL_OAUTH_CLIENT_ID",
|
|
174
|
+
"MODAL_OAUTH_CLIENT_SECRET",
|
|
175
|
+
]
|
|
176
|
+
env_vars = [var if os.environ.get(var) else None for var in credential_env_vars]
|
|
170
177
|
env_vars_used = [var for var in env_vars if var is not None]
|
|
171
178
|
env_vars_str = " / ".join(env_vars_used)
|
|
172
179
|
if env_vars_used:
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Copyright Modal Labs 2023
|
|
2
2
|
import asyncio
|
|
3
|
+
import base64
|
|
3
4
|
import builtins
|
|
4
5
|
import concurrent.futures
|
|
5
6
|
import functools
|
|
7
|
+
import hashlib
|
|
6
8
|
import multiprocessing
|
|
7
9
|
import os
|
|
8
10
|
import platform
|
|
@@ -79,6 +81,21 @@ from .types import FileEntry, FileEntryType as FileEntryType, VolumeCreateOption
|
|
|
79
81
|
VOLUME_PUT_FILE_CLIENT_TIMEOUT = 60 * 60
|
|
80
82
|
|
|
81
83
|
|
|
84
|
+
def _expected_block_lengths(start: int, length: int, num_blocks: int) -> list[int]:
|
|
85
|
+
"""Number of bytes of a byte range that fall into each of its `BLOCK_SIZE`-aligned blocks.
|
|
86
|
+
|
|
87
|
+
A downloaded block may be shorter than this (trailing zero bytes are left out),
|
|
88
|
+
so the caller extends it with zeros to the expected length.
|
|
89
|
+
"""
|
|
90
|
+
lengths = []
|
|
91
|
+
pos = start
|
|
92
|
+
for idx in range(num_blocks):
|
|
93
|
+
block_end = min(start + length, (start // BLOCK_SIZE + idx + 1) * BLOCK_SIZE)
|
|
94
|
+
lengths.append(max(0, block_end - pos))
|
|
95
|
+
pos = block_end
|
|
96
|
+
return lengths
|
|
97
|
+
|
|
98
|
+
|
|
82
99
|
async def _raise_on_block_response_error(response) -> None:
|
|
83
100
|
"""Raise a picklable Modal exception on error.
|
|
84
101
|
|
|
@@ -99,6 +116,91 @@ async def _raise_on_block_response_error(response) -> None:
|
|
|
99
116
|
raise ExecutionError(f"Request failed with status {response.status} {response.reason}: {body}")
|
|
100
117
|
|
|
101
118
|
|
|
119
|
+
# A block download may carry an RFC 9530 `Repr-Digest`. Only the `sha-256`
|
|
120
|
+
# member is understood, and it covers the whole body; every other algorithm
|
|
121
|
+
# key is ignored, as the RFC prescribes, and a body carrying only unknown keys
|
|
122
|
+
# is left unverified. The field is a Structured Fields Dictionary (RFC 8941):
|
|
123
|
+
# parameters on a member carry no meaning here and are dropped, and when a key
|
|
124
|
+
# repeats the last occurrence wins.
|
|
125
|
+
_REPR_DIGEST_MEMBER_RE = re.compile(r"^sha-256=:([A-Za-z0-9+/]+={0,2}):(;.*)?$")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _split_sf_members(header: str) -> list[str]:
|
|
129
|
+
# Splits a Structured Fields List/Dictionary on commas, except commas inside
|
|
130
|
+
# quoted (possibly backslash-escaped) parameter strings.
|
|
131
|
+
members: list[str] = []
|
|
132
|
+
start = 0
|
|
133
|
+
in_quote = False
|
|
134
|
+
escaped = False
|
|
135
|
+
for i, ch in enumerate(header):
|
|
136
|
+
if escaped:
|
|
137
|
+
escaped = False
|
|
138
|
+
elif in_quote:
|
|
139
|
+
if ch == "\\":
|
|
140
|
+
escaped = True
|
|
141
|
+
elif ch == '"':
|
|
142
|
+
in_quote = False
|
|
143
|
+
elif ch == '"':
|
|
144
|
+
in_quote = True
|
|
145
|
+
elif ch == ",":
|
|
146
|
+
members.append(header[start:i])
|
|
147
|
+
start = i + 1
|
|
148
|
+
members.append(header[start:])
|
|
149
|
+
return members
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _parse_repr_digest(header: str | None) -> bytes | None:
|
|
153
|
+
if not header:
|
|
154
|
+
return None
|
|
155
|
+
sha256: bytes | None = None
|
|
156
|
+
for member in _split_sf_members(header):
|
|
157
|
+
match = _REPR_DIGEST_MEMBER_RE.match(member.strip())
|
|
158
|
+
if match is None:
|
|
159
|
+
continue
|
|
160
|
+
try:
|
|
161
|
+
decoded = base64.b64decode(match.group(1), validate=True)
|
|
162
|
+
except ValueError:
|
|
163
|
+
continue
|
|
164
|
+
if len(decoded) != 32:
|
|
165
|
+
continue
|
|
166
|
+
sha256 = decoded
|
|
167
|
+
if sha256 is None:
|
|
168
|
+
logger.debug(f"ignoring unrecognized Repr-Digest: {header!r}")
|
|
169
|
+
return sha256
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _BlockDigestVerifier:
|
|
173
|
+
"""Checks a streamed block body against the digest its response advertised.
|
|
174
|
+
|
|
175
|
+
Feed the body through `update` in order, then call `finish`. Without a
|
|
176
|
+
digest both are no-ops, so callers can use one unconditionally.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, sha256: bytes | None):
|
|
180
|
+
self._sha256 = sha256
|
|
181
|
+
self._hasher = hashlib.sha256() if sha256 is not None else None
|
|
182
|
+
|
|
183
|
+
def update(self, chunk: bytes) -> None:
|
|
184
|
+
if self._hasher is not None:
|
|
185
|
+
self._hasher.update(chunk)
|
|
186
|
+
|
|
187
|
+
def finish(self) -> None:
|
|
188
|
+
if self._sha256 is None or self._hasher is None:
|
|
189
|
+
return
|
|
190
|
+
actual = self._hasher.digest()
|
|
191
|
+
if actual != self._sha256:
|
|
192
|
+
raise ExecutionError(f"Block download corrupted: expected sha256 {self._sha256.hex()}, got {actual.hex()}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def _read_block_body(response) -> bytes:
|
|
196
|
+
"""Read a block response body in full, verifying any digest it advertises."""
|
|
197
|
+
verifier = _BlockDigestVerifier(_parse_repr_digest(response.headers.get("Repr-Digest")))
|
|
198
|
+
body = await response.content.read()
|
|
199
|
+
verifier.update(body)
|
|
200
|
+
verifier.finish()
|
|
201
|
+
return body
|
|
202
|
+
|
|
203
|
+
|
|
102
204
|
def _validate_volume_version(
|
|
103
205
|
requested_version: int,
|
|
104
206
|
actual_version: int,
|
|
@@ -854,7 +956,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
854
956
|
print(len(data)) # == 1024 * 1024
|
|
855
957
|
```
|
|
856
958
|
"""
|
|
857
|
-
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
|
|
959
|
+
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path, client_pads_blocks=True)
|
|
858
960
|
|
|
859
961
|
try:
|
|
860
962
|
response = await self.client.stub.VolumeGetFile2(req)
|
|
@@ -862,14 +964,20 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
862
964
|
raise FileNotFoundError(exc.args[0])
|
|
863
965
|
|
|
864
966
|
@retry(n_attempts=5, base_delay=0.1, attempt_timeout=None)
|
|
865
|
-
async def read_block(
|
|
967
|
+
async def read_block(block: tuple[str, int]) -> bytes:
|
|
968
|
+
block_url, expected_len = block
|
|
866
969
|
async with ClientSessionRegistry.get_session().get(block_url) as get_response:
|
|
867
970
|
await _raise_on_block_response_error(get_response)
|
|
868
|
-
|
|
971
|
+
body = await _read_block_body(get_response)
|
|
972
|
+
if len(body) > expected_len:
|
|
973
|
+
raise ExecutionError(f"Block body is {len(body)} bytes, expected at most {expected_len}")
|
|
974
|
+
return body.ljust(expected_len, b"\0")
|
|
869
975
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
976
|
+
block_lengths = _expected_block_lengths(response.start, response.len, len(response.get_urls))
|
|
977
|
+
|
|
978
|
+
async def iter_urls() -> AsyncGenerator[tuple[str, int]]:
|
|
979
|
+
for url, expected_len in zip(response.get_urls, block_lengths):
|
|
980
|
+
yield url, expected_len
|
|
873
981
|
|
|
874
982
|
# TODO(dflemstr): Reasonable default? Make configurable?
|
|
875
983
|
prefetch_num_blocks = multiprocessing.cpu_count()
|
|
@@ -910,7 +1018,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
910
1018
|
if concurrency is None:
|
|
911
1019
|
concurrency = multiprocessing.cpu_count()
|
|
912
1020
|
|
|
913
|
-
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
|
|
1021
|
+
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path, client_pads_blocks=True)
|
|
914
1022
|
|
|
915
1023
|
# Acquire RPC semaphore if provided to limit concurrent VolumeGetFile2 RPCs.
|
|
916
1024
|
# This is used by CLI downloads to prevent overwhelming the server.
|
|
@@ -929,14 +1037,23 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
929
1037
|
write_lock = asyncio.Lock()
|
|
930
1038
|
start_pos = fileobj.tell()
|
|
931
1039
|
|
|
1040
|
+
block_lengths = _expected_block_lengths(response.start, response.len, len(response.get_urls))
|
|
1041
|
+
|
|
932
1042
|
@retry(n_attempts=5, base_delay=0.1, attempt_timeout=None)
|
|
933
1043
|
async def download_block(idx, url) -> int:
|
|
934
1044
|
block_start_pos = start_pos + idx * BLOCK_SIZE
|
|
1045
|
+
# Every attempt writes into exactly this slice, so a retry after a rejected
|
|
1046
|
+
# response overwrites everything the rejected attempt wrote.
|
|
1047
|
+
expected_len = block_lengths[idx]
|
|
935
1048
|
num_bytes_written = 0
|
|
936
1049
|
|
|
937
1050
|
async with download_semaphore, ClientSessionRegistry.get_session().get(url) as get_response:
|
|
938
1051
|
await _raise_on_block_response_error(get_response)
|
|
1052
|
+
verifier = _BlockDigestVerifier(_parse_repr_digest(get_response.headers.get("Repr-Digest")))
|
|
939
1053
|
async for chunk in get_response.content.iter_any():
|
|
1054
|
+
if num_bytes_written + len(chunk) > expected_len:
|
|
1055
|
+
raise ExecutionError(f"Block {idx} response is longer than the expected {expected_len} bytes")
|
|
1056
|
+
verifier.update(chunk)
|
|
940
1057
|
num_chunk_bytes_written = 0
|
|
941
1058
|
|
|
942
1059
|
while num_chunk_bytes_written < len(chunk):
|
|
@@ -944,13 +1061,26 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
944
1061
|
fileobj.seek(block_start_pos + num_bytes_written + num_chunk_bytes_written)
|
|
945
1062
|
# TODO(dflemstr): this is a small write, but nonetheless might block the event loop for some
|
|
946
1063
|
# time:
|
|
947
|
-
n = fileobj.write(chunk)
|
|
1064
|
+
n = fileobj.write(chunk[num_chunk_bytes_written:])
|
|
1065
|
+
if not n:
|
|
1066
|
+
raise OSError("File object write made no progress")
|
|
948
1067
|
|
|
949
1068
|
num_chunk_bytes_written += n
|
|
950
1069
|
progress_cb(advance=n)
|
|
951
1070
|
|
|
952
1071
|
num_bytes_written += len(chunk)
|
|
953
1072
|
|
|
1073
|
+
# Trailing zero bytes may be omitted from the body; restore them.
|
|
1074
|
+
while num_bytes_written < expected_len:
|
|
1075
|
+
padding = b"\0" * min(expected_len - num_bytes_written, 1024 * 1024)
|
|
1076
|
+
async with write_lock:
|
|
1077
|
+
fileobj.seek(block_start_pos + num_bytes_written)
|
|
1078
|
+
n = fileobj.write(padding)
|
|
1079
|
+
num_bytes_written += n
|
|
1080
|
+
progress_cb(advance=n)
|
|
1081
|
+
|
|
1082
|
+
verifier.finish()
|
|
1083
|
+
|
|
954
1084
|
return num_bytes_written
|
|
955
1085
|
|
|
956
1086
|
coros = [download_block(idx, url) for idx, url in enumerate(response.get_urls)]
|
|
@@ -15,6 +15,14 @@ import synchronicity.combined_types
|
|
|
15
15
|
import typing
|
|
16
16
|
import typing_extensions
|
|
17
17
|
|
|
18
|
+
def _expected_block_lengths(start: int, length: int, num_blocks: int) -> list[int]:
|
|
19
|
+
"""Number of bytes of a byte range that fall into each of its `BLOCK_SIZE`-aligned blocks.
|
|
20
|
+
|
|
21
|
+
A downloaded block may be shorter than this (trailing zero bytes are left out),
|
|
22
|
+
so the caller extends it with zeros to the expected length.
|
|
23
|
+
"""
|
|
24
|
+
...
|
|
25
|
+
|
|
18
26
|
async def _raise_on_block_response_error(response) -> None:
|
|
19
27
|
"""Raise a picklable Modal exception on error.
|
|
20
28
|
|
|
@@ -24,6 +32,26 @@ async def _raise_on_block_response_error(response) -> None:
|
|
|
24
32
|
"""
|
|
25
33
|
...
|
|
26
34
|
|
|
35
|
+
def _split_sf_members(header: str) -> list[str]: ...
|
|
36
|
+
def _parse_repr_digest(header: typing.Optional[str]) -> typing.Optional[bytes]: ...
|
|
37
|
+
|
|
38
|
+
class _BlockDigestVerifier:
|
|
39
|
+
"""Checks a streamed block body against the digest its response advertised.
|
|
40
|
+
|
|
41
|
+
Feed the body through `update` in order, then call `finish`. Without a
|
|
42
|
+
digest both are no-ops, so callers can use one unconditionally.
|
|
43
|
+
"""
|
|
44
|
+
def __init__(self, sha256: typing.Optional[bytes]):
|
|
45
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def update(self, chunk: bytes) -> None: ...
|
|
49
|
+
def finish(self) -> None: ...
|
|
50
|
+
|
|
51
|
+
async def _read_block_body(response) -> bytes:
|
|
52
|
+
"""Read a block response body in full, verifying any digest it advertises."""
|
|
53
|
+
...
|
|
54
|
+
|
|
27
55
|
def _validate_volume_version(requested_version: int, actual_version: int, volume_name: str) -> None:
|
|
28
56
|
"""Validate that the returned volume version matches the requested version."""
|
|
29
57
|
...
|