modal 1.5.6.dev25__tar.gz → 1.5.6.dev27__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/PKG-INFO +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/__init__.py +6 -2
- modal-1.5.6.dev27/modal/_cluster.py +192 -0
- modal-1.5.6.dev27/modal/_cluster.pyi +108 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_container_entrypoint.py +3 -11
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_environments.py +72 -15
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_function_variants.py +75 -23
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_functions.py +120 -97
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_image.py +10 -7
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_logs.py +5 -5
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_logs_manager.py +2 -2
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_object.py +0 -31
- modal-1.5.6.dev25/modal/outbound_policy.py → modal-1.5.6.dev27/modal/_outbound_policy.py +10 -4
- modal-1.5.6.dev25/modal/outbound_policy.pyi → modal-1.5.6.dev27/modal/_outbound_policy.pyi +10 -4
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/pty.py +2 -2
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_partial_function.py +116 -26
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/container_io_manager.py +15 -15
- modal-1.5.6.dev27/modal/_runtime/gpu_memory_snapshot.py +56 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/task_lifecycle_manager.py +20 -32
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/task_lifecycle_manager.pyi +0 -5
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_server.py +189 -2
- modal-1.5.6.dev27/modal/_server.pyi +411 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_tunnel.py +2 -2
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/function_utils.py +3 -3
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/grpc_utils.py +10 -6
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/task_command_router_client.py +16 -6
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_workspace.py +34 -14
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/app.py +55 -59
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/app.pyi +29 -56
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/app.py +18 -14
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/container.py +5 -5
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/curl.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/dashboard.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/endpoint.py +5 -5
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/entry_point.py +0 -3
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/function.py +9 -7
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/image.py +2 -2
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/network_file_system.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/queues.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/secret.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/server.py +5 -5
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/shell.py +27 -10
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/token.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/utils.py +4 -2
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/workspace.py +24 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/client.py +44 -20
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/client.pyi +61 -29
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cloud_bucket_mount.py +16 -9
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cloud_bucket_mount.pyi +32 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cls.py +14 -34
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cls.pyi +0 -20
- modal-1.5.6.dev27/modal/cluster.py +7 -0
- modal-1.5.6.dev27/modal/cluster.pyi +108 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/config.py +2 -3
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/dict.py +17 -17
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/environments.py +2 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/environments.pyi +53 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/exception.py +4 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/__init__.py +39 -14
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/_flash_container_entry.py +2 -2
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/flash.py +5 -5
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/flash_server.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/functions.pyi +24 -98
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/io_streams.py +166 -75
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/io_streams.pyi +65 -40
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/mount.py +5 -5
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/network_file_system.py +11 -11
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/object.pyi +0 -15
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/parallel_map.py +17 -17
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/partial_function.py +4 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/partial_function.pyi +91 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/proxy.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/queue.py +14 -14
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/runner.py +12 -12
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/sandbox.py +116 -94
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/sandbox.pyi +69 -35
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/secret.py +9 -9
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/server.pyi +77 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/snapshot.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/token_flow.py +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/types.py +259 -91
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/volume.py +24 -24
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/workspace.pyi +37 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/SOURCES.txt +7 -5
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/mdmd/mdmd.py +24 -3
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_grpc.py +16 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_pb2.py +1423 -1413
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_pb2.pyi +28 -3
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_pb2_grpc.py +33 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/api_pb2_grpc.pyi +10 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_version/__init__.py +1 -1
- modal-1.5.6.dev25/modal/_clustered_functions.py +0 -97
- modal-1.5.6.dev25/modal/_clustered_functions.pyi +0 -38
- modal-1.5.6.dev25/modal/_runtime/gpu_memory_snapshot.py +0 -281
- modal-1.5.6.dev25/modal/cli/cluster.py +0 -117
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/LICENSE +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/README.md +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/__main__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_billing.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_location.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/manager.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/rich.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_output/status.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_resources.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_traceback.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/billing.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_logs.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_stats.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/config.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/import_refs.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/profile.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/container_process.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/dict.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/functions.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/image.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/image.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/network_file_system.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/object.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/output.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/parallel_map.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/py.typed +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/retries.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/running_app.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/sandbox_fs.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/schedule.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/secret.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/server.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/serving.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/token_flow.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/volume.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal/workspace.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/pyproject.toml +0 -0
- {modal-1.5.6.dev25 → modal-1.5.6.dev27}/setup.cfg +0 -0
|
@@ -16,6 +16,7 @@ try:
|
|
|
16
16
|
from .client import Client
|
|
17
17
|
from .cloud_bucket_mount import CloudBucketMount
|
|
18
18
|
from .cls import Cls, parameter
|
|
19
|
+
from .cluster import Cluster
|
|
19
20
|
from .dict import Dict
|
|
20
21
|
from .environments import Environment
|
|
21
22
|
from .exception import Error
|
|
@@ -23,16 +24,17 @@ try:
|
|
|
23
24
|
from .functions import Function, FunctionCall
|
|
24
25
|
from .image import Image
|
|
25
26
|
from .network_file_system import NetworkFileSystem
|
|
26
|
-
from .outbound_policy import OutboundPolicy
|
|
27
27
|
from .output import enable_output
|
|
28
28
|
from .partial_function import (
|
|
29
29
|
asgi_app,
|
|
30
30
|
batched,
|
|
31
|
+
clustered,
|
|
31
32
|
concurrent,
|
|
32
33
|
enter,
|
|
33
34
|
exit,
|
|
34
35
|
fastapi_endpoint,
|
|
35
36
|
method,
|
|
37
|
+
sessioned,
|
|
36
38
|
web_endpoint,
|
|
37
39
|
web_server,
|
|
38
40
|
wsgi_app,
|
|
@@ -62,6 +64,7 @@ __all__ = [
|
|
|
62
64
|
"__version__",
|
|
63
65
|
"App",
|
|
64
66
|
"Client",
|
|
67
|
+
"Cluster",
|
|
65
68
|
"Cls",
|
|
66
69
|
"Cron",
|
|
67
70
|
"Dict",
|
|
@@ -72,7 +75,6 @@ __all__ = [
|
|
|
72
75
|
"FunctionCall",
|
|
73
76
|
"Image",
|
|
74
77
|
"NetworkFileSystem",
|
|
75
|
-
"OutboundPolicy",
|
|
76
78
|
"Period",
|
|
77
79
|
"Probe",
|
|
78
80
|
"Proxy",
|
|
@@ -90,6 +92,7 @@ __all__ = [
|
|
|
90
92
|
"asgi_app",
|
|
91
93
|
"batched",
|
|
92
94
|
"billing",
|
|
95
|
+
"clustered",
|
|
93
96
|
"concurrent",
|
|
94
97
|
"current_function_call_id",
|
|
95
98
|
"current_input_id",
|
|
@@ -103,6 +106,7 @@ __all__ = [
|
|
|
103
106
|
"interact",
|
|
104
107
|
"method",
|
|
105
108
|
"parameter",
|
|
109
|
+
"sessioned",
|
|
106
110
|
"types",
|
|
107
111
|
"web_endpoint",
|
|
108
112
|
"web_server",
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal, cast
|
|
6
|
+
|
|
7
|
+
from google.protobuf.message import Message
|
|
8
|
+
|
|
9
|
+
from modal_proto import api_pb2
|
|
10
|
+
|
|
11
|
+
from ._load_context import LoadContext
|
|
12
|
+
from ._object import _Object, live_method
|
|
13
|
+
from ._resolver import Resolver
|
|
14
|
+
from ._utils.async_utils import synchronize_api
|
|
15
|
+
from .client import _Client
|
|
16
|
+
from .exception import InvalidError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ClusterContext:
|
|
21
|
+
"""Cluster metadata and the executing container's rank, initialized at startup."""
|
|
22
|
+
|
|
23
|
+
rank: int
|
|
24
|
+
cluster_id: str
|
|
25
|
+
container_ips: list[str]
|
|
26
|
+
container_ipv4_ips: list[str]
|
|
27
|
+
fabric_ids: list[str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
current_cluster_context: ClusterContext | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_current_cluster_context() -> ClusterContext:
|
|
34
|
+
"""Return the executing container's initialized cluster context without an RPC."""
|
|
35
|
+
if current_cluster_context is None:
|
|
36
|
+
raise InvalidError(
|
|
37
|
+
"Cluster context is not initialized. This operation requires execution "
|
|
38
|
+
"inside a clustered Function or Server."
|
|
39
|
+
)
|
|
40
|
+
return current_cluster_context
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _Cluster(_Object, type_prefix="cu"):
|
|
44
|
+
"""A group of containers scheduled together for a clustered Function or Server.
|
|
45
|
+
|
|
46
|
+
Use `Cluster.from_context()` inside a cluster, or `Cluster.from_id()` to
|
|
47
|
+
inspect a cluster remotely. Containers are ordered by cluster rank.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_metadata: api_pb2.ClusterStats | None = None
|
|
51
|
+
|
|
52
|
+
def _hydrate_metadata(self, metadata: Message | None):
|
|
53
|
+
if metadata is not None:
|
|
54
|
+
assert isinstance(metadata, api_pb2.ClusterStats)
|
|
55
|
+
self._metadata = metadata
|
|
56
|
+
|
|
57
|
+
def _get_metadata(self) -> api_pb2.ClusterStats:
|
|
58
|
+
assert self._metadata is not None
|
|
59
|
+
return self._metadata
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def from_context() -> "_Cluster":
|
|
63
|
+
"""Reference a Cluster from within one of its containers.
|
|
64
|
+
|
|
65
|
+
Raises `InvalidError` outside an initialized clustered execution.
|
|
66
|
+
"""
|
|
67
|
+
context = get_current_cluster_context()
|
|
68
|
+
if not context.cluster_id:
|
|
69
|
+
raise InvalidError("The executing container does not have an initialized cluster ID.")
|
|
70
|
+
return _Cluster.from_id(context.cluster_id)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def from_id(cluster_id: str, *, client: _Client | None = None) -> "_Cluster":
|
|
74
|
+
"""Reference a Cluster by its ID.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
cluster_id: ID of the cluster.
|
|
78
|
+
client: Modal client to use; defaults to `Client.from_env()` when omitted.
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
```python notest
|
|
82
|
+
cluster = modal.Cluster.from_id("cu-123")
|
|
83
|
+
```
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
async def _load(self: _Cluster, resolver: Resolver, context: LoadContext, existing_object_id: str | None):
|
|
87
|
+
response = await context.client._stub.ClusterGet(api_pb2.ClusterGetRequest(cluster_id=cluster_id))
|
|
88
|
+
self._hydrate(cluster_id, context.client, response.cluster)
|
|
89
|
+
|
|
90
|
+
obj = _Cluster._from_loader(
|
|
91
|
+
_load,
|
|
92
|
+
f"modal.Cluster.from_id({cluster_id!r})",
|
|
93
|
+
hydrate_lazily=True,
|
|
94
|
+
load_context_overrides=LoadContext(client=client),
|
|
95
|
+
)
|
|
96
|
+
obj._object_id = cluster_id
|
|
97
|
+
return obj
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def object_id(self) -> str:
|
|
101
|
+
"""The cluster's unique `cu-` object ID."""
|
|
102
|
+
return super().object_id
|
|
103
|
+
|
|
104
|
+
@live_method
|
|
105
|
+
async def container_ids(self) -> list[str]:
|
|
106
|
+
"""Return container IDs ordered by cluster rank."""
|
|
107
|
+
return list(self._get_metadata().task_ids)
|
|
108
|
+
|
|
109
|
+
async def container_ips(self, family: Literal["ipv4", "ipv6"] = "ipv6") -> list[str]:
|
|
110
|
+
"""Return container IP addresses ordered by cluster rank.
|
|
111
|
+
|
|
112
|
+
Returns IPv6 addresses by default; pass `family="ipv4"` for IPv4.
|
|
113
|
+
These addresses are for intra-cluster communication.
|
|
114
|
+
|
|
115
|
+
Must be called from a container in this cluster; otherwise raises `InvalidError`.
|
|
116
|
+
"""
|
|
117
|
+
if family not in ("ipv4", "ipv6"):
|
|
118
|
+
raise InvalidError("family must be 'ipv4' or 'ipv6'.")
|
|
119
|
+
context = get_current_cluster_context()
|
|
120
|
+
if context.cluster_id != self.object_id:
|
|
121
|
+
raise InvalidError("container_ips() must be called from a container in the referenced cluster.")
|
|
122
|
+
return list(context.container_ipv4_ips if family == "ipv4" else context.container_ips)
|
|
123
|
+
|
|
124
|
+
async def container_rank(self, container_id: str | None = None) -> int:
|
|
125
|
+
"""Return a container's rank within this cluster.
|
|
126
|
+
|
|
127
|
+
With no argument, return the executing container's rank without a network
|
|
128
|
+
request. Raises `InvalidError` if it is not a member of this cluster.
|
|
129
|
+
|
|
130
|
+
With an explicit container ID, look up its rank in the cluster's membership.
|
|
131
|
+
Raises `InvalidError` for nonmembers.
|
|
132
|
+
"""
|
|
133
|
+
if container_id is None:
|
|
134
|
+
context = get_current_cluster_context()
|
|
135
|
+
if context.cluster_id != self.object_id:
|
|
136
|
+
raise InvalidError("The executing container is not a member of the referenced cluster.")
|
|
137
|
+
return context.rank
|
|
138
|
+
|
|
139
|
+
container_ids = await self.container_ids()
|
|
140
|
+
try:
|
|
141
|
+
return container_ids.index(container_id)
|
|
142
|
+
except ValueError:
|
|
143
|
+
raise InvalidError(f"Container {container_id!r} is not a member of cluster {self.object_id!r}.") from None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def _initialize_clustered_function(client: _Client, task_id: str):
|
|
147
|
+
global current_cluster_context
|
|
148
|
+
|
|
149
|
+
def get_i6pn() -> str:
|
|
150
|
+
"""Returns the ipv6 address assigned to this container."""
|
|
151
|
+
# An AF_INET6 result always carries a (host, port, flowinfo, scope_id) sockaddr,
|
|
152
|
+
# but getaddrinfo is typed for every address family, some of which lead with an int
|
|
153
|
+
return cast(str, socket.getaddrinfo("i6pn.modal.local", None, socket.AF_INET6)[0][4][0])
|
|
154
|
+
|
|
155
|
+
hostname = socket.gethostname()
|
|
156
|
+
container_ip = get_i6pn()
|
|
157
|
+
|
|
158
|
+
# nccl's default host ID is $(hostname)$(cat /proc/sys/kernel/random/boot_id).
|
|
159
|
+
# on runc, if two i6pn-linked containers get scheduled on the same worker,
|
|
160
|
+
# their boot ID and hostname will both be identical, causing nccl to break.
|
|
161
|
+
# As a workaround, we can explicitly specify a unique host ID here.
|
|
162
|
+
# See MOD-4067.
|
|
163
|
+
os.environ["NCCL_HOSTID"] = f"{hostname}{container_ip}"
|
|
164
|
+
|
|
165
|
+
# We found these settings to work well in most cases. You may be able to achieve
|
|
166
|
+
# better performance by tuning these settings.
|
|
167
|
+
if os.environ["MODAL_CLOUD_PROVIDER"] in ("CLOUD_PROVIDER_GCP", "CLOUD_PROVIDER_OCI"):
|
|
168
|
+
os.environ["NCCL_SOCKET_NTHREADS"] = "4"
|
|
169
|
+
os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
|
|
170
|
+
elif os.environ["MODAL_CLOUD_PROVIDER"] == "CLOUD_PROVIDER_AWS":
|
|
171
|
+
os.environ["NCCL_SOCKET_NTHREADS"] = "2"
|
|
172
|
+
os.environ["NCCL_NSOCKS_PERTHREAD"] = "8"
|
|
173
|
+
else:
|
|
174
|
+
os.environ["NCCL_SOCKET_NTHREADS"] = "1"
|
|
175
|
+
os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
|
|
176
|
+
|
|
177
|
+
resp = await client._stub.TaskClusterHello(
|
|
178
|
+
api_pb2.TaskClusterHelloRequest(
|
|
179
|
+
task_id=task_id,
|
|
180
|
+
container_ip=container_ip,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
current_cluster_context = ClusterContext(
|
|
184
|
+
rank=resp.cluster_rank,
|
|
185
|
+
cluster_id=resp.cluster_id,
|
|
186
|
+
container_ips=list(resp.container_ips),
|
|
187
|
+
container_ipv4_ips=list(resp.container_ipv4_ips),
|
|
188
|
+
fabric_ids=list(resp.fabric_ids),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
initialize_clustered_function = synchronize_api(_initialize_clustered_function)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import google.protobuf.message
|
|
2
|
+
import modal._object
|
|
3
|
+
import modal.client
|
|
4
|
+
import modal_proto.api_pb2
|
|
5
|
+
import types
|
|
6
|
+
import typing
|
|
7
|
+
import typing_extensions
|
|
8
|
+
|
|
9
|
+
class ClusterContext:
|
|
10
|
+
"""Cluster metadata and the executing container's rank, initialized at startup."""
|
|
11
|
+
|
|
12
|
+
rank: int
|
|
13
|
+
cluster_id: str
|
|
14
|
+
container_ips: list[str]
|
|
15
|
+
container_ipv4_ips: list[str]
|
|
16
|
+
fabric_ids: list[str]
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str], fabric_ids: list[str]
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
"""Return repr(self)."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def __eq__(self, other):
|
|
29
|
+
"""Return self==value."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def get_current_cluster_context() -> ClusterContext:
|
|
33
|
+
"""Return the executing container's initialized cluster context without an RPC."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
class _Cluster(modal._object._Object):
|
|
37
|
+
"""A group of containers scheduled together for a clustered Function or Server.
|
|
38
|
+
|
|
39
|
+
Use `Cluster.from_context()` inside a cluster, or `Cluster.from_id()` to
|
|
40
|
+
inspect a cluster remotely. Containers are ordered by cluster rank.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_metadata: typing.Optional[modal_proto.api_pb2.ClusterStats]
|
|
44
|
+
|
|
45
|
+
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
46
|
+
def _get_metadata(self) -> modal_proto.api_pb2.ClusterStats: ...
|
|
47
|
+
@staticmethod
|
|
48
|
+
def from_context() -> _Cluster:
|
|
49
|
+
"""Reference a Cluster from within one of its containers.
|
|
50
|
+
|
|
51
|
+
Raises `InvalidError` outside an initialized clustered execution.
|
|
52
|
+
"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def from_id(cluster_id: str, *, client: typing.Optional[modal.client._Client] = None) -> _Cluster:
|
|
57
|
+
"""Reference a Cluster by its ID.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
cluster_id: ID of the cluster.
|
|
61
|
+
client: Modal client to use; defaults to `Client.from_env()` when omitted.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
```python notest
|
|
65
|
+
cluster = modal.Cluster.from_id("cu-123")
|
|
66
|
+
```
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def object_id(self) -> str:
|
|
72
|
+
"""The cluster's unique `cu-` object ID."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
async def container_ids(self) -> list[str]:
|
|
76
|
+
"""Return container IDs ordered by cluster rank."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
async def container_ips(self, family: typing.Literal["ipv4", "ipv6"] = "ipv6") -> list[str]:
|
|
80
|
+
"""Return container IP addresses ordered by cluster rank.
|
|
81
|
+
|
|
82
|
+
Returns IPv6 addresses by default; pass `family="ipv4"` for IPv4.
|
|
83
|
+
These addresses are for intra-cluster communication.
|
|
84
|
+
|
|
85
|
+
Must be called from a container in this cluster; otherwise raises `InvalidError`.
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
async def container_rank(self, container_id: typing.Optional[str] = None) -> int:
|
|
90
|
+
"""Return a container's rank within this cluster.
|
|
91
|
+
|
|
92
|
+
With no argument, return the executing container's rank without a network
|
|
93
|
+
request. Raises `InvalidError` if it is not a member of this cluster.
|
|
94
|
+
|
|
95
|
+
With an explicit container ID, look up its rank in the cluster's membership.
|
|
96
|
+
Raises `InvalidError` for nonmembers.
|
|
97
|
+
"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
async def _initialize_clustered_function(client: modal.client._Client, task_id: str): ...
|
|
101
|
+
|
|
102
|
+
class __initialize_clustered_function_spec(typing_extensions.Protocol):
|
|
103
|
+
def __call__(self, /, client: modal.client.Client, task_id: str): ...
|
|
104
|
+
async def aio(self, /, client: modal.client.Client, task_id: str): ...
|
|
105
|
+
|
|
106
|
+
initialize_clustered_function: __initialize_clustered_function_spec
|
|
107
|
+
|
|
108
|
+
current_cluster_context: ClusterContext | None
|
|
@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
24
24
|
|
|
25
25
|
from google.protobuf.message import Message
|
|
26
26
|
|
|
27
|
-
from modal.
|
|
27
|
+
from modal._cluster import initialize_clustered_function
|
|
28
28
|
from modal._runtime.user_code_event_loop import UserCodeEventLoop
|
|
29
29
|
from modal._serialization import deserialize, deserialize_params
|
|
30
30
|
from modal._utils.async_utils import TaskContext, aclosing, synchronizer
|
|
@@ -407,11 +407,7 @@ def run_server(container_args: api_pb2.ContainerArguments, client: Client):
|
|
|
407
407
|
service: Service = hydrate_function(container_args, task_lifecycle_manager, function_def, _client)
|
|
408
408
|
# Initialize clustered functions.
|
|
409
409
|
if function_def._experimental_group_size > 0:
|
|
410
|
-
initialize_clustered_function(
|
|
411
|
-
client,
|
|
412
|
-
container_args.task_id,
|
|
413
|
-
function_def._experimental_group_size,
|
|
414
|
-
)
|
|
410
|
+
initialize_clustered_function(client, container_args.task_id)
|
|
415
411
|
|
|
416
412
|
with UserCodeEventLoop() as event_loop:
|
|
417
413
|
with service.lifecycle_context(
|
|
@@ -452,11 +448,7 @@ def run_function(container_args: api_pb2.ContainerArguments, client: Client):
|
|
|
452
448
|
|
|
453
449
|
# Initialize clustered functions.
|
|
454
450
|
if function_def._experimental_group_size > 0:
|
|
455
|
-
initialize_clustered_function(
|
|
456
|
-
client,
|
|
457
|
-
container_args.task_id,
|
|
458
|
-
function_def._experimental_group_size,
|
|
459
|
-
)
|
|
451
|
+
initialize_clustered_function(client, container_args.task_id)
|
|
460
452
|
|
|
461
453
|
with service.function_execution_context(
|
|
462
454
|
event_loop=event_loop,
|
|
@@ -4,7 +4,7 @@ import builtins
|
|
|
4
4
|
from collections.abc import Iterable, Mapping
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
|
-
from typing import Any, Literal
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
8
|
|
|
9
9
|
from google.protobuf.empty_pb2 import Empty
|
|
10
10
|
from google.protobuf.message import Message
|
|
@@ -24,6 +24,9 @@ from .config import config, logger
|
|
|
24
24
|
from .exception import InvalidError, WorkspaceManagementError
|
|
25
25
|
from .types import BillingReportItem, EnvironmentBillingSummary
|
|
26
26
|
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
import modal
|
|
29
|
+
|
|
27
30
|
|
|
28
31
|
@dataclass(frozen=True)
|
|
29
32
|
class EnvironmentSettings:
|
|
@@ -59,7 +62,7 @@ class _EnvironmentManager:
|
|
|
59
62
|
request.default_member_role_str = default_role
|
|
60
63
|
if (experimental_options or {}).get("is_public", False):
|
|
61
64
|
request.environment_type = api_pb2.ENVIRONMENT_TYPE_PUBLIC
|
|
62
|
-
await client.
|
|
65
|
+
await client._stub.EnvironmentCreate(request)
|
|
63
66
|
|
|
64
67
|
async def list(
|
|
65
68
|
self,
|
|
@@ -76,7 +79,7 @@ class _EnvironmentManager:
|
|
|
76
79
|
```
|
|
77
80
|
"""
|
|
78
81
|
client = await _Client.from_env() if client is None else client
|
|
79
|
-
resp = await client.
|
|
82
|
+
resp = await client._stub.EnvironmentList(Empty())
|
|
80
83
|
environments = []
|
|
81
84
|
for item in resp.items:
|
|
82
85
|
metadata = api_pb2.EnvironmentMetadata(
|
|
@@ -110,7 +113,7 @@ class _EnvironmentManager:
|
|
|
110
113
|
```
|
|
111
114
|
"""
|
|
112
115
|
client = await _Client.from_env() if client is None else client
|
|
113
|
-
await client.
|
|
116
|
+
await client._stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
|
|
114
117
|
|
|
115
118
|
|
|
116
119
|
class _EnvironmentRolesManager:
|
|
@@ -139,7 +142,7 @@ class _EnvironmentRolesManager:
|
|
|
139
142
|
"""
|
|
140
143
|
await self._environment.hydrate()
|
|
141
144
|
req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
|
|
142
|
-
resp = await self._environment.client.
|
|
145
|
+
resp = await self._environment.client._stub.EnvironmentGetRoles(req)
|
|
143
146
|
|
|
144
147
|
users: dict[str, str] = {}
|
|
145
148
|
service_users: dict[str, str] = {}
|
|
@@ -180,7 +183,7 @@ class _EnvironmentRolesManager:
|
|
|
180
183
|
service_users = service_users or {}
|
|
181
184
|
|
|
182
185
|
req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
|
|
183
|
-
resp = await self._environment.client.
|
|
186
|
+
resp = await self._environment.client._stub.EnvironmentGetRoles(req)
|
|
184
187
|
|
|
185
188
|
# EnvironmentGetRoles returns every workspace principal
|
|
186
189
|
user_name_to_id: dict[str, str] = {}
|
|
@@ -214,7 +217,7 @@ class _EnvironmentRolesManager:
|
|
|
214
217
|
async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
|
|
215
218
|
"""Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
|
|
216
219
|
results = await asyncio.gather(
|
|
217
|
-
*(self._environment.client.
|
|
220
|
+
*(self._environment.client._stub.EnvironmentRoleSet(req) for req in requests.values()),
|
|
218
221
|
return_exceptions=True,
|
|
219
222
|
)
|
|
220
223
|
errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
|
|
@@ -269,7 +272,7 @@ class _EnvironmentMembersManager:
|
|
|
269
272
|
service_users = service_users or []
|
|
270
273
|
|
|
271
274
|
req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
|
|
272
|
-
resp = await self._environment.client.
|
|
275
|
+
resp = await self._environment.client._stub.EnvironmentGetRoles(req)
|
|
273
276
|
|
|
274
277
|
user_name_to_id: dict[str, str] = {}
|
|
275
278
|
service_user_name_to_id: dict[str, str] = {}
|
|
@@ -300,6 +303,55 @@ class _EnvironmentMembersManager:
|
|
|
300
303
|
await _EnvironmentRolesManager(self._environment)._dispatch_role_updates(requests)
|
|
301
304
|
|
|
302
305
|
|
|
306
|
+
class _EnvironmentAppsManager:
|
|
307
|
+
"""mdmd:namespace"""
|
|
308
|
+
|
|
309
|
+
def __init__(self, environment: "_Environment"):
|
|
310
|
+
"""mdmd:hidden"""
|
|
311
|
+
self._environment = environment
|
|
312
|
+
|
|
313
|
+
async def list(self) -> builtins.list["modal.app._App"]:
|
|
314
|
+
"""Return handles for live Apps in this Environment.
|
|
315
|
+
|
|
316
|
+
The returned handles reference existing remote Apps and can be used with APIs such as
|
|
317
|
+
[`App.info()`](https://modal.com/docs/sdk/py/latest/App#info),
|
|
318
|
+
[`App.logs`](https://modal.com/docs/sdk/py/latest/App#logs), and
|
|
319
|
+
[`Sandbox.create()`](https://modal.com/docs/sdk/py/latest/Sandbox#create). Listing Apps does not create
|
|
320
|
+
Apps.
|
|
321
|
+
|
|
322
|
+
Examples:
|
|
323
|
+
|
|
324
|
+
```python notest
|
|
325
|
+
environment = modal.Environment.from_name("prod")
|
|
326
|
+
apps = environment.apps.list()
|
|
327
|
+
print([app.name for app in apps])
|
|
328
|
+
```
|
|
329
|
+
"""
|
|
330
|
+
from .app import _App
|
|
331
|
+
|
|
332
|
+
await self._environment.hydrate()
|
|
333
|
+
assert self._environment.name is not None
|
|
334
|
+
|
|
335
|
+
response = await self._environment.client._stub.AppList(
|
|
336
|
+
api_pb2.AppListRequest(environment_name=self._environment.name)
|
|
337
|
+
)
|
|
338
|
+
stopped_app_states = (
|
|
339
|
+
api_pb2.APP_STATE_STOPPED,
|
|
340
|
+
api_pb2.APP_STATE_DISABLED,
|
|
341
|
+
)
|
|
342
|
+
return [
|
|
343
|
+
_App._new_remote(
|
|
344
|
+
item.name or None,
|
|
345
|
+
item.app_id,
|
|
346
|
+
self._environment.name,
|
|
347
|
+
self._environment.client,
|
|
348
|
+
description=item.description or None,
|
|
349
|
+
)
|
|
350
|
+
for item in response.apps
|
|
351
|
+
if item.state not in stopped_app_states
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
|
|
303
355
|
class _Environment(_Object, type_prefix="en"):
|
|
304
356
|
_name: str | None = None
|
|
305
357
|
_settings: EnvironmentSettings
|
|
@@ -334,6 +386,11 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
334
386
|
# Deprecated alias for `Environment.roles`.
|
|
335
387
|
return _EnvironmentMembersManager(self)
|
|
336
388
|
|
|
389
|
+
@property
|
|
390
|
+
def apps(self) -> "_EnvironmentAppsManager":
|
|
391
|
+
"""Namespace for accessing Apps deployed in this Environment."""
|
|
392
|
+
return _EnvironmentAppsManager(self)
|
|
393
|
+
|
|
337
394
|
# TODO(michael) Keeping this private for now until we decide what else should be in it
|
|
338
395
|
# And what the rules should be about updates / mutability
|
|
339
396
|
# @property
|
|
@@ -366,7 +423,7 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
366
423
|
else api_pb2.OBJECT_CREATION_TYPE_UNSPECIFIED
|
|
367
424
|
),
|
|
368
425
|
)
|
|
369
|
-
response = await load_context.client.
|
|
426
|
+
response = await load_context.client._stub.EnvironmentGetOrCreate(request)
|
|
370
427
|
logger.debug(f"Created environment with id {response.environment_id}")
|
|
371
428
|
self._hydrate(response.environment_id, load_context.client, response.metadata)
|
|
372
429
|
|
|
@@ -491,7 +548,7 @@ class _EnvironmentBillingManager:
|
|
|
491
548
|
|
|
492
549
|
return [
|
|
493
550
|
BillingReportItem._from_proto(pb_item)
|
|
494
|
-
async for pb_item in self._environment.client.
|
|
551
|
+
async for pb_item in self._environment.client._stub.WorkspaceBillingReport.unary_stream(request)
|
|
495
552
|
]
|
|
496
553
|
|
|
497
554
|
async def summary(
|
|
@@ -556,7 +613,7 @@ class _EnvironmentBillingManager:
|
|
|
556
613
|
request.start_timestamp.FromDatetime(cycle)
|
|
557
614
|
|
|
558
615
|
return EnvironmentBillingSummary._from_proto(
|
|
559
|
-
await self._environment.client.
|
|
616
|
+
await self._environment.client._stub.EnvironmentBillingSummary(request)
|
|
560
617
|
)
|
|
561
618
|
|
|
562
619
|
|
|
@@ -582,7 +639,7 @@ async def _get_environment_cached(name: str, client: _Client) -> _Environment:
|
|
|
582
639
|
async def _delete_environment(name: str, client: _Client | None = None):
|
|
583
640
|
if client is None:
|
|
584
641
|
client = await _Client.from_env()
|
|
585
|
-
await client.
|
|
642
|
+
await client._stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
|
|
586
643
|
|
|
587
644
|
|
|
588
645
|
async def _update_environment(
|
|
@@ -608,19 +665,19 @@ async def _update_environment(
|
|
|
608
665
|
)
|
|
609
666
|
if client is None:
|
|
610
667
|
client = await _Client.from_env()
|
|
611
|
-
await client.
|
|
668
|
+
await client._stub.EnvironmentUpdate(update_payload)
|
|
612
669
|
|
|
613
670
|
|
|
614
671
|
async def _create_environment(name: str, client: _Client | None = None):
|
|
615
672
|
if client is None:
|
|
616
673
|
client = await _Client.from_env()
|
|
617
|
-
await client.
|
|
674
|
+
await client._stub.EnvironmentCreate(api_pb2.EnvironmentCreateRequest(name=name))
|
|
618
675
|
|
|
619
676
|
|
|
620
677
|
async def _list_environments(client: _Client | None = None) -> list[api_pb2.EnvironmentListItem]:
|
|
621
678
|
if client is None:
|
|
622
679
|
client = await _Client.from_env()
|
|
623
|
-
resp = await client.
|
|
680
|
+
resp = await client._stub.EnvironmentList(Empty())
|
|
624
681
|
return list(resp.items)
|
|
625
682
|
|
|
626
683
|
|