modal 1.5.6.dev24__tar.gz → 1.5.6.dev25__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.dev24 → modal-1.5.6.dev25}/PKG-INFO +1 -1
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/__init__.py +2 -1
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_traceback.py +14 -2
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/grpc_utils.py +43 -1
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/task_command_router_client.py +45 -5
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/token.py +20 -2
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/client.pyi +2 -2
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/secret.py +69 -1
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/secret.pyi +70 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/token_flow.py +4 -2
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/token_flow.pyi +15 -3
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/types.py +1 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/api_grpc.py +16 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/api_pb2.py +409 -389
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/api_pb2.pyi +34 -1
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/api_pb2_grpc.py +33 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/api_pb2_grpc.pyi +10 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_version/__init__.py +1 -1
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/LICENSE +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/README.md +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/__main__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_billing.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_clustered_functions.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_environments.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_function_variants.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_functions.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_image.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_location.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_logs.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_logs_manager.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_object.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_output/manager.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_output/pty.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_output/rich.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_output/status.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_partial_function.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_resources.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_server.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_tunnel.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/_workspace.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/app.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/app.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/billing.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/_logs.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/_stats.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/app.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/cluster.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/config.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/container.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/curl.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/dashboard.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/endpoint.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/entry_point.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/function.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/image.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/import_refs.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/profile.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/queues.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/secret.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/server.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/shell.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/utils.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cli/workspace.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/client.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cls.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/cls.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/config.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/container_process.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/dict.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/dict.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/environments.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/environments.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/exception.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/experimental/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/experimental/flash.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/experimental/flash_server.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/functions.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/functions.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/image.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/image.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/io_streams.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/io_streams.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/mount.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/network_file_system.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/network_file_system.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/object.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/object.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/outbound_policy.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/outbound_policy.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/output.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/parallel_map.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/parallel_map.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/partial_function.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/partial_function.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/proxy.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/py.typed +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/queue.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/retries.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/runner.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/running_app.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/sandbox.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/sandbox.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/sandbox_fs.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/schedule.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/server.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/server.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/serving.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/snapshot.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/volume.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/volume.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/workspace.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal/workspace.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/pyproject.toml +0 -0
- {modal-1.5.6.dev24 → modal-1.5.6.dev25}/setup.cfg +0 -0
|
@@ -9,7 +9,7 @@ if sys.version_info[:2] >= (3, 15):
|
|
|
9
9
|
from modal_version import __version__
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
|
-
from . import billing, types
|
|
12
|
+
from . import billing, experimental, types
|
|
13
13
|
from ._runtime.execution_context import current_function_call_id, current_input_id, interact, is_local
|
|
14
14
|
from ._tunnel import Tunnel, forward
|
|
15
15
|
from .app import App
|
|
@@ -96,6 +96,7 @@ __all__ = [
|
|
|
96
96
|
"enable_output",
|
|
97
97
|
"enter",
|
|
98
98
|
"exit",
|
|
99
|
+
"experimental",
|
|
99
100
|
"fastapi_endpoint",
|
|
100
101
|
"forward",
|
|
101
102
|
"is_local",
|
|
@@ -123,13 +123,25 @@ def print_exception(exc: type[BaseException] | None, value: BaseException | None
|
|
|
123
123
|
print(*notes, sep="\n", file=sys.stderr) # noqa: T201
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
_server_warning_registry: dict[Any, Any] = {}
|
|
127
|
+
SERVER_WARNING_REGISTRY_LIMIT = 2048
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def print_server_warning(message: str):
|
|
127
131
|
"""Issue a warning originating from the server with empty metadata about local origin.
|
|
128
132
|
|
|
129
133
|
When using the Modal CLI, these warnings should get caught and coerced into Rich panels.
|
|
130
134
|
"""
|
|
135
|
+
if len(_server_warning_registry) > SERVER_WARNING_REGISTRY_LIMIT:
|
|
136
|
+
# Start over instead of going quiet: overflow costs a repeat, never a hidden warning.
|
|
137
|
+
_server_warning_registry.clear()
|
|
138
|
+
warnings.warn_explicit(message, ServerWarning, "<modal-server>", 0, registry=_server_warning_registry)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
|
|
142
|
+
"""Issue warnings that the server attached to a response message."""
|
|
131
143
|
for warning in server_warnings:
|
|
132
|
-
|
|
144
|
+
print_server_warning(warning.message)
|
|
133
145
|
|
|
134
146
|
|
|
135
147
|
# for some reason, the traceback cleanup here can't be moved into a context manager :(
|
|
@@ -24,12 +24,13 @@ from grpclib.config import Configuration
|
|
|
24
24
|
from grpclib.encoding.base import CodecBase, StatusDetailsCodecBase
|
|
25
25
|
from grpclib.exceptions import StreamTerminatedError
|
|
26
26
|
from grpclib.protocol import H2Protocol
|
|
27
|
+
from h2.exceptions import ProtocolError as H2ProtocolError
|
|
27
28
|
|
|
28
29
|
from modal.exception import ClientClosed, ConnectionError, NotFoundError
|
|
29
30
|
from modal_proto import api_pb2
|
|
30
31
|
from modal_version import __version__
|
|
31
32
|
|
|
32
|
-
from .._traceback import suppress_tb_frame
|
|
33
|
+
from .._traceback import print_server_warning, suppress_tb_frame
|
|
33
34
|
from ..config import config
|
|
34
35
|
from .async_utils import TaskContext, retry
|
|
35
36
|
from .logger import logger
|
|
@@ -130,6 +131,24 @@ class ModalChannel(grpclib.client.Channel):
|
|
|
130
131
|
return await super().__connect__()
|
|
131
132
|
|
|
132
133
|
async def _create_connection(self) -> H2Protocol:
|
|
134
|
+
target = self._path if self.__use_unix_socket else f"{self.__target_host}:{self.__target_port}"
|
|
135
|
+
tls = self.__ssl_context is not None
|
|
136
|
+
started_at = time.monotonic()
|
|
137
|
+
# Logged before awaiting: a reconnect is invisible until it resolves, which
|
|
138
|
+
# can be long after the RPC waiting on it appears to hang.
|
|
139
|
+
logger.debug(f"Starting connection attempt to {target} (TLS={tls})")
|
|
140
|
+
try:
|
|
141
|
+
protocol = await self._open_connection()
|
|
142
|
+
except (asyncio.CancelledError, Exception) as exc:
|
|
143
|
+
outcome = "cancelled" if isinstance(exc, asyncio.CancelledError) else "failed"
|
|
144
|
+
elapsed = time.monotonic() - started_at
|
|
145
|
+
logger.debug(f"Connection attempt to {target} {outcome} after {elapsed:.3f}s: {exc!r}")
|
|
146
|
+
raise
|
|
147
|
+
elapsed = time.monotonic() - started_at
|
|
148
|
+
logger.debug(f"Connection to {target} established after {elapsed:.3f}s")
|
|
149
|
+
return protocol
|
|
150
|
+
|
|
151
|
+
async def _open_connection(self) -> H2Protocol:
|
|
133
152
|
if self.__use_unix_socket:
|
|
134
153
|
return await super()._create_connection()
|
|
135
154
|
|
|
@@ -308,6 +327,20 @@ def create_channel_config(*, sustained_keepalive: bool = False) -> grpclib.confi
|
|
|
308
327
|
)
|
|
309
328
|
|
|
310
329
|
|
|
330
|
+
# Header the server uses to attach a non-fatal message to any response, one entry per
|
|
331
|
+
# percent-encoded warning. It rides in the trailing metadata, which grpclib also
|
|
332
|
+
# surfaces for trailers-only responses, i.e. failed requests.
|
|
333
|
+
SERVER_WARNING_HEADER = "x-modal-warning"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _issue_server_warnings(metadata: Any) -> None:
|
|
337
|
+
"""Issue the warnings in a response's metadata, which `print_server_warning` deduplicates."""
|
|
338
|
+
for key, encoded_message in metadata.items():
|
|
339
|
+
if key != SERVER_WARNING_HEADER:
|
|
340
|
+
continue
|
|
341
|
+
print_server_warning(urllib.parse.unquote(encoded_message))
|
|
342
|
+
|
|
343
|
+
|
|
311
344
|
def create_channel(
|
|
312
345
|
server_url: str,
|
|
313
346
|
metadata: dict[str, str] = {},
|
|
@@ -356,6 +389,12 @@ def create_channel(
|
|
|
356
389
|
|
|
357
390
|
grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
|
|
358
391
|
|
|
392
|
+
# Surface warnings the server attached to any response on this channel.
|
|
393
|
+
async def recv_trailing_metadata(event: grpclib.events.RecvTrailingMetadata) -> None:
|
|
394
|
+
_issue_server_warnings(event.metadata)
|
|
395
|
+
|
|
396
|
+
grpclib.events.listen(channel, grpclib.events.RecvTrailingMetadata, recv_trailing_metadata)
|
|
397
|
+
|
|
359
398
|
return channel
|
|
360
399
|
|
|
361
400
|
|
|
@@ -496,6 +535,8 @@ def process_exception_before_retry(
|
|
|
496
535
|
raise ConnectionError(str(exc))
|
|
497
536
|
elif isinstance(exc, asyncio.TimeoutError):
|
|
498
537
|
raise ConnectionError(str(exc))
|
|
538
|
+
elif isinstance(exc, H2ProtocolError):
|
|
539
|
+
raise ConnectionError(str(exc))
|
|
499
540
|
else:
|
|
500
541
|
raise exc
|
|
501
542
|
|
|
@@ -583,6 +624,7 @@ async def _retry_transient_errors(
|
|
|
583
624
|
OSError,
|
|
584
625
|
asyncio.TimeoutError,
|
|
585
626
|
AttributeError,
|
|
627
|
+
H2ProtocolError,
|
|
586
628
|
) as exc:
|
|
587
629
|
# Note that we only catch AttributeError to handle a specific case that works around a bug
|
|
588
630
|
# in grpclib<=0.4.7. See above (search for `write_appdata`).
|
|
@@ -89,6 +89,11 @@ def _parse_jwt_expiration(jwt_token: str) -> float | None:
|
|
|
89
89
|
return None
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
# Enough consecutive failures to distinguish a connection that keeps breaking from
|
|
93
|
+
# an isolated blip, without leaving a stuck call silent for long.
|
|
94
|
+
RETRY_WARNING_THRESHOLD = 3
|
|
95
|
+
|
|
96
|
+
|
|
92
97
|
async def call_with_retries_on_transient_errors(
|
|
93
98
|
func,
|
|
94
99
|
*,
|
|
@@ -97,6 +102,9 @@ async def call_with_retries_on_transient_errors(
|
|
|
97
102
|
max_retries: int | None = 10,
|
|
98
103
|
exclude_status_codes: list[Status] | None = None,
|
|
99
104
|
timeout_deadline: float | None = None,
|
|
105
|
+
rpc_name: str = "RPC",
|
|
106
|
+
exec_id: str | None = None,
|
|
107
|
+
warning_message: str | None = None,
|
|
100
108
|
):
|
|
101
109
|
"""Call func() with transient error retries and exponential backoff.
|
|
102
110
|
|
|
@@ -113,10 +121,18 @@ async def call_with_retries_on_transient_errors(
|
|
|
113
121
|
exception (e.g., into a TimeoutError) based on the deadline check.
|
|
114
122
|
The caller is also responsible for propagating the remaining budget
|
|
115
123
|
into `func()` (typically as the per-call gRPC timeout).
|
|
124
|
+
rpc_name: RPC name to include in retry diagnostics.
|
|
125
|
+
exec_id: Execution ID to include in retry diagnostics, for correlating
|
|
126
|
+
the retries of concurrent executions.
|
|
127
|
+
warning_message: Optional warning to emit once after
|
|
128
|
+
RETRY_WARNING_THRESHOLD consecutive non-timeout failures.
|
|
116
129
|
"""
|
|
117
130
|
delay_secs = base_delay_secs
|
|
118
131
|
num_retries = 0
|
|
132
|
+
consecutive_failures = 0
|
|
133
|
+
warned = False
|
|
119
134
|
exclude_status_codes = exclude_status_codes or []
|
|
135
|
+
called_for = f" for exec {exec_id}" if exec_id is not None else ""
|
|
120
136
|
|
|
121
137
|
def is_retryable_status(status: Status) -> bool:
|
|
122
138
|
return status in RETRYABLE_GRPC_STATUS_CODES and status not in exclude_status_codes
|
|
@@ -128,25 +144,41 @@ async def call_with_retries_on_transient_errors(
|
|
|
128
144
|
return False
|
|
129
145
|
return True
|
|
130
146
|
|
|
131
|
-
async def sleep_and_advance(e: Exception):
|
|
132
|
-
nonlocal delay_secs, num_retries
|
|
147
|
+
async def sleep_and_advance(e: Exception, *, count_failure: bool = True):
|
|
148
|
+
nonlocal delay_secs, num_retries, consecutive_failures, warned
|
|
133
149
|
# Clamp the backoff sleep to the remaining deadline so we don't sleep
|
|
134
150
|
# past it just to fail on the next iteration's deadline check.
|
|
135
151
|
sleep_for = delay_secs
|
|
136
152
|
if timeout_deadline is not None:
|
|
137
153
|
sleep_for = min(sleep_for, max(0.0, timeout_deadline - time.monotonic()))
|
|
138
|
-
|
|
154
|
+
elapsed = time.monotonic() - attempt_started_at
|
|
155
|
+
logger.debug(
|
|
156
|
+
f"{rpc_name}{called_for} attempt {num_retries + 1} failed after {elapsed:.3f}s, "
|
|
157
|
+
f"retrying in {sleep_for}s: {e!r}"
|
|
158
|
+
)
|
|
159
|
+
if count_failure:
|
|
160
|
+
consecutive_failures += 1
|
|
161
|
+
if warning_message is not None and consecutive_failures >= RETRY_WARNING_THRESHOLD and not warned:
|
|
162
|
+
logger.warning(
|
|
163
|
+
f"{warning_message} Retrying after {consecutive_failures} consecutive failures. "
|
|
164
|
+
"Set MODAL_LOGLEVEL=DEBUG for details."
|
|
165
|
+
)
|
|
166
|
+
warned = True
|
|
167
|
+
else:
|
|
168
|
+
consecutive_failures = 0
|
|
139
169
|
await asyncio.sleep(sleep_for)
|
|
140
170
|
delay_secs *= delay_factor
|
|
141
171
|
num_retries += 1
|
|
142
172
|
|
|
143
173
|
while True:
|
|
174
|
+
attempt_started_at = time.monotonic()
|
|
144
175
|
try:
|
|
145
176
|
return await func()
|
|
146
177
|
except GRPCError as e:
|
|
147
178
|
if not is_retryable_status(e.status) or not can_retry():
|
|
148
179
|
raise
|
|
149
|
-
|
|
180
|
+
# Long-poll deadlines can expire while the process is still running on a healthy connection.
|
|
181
|
+
await sleep_and_advance(e, count_failure=e.status != Status.DEADLINE_EXCEEDED)
|
|
150
182
|
except AttributeError as e:
|
|
151
183
|
# StreamTerminatedError are not properly raised in grpclib<=0.4.7
|
|
152
184
|
# fixed in https://github.com/vmagamedov/grpclib/issues/185
|
|
@@ -165,7 +197,9 @@ async def call_with_retries_on_transient_errors(
|
|
|
165
197
|
# `timeout_deadline` can further translate this based on
|
|
166
198
|
# whether the deadline has elapsed.
|
|
167
199
|
raise ConnectionError(str(e))
|
|
168
|
-
|
|
200
|
+
# A client-side timeout says nothing about the connection's health, so it
|
|
201
|
+
# doesn't count toward the warning.
|
|
202
|
+
await sleep_and_advance(e, count_failure=not isinstance(e, asyncio.TimeoutError))
|
|
169
203
|
|
|
170
204
|
|
|
171
205
|
_StdioReq = TypeVar("_StdioReq")
|
|
@@ -828,6 +862,12 @@ class TaskCommandRouterClient:
|
|
|
828
862
|
# infinitely retry. For callers without an exec deadline, this
|
|
829
863
|
# could hang indefinitely.
|
|
830
864
|
lambda: self._call_with_auth_retry(self._stub.TaskExecWait, request, timeout=60),
|
|
865
|
+
rpc_name="TaskExecWait",
|
|
866
|
+
exec_id=exec_id,
|
|
867
|
+
warning_message=(
|
|
868
|
+
f"Cannot retrieve the exit status for exec {exec_id} due to connection or service errors. "
|
|
869
|
+
"The command may have already exited."
|
|
870
|
+
),
|
|
831
871
|
base_delay_secs=1, # Retry after 1s since total time is expected to be long.
|
|
832
872
|
delay_factor=1, # Fixed delay.
|
|
833
873
|
max_retries=None, # Retry forever.
|
|
@@ -6,6 +6,7 @@ from datetime import datetime
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
8
|
from modal._utils.async_utils import synchronizer
|
|
9
|
+
from modal._utils.time_utils import parse_duration
|
|
9
10
|
from modal.client import _Client
|
|
10
11
|
from modal.output import OutputManager
|
|
11
12
|
from modal.token_flow import _new_token, _set_token
|
|
@@ -45,11 +46,28 @@ async def set(
|
|
|
45
46
|
@token_cli.command("new")
|
|
46
47
|
@click.option("--activate/--no-activate", default=True, help=_ACTIVATE_HELP)
|
|
47
48
|
@click.option("--verify/--no-verify", default=True, help=_VERIFY_HELP)
|
|
49
|
+
@click.option(
|
|
50
|
+
"--expires-in",
|
|
51
|
+
default=None,
|
|
52
|
+
help="Lifetime of the new token, e.g. 12h, 7d, or 90d. Defaults to the workspace's maximum allowed lifetime.",
|
|
53
|
+
)
|
|
48
54
|
@click.option("--source", default=None, hidden=True)
|
|
49
55
|
@synchronizer.create_blocking
|
|
50
|
-
async def new(activate: bool, verify: bool, source: str | None):
|
|
56
|
+
async def new(activate: bool, verify: bool, expires_in: str | None, source: str | None):
|
|
51
57
|
"""Create a new token by using an authenticated web session."""
|
|
52
|
-
|
|
58
|
+
expires_in_seconds: int | None = None
|
|
59
|
+
if expires_in is not None:
|
|
60
|
+
try:
|
|
61
|
+
expires_in_seconds = int(parse_duration(expires_in).total_seconds())
|
|
62
|
+
except (OverflowError, ValueError):
|
|
63
|
+
raise click.BadParameter(
|
|
64
|
+
"Expected a duration such as 30m, 12h, or 7d.", param_hint="--expires-in"
|
|
65
|
+
) from None
|
|
66
|
+
if expires_in_seconds <= 0:
|
|
67
|
+
raise click.BadParameter("Duration must be positive.", param_hint="--expires-in")
|
|
68
|
+
if expires_in_seconds > 2**32 - 1:
|
|
69
|
+
raise click.BadParameter("Duration is too large.", param_hint="--expires-in")
|
|
70
|
+
await _new_token(activate=activate, verify=verify, source=source, expires_in_seconds=expires_in_seconds)
|
|
53
71
|
|
|
54
72
|
|
|
55
73
|
@token_cli.command("info")
|
|
@@ -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.dev25",
|
|
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.dev25",
|
|
281
281
|
*,
|
|
282
282
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
283
283
|
):
|
|
@@ -242,6 +242,7 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
242
242
|
"""
|
|
243
243
|
|
|
244
244
|
_metadata: api_pb2.SecretMetadata | None = None
|
|
245
|
+
_keys: set[str] | None = None
|
|
245
246
|
_load_env_dict: Callable[[], dict[str, str]] | None = None
|
|
246
247
|
|
|
247
248
|
@classproperty
|
|
@@ -253,16 +254,35 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
253
254
|
def name(self) -> str | None:
|
|
254
255
|
return self._name
|
|
255
256
|
|
|
257
|
+
async def _refresh_metadata(self):
|
|
258
|
+
# Not using @live_method because if we are dehydrated, the act of hydration will refresh
|
|
259
|
+
# metadata anyway, so we don't need to do the SecretGetInfo rpc
|
|
260
|
+
if not self._is_hydrated:
|
|
261
|
+
await self.hydrate()
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
req = api_pb2.SecretGetInfoRequest(secret_id=self.object_id)
|
|
265
|
+
response = await self.client.stub.SecretGetInfo(req)
|
|
266
|
+
self._hydrate(self.object_id, self.client, response.metadata)
|
|
267
|
+
|
|
256
268
|
def _hydrate_metadata(self, metadata: Message | None):
|
|
257
269
|
if metadata:
|
|
258
270
|
assert isinstance(metadata, api_pb2.SecretMetadata)
|
|
259
271
|
self._metadata = metadata
|
|
260
272
|
self._name = metadata.name
|
|
273
|
+
self._keys = set(metadata.keys)
|
|
261
274
|
|
|
262
275
|
def _get_metadata(self) -> api_pb2.SecretMetadata:
|
|
263
276
|
assert self._metadata
|
|
264
277
|
return self._metadata
|
|
265
278
|
|
|
279
|
+
async def _get_keys(self, *, refresh: bool = False) -> set[str]:
|
|
280
|
+
if refresh or self._keys is None:
|
|
281
|
+
await self._refresh_metadata()
|
|
282
|
+
|
|
283
|
+
assert self._keys is not None
|
|
284
|
+
return set(self._keys)
|
|
285
|
+
|
|
266
286
|
@property
|
|
267
287
|
def _is_ephemeral(self) -> bool:
|
|
268
288
|
"""Whether this Secret is backed by a locally-resolvable env dict rather than a named deployment.
|
|
@@ -307,9 +327,12 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
307
327
|
|
|
308
328
|
rep = f"Secret.from_dict([{', '.join(env_dict.keys())}])"
|
|
309
329
|
# TODO: scoping - these should probably not be lazily hydrated without having an app and/or sandbox association
|
|
310
|
-
|
|
330
|
+
obj = _Secret._from_load_env_dict(
|
|
311
331
|
_load_env_dict, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty()
|
|
312
332
|
)
|
|
333
|
+
obj._keys = set(env_dict_filtered.keys())
|
|
334
|
+
|
|
335
|
+
return obj
|
|
313
336
|
|
|
314
337
|
@staticmethod
|
|
315
338
|
def from_local_environ(
|
|
@@ -458,6 +481,45 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
458
481
|
skip_reload=True,
|
|
459
482
|
)
|
|
460
483
|
|
|
484
|
+
@staticmethod
|
|
485
|
+
def _from_id(secret_id: str, *, client: _Client | None = None):
|
|
486
|
+
"""mdmd:hidden
|
|
487
|
+
|
|
488
|
+
Reference a Secret by ID.
|
|
489
|
+
|
|
490
|
+
Hydration is lazy until the Secret is used.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
secret_id: ID of the Secret.
|
|
494
|
+
client: Modal client to use for loading; defaults to `Client.from_env()` when omitted.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
A `Secret` handle (possibly not yet hydrated).
|
|
498
|
+
|
|
499
|
+
Examples:
|
|
500
|
+
```python
|
|
501
|
+
secret = modal.Secret._from_id("st-1234")
|
|
502
|
+
|
|
503
|
+
@app.function(secrets=[secret])
|
|
504
|
+
def run():
|
|
505
|
+
...
|
|
506
|
+
```
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
async def _load(self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
510
|
+
req = api_pb2.SecretGetInfoRequest(secret_id=secret_id)
|
|
511
|
+
response = await load_context.client.stub.SecretGetInfo(req)
|
|
512
|
+
self._hydrate(secret_id, load_context.client, response.metadata)
|
|
513
|
+
|
|
514
|
+
rep = f"modal.Secret.from_id({secret_id!r})"
|
|
515
|
+
return _Secret._from_loader(
|
|
516
|
+
_load,
|
|
517
|
+
rep,
|
|
518
|
+
hydrate_lazily=True,
|
|
519
|
+
load_context_overrides=LoadContext(client=client),
|
|
520
|
+
skip_reload=True,
|
|
521
|
+
)
|
|
522
|
+
|
|
461
523
|
@staticmethod
|
|
462
524
|
async def _create_deployed(
|
|
463
525
|
deployment_name: str,
|
|
@@ -493,6 +555,7 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
493
555
|
name=metadata.name or None,
|
|
494
556
|
created_at=timestamp_to_localized_dt(creation_info.created_at),
|
|
495
557
|
created_by=creation_info.created_by or None,
|
|
558
|
+
environment_name=metadata.environment_name,
|
|
496
559
|
)
|
|
497
560
|
|
|
498
561
|
@live_method
|
|
@@ -511,8 +574,13 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
511
574
|
raise InvalidError(err)
|
|
512
575
|
updates = [api_pb2.SecretUpdateRequest.Update(key=k, value=v) for k, v in env_dict.items()]
|
|
513
576
|
req = api_pb2.SecretUpdateRequest(secret_id=self.object_id, updates=updates)
|
|
577
|
+
|
|
514
578
|
await self.client.stub.SecretUpdate(req)
|
|
515
579
|
|
|
580
|
+
# Since we are hydrated at this point, this will never be None
|
|
581
|
+
assert self._keys is not None
|
|
582
|
+
self._keys = self._keys | set(env_dict.keys())
|
|
583
|
+
|
|
516
584
|
|
|
517
585
|
def _split_env_dict_and_resolvable_secrets(secrets: list[_Secret]) -> tuple[dict[str, str], list[_Secret]]:
|
|
518
586
|
"""Split secrets into secrets that can be resolved locally and secrets that are remote.
|
|
@@ -423,6 +423,7 @@ class _Secret(modal._object._Object):
|
|
|
423
423
|
"""
|
|
424
424
|
|
|
425
425
|
_metadata: typing.Optional[modal_proto.api_pb2.SecretMetadata]
|
|
426
|
+
_keys: typing.Optional[set[str]]
|
|
426
427
|
_load_env_dict: typing.Optional[collections.abc.Callable[[], dict[str, str]]]
|
|
427
428
|
|
|
428
429
|
@synchronicity.classproperty
|
|
@@ -430,8 +431,10 @@ class _Secret(modal._object._Object):
|
|
|
430
431
|
def objects(cls) -> _SecretManager: ...
|
|
431
432
|
@property
|
|
432
433
|
def name(self) -> typing.Optional[str]: ...
|
|
434
|
+
async def _refresh_metadata(self): ...
|
|
433
435
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
434
436
|
def _get_metadata(self) -> modal_proto.api_pb2.SecretMetadata: ...
|
|
437
|
+
async def _get_keys(self, *, refresh: bool = False) -> set[str]: ...
|
|
435
438
|
@property
|
|
436
439
|
def _is_ephemeral(self) -> bool:
|
|
437
440
|
"""Whether this Secret is backed by a locally-resolvable env dict rather than a named deployment.
|
|
@@ -542,6 +545,32 @@ class _Secret(modal._object._Object):
|
|
|
542
545
|
"""
|
|
543
546
|
...
|
|
544
547
|
|
|
548
|
+
@staticmethod
|
|
549
|
+
def _from_id(secret_id: str, *, client: typing.Optional[modal.client._Client] = None):
|
|
550
|
+
"""mdmd:hidden
|
|
551
|
+
|
|
552
|
+
Reference a Secret by ID.
|
|
553
|
+
|
|
554
|
+
Hydration is lazy until the Secret is used.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
secret_id: ID of the Secret.
|
|
558
|
+
client: Modal client to use for loading; defaults to `Client.from_env()` when omitted.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
A `Secret` handle (possibly not yet hydrated).
|
|
562
|
+
|
|
563
|
+
Examples:
|
|
564
|
+
```python
|
|
565
|
+
secret = modal.Secret._from_id("st-1234")
|
|
566
|
+
|
|
567
|
+
@app.function(secrets=[secret])
|
|
568
|
+
def run():
|
|
569
|
+
...
|
|
570
|
+
```
|
|
571
|
+
"""
|
|
572
|
+
...
|
|
573
|
+
|
|
545
574
|
@staticmethod
|
|
546
575
|
async def _create_deployed(
|
|
547
576
|
deployment_name: str,
|
|
@@ -584,6 +613,7 @@ class Secret(modal.object.Object):
|
|
|
584
613
|
"""
|
|
585
614
|
|
|
586
615
|
_metadata: typing.Optional[modal_proto.api_pb2.SecretMetadata]
|
|
616
|
+
_keys: typing.Optional[set[str]]
|
|
587
617
|
_load_env_dict: typing.Optional[collections.abc.Callable[[], dict[str, str]]]
|
|
588
618
|
|
|
589
619
|
def __init__(self, *args, **kwargs):
|
|
@@ -595,8 +625,22 @@ class Secret(modal.object.Object):
|
|
|
595
625
|
def objects(cls) -> SecretManager: ...
|
|
596
626
|
@property
|
|
597
627
|
def name(self) -> typing.Optional[str]: ...
|
|
628
|
+
|
|
629
|
+
class ___refresh_metadata_spec(typing_extensions.Protocol):
|
|
630
|
+
def __call__(self, /): ...
|
|
631
|
+
async def aio(self, /): ...
|
|
632
|
+
|
|
633
|
+
_refresh_metadata: ___refresh_metadata_spec
|
|
634
|
+
|
|
598
635
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
599
636
|
def _get_metadata(self) -> modal_proto.api_pb2.SecretMetadata: ...
|
|
637
|
+
|
|
638
|
+
class ___get_keys_spec(typing_extensions.Protocol):
|
|
639
|
+
def __call__(self, /, *, refresh: bool = False) -> set[str]: ...
|
|
640
|
+
async def aio(self, /, *, refresh: bool = False) -> set[str]: ...
|
|
641
|
+
|
|
642
|
+
_get_keys: ___get_keys_spec
|
|
643
|
+
|
|
600
644
|
@property
|
|
601
645
|
def _is_ephemeral(self) -> bool:
|
|
602
646
|
"""Whether this Secret is backed by a locally-resolvable env dict rather than a named deployment.
|
|
@@ -707,6 +751,32 @@ class Secret(modal.object.Object):
|
|
|
707
751
|
"""
|
|
708
752
|
...
|
|
709
753
|
|
|
754
|
+
@staticmethod
|
|
755
|
+
def _from_id(secret_id: str, *, client: typing.Optional[modal.client.Client] = None):
|
|
756
|
+
"""mdmd:hidden
|
|
757
|
+
|
|
758
|
+
Reference a Secret by ID.
|
|
759
|
+
|
|
760
|
+
Hydration is lazy until the Secret is used.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
secret_id: ID of the Secret.
|
|
764
|
+
client: Modal client to use for loading; defaults to `Client.from_env()` when omitted.
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
A `Secret` handle (possibly not yet hydrated).
|
|
768
|
+
|
|
769
|
+
Examples:
|
|
770
|
+
```python
|
|
771
|
+
secret = modal.Secret._from_id("st-1234")
|
|
772
|
+
|
|
773
|
+
@app.function(secrets=[secret])
|
|
774
|
+
def run():
|
|
775
|
+
...
|
|
776
|
+
```
|
|
777
|
+
"""
|
|
778
|
+
...
|
|
779
|
+
|
|
710
780
|
class ___create_deployed_spec(typing_extensions.Protocol):
|
|
711
781
|
def __call__(
|
|
712
782
|
self,
|
|
@@ -23,7 +23,7 @@ class _TokenFlow:
|
|
|
23
23
|
|
|
24
24
|
@asynccontextmanager
|
|
25
25
|
async def start(
|
|
26
|
-
self, utm_source: str | None = None, next_url: str | None = None
|
|
26
|
+
self, utm_source: str | None = None, next_url: str | None = None, expires_in_seconds: int | None = None
|
|
27
27
|
) -> AsyncGenerator[tuple[str, str, str], None]:
|
|
28
28
|
"""mdmd:hidden"""
|
|
29
29
|
# Run a temporary http server returning the token id on /
|
|
@@ -41,6 +41,7 @@ class _TokenFlow:
|
|
|
41
41
|
utm_source=utm_source or "",
|
|
42
42
|
next_url=next_url or "",
|
|
43
43
|
localhost_port=int(url.split(":")[-1]),
|
|
44
|
+
expires_in_seconds=expires_in_seconds or 0,
|
|
44
45
|
)
|
|
45
46
|
resp = await self.stub.TokenFlowCreate(req)
|
|
46
47
|
self.token_flow_id = resp.token_flow_id
|
|
@@ -72,6 +73,7 @@ async def _new_token(
|
|
|
72
73
|
verify: bool = True,
|
|
73
74
|
source: str | None = None,
|
|
74
75
|
next_url: str | None = None,
|
|
76
|
+
expires_in_seconds: int | None = None,
|
|
75
77
|
):
|
|
76
78
|
server_url = config.get("server_url", profile=profile)
|
|
77
79
|
|
|
@@ -80,7 +82,7 @@ async def _new_token(
|
|
|
80
82
|
async with _Client.anonymous(server_url) as client:
|
|
81
83
|
token_flow = _TokenFlow(client)
|
|
82
84
|
|
|
83
|
-
async with token_flow.start(source, next_url) as (_, web_url, code):
|
|
85
|
+
async with token_flow.start(source, next_url, expires_in_seconds) as (_, web_url, code):
|
|
84
86
|
with output.status("Waiting for authentication in the web browser"):
|
|
85
87
|
# Open the web url in the browser
|
|
86
88
|
if open_url(web_url):
|
|
@@ -10,7 +10,10 @@ class _TokenFlow:
|
|
|
10
10
|
...
|
|
11
11
|
|
|
12
12
|
def start(
|
|
13
|
-
self,
|
|
13
|
+
self,
|
|
14
|
+
utm_source: typing.Optional[str] = None,
|
|
15
|
+
next_url: typing.Optional[str] = None,
|
|
16
|
+
expires_in_seconds: typing.Optional[int] = None,
|
|
14
17
|
) -> typing.AsyncContextManager[tuple[str, str, str]]:
|
|
15
18
|
"""mdmd:hidden"""
|
|
16
19
|
...
|
|
@@ -26,13 +29,21 @@ class TokenFlow:
|
|
|
26
29
|
|
|
27
30
|
class __start_spec(typing_extensions.Protocol):
|
|
28
31
|
def __call__(
|
|
29
|
-
self,
|
|
32
|
+
self,
|
|
33
|
+
/,
|
|
34
|
+
utm_source: typing.Optional[str] = None,
|
|
35
|
+
next_url: typing.Optional[str] = None,
|
|
36
|
+
expires_in_seconds: typing.Optional[int] = None,
|
|
30
37
|
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[tuple[str, str, str]]:
|
|
31
38
|
"""mdmd:hidden"""
|
|
32
39
|
...
|
|
33
40
|
|
|
34
41
|
def aio(
|
|
35
|
-
self,
|
|
42
|
+
self,
|
|
43
|
+
/,
|
|
44
|
+
utm_source: typing.Optional[str] = None,
|
|
45
|
+
next_url: typing.Optional[str] = None,
|
|
46
|
+
expires_in_seconds: typing.Optional[int] = None,
|
|
36
47
|
) -> typing.AsyncContextManager[tuple[str, str, str]]:
|
|
37
48
|
"""mdmd:hidden"""
|
|
38
49
|
...
|
|
@@ -61,6 +72,7 @@ async def _new_token(
|
|
|
61
72
|
verify: bool = True,
|
|
62
73
|
source: typing.Optional[str] = None,
|
|
63
74
|
next_url: typing.Optional[str] = None,
|
|
75
|
+
expires_in_seconds: typing.Optional[int] = None,
|
|
64
76
|
): ...
|
|
65
77
|
async def _set_token(
|
|
66
78
|
token_id: str,
|