modal 1.5.6.dev18__tar.gz → 1.5.6.dev20__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.dev18 → modal-1.5.6.dev20}/PKG-INFO +1 -1
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/__init__.py +2 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_functions.py +0 -2
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/user_code_imports.py +0 -1
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/task_command_router_client.py +6 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/app.py +76 -19
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/entry_point.py +1 -3
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/utils.py +14 -5
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/client.pyi +2 -2
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/config.py +1 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/network_file_system.py +12 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/network_file_system.pyi +1 -0
- modal-1.5.6.dev20/modal/outbound_policy.py +167 -0
- modal-1.5.6.dev20/modal/outbound_policy.pyi +184 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/sandbox.py +111 -16
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/sandbox.pyi +77 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/types.py +20 -11
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/SOURCES.txt +2 -1
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/gen_reference_docs.py +4 -2
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_pb2.py +1046 -1041
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_pb2.pyi +37 -38
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_version/__init__.py +1 -1
- modal-1.5.6.dev18/modal/cli/bootstrap.py +0 -139
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/LICENSE +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/README.md +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/__main__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_billing.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_clustered_functions.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_environments.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_function_variants.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_image.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_location.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_logs.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_logs_manager.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_object.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/manager.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/pty.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/rich.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/status.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_partial_function.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_resources.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_server.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_traceback.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_tunnel.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_workspace.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/app.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/app.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/billing.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_logs.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_stats.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/cluster.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/config.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/container.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/curl.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/dashboard.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/endpoint.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/function.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/image.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/import_refs.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/launch.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/profile.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/queues.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/secret.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/server.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/shell.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/token.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/workspace.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/client.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cls.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cls.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/container_process.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/dict.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/dict.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/environments.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/environments.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/exception.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/experimental/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/experimental/flash.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/functions.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/functions.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/image.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/image.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/io_streams.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/io_streams.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/mount.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/object.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/object.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/output.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/parallel_map.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/parallel_map.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/partial_function.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/partial_function.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/proxy.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/py.typed +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/queue.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/retries.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/runner.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/running_app.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/sandbox_fs.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/schedule.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/secret.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/secret.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/server.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/server.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/serving.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/snapshot.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/token_flow.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/token_flow.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/volume.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/volume.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/workspace.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/workspace.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_grpc.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/pyproject.toml +0 -0
- {modal-1.5.6.dev18 → modal-1.5.6.dev20}/setup.cfg +0 -0
|
@@ -23,6 +23,7 @@ try:
|
|
|
23
23
|
from .functions import Function, FunctionCall
|
|
24
24
|
from .image import Image
|
|
25
25
|
from .network_file_system import NetworkFileSystem
|
|
26
|
+
from .outbound_policy import OutboundPolicy
|
|
26
27
|
from .output import enable_output
|
|
27
28
|
from .partial_function import (
|
|
28
29
|
asgi_app,
|
|
@@ -71,6 +72,7 @@ __all__ = [
|
|
|
71
72
|
"FunctionCall",
|
|
72
73
|
"Image",
|
|
73
74
|
"NetworkFileSystem",
|
|
75
|
+
"OutboundPolicy",
|
|
74
76
|
"Period",
|
|
75
77
|
"Probe",
|
|
76
78
|
"Proxy",
|
|
@@ -1103,8 +1103,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1103
1103
|
is_class=function_definition.is_class,
|
|
1104
1104
|
class_parameter_info=function_definition.class_parameter_info,
|
|
1105
1105
|
is_method=function_definition.is_method,
|
|
1106
|
-
use_function_id=function_definition.use_function_id,
|
|
1107
|
-
use_method_name=function_definition.use_method_name,
|
|
1108
1106
|
method_definitions=function_definition.method_definitions,
|
|
1109
1107
|
method_definitions_set=function_definition.method_definitions_set,
|
|
1110
1108
|
experimental_options=experimental_options or {},
|
|
@@ -562,7 +562,6 @@ def _get_cls_or_user_cls(
|
|
|
562
562
|
f"Internal error: Invalid 'service function' identifier {qual_name}. Please contact Modal support"
|
|
563
563
|
)
|
|
564
564
|
|
|
565
|
-
assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
|
|
566
565
|
cls_name = parts[0]
|
|
567
566
|
cls_or_user_cls = getattr(module, cls_name)
|
|
568
567
|
return cls_or_user_cls
|
|
@@ -1077,6 +1077,12 @@ class TaskCommandRouterClient:
|
|
|
1077
1077
|
lambda: self._call_with_auth_retry(self._stub.TaskSetNetworkAccess, request)
|
|
1078
1078
|
)
|
|
1079
1079
|
|
|
1080
|
+
async def set_outbound_policy(self, request: sr_pb2.TaskSetOutboundPolicyRequest):
|
|
1081
|
+
with grpc_error_converter():
|
|
1082
|
+
return await call_with_retries_on_transient_errors(
|
|
1083
|
+
lambda: self._call_with_auth_retry(self._stub.TaskSetOutboundPolicy, request)
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1080
1086
|
async def reload_volumes(
|
|
1081
1087
|
self, task_id: str, timeout: float, container_id: str = ""
|
|
1082
1088
|
) -> sr_pb2.TaskReloadVolumesResponse:
|
|
@@ -3,13 +3,15 @@ import re
|
|
|
3
3
|
import sys
|
|
4
4
|
import time
|
|
5
5
|
import warnings
|
|
6
|
+
from collections.abc import Mapping
|
|
6
7
|
from json import dumps
|
|
7
8
|
from typing import Literal, get_args
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
11
|
import rich
|
|
11
12
|
from click import UsageError
|
|
12
|
-
from
|
|
13
|
+
from google.protobuf.json_format import MessageToDict
|
|
14
|
+
from rich.table import Column, Table
|
|
13
15
|
from rich.text import Text
|
|
14
16
|
|
|
15
17
|
from modal._environments import ensure_env
|
|
@@ -620,8 +622,9 @@ async def dashboard(
|
|
|
620
622
|
@click.argument("app_identifier")
|
|
621
623
|
@env_option
|
|
622
624
|
@click.option("--json", is_flag=True, default=False)
|
|
625
|
+
@click.option("--no-color", is_flag=True, default=False, help="Disable colors in the output.")
|
|
623
626
|
@synchronizer.create_blocking
|
|
624
|
-
async def info(app_identifier: str, *, env: str | None = None, json: bool = False):
|
|
627
|
+
async def info(app_identifier: str, *, env: str | None = None, json: bool = False, no_color: bool = False):
|
|
625
628
|
"""Show an App's lifecycle, Functions, and Servers.
|
|
626
629
|
|
|
627
630
|
Examples:
|
|
@@ -650,6 +653,17 @@ async def info(app_identifier: str, *, env: str | None = None, json: bool = Fals
|
|
|
650
653
|
state = APP_STATE_TO_MESSAGE.get(lifecycle.app_state, Text("unknown", style="gray"))
|
|
651
654
|
|
|
652
655
|
if json:
|
|
656
|
+
summaries = {
|
|
657
|
+
function_id: MessageToDict(summary, preserving_proto_field_name=True)
|
|
658
|
+
for function_id, summary in resp.function_info_summaries.items()
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
def entries_with_info(entries: Mapping[str, str]) -> dict[str, dict]:
|
|
662
|
+
return {
|
|
663
|
+
name: {"id": function_id, "summary": summaries.get(function_id, {})}
|
|
664
|
+
for name, function_id in sorted(entries.items())
|
|
665
|
+
}
|
|
666
|
+
|
|
653
667
|
output.print_json(
|
|
654
668
|
dumps(
|
|
655
669
|
{
|
|
@@ -665,28 +679,71 @@ async def info(app_identifier: str, *, env: str | None = None, json: bool = Fals
|
|
|
665
679
|
"stopped_at": timestamp_to_localized_str(lifecycle.stopped_at, json),
|
|
666
680
|
"stopped_by": lifecycle.stopped_by or None,
|
|
667
681
|
},
|
|
668
|
-
"functions":
|
|
669
|
-
"servers":
|
|
682
|
+
"functions": entries_with_info(app_info.functions),
|
|
683
|
+
"servers": entries_with_info(app_info.servers),
|
|
670
684
|
}
|
|
671
685
|
)
|
|
672
686
|
)
|
|
673
687
|
return
|
|
674
688
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
)
|
|
689
|
+
header = Table(show_header=False, box=None, pad_edge=False, padding=(0, 1))
|
|
690
|
+
header.add_row(Text("App:"), Text(app_info.description))
|
|
691
|
+
header.add_row(Text("App ID:"), Text(app_info.app_id))
|
|
692
|
+
header.add_row(Text("State:"), Text(state.plain))
|
|
678
693
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
694
|
+
def event(label: str, timestamp: float, actor: str, version: int = 0) -> None:
|
|
695
|
+
parts = [timestamp_to_localized_str(timestamp, isotz=False), actor]
|
|
696
|
+
if version:
|
|
697
|
+
parts.insert(0, f"v{version}")
|
|
698
|
+
header.add_row(Text(label + ":"), Text(" · ".join(p for p in parts if p)))
|
|
683
699
|
|
|
684
|
-
events: list[list[Text | str | None]] = [
|
|
685
|
-
["Created", timestamp_to_localized_str(lifecycle.created_at, json), lifecycle.created_by]
|
|
686
|
-
]
|
|
687
|
-
if lifecycle.deployed_at:
|
|
688
|
-
label = f"Deployed (v{lifecycle.version})" if lifecycle.version else "Deployed"
|
|
689
|
-
events.append([label, timestamp_to_localized_str(lifecycle.deployed_at, json), lifecycle.deployed_by])
|
|
690
700
|
if lifecycle.stopped_at:
|
|
691
|
-
|
|
692
|
-
|
|
701
|
+
event("Stopped", lifecycle.stopped_at, lifecycle.stopped_by)
|
|
702
|
+
if lifecycle.deployed_at:
|
|
703
|
+
event("Deployment", lifecycle.deployed_at, lifecycle.deployed_by, lifecycle.version)
|
|
704
|
+
event("Created", lifecycle.created_at, lifecycle.created_by)
|
|
705
|
+
output.print(header)
|
|
706
|
+
|
|
707
|
+
name_width = max((len(name) for name in [*app_info.functions, *app_info.servers]), default=0)
|
|
708
|
+
for title, entries in (("Functions", app_info.functions), ("Servers", app_info.servers)):
|
|
709
|
+
if not entries:
|
|
710
|
+
continue
|
|
711
|
+
output.print("")
|
|
712
|
+
output.print(Text(f"{title} ({len(entries)}):"))
|
|
713
|
+
for index, (name, function_id) in enumerate(sorted(entries.items())):
|
|
714
|
+
if index:
|
|
715
|
+
output.print("")
|
|
716
|
+
identity = Table(show_header=False, box=None, pad_edge=False, padding=(0, 1))
|
|
717
|
+
identity.add_column(width=name_width + 2)
|
|
718
|
+
identity.add_column(no_wrap=True)
|
|
719
|
+
identity.add_row(Text(f" {name}"), Text(function_id))
|
|
720
|
+
output.print(identity)
|
|
721
|
+
if function_id not in resp.function_info_summaries:
|
|
722
|
+
continue
|
|
723
|
+
summary = resp.function_info_summaries[function_id]
|
|
724
|
+
hardware = [f"{gpu.count} × {gpu.gpu_type} GPU" for gpu in summary.gpu_config]
|
|
725
|
+
metadata = Text(f" {hardware[0] if hardware else 'CPU'}")
|
|
726
|
+
if len(hardware) > 1:
|
|
727
|
+
metadata.append(f" ({', '.join(hardware[1:])})", style=None if no_color else "bright_black")
|
|
728
|
+
if summary.web_function:
|
|
729
|
+
metadata.append(" · Web Function")
|
|
730
|
+
if schedule := _app_function_schedule(summary.schedule):
|
|
731
|
+
metadata.append(" · ")
|
|
732
|
+
metadata.append(schedule)
|
|
733
|
+
if summary.HasField("requires_proxy_auth") and not summary.requires_proxy_auth:
|
|
734
|
+
metadata.append(" · ")
|
|
735
|
+
metadata.append("Unauthenticated", style=None if no_color else "yellow")
|
|
736
|
+
output.print(metadata)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _app_function_schedule(schedule: api_pb2.Schedule) -> str | None:
|
|
740
|
+
if schedule.HasField("cron"):
|
|
741
|
+
return f"{schedule.cron.cron_string} ({schedule.cron.timezone or 'UTC'})"
|
|
742
|
+
if schedule.HasField("period"):
|
|
743
|
+
parts = [
|
|
744
|
+
f"{value:g} {field.name.removesuffix('s') if value == 1 else field.name}"
|
|
745
|
+
for field, value in schedule.period.ListFields()
|
|
746
|
+
if value
|
|
747
|
+
]
|
|
748
|
+
return "Every " + ", ".join(parts)
|
|
749
|
+
return None
|
|
@@ -11,7 +11,6 @@ from . import run, shell as shell_module
|
|
|
11
11
|
from ._help import ModalCommand, ModalGroup, ModalProfileOption
|
|
12
12
|
from .app import app_cli
|
|
13
13
|
from .billing import billing_cli
|
|
14
|
-
from .bootstrap import bootstrap
|
|
15
14
|
from .changelog import changelog
|
|
16
15
|
from .cluster import cluster_cli
|
|
17
16
|
from .config import config_cli
|
|
@@ -131,8 +130,6 @@ entrypoint_cli.add_command(secret_cli, panel="Storage")
|
|
|
131
130
|
entrypoint_cli.add_command(queue_cli, panel="Storage")
|
|
132
131
|
entrypoint_cli.add_command(volume_cli, panel="Storage")
|
|
133
132
|
|
|
134
|
-
entrypoint_cli.add_command(setup, panel="Onboarding")
|
|
135
|
-
entrypoint_cli.add_command(bootstrap, panel="Onboarding")
|
|
136
133
|
|
|
137
134
|
entrypoint_cli.add_command(workspace_cli, panel="Configuration")
|
|
138
135
|
entrypoint_cli.add_command(environment_cli, panel="Configuration")
|
|
@@ -140,6 +137,7 @@ entrypoint_cli.add_command(profile_cli, panel="Configuration")
|
|
|
140
137
|
entrypoint_cli.add_command(config_cli, panel="Configuration")
|
|
141
138
|
entrypoint_cli.add_command(token_cli, panel="Configuration")
|
|
142
139
|
entrypoint_cli.add_command(skills_cli, panel="Configuration")
|
|
140
|
+
entrypoint_cli.add_command(setup, panel="Configuration")
|
|
143
141
|
|
|
144
142
|
entrypoint_cli.add_command(billing_cli, panel="Observability")
|
|
145
143
|
entrypoint_cli.add_command(changelog, panel="Observability")
|
|
@@ -248,12 +248,21 @@ async def _resolve_function_id(
|
|
|
248
248
|
raise
|
|
249
249
|
|
|
250
250
|
print_server_warnings(response.server_warnings)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
function_id = response.function_id
|
|
252
|
+
function = response.function
|
|
253
|
+
metadata = response.handle_metadata
|
|
254
|
+
elif _is_function_id(function_identifier):
|
|
254
255
|
get_by_id_response = await client.stub.FunctionGetById(
|
|
255
256
|
api_pb2.FunctionGetByIdRequest(function_id=function_identifier)
|
|
256
257
|
)
|
|
257
|
-
|
|
258
|
+
function_id = function_identifier
|
|
259
|
+
function = get_by_id_response.function
|
|
260
|
+
metadata = get_by_id_response.handle_metadata
|
|
261
|
+
else:
|
|
262
|
+
raise UsageError(usage)
|
|
263
|
+
|
|
264
|
+
if function.is_server != (object_type == "Server"):
|
|
265
|
+
actual_type = "Server" if function.is_server else "Function"
|
|
266
|
+
raise UsageError(f"'{function_identifier}' is a {actual_type}. Use `modal {actual_type.lower()} {command}`.")
|
|
258
267
|
|
|
259
|
-
|
|
268
|
+
return function_id, metadata
|
|
@@ -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.dev20",
|
|
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.dev20",
|
|
281
281
|
*,
|
|
282
282
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
283
283
|
):
|
|
@@ -377,6 +377,7 @@ _SETTINGS = {
|
|
|
377
377
|
"image_builder_version": _Setting(),
|
|
378
378
|
"strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
|
|
379
379
|
"sandbox_v2": _Setting(None, transform=_to_boolean),
|
|
380
|
+
"use_control_plane_sidecar_create": _Setting(False, transform=_to_boolean), # For internal/experimental use
|
|
380
381
|
"snapshot_debug": _Setting(False, transform=_to_boolean),
|
|
381
382
|
"cuda_checkpoint_path": _Setting("/__modal/.bin/cuda-checkpoint"), # Used for snapshotting GPU memory.
|
|
382
383
|
# Used to indicate gVisor cuda-checkpoint automation.
|
|
@@ -22,6 +22,7 @@ from ._object import (
|
|
|
22
22
|
from ._resolver import Resolver
|
|
23
23
|
from ._utils.async_utils import TaskContext, aclosing, async_map, sync_or_async_iter, synchronize_api
|
|
24
24
|
from ._utils.blob_utils import LARGE_FILE_LIMIT, blob_iter, blob_upload_file
|
|
25
|
+
from ._utils.deprecation import deprecation_warning
|
|
25
26
|
from ._utils.hash_utils import get_sha256_hex
|
|
26
27
|
from ._utils.name_utils import check_object_name
|
|
27
28
|
from .client import _Client
|
|
@@ -33,6 +34,14 @@ NETWORK_FILE_SYSTEM_PUT_FILE_CLIENT_TIMEOUT = (
|
|
|
33
34
|
) # 10 min max for transferring files (does not include upload time to s3)
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
def _warn_network_file_system_deprecated() -> None:
|
|
38
|
+
deprecation_warning(
|
|
39
|
+
(2026, 9, 16),
|
|
40
|
+
"`modal.NetworkFileSystem` is deprecated and will be removed in a future release. Use `modal.Volume` instead.",
|
|
41
|
+
show_source=False,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
36
45
|
def network_file_system_mount_protos(
|
|
37
46
|
validated_network_file_systems: list[tuple[str, "_NetworkFileSystem"]],
|
|
38
47
|
) -> list[api_pb2.SharedVolumeMount]:
|
|
@@ -119,6 +128,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
119
128
|
```
|
|
120
129
|
"""
|
|
121
130
|
check_object_name(name, "NetworkFileSystem")
|
|
131
|
+
_warn_network_file_system_deprecated()
|
|
122
132
|
|
|
123
133
|
async def _load(
|
|
124
134
|
self: _NetworkFileSystem, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None
|
|
@@ -175,6 +185,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
175
185
|
assert await nfs.listdir("/") == []
|
|
176
186
|
```
|
|
177
187
|
"""
|
|
188
|
+
_warn_network_file_system_deprecated()
|
|
178
189
|
if client is None:
|
|
179
190
|
client = await _Client.from_env()
|
|
180
191
|
request = api_pb2.SharedVolumeGetOrCreateRequest(
|
|
@@ -201,6 +212,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
201
212
|
) -> str:
|
|
202
213
|
"""mdmd:hidden"""
|
|
203
214
|
check_object_name(deployment_name, "NetworkFileSystem")
|
|
215
|
+
_warn_network_file_system_deprecated()
|
|
204
216
|
if client is None:
|
|
205
217
|
client = await _Client.from_env()
|
|
206
218
|
request = api_pb2.SharedVolumeGetOrCreateRequest(
|
|
@@ -9,6 +9,7 @@ import synchronicity.combined_types
|
|
|
9
9
|
import typing
|
|
10
10
|
import typing_extensions
|
|
11
11
|
|
|
12
|
+
def _warn_network_file_system_deprecated() -> None: ...
|
|
12
13
|
def network_file_system_mount_protos(
|
|
13
14
|
validated_network_file_systems: list[tuple[str, _NetworkFileSystem]],
|
|
14
15
|
) -> list[modal_proto.api_pb2.SharedVolumeMount]:
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
"""Immutable configuration for replacing headers in outbound HTTPS traffic."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
from modal_proto import api_pb2
|
|
11
|
+
|
|
12
|
+
from ._utils.async_utils import synchronize_api
|
|
13
|
+
from .exception import InvalidError
|
|
14
|
+
from .secret import _Secret
|
|
15
|
+
|
|
16
|
+
_DOMAIN_RE = re.compile(
|
|
17
|
+
r"^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\Z"
|
|
18
|
+
)
|
|
19
|
+
_HEADER_NAME_RE = re.compile(r"^[A-Za-z0-9!#$%&'*+.^_`|~-]+\Z")
|
|
20
|
+
|
|
21
|
+
_MAX_HEADER_REPLACEMENTS = 25
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _header_value_has_invalid_chars(value: str) -> bool:
|
|
25
|
+
return any((ord(c) < 0x20 and c != "\t") or ord(c) == 0x7F for c in value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _template_references_key(template: str) -> bool:
|
|
29
|
+
"""Whether a header value template references a `$KEY`."""
|
|
30
|
+
i = template.find("$")
|
|
31
|
+
while i != -1 and i + 1 < len(template):
|
|
32
|
+
c = template[i + 1]
|
|
33
|
+
if c == "$":
|
|
34
|
+
i = template.find("$", i + 2)
|
|
35
|
+
continue
|
|
36
|
+
if c.isascii() and (c.isalpha() or c == "_"):
|
|
37
|
+
return True
|
|
38
|
+
i = template.find("$", i + 1)
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class _HeaderReplacement:
|
|
44
|
+
domain: str
|
|
45
|
+
headers: dict[str, str]
|
|
46
|
+
secret: _Secret | None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _OutboundPolicy:
|
|
50
|
+
"""Immutable configuration for replacing headers in outbound HTTPS requests
|
|
51
|
+
from a Sandbox.
|
|
52
|
+
|
|
53
|
+
Header values support templating with keys in a replacement's secret: a
|
|
54
|
+
`$`-prefixed key name in the secret is replaced with the secret value.
|
|
55
|
+
Literal `$` characters are written `$$`.
|
|
56
|
+
|
|
57
|
+
Secret values never enter the Sandbox: they are resolved and injected into
|
|
58
|
+
matching requests outside the container.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
```python
|
|
62
|
+
import modal
|
|
63
|
+
|
|
64
|
+
secret = modal.Secret.from_name("api-token")
|
|
65
|
+
|
|
66
|
+
outbound_policy = (
|
|
67
|
+
modal.OutboundPolicy()
|
|
68
|
+
# Inject a secret-backed Authorization header.
|
|
69
|
+
.with_header_replacement(
|
|
70
|
+
domain="example.com",
|
|
71
|
+
secret=secret,
|
|
72
|
+
headers={"Authorization": "Bearer $API_TOKEN"},
|
|
73
|
+
)
|
|
74
|
+
# Inject a static header into requests to another domain.
|
|
75
|
+
.with_header_replacement(
|
|
76
|
+
domain="modal.com",
|
|
77
|
+
headers={"X-Trace-Token": "trace_abcd"},
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
sb = modal.Sandbox.create(outbound_policy=outbound_policy)
|
|
82
|
+
```
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
_replacements: tuple[_HeaderReplacement, ...]
|
|
86
|
+
|
|
87
|
+
def __init__(self, _replacements: tuple[_HeaderReplacement, ...] = ()):
|
|
88
|
+
self._replacements = _replacements
|
|
89
|
+
|
|
90
|
+
def with_header_replacement(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
domain: str,
|
|
94
|
+
headers: Mapping[str, str],
|
|
95
|
+
secret: _Secret | None = None,
|
|
96
|
+
) -> _OutboundPolicy:
|
|
97
|
+
"""Return a new `OutboundPolicy` with an added header replacement.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
domain: Domain the replacements are scoped to. Supports `*.` wildcard
|
|
101
|
+
prefixes (matching the apex domain and subdomains) and a bare `"*"`.
|
|
102
|
+
headers: Header name -> header value. Values support `$KEY` templates
|
|
103
|
+
referencing keys in the replacement's `secret`.
|
|
104
|
+
secret: Named Secret (e.g. from `Secret.from_name`) whose keys may be
|
|
105
|
+
referenced in the header value templates. Static replacements pass no
|
|
106
|
+
secret.
|
|
107
|
+
"""
|
|
108
|
+
replacement = _HeaderReplacement(domain=domain, headers=dict(headers), secret=secret)
|
|
109
|
+
return _OutboundPolicy(self._replacements + (replacement,))
|
|
110
|
+
|
|
111
|
+
def _validate(self) -> None:
|
|
112
|
+
"""Check all replacements, raising `InvalidError` on the first violation found."""
|
|
113
|
+
total = 0
|
|
114
|
+
for replacement in self._replacements:
|
|
115
|
+
if replacement.domain != "*" and not _DOMAIN_RE.match(replacement.domain):
|
|
116
|
+
raise InvalidError(f"Invalid domain: {replacement.domain!r}")
|
|
117
|
+
if not replacement.headers:
|
|
118
|
+
raise InvalidError("`headers` must contain at least one header")
|
|
119
|
+
for name, value in replacement.headers.items():
|
|
120
|
+
if not _HEADER_NAME_RE.match(name):
|
|
121
|
+
raise InvalidError(f"Invalid header name: {name!r}")
|
|
122
|
+
if _header_value_has_invalid_chars(value):
|
|
123
|
+
raise InvalidError(f"Header value for {name!r} must not contain control characters (except tab)")
|
|
124
|
+
if replacement.secret is None and _template_references_key(value):
|
|
125
|
+
raise InvalidError(f"Header value for {name!r} references a secret key, but no `secret` was passed")
|
|
126
|
+
if replacement.secret is not None and replacement.secret._is_ephemeral:
|
|
127
|
+
raise InvalidError(
|
|
128
|
+
"Outbound policies only support named secrets (e.g. `Secret.from_name`); "
|
|
129
|
+
"ephemeral secrets (e.g. `Secret.from_dict`) are not yet supported"
|
|
130
|
+
)
|
|
131
|
+
total += len(replacement.headers)
|
|
132
|
+
if total > _MAX_HEADER_REPLACEMENTS:
|
|
133
|
+
raise InvalidError(f"Outbound policy cannot have more than {_MAX_HEADER_REPLACEMENTS} header replacements")
|
|
134
|
+
|
|
135
|
+
def _secrets(self) -> list[_Secret]:
|
|
136
|
+
"""Deduplicated list of secrets referenced by the policy's replacements."""
|
|
137
|
+
return list(dict.fromkeys(r.secret for r in self._replacements if r.secret is not None))
|
|
138
|
+
|
|
139
|
+
def _to_proto(self) -> api_pb2.OutboundPolicy:
|
|
140
|
+
"""Convert to the wire format. Referenced secrets must be hydrated first."""
|
|
141
|
+
return api_pb2.OutboundPolicy(
|
|
142
|
+
header_replacements=[
|
|
143
|
+
api_pb2.OutboundPolicy.HeaderReplacement(
|
|
144
|
+
domain=replacement.domain,
|
|
145
|
+
secret_id=(replacement.secret.object_id if replacement.secret is not None else ""),
|
|
146
|
+
headers=replacement.headers,
|
|
147
|
+
)
|
|
148
|
+
for replacement in self._replacements
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _validate_compatible_network_access(
|
|
154
|
+
outbound_policy: _OutboundPolicy | None,
|
|
155
|
+
block_network: bool,
|
|
156
|
+
outbound_domain_allowlist: Sequence[str] | None,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Reject combinations of network configuration and outbound policy that the server does not support."""
|
|
159
|
+
if outbound_policy is None:
|
|
160
|
+
return
|
|
161
|
+
if block_network:
|
|
162
|
+
raise InvalidError("`outbound_policy` cannot be used when `block_network` is enabled")
|
|
163
|
+
if outbound_domain_allowlist:
|
|
164
|
+
raise InvalidError("`outbound_policy` cannot be used with `outbound_domain_allowlist`")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
OutboundPolicy = synchronize_api(_OutboundPolicy)
|