modal 1.6.4.dev1__tar.gz → 1.6.4.dev3__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.6.4.dev1 → modal-1.6.4.dev3}/PKG-INFO +1 -1
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/task_lifecycle_manager.py +23 -2
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/task_lifecycle_manager.pyi +6 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/app.py +84 -1
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/app.pyi +154 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/endpoint.py +109 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/image.py +119 -1
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/client.pyi +2 -2
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/config.py +5 -7
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/functions.pyi +6 -6
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/runner.py +10 -7
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/runner.pyi +5 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/types.py +21 -1
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_pb2.py +1407 -1406
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_pb2.pyi +39 -58
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_version/__init__.py +1 -1
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/pyproject.toml +0 -1
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/LICENSE +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/README.md +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/__main__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_billing.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_cluster.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_cluster.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_environments.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_function_variants.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_functions.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_grpc_client.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_image.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_ipython.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_load_context.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_location.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_logs.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_logs_manager.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_metrics.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_metrics.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_object.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_outbound_policy.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_outbound_policy.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/manager.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/pty.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/rich.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/status.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_partial_function.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_resolver.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_resources.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_serialization.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_server.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_server.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_supports_logs.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_traceback.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_tunnel.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_tunnel.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_type_manager.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/async_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/blob_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/docker_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/logger.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/name_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/oauth.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/time_utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/version.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_watcher.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_workspace.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/billing.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/billing.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2025.06.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2026.10.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/README.md +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/base-images.json +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/call_graph.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_download.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_help.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_logs.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_stats.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_traceback.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/app.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/billing.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/changelog.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/config.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/container.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/curl.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/dashboard.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/dict.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/entry_point.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/environment.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/function.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/import_refs.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/logo.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/network_file_system.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/profile.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/queues.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/run.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/secret.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/selector.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/server.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/shell.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/skills.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/token.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/utils.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/volume.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/workspace.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/client.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cls.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cls.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cluster.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cluster.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/container_process.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/container_process.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/dict.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/dict.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/environments.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/environments.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/exception.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/flash.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/ipython.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/functions.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/image.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/image.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/io_streams.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/io_streams.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/mount.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/mount.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/network_file_system.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/network_file_system.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/object.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/object.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/output.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/parallel_map.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/parallel_map.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/partial_function.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/partial_function.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/proxy.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/proxy.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/py.typed +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/queue.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/queue.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/retries.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/running_app.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_common.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_filesystem.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_impl_persistent.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_impl_v1.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_impl_v2.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_sandbox.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_sidecar.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_snapshot.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_task_command_router_client.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/sandbox.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/sandbox.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/schedule.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/scheduler_placement.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/secret.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/secret.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/server.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/server.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/serving.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/serving.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/stream_type.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/token_flow.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/token_flow.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/volume.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/volume.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/workspace.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/workspace.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/requires.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/__init__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_grpc.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/py.typed +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_version/__main__.py +0 -0
- {modal-1.6.4.dev1 → modal-1.6.4.dev3}/setup.cfg +0 -0
|
@@ -25,9 +25,19 @@ from modal._utils.blob_utils import format_blob_data
|
|
|
25
25
|
from modal._utils.grpc_utils import Retry
|
|
26
26
|
from modal._utils.package_utils import parse_major_minor_version
|
|
27
27
|
from modal.client import _Client
|
|
28
|
-
from modal.config import config, logger
|
|
28
|
+
from modal.config import _env_var_name, config, logger
|
|
29
29
|
from modal_proto import api_pb2
|
|
30
30
|
|
|
31
|
+
SPEC_ENVIRON_PATH: str = "/proc/gvisor/spec_environ"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _read_spec_environ() -> dict[str, str]:
|
|
35
|
+
path = Path(SPEC_ENVIRON_PATH)
|
|
36
|
+
if not path.exists():
|
|
37
|
+
return {}
|
|
38
|
+
entries = (entry.partition("=") for entry in path.read_text("utf8").split("\0"))
|
|
39
|
+
return {key: value for key, sep, value in entries if sep}
|
|
40
|
+
|
|
31
41
|
|
|
32
42
|
class UserException(Exception):
|
|
33
43
|
"""Used to shut down the task gracefully."""
|
|
@@ -42,6 +52,7 @@ class _TaskLifecycleManager:
|
|
|
42
52
|
checkpoint_id: str | None
|
|
43
53
|
_client: _Client
|
|
44
54
|
_cuda_checkpoint_session: gpu_memory_snapshot.CudaCheckpointSession | None
|
|
55
|
+
_snapshot_spec_environ: dict[str, str]
|
|
45
56
|
|
|
46
57
|
def _init(
|
|
47
58
|
self,
|
|
@@ -57,6 +68,7 @@ class _TaskLifecycleManager:
|
|
|
57
68
|
self.checkpoint_id = checkpoint_id
|
|
58
69
|
self._client = client
|
|
59
70
|
self._cuda_checkpoint_session = None
|
|
71
|
+
self._snapshot_spec_environ = {}
|
|
60
72
|
|
|
61
73
|
def __new__(
|
|
62
74
|
cls,
|
|
@@ -171,10 +183,18 @@ class _TaskLifecycleManager:
|
|
|
171
183
|
setattr(self, key, restored_state[key])
|
|
172
184
|
|
|
173
185
|
# Env vars and global state.
|
|
186
|
+
env_updates = {}
|
|
187
|
+
for key, value in _read_spec_environ().items():
|
|
188
|
+
if self._snapshot_spec_environ.get(key) != value:
|
|
189
|
+
env_updates[key] = value
|
|
190
|
+
|
|
191
|
+
# Values from the restore state take precedence.
|
|
174
192
|
for key, value in restored_state.items():
|
|
175
193
|
# Empty string indicates that value does not need to be updated.
|
|
176
194
|
if value != "":
|
|
177
|
-
|
|
195
|
+
env_updates[_env_var_name(key)] = value
|
|
196
|
+
|
|
197
|
+
os.environ.update(env_updates)
|
|
178
198
|
|
|
179
199
|
# Restore GPU memory.
|
|
180
200
|
if self.function_def._experimental_enable_gpu_snapshot and self.function_def.resources.gpu_config.gpu_type:
|
|
@@ -205,6 +225,7 @@ class _TaskLifecycleManager:
|
|
|
205
225
|
self._cuda_checkpoint_session = gpu_memory_snapshot.CudaCheckpointSession()
|
|
206
226
|
self._cuda_checkpoint_session.checkpoint()
|
|
207
227
|
|
|
228
|
+
self._snapshot_spec_environ = _read_spec_environ()
|
|
208
229
|
await self._client._stub.ContainerCheckpoint(
|
|
209
230
|
api_pb2.ContainerCheckpointRequest(checkpoint_id=self.checkpoint_id)
|
|
210
231
|
)
|
|
@@ -5,6 +5,8 @@ import synchronicity.combined_types
|
|
|
5
5
|
import typing
|
|
6
6
|
import typing_extensions
|
|
7
7
|
|
|
8
|
+
def _read_spec_environ() -> dict[str, str]: ...
|
|
9
|
+
|
|
8
10
|
class UserException(Exception):
|
|
9
11
|
"""Used to shut down the task gracefully."""
|
|
10
12
|
|
|
@@ -18,6 +20,7 @@ class _TaskLifecycleManager:
|
|
|
18
20
|
checkpoint_id: typing.Optional[str]
|
|
19
21
|
_client: modal.client._Client
|
|
20
22
|
_cuda_checkpoint_session: typing.Optional[modal._runtime.gpu_memory_snapshot.CudaCheckpointSession]
|
|
23
|
+
_snapshot_spec_environ: dict[str, str]
|
|
21
24
|
|
|
22
25
|
def _init(
|
|
23
26
|
self,
|
|
@@ -67,6 +70,7 @@ class TaskLifecycleManager:
|
|
|
67
70
|
checkpoint_id: typing.Optional[str]
|
|
68
71
|
_client: modal.client.Client
|
|
69
72
|
_cuda_checkpoint_session: typing.Optional[modal._runtime.gpu_memory_snapshot.CudaCheckpointSession]
|
|
73
|
+
_snapshot_spec_environ: dict[str, str]
|
|
70
74
|
|
|
71
75
|
def __init__(self, /, *args, **kwargs):
|
|
72
76
|
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
@@ -136,3 +140,5 @@ def check_fastapi_pydantic_compatibility(exc: ImportError) -> None:
|
|
|
136
140
|
building an image that specifies a more recent version only for pydantic.
|
|
137
141
|
"""
|
|
138
142
|
...
|
|
143
|
+
|
|
144
|
+
SPEC_ENVIRON_PATH: str
|
|
@@ -3,6 +3,7 @@ import inspect
|
|
|
3
3
|
import typing
|
|
4
4
|
from collections.abc import AsyncGenerator, Callable, Collection, Coroutine, Mapping, Sequence
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
6
7
|
from pathlib import PurePosixPath
|
|
7
8
|
from textwrap import dedent
|
|
8
9
|
from typing import (
|
|
@@ -56,7 +57,7 @@ from .retries import Retries
|
|
|
56
57
|
from .running_app import RunningApp
|
|
57
58
|
from .schedule import Schedule
|
|
58
59
|
from .secret import _Secret
|
|
59
|
-
from .types import AppInfo
|
|
60
|
+
from .types import AppInfo, SandboxGroupStats
|
|
60
61
|
from .volume import _Volume
|
|
61
62
|
|
|
62
63
|
_default_image: _Image = _Image.debian_slim()
|
|
@@ -135,6 +136,82 @@ class _LocalAppState:
|
|
|
135
136
|
volumes_default: dict[str | PurePosixPath, _Volume]
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
class _AppSandboxesManager:
|
|
140
|
+
"""mdmd:namespace"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, app: "_App"):
|
|
143
|
+
"""mdmd:hidden"""
|
|
144
|
+
self._app = app
|
|
145
|
+
|
|
146
|
+
async def stats(
|
|
147
|
+
self,
|
|
148
|
+
*,
|
|
149
|
+
since: datetime,
|
|
150
|
+
until: datetime | None = None,
|
|
151
|
+
group_by_tags: Sequence[str] | None = None,
|
|
152
|
+
tags: dict[str, str] | None = None,
|
|
153
|
+
) -> AsyncGenerator[SandboxGroupStats, None]:
|
|
154
|
+
"""Return statistics for this App's Sandboxes over a time range, optionally grouped by tag values.
|
|
155
|
+
|
|
156
|
+
The maximum time range is 7 days.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
since: The beginning of the time range, inclusive, on a UTC hour boundary. Values without a
|
|
160
|
+
timezone are interpreted as local time.
|
|
161
|
+
until: The end of the time range, exclusive, on a UTC hour boundary. If omitted, this defaults to
|
|
162
|
+
the current time. Values without a timezone are interpreted as local time.
|
|
163
|
+
group_by_tags: Tag names to group by. Sandboxes with the same values for these names form one group;
|
|
164
|
+
a name not present on a Sandbox counts as `None`. If omitted, all matching Sandboxes are returned
|
|
165
|
+
as a single group.
|
|
166
|
+
tags: Only include Sandboxes that have at least these tags.
|
|
167
|
+
|
|
168
|
+
Yields:
|
|
169
|
+
`SandboxGroupStats` objects, one per combination of tag values, with the following fields:
|
|
170
|
+
- `tags`: the value of each name in `group_by_tags` for the group's Sandboxes, `None` where not present.
|
|
171
|
+
- `cost`: total cost in USD, the sum of `cost_by_resource`.
|
|
172
|
+
- `cost_by_resource`: cost in USD by billed resource: `CPU`, `Memory`, and GPU types such as
|
|
173
|
+
`H100`. The resource names are subject to change as Modal's billing model evolves.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
InvalidError: If the time range is invalid or there are more than 10,000 groups.
|
|
177
|
+
|
|
178
|
+
Examples:
|
|
179
|
+
```python notest
|
|
180
|
+
app = modal.App.lookup("my-app")
|
|
181
|
+
since = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) - timedelta(days=1)
|
|
182
|
+
for group in app.sandboxes.stats(since=since, group_by_tags=["team"]):
|
|
183
|
+
print(group.tags, group.cost, group.cost_by_resource)
|
|
184
|
+
```
|
|
185
|
+
"""
|
|
186
|
+
if self._app._app_id is None:
|
|
187
|
+
raise InvalidError("`App.sandboxes.stats` requires a running, deployed or looked-up App.")
|
|
188
|
+
if until is not None:
|
|
189
|
+
if until.tzinfo is None:
|
|
190
|
+
until = until.astimezone()
|
|
191
|
+
until = until.astimezone(timezone.utc)
|
|
192
|
+
if since.tzinfo is None:
|
|
193
|
+
since = since.astimezone()
|
|
194
|
+
since = since.astimezone(timezone.utc)
|
|
195
|
+
if since >= datetime.now(timezone.utc):
|
|
196
|
+
raise InvalidError("`since` must be in the past.")
|
|
197
|
+
if until is not None and since >= until:
|
|
198
|
+
raise InvalidError("`since` must be before `until`.")
|
|
199
|
+
client = self._app._client or await _Client.from_env()
|
|
200
|
+
request = api_pb2.SandboxGetTimeRangeStatsRequest(
|
|
201
|
+
group_by_tags=list(group_by_tags or []), tags=tags or {}, app_id=self._app._app_id
|
|
202
|
+
)
|
|
203
|
+
request.since.FromDatetime(since)
|
|
204
|
+
if until is not None:
|
|
205
|
+
request.until.FromDatetime(until)
|
|
206
|
+
while True:
|
|
207
|
+
response = await client._stub.SandboxGetTimeRangeStats(request)
|
|
208
|
+
for group in response.groups:
|
|
209
|
+
yield SandboxGroupStats._from_proto(group)
|
|
210
|
+
if not response.next_page_token:
|
|
211
|
+
return
|
|
212
|
+
request.page_token = response.next_page_token
|
|
213
|
+
|
|
214
|
+
|
|
138
215
|
class _App:
|
|
139
216
|
"""A Modal App is a group of functions and classes that are deployed together.
|
|
140
217
|
|
|
@@ -1679,6 +1756,11 @@ class _App:
|
|
|
1679
1756
|
|
|
1680
1757
|
return _AppLogsManager(self)
|
|
1681
1758
|
|
|
1759
|
+
@property
|
|
1760
|
+
def sandboxes(self) -> _AppSandboxesManager:
|
|
1761
|
+
"""Namespace for the Sandboxes belonging to this App."""
|
|
1762
|
+
return _AppSandboxesManager(self)
|
|
1763
|
+
|
|
1682
1764
|
async def info(self, refresh: bool = False) -> AppInfo:
|
|
1683
1765
|
"""Return information for a modal `App`.
|
|
1684
1766
|
|
|
@@ -1711,4 +1793,5 @@ class _App:
|
|
|
1711
1793
|
return self._info
|
|
1712
1794
|
|
|
1713
1795
|
|
|
1796
|
+
AppSandboxesManager = synchronize_api(_AppSandboxesManager)
|
|
1714
1797
|
App = synchronize_api(_App)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import collections.abc
|
|
2
|
+
import datetime
|
|
2
3
|
import modal._functions
|
|
3
4
|
import modal._image
|
|
4
5
|
import modal._load_context
|
|
@@ -115,6 +116,54 @@ class _LocalAppState:
|
|
|
115
116
|
"""Return self==value."""
|
|
116
117
|
...
|
|
117
118
|
|
|
119
|
+
class _AppSandboxesManager:
|
|
120
|
+
"""mdmd:namespace"""
|
|
121
|
+
def __init__(self, app: _App):
|
|
122
|
+
"""mdmd:hidden"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
def stats(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
since: datetime.datetime,
|
|
129
|
+
until: typing.Optional[datetime.datetime] = None,
|
|
130
|
+
group_by_tags: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
131
|
+
tags: typing.Optional[dict[str, str]] = None,
|
|
132
|
+
) -> collections.abc.AsyncGenerator[modal.types.SandboxGroupStats, None]:
|
|
133
|
+
"""Return statistics for this App's Sandboxes over a time range, optionally grouped by tag values.
|
|
134
|
+
|
|
135
|
+
The maximum time range is 7 days.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
since: The beginning of the time range, inclusive, on a UTC hour boundary. Values without a
|
|
139
|
+
timezone are interpreted as local time.
|
|
140
|
+
until: The end of the time range, exclusive, on a UTC hour boundary. If omitted, this defaults to
|
|
141
|
+
the current time. Values without a timezone are interpreted as local time.
|
|
142
|
+
group_by_tags: Tag names to group by. Sandboxes with the same values for these names form one group;
|
|
143
|
+
a name not present on a Sandbox counts as `None`. If omitted, all matching Sandboxes are returned
|
|
144
|
+
as a single group.
|
|
145
|
+
tags: Only include Sandboxes that have at least these tags.
|
|
146
|
+
|
|
147
|
+
Yields:
|
|
148
|
+
`SandboxGroupStats` objects, one per combination of tag values, with the following fields:
|
|
149
|
+
- `tags`: the value of each name in `group_by_tags` for the group's Sandboxes, `None` where not present.
|
|
150
|
+
- `cost`: total cost in USD, the sum of `cost_by_resource`.
|
|
151
|
+
- `cost_by_resource`: cost in USD by billed resource: `CPU`, `Memory`, and GPU types such as
|
|
152
|
+
`H100`. The resource names are subject to change as Modal's billing model evolves.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
InvalidError: If the time range is invalid or there are more than 10,000 groups.
|
|
156
|
+
|
|
157
|
+
Examples:
|
|
158
|
+
```python notest
|
|
159
|
+
app = modal.App.lookup("my-app")
|
|
160
|
+
since = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) - timedelta(days=1)
|
|
161
|
+
for group in app.sandboxes.stats(since=since, group_by_tags=["team"]):
|
|
162
|
+
print(group.tags, group.cost, group.cost_by_resource)
|
|
163
|
+
```
|
|
164
|
+
"""
|
|
165
|
+
...
|
|
166
|
+
|
|
118
167
|
class _App:
|
|
119
168
|
"""A Modal App is a group of functions and classes that are deployed together.
|
|
120
169
|
|
|
@@ -953,6 +1002,11 @@ class _App:
|
|
|
953
1002
|
"""
|
|
954
1003
|
...
|
|
955
1004
|
|
|
1005
|
+
@property
|
|
1006
|
+
def sandboxes(self) -> _AppSandboxesManager:
|
|
1007
|
+
"""Namespace for the Sandboxes belonging to this App."""
|
|
1008
|
+
...
|
|
1009
|
+
|
|
956
1010
|
async def info(self, refresh: bool = False) -> modal.types.AppInfo:
|
|
957
1011
|
"""Return information for a modal `App`.
|
|
958
1012
|
|
|
@@ -971,6 +1025,101 @@ class _App:
|
|
|
971
1025
|
"""
|
|
972
1026
|
...
|
|
973
1027
|
|
|
1028
|
+
class AppSandboxesManager:
|
|
1029
|
+
"""mdmd:namespace"""
|
|
1030
|
+
def __init__(self, app: App):
|
|
1031
|
+
"""mdmd:hidden"""
|
|
1032
|
+
...
|
|
1033
|
+
|
|
1034
|
+
class __stats_spec(typing_extensions.Protocol):
|
|
1035
|
+
def __call__(
|
|
1036
|
+
self,
|
|
1037
|
+
/,
|
|
1038
|
+
*,
|
|
1039
|
+
since: datetime.datetime,
|
|
1040
|
+
until: typing.Optional[datetime.datetime] = None,
|
|
1041
|
+
group_by_tags: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
1042
|
+
tags: typing.Optional[dict[str, str]] = None,
|
|
1043
|
+
) -> typing.Generator[modal.types.SandboxGroupStats, None, None]:
|
|
1044
|
+
"""Return statistics for this App's Sandboxes over a time range, optionally grouped by tag values.
|
|
1045
|
+
|
|
1046
|
+
The maximum time range is 7 days.
|
|
1047
|
+
|
|
1048
|
+
Args:
|
|
1049
|
+
since: The beginning of the time range, inclusive, on a UTC hour boundary. Values without a
|
|
1050
|
+
timezone are interpreted as local time.
|
|
1051
|
+
until: The end of the time range, exclusive, on a UTC hour boundary. If omitted, this defaults to
|
|
1052
|
+
the current time. Values without a timezone are interpreted as local time.
|
|
1053
|
+
group_by_tags: Tag names to group by. Sandboxes with the same values for these names form one group;
|
|
1054
|
+
a name not present on a Sandbox counts as `None`. If omitted, all matching Sandboxes are returned
|
|
1055
|
+
as a single group.
|
|
1056
|
+
tags: Only include Sandboxes that have at least these tags.
|
|
1057
|
+
|
|
1058
|
+
Yields:
|
|
1059
|
+
`SandboxGroupStats` objects, one per combination of tag values, with the following fields:
|
|
1060
|
+
- `tags`: the value of each name in `group_by_tags` for the group's Sandboxes, `None` where not present.
|
|
1061
|
+
- `cost`: total cost in USD, the sum of `cost_by_resource`.
|
|
1062
|
+
- `cost_by_resource`: cost in USD by billed resource: `CPU`, `Memory`, and GPU types such as
|
|
1063
|
+
`H100`. The resource names are subject to change as Modal's billing model evolves.
|
|
1064
|
+
|
|
1065
|
+
Raises:
|
|
1066
|
+
InvalidError: If the time range is invalid or there are more than 10,000 groups.
|
|
1067
|
+
|
|
1068
|
+
Examples:
|
|
1069
|
+
```python notest
|
|
1070
|
+
app = modal.App.lookup("my-app")
|
|
1071
|
+
since = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) - timedelta(days=1)
|
|
1072
|
+
for group in app.sandboxes.stats(since=since, group_by_tags=["team"]):
|
|
1073
|
+
print(group.tags, group.cost, group.cost_by_resource)
|
|
1074
|
+
```
|
|
1075
|
+
"""
|
|
1076
|
+
...
|
|
1077
|
+
|
|
1078
|
+
def aio(
|
|
1079
|
+
self,
|
|
1080
|
+
/,
|
|
1081
|
+
*,
|
|
1082
|
+
since: datetime.datetime,
|
|
1083
|
+
until: typing.Optional[datetime.datetime] = None,
|
|
1084
|
+
group_by_tags: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
1085
|
+
tags: typing.Optional[dict[str, str]] = None,
|
|
1086
|
+
) -> collections.abc.AsyncGenerator[modal.types.SandboxGroupStats, None]:
|
|
1087
|
+
"""Return statistics for this App's Sandboxes over a time range, optionally grouped by tag values.
|
|
1088
|
+
|
|
1089
|
+
The maximum time range is 7 days.
|
|
1090
|
+
|
|
1091
|
+
Args:
|
|
1092
|
+
since: The beginning of the time range, inclusive, on a UTC hour boundary. Values without a
|
|
1093
|
+
timezone are interpreted as local time.
|
|
1094
|
+
until: The end of the time range, exclusive, on a UTC hour boundary. If omitted, this defaults to
|
|
1095
|
+
the current time. Values without a timezone are interpreted as local time.
|
|
1096
|
+
group_by_tags: Tag names to group by. Sandboxes with the same values for these names form one group;
|
|
1097
|
+
a name not present on a Sandbox counts as `None`. If omitted, all matching Sandboxes are returned
|
|
1098
|
+
as a single group.
|
|
1099
|
+
tags: Only include Sandboxes that have at least these tags.
|
|
1100
|
+
|
|
1101
|
+
Yields:
|
|
1102
|
+
`SandboxGroupStats` objects, one per combination of tag values, with the following fields:
|
|
1103
|
+
- `tags`: the value of each name in `group_by_tags` for the group's Sandboxes, `None` where not present.
|
|
1104
|
+
- `cost`: total cost in USD, the sum of `cost_by_resource`.
|
|
1105
|
+
- `cost_by_resource`: cost in USD by billed resource: `CPU`, `Memory`, and GPU types such as
|
|
1106
|
+
`H100`. The resource names are subject to change as Modal's billing model evolves.
|
|
1107
|
+
|
|
1108
|
+
Raises:
|
|
1109
|
+
InvalidError: If the time range is invalid or there are more than 10,000 groups.
|
|
1110
|
+
|
|
1111
|
+
Examples:
|
|
1112
|
+
```python notest
|
|
1113
|
+
app = modal.App.lookup("my-app")
|
|
1114
|
+
since = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) - timedelta(days=1)
|
|
1115
|
+
for group in app.sandboxes.stats(since=since, group_by_tags=["team"]):
|
|
1116
|
+
print(group.tags, group.cost, group.cost_by_resource)
|
|
1117
|
+
```
|
|
1118
|
+
"""
|
|
1119
|
+
...
|
|
1120
|
+
|
|
1121
|
+
stats: __stats_spec
|
|
1122
|
+
|
|
974
1123
|
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
|
975
1124
|
|
|
976
1125
|
class App:
|
|
@@ -2073,6 +2222,11 @@ class App:
|
|
|
2073
2222
|
"""
|
|
2074
2223
|
...
|
|
2075
2224
|
|
|
2225
|
+
@property
|
|
2226
|
+
def sandboxes(self) -> AppSandboxesManager:
|
|
2227
|
+
"""Namespace for the Sandboxes belonging to this App."""
|
|
2228
|
+
...
|
|
2229
|
+
|
|
2076
2230
|
class __info_spec(typing_extensions.Protocol):
|
|
2077
2231
|
def __call__(self, /, refresh: bool = False) -> modal.types.AppInfo:
|
|
2078
2232
|
"""Return information for a modal `App`.
|
|
@@ -5,6 +5,7 @@ from json import dumps
|
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
|
+
from rich.box import SIMPLE
|
|
8
9
|
from rich.table import Column, Table
|
|
9
10
|
from rich.text import Text
|
|
10
11
|
|
|
@@ -21,6 +22,7 @@ from modal_proto import api_pb2
|
|
|
21
22
|
|
|
22
23
|
from ._help import ModalGroup
|
|
23
24
|
from ._logs import _validate_logs_args
|
|
25
|
+
from ._stats import STATS_METADATA_STYLE
|
|
24
26
|
from .server import _run_server_logs, _run_server_stats, _server_stats_time_range
|
|
25
27
|
|
|
26
28
|
_ENDPOINT_HELP = """
|
|
@@ -41,6 +43,11 @@ _SERVING_MODE_LABELS = {
|
|
|
41
43
|
api_pb2.ENDPOINT_SERVING_MODE_DEDICATED: "dedicated",
|
|
42
44
|
api_pb2.ENDPOINT_SERVING_MODE_SHARED: "shared",
|
|
43
45
|
}
|
|
46
|
+
_INPUT_MODALITY_LABELS = {
|
|
47
|
+
api_pb2.ENDPOINT_INPUT_MODALITY_TEXT: "text",
|
|
48
|
+
api_pb2.ENDPOINT_INPUT_MODALITY_IMAGE: "image",
|
|
49
|
+
api_pb2.ENDPOINT_INPUT_MODALITY_AUDIO: "audio",
|
|
50
|
+
}
|
|
44
51
|
_ENDPOINT_STATUS_LABELS = {
|
|
45
52
|
api_pb2.EndpointGetInfoResponse.ENDPOINT_STATUS_PROVISIONING: "provisioning",
|
|
46
53
|
api_pb2.EndpointGetInfoResponse.ENDPOINT_STATUS_LIVE: "live",
|
|
@@ -502,6 +509,108 @@ async def stop(
|
|
|
502
509
|
output.print(f"[green]✓[/green] Stopped Endpoint {endpoint_id}.")
|
|
503
510
|
|
|
504
511
|
|
|
512
|
+
@endpoint_cli.command("library", panel="Inspection")
|
|
513
|
+
@click.option(
|
|
514
|
+
"--input-modality",
|
|
515
|
+
type=click.Choice(["text", "image", "audio"]),
|
|
516
|
+
multiple=True,
|
|
517
|
+
help="Only show models that accept any of these inputs. Can be repeated.",
|
|
518
|
+
)
|
|
519
|
+
@click.option("--json", "json_output", is_flag=True, help="Output models as JSON.")
|
|
520
|
+
@synchronizer.create_blocking
|
|
521
|
+
async def library(*, input_modality: tuple[str, ...] = (), json_output: bool = False) -> None:
|
|
522
|
+
"""List models you can serve with Modal Endpoints.
|
|
523
|
+
|
|
524
|
+
Models are listed by their Hugging Face repo ID, with the input types they
|
|
525
|
+
accept and whether they're available as shared endpoints, dedicated
|
|
526
|
+
endpoints, or both.
|
|
527
|
+
|
|
528
|
+
This is Modal's catalog of supported models, not the Endpoints in your
|
|
529
|
+
workspace. To see those, run `modal endpoint list`.
|
|
530
|
+
|
|
531
|
+
Examples:
|
|
532
|
+
|
|
533
|
+
List models that support text input modality:
|
|
534
|
+
|
|
535
|
+
```
|
|
536
|
+
modal endpoint library --input-modality text
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Pick a model ID and then create a shared endpoint:
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
modal endpoint create --model Qwen/Qwen3.8-27B --shared
|
|
543
|
+
```
|
|
544
|
+
"""
|
|
545
|
+
client = await _Client.from_env()
|
|
546
|
+
response = await client._stub.EndpointGetLibrary(api_pb2.EndpointGetLibraryRequest())
|
|
547
|
+
modalities = {modality for modality, label in _INPUT_MODALITY_LABELS.items() if label in input_modality}
|
|
548
|
+
|
|
549
|
+
models = sorted(
|
|
550
|
+
(
|
|
551
|
+
model
|
|
552
|
+
for model in response.models
|
|
553
|
+
if (not modalities or modalities.intersection(model.supported_input_modalities))
|
|
554
|
+
),
|
|
555
|
+
key=lambda model: model.base_model_repo_id,
|
|
556
|
+
)
|
|
557
|
+
output = OutputManager.get()
|
|
558
|
+
if json_output:
|
|
559
|
+
output.print_json(
|
|
560
|
+
dumps(
|
|
561
|
+
[
|
|
562
|
+
{
|
|
563
|
+
"base_model_repo_id": model.base_model_repo_id,
|
|
564
|
+
"input_modalities": [
|
|
565
|
+
_INPUT_MODALITY_LABELS.get(modality, "unknown")
|
|
566
|
+
for modality in model.supported_input_modalities
|
|
567
|
+
],
|
|
568
|
+
"shared": api_pb2.ENDPOINT_SERVING_MODE_SHARED in model.available_serving_modes,
|
|
569
|
+
"dedicated": api_pb2.ENDPOINT_SERVING_MODE_DEDICATED in model.available_serving_modes,
|
|
570
|
+
}
|
|
571
|
+
for model in models
|
|
572
|
+
]
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
return
|
|
576
|
+
output.print(
|
|
577
|
+
Text(
|
|
578
|
+
f"Displaying {len(models):,} {'model' if len(models) == 1 else 'models'}",
|
|
579
|
+
style=STATS_METADATA_STYLE,
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
if not models:
|
|
583
|
+
output.print(
|
|
584
|
+
"No endpoint-compatible models match the filters."
|
|
585
|
+
if input_modality
|
|
586
|
+
else "No endpoint-compatible models are available."
|
|
587
|
+
)
|
|
588
|
+
return
|
|
589
|
+
display_table(
|
|
590
|
+
[
|
|
591
|
+
Column("Model ID", overflow="fold"),
|
|
592
|
+
"Input Modalities",
|
|
593
|
+
Column("Shared", justify="center"),
|
|
594
|
+
Column("Dedicated", justify="center"),
|
|
595
|
+
],
|
|
596
|
+
[
|
|
597
|
+
(
|
|
598
|
+
Text.assemble(
|
|
599
|
+
(model.base_model_repo_id, f"link https://huggingface.co/{model.base_model_repo_id}"),
|
|
600
|
+
(" ↗", "bright_blue not underline"),
|
|
601
|
+
),
|
|
602
|
+
", ".join(
|
|
603
|
+
_INPUT_MODALITY_LABELS.get(modality, "unknown") for modality in model.supported_input_modalities
|
|
604
|
+
),
|
|
605
|
+
"✓" if api_pb2.ENDPOINT_SERVING_MODE_SHARED in model.available_serving_modes else "—",
|
|
606
|
+
"✓" if api_pb2.ENDPOINT_SERVING_MODE_DEDICATED in model.available_serving_modes else "—",
|
|
607
|
+
)
|
|
608
|
+
for model in models
|
|
609
|
+
],
|
|
610
|
+
table_box=SIMPLE,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
505
614
|
@endpoint_cli.command("info", panel="Inspection", no_args_is_help=True)
|
|
506
615
|
@click.argument("endpoint_identifier")
|
|
507
616
|
@env_option
|
|
@@ -4,21 +4,27 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json as json_module
|
|
7
|
+
import os
|
|
7
8
|
from collections.abc import AsyncIterator
|
|
8
9
|
from typing import Optional
|
|
9
10
|
|
|
10
11
|
import click
|
|
12
|
+
from rich.syntax import Syntax
|
|
11
13
|
|
|
12
14
|
from modal._environments import ensure_env
|
|
13
|
-
from modal._image import _upgrade_image_name
|
|
15
|
+
from modal._image import _Image, _infer_registry_auth_type, _upgrade_image_name
|
|
14
16
|
from modal._logs_manager import _resolve_source
|
|
15
17
|
from modal._utils.async_utils import synchronizer
|
|
16
18
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
19
|
+
from modal.app import _App
|
|
17
20
|
from modal.cli.utils import display_table, env_option
|
|
18
21
|
from modal.client import _Client
|
|
19
22
|
from modal.config import config
|
|
23
|
+
from modal.exception import InvalidError
|
|
20
24
|
from modal.image import Image
|
|
21
25
|
from modal.output import OutputManager
|
|
26
|
+
from modal.runner import _run_app
|
|
27
|
+
from modal.secret import _Secret
|
|
22
28
|
from modal_proto import api_pb2
|
|
23
29
|
|
|
24
30
|
from ._help import ModalGroup
|
|
@@ -36,6 +42,118 @@ def _print_result_summary(count: int) -> None:
|
|
|
36
42
|
OutputManager.get().print(f"Showing {count} {result_label}")
|
|
37
43
|
|
|
38
44
|
|
|
45
|
+
async def _registry_secret(
|
|
46
|
+
secret_name: Optional[str], secret_vars: Optional[str], env: str
|
|
47
|
+
) -> tuple[Optional[_Secret], set[str]]:
|
|
48
|
+
if secret_name and secret_vars:
|
|
49
|
+
raise click.UsageError("--registry-secret and --registry-secret-from-env cannot be used together.")
|
|
50
|
+
if secret_name:
|
|
51
|
+
secret = _Secret.from_name(secret_name, environment_name=env)
|
|
52
|
+
return secret, await secret._get_keys()
|
|
53
|
+
keys = [key.strip() for key in (secret_vars or "").split(",") if key.strip()]
|
|
54
|
+
if not keys:
|
|
55
|
+
return None, set()
|
|
56
|
+
return _Secret.from_local_environ(keys), set(keys)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@image_cli.command("build", no_args_is_help=True)
|
|
60
|
+
@click.argument("context_dir", metavar="CONTEXT_DIR")
|
|
61
|
+
@click.option(
|
|
62
|
+
"-t",
|
|
63
|
+
"--name",
|
|
64
|
+
"name",
|
|
65
|
+
metavar="[ENV/]NAME[:TAG]",
|
|
66
|
+
required=True,
|
|
67
|
+
help="Name to publish the Image as, e.g. `my-image`, `my-image:v2`, or `my-env/my-image`.",
|
|
68
|
+
)
|
|
69
|
+
@click.option(
|
|
70
|
+
"-f",
|
|
71
|
+
"--file",
|
|
72
|
+
"dockerfile",
|
|
73
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
74
|
+
default=None,
|
|
75
|
+
help="Path to the Dockerfile. Defaults to `CONTEXT_DIR/Dockerfile`.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--registry-secret",
|
|
79
|
+
"registry_secret_name",
|
|
80
|
+
metavar="SECRET_NAME",
|
|
81
|
+
default=None,
|
|
82
|
+
help="Name of a Secret with credentials for pulling the base image from a private registry.",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--registry-secret-from-env",
|
|
86
|
+
"registry_secret_vars",
|
|
87
|
+
default=None,
|
|
88
|
+
metavar="VAR[,VAR...]",
|
|
89
|
+
help=(
|
|
90
|
+
"Comma-separated local environment variable names holding registry credentials. "
|
|
91
|
+
"Cannot be combined with --registry-secret."
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
@click.option("--force-build", is_flag=True, help="Rebuild every layer of the Image, ignoring cached builds.")
|
|
95
|
+
@env_option
|
|
96
|
+
@synchronizer.create_blocking
|
|
97
|
+
async def build(
|
|
98
|
+
context_dir: str,
|
|
99
|
+
name: str,
|
|
100
|
+
dockerfile: Optional[str],
|
|
101
|
+
registry_secret_name: Optional[str],
|
|
102
|
+
registry_secret_vars: Optional[str],
|
|
103
|
+
force_build: bool,
|
|
104
|
+
env: Optional[str],
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Build an Image from a Dockerfile and publish it as NAME.
|
|
107
|
+
|
|
108
|
+
CONTEXT_DIR is the build context used to resolve relative COPY paths. Use `.` for the current directory.
|
|
109
|
+
|
|
110
|
+
NAME defaults to the `latest` tag. `--env` sets the environment the build runs in and where
|
|
111
|
+
`--registry-secret` is looked up. The Image is published into that environment unless NAME has an
|
|
112
|
+
environment prefix, e.g. `my-env/my-image`.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
modal image build -t my-image .
|
|
118
|
+
modal image build --name my-image:v2 -f docker/Dockerfile .
|
|
119
|
+
modal image build -t my-env/my-image .
|
|
120
|
+
modal image build -t my-image --registry-secret my-registry-creds .
|
|
121
|
+
modal image build -t my-image --registry-secret-from-env REGISTRY_USERNAME,REGISTRY_PASSWORD .
|
|
122
|
+
```
|
|
123
|
+
"""
|
|
124
|
+
if not os.path.isdir(context_dir):
|
|
125
|
+
raise click.UsageError(f"CONTEXT_DIR {context_dir!r} is not a directory.")
|
|
126
|
+
if dockerfile is None:
|
|
127
|
+
dockerfile = os.path.join(context_dir, "Dockerfile")
|
|
128
|
+
if not os.path.isfile(dockerfile):
|
|
129
|
+
raise click.UsageError(f"No Dockerfile found at {dockerfile}. Use --file to specify one.")
|
|
130
|
+
env = ensure_env(env)
|
|
131
|
+
try:
|
|
132
|
+
tag = _upgrade_image_name(name, fallback_environment_name=env)
|
|
133
|
+
except InvalidError as exc:
|
|
134
|
+
raise click.UsageError(str(exc))
|
|
135
|
+
|
|
136
|
+
secret, keys = await _registry_secret(registry_secret_name, registry_secret_vars, env)
|
|
137
|
+
if secret is not None:
|
|
138
|
+
# Fail before creating the app; from_dockerfile runs the same check during the build
|
|
139
|
+
try:
|
|
140
|
+
_infer_registry_auth_type(keys)
|
|
141
|
+
except InvalidError as exc:
|
|
142
|
+
raise click.UsageError(str(exc)) from exc
|
|
143
|
+
image = _Image.from_dockerfile(dockerfile, context_dir=context_dir, registry_secret=secret, force_build=force_build)
|
|
144
|
+
app = _App("modal-image-build")
|
|
145
|
+
# Only show the Image build output, which includes its logs
|
|
146
|
+
async with _run_app(app, environment_name=env, stream_output=False):
|
|
147
|
+
await image.build(app)
|
|
148
|
+
await image.publish(tag)
|
|
149
|
+
|
|
150
|
+
output_mgr = OutputManager.get()
|
|
151
|
+
output_mgr.step_completed(f"Image built: [bold]{image.object_id}[/bold]")
|
|
152
|
+
output_mgr.step_completed(f"Image published as [bold magenta]{tag}[/bold magenta]! 🎉")
|
|
153
|
+
output_mgr.print("\nUse it with:\n")
|
|
154
|
+
output_mgr.print(Syntax(f'modal.Image.from_name("{tag}")', "python"))
|
|
155
|
+
|
|
156
|
+
|
|
39
157
|
@image_cli.command("logs", no_args_is_help=True)
|
|
40
158
|
@click.argument("image_id")
|
|
41
159
|
@click.option(
|