modal 1.5.6.dev5__tar.gz → 1.5.6.dev7__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.dev5 → modal-1.5.6.dev7}/PKG-INFO +1 -1
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_function_variants.py +77 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_functions.py +74 -8
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_object.py +2 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/function_utils.py +29 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/grpc_utils.py +17 -1
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/task_command_router_client.py +1 -1
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/app.py +35 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/app.pyi +60 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/app.py +77 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/client.pyi +2 -2
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cloud_bucket_mount.py +6 -4
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cloud_bucket_mount.pyi +3 -1
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cls.py +15 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/functions.pyi +43 -10
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/sandbox.py +1 -1
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/schedule.py +1 -1
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/types.py +270 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_pb2.py +815 -815
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_pb2.pyi +6 -2
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_version/__init__.py +1 -1
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/LICENSE +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/README.md +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/__main__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_billing.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_clustered_functions.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_environments.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_grpc_client.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_image.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_ipython.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_load_context.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_location.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_logs.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_logs_manager.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_logs_manager.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/manager.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/pty.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/rich.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/status.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_partial_function.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_resolver.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_resources.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_serialization.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_server.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_supports_logs.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_traceback.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_tunnel.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_tunnel.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_type_manager.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/display_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/logger.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/time_utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/version.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_watcher.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_workspace.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/billing.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/billing.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/README.md +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/base-images.json +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/call_graph.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/_download.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/_help.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/_traceback.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/billing.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/changelog.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/cluster.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/config.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/container.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/curl.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/dashboard.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/dict.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/endpoint.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/entry_point.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/environment.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/image.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/import_refs.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/launch.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/logo.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/profile.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/queues.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/run.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/secret.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/selector.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/shell.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/skills.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/token.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/utils.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/volume.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/workspace.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/client.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cls.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/config.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/container_process.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/container_process.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/dict.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/dict.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/environments.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/environments.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/exception.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/experimental/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/experimental/flash.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/experimental/ipython.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/file_io.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/file_io.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/functions.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/image.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/image.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/io_streams.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/io_streams.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/mount.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/mount.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/network_file_system.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/network_file_system.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/object.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/object.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/output.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/parallel_map.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/parallel_map.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/partial_function.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/partial_function.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/proxy.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/proxy.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/py.typed +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/queue.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/queue.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/retries.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/runner.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/runner.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/running_app.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/sandbox.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/sandbox_fs.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/scheduler_placement.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/secret.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/secret.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/server.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/server.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/serving.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/serving.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/snapshot.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/snapshot.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/stream_type.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/token_flow.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/token_flow.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/volume.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/volume.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/workspace.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/workspace.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/reference_intro.md +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/__init__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_grpc.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/py.typed +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_version/__main__.py +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/pyproject.toml +0 -0
- {modal-1.5.6.dev5 → modal-1.5.6.dev7}/setup.cfg +0 -0
|
@@ -4,6 +4,7 @@ from collections.abc import Collection, Sequence, Sized
|
|
|
4
4
|
from pathlib import PurePosixPath
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
+
from modal.types import CloudBucketMountInfo, FunctionInfo, VolumeMountInfo
|
|
7
8
|
from modal_proto import api_pb2
|
|
8
9
|
|
|
9
10
|
from ._resources import convert_fn_config_to_resources_config
|
|
@@ -248,6 +249,7 @@ def _make_function_variant(
|
|
|
248
249
|
):
|
|
249
250
|
if not base_function._is_hydrated:
|
|
250
251
|
await base_function.hydrate(load_context.client)
|
|
252
|
+
|
|
251
253
|
assert base_function._client and base_function._client.stub
|
|
252
254
|
|
|
253
255
|
if parameter_schema is None:
|
|
@@ -278,6 +280,12 @@ def _make_function_variant(
|
|
|
278
280
|
response = await _function_bind_params_cached(base_function, req)
|
|
279
281
|
function_variant._hydrate(response.bound_function_id, base_function._client, response.handle_metadata)
|
|
280
282
|
|
|
283
|
+
if base_function._function_info is not None:
|
|
284
|
+
if options is not None:
|
|
285
|
+
function_variant._function_info = _get_function_info_with_options(base_function._function_info, options)
|
|
286
|
+
else:
|
|
287
|
+
function_variant._function_info = base_function._function_info._get_copy()
|
|
288
|
+
|
|
281
289
|
def _deps():
|
|
282
290
|
if options:
|
|
283
291
|
return options._unhydrated_object_deps()
|
|
@@ -292,4 +300,73 @@ def _make_function_variant(
|
|
|
292
300
|
)
|
|
293
301
|
fun._source_info = base_function._source_info
|
|
294
302
|
fun._spec = base_function._spec # TODO (elias): fix - this is incorrect when using with_options
|
|
303
|
+
|
|
304
|
+
# If we already have a ._function_info for the base function, we can pass it down - otherwise this
|
|
305
|
+
# will be filled in during hydration
|
|
306
|
+
if base_function._function_info is not None:
|
|
307
|
+
if options is not None:
|
|
308
|
+
fun._function_info = _get_function_info_with_options(base_function._function_info, options)
|
|
309
|
+
else:
|
|
310
|
+
fun._function_info = base_function._function_info._get_copy()
|
|
311
|
+
|
|
295
312
|
return fun
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _get_function_info_with_options(info: FunctionInfo, options: _FunctionOptions):
|
|
316
|
+
new_info = info._get_copy()
|
|
317
|
+
|
|
318
|
+
if options.resources is not None:
|
|
319
|
+
res = options.resources
|
|
320
|
+
if res.milli_cpu_max > 0:
|
|
321
|
+
new_info.cpu = (res.milli_cpu / 1000, res.milli_cpu_max / 1000)
|
|
322
|
+
elif res.milli_cpu > 0:
|
|
323
|
+
new_info.cpu = res.milli_cpu / 1000
|
|
324
|
+
|
|
325
|
+
if res.memory_mb_max > 0:
|
|
326
|
+
new_info.memory_mib = (res.memory_mb, res.memory_mb_max)
|
|
327
|
+
elif res.memory_mb > 0:
|
|
328
|
+
new_info.memory_mib = res.memory_mb
|
|
329
|
+
|
|
330
|
+
if res.HasField("gpu_config") and res.gpu_config.count > 0:
|
|
331
|
+
new_info.gpus = [(res.gpu_config.count, res.gpu_config.gpu_type)]
|
|
332
|
+
|
|
333
|
+
if res.ephemeral_disk_mb > 0:
|
|
334
|
+
new_info.ephemeral_disk_mib = res.ephemeral_disk_mb
|
|
335
|
+
|
|
336
|
+
if options.scheduler_placement is not None:
|
|
337
|
+
sp = options.scheduler_placement
|
|
338
|
+
# Note: not putting `nonpreemptible` here as that is not currently possible to set via
|
|
339
|
+
# `.with_options(...)`
|
|
340
|
+
if len(sp.regions) > 0:
|
|
341
|
+
new_info.regions = list(sp.regions)
|
|
342
|
+
|
|
343
|
+
if options.cloud is not None:
|
|
344
|
+
new_info.cloud = options.cloud
|
|
345
|
+
|
|
346
|
+
if len(options.validated_volumes) > 0:
|
|
347
|
+
new_info.volumes = {
|
|
348
|
+
mount_path: VolumeMountInfo(vol._name, vol._object_id, False, None)
|
|
349
|
+
if vol._mount_options is None
|
|
350
|
+
else VolumeMountInfo(
|
|
351
|
+
vol._name,
|
|
352
|
+
vol._object_id,
|
|
353
|
+
vol._mount_options.read_only,
|
|
354
|
+
vol._mount_options.sub_path,
|
|
355
|
+
)
|
|
356
|
+
for mount_path, vol in options.validated_volumes
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if len(options.cloud_bucket_mounts) > 0:
|
|
360
|
+
new_info.cloud_bucket_mounts = {}
|
|
361
|
+
protos, _ = cloud_bucket_mounts_to_proto(options.cloud_bucket_mounts, include_secrets=False)
|
|
362
|
+
|
|
363
|
+
for proto in protos:
|
|
364
|
+
new_info.cloud_bucket_mounts[proto.mount_path] = CloudBucketMountInfo._from_proto(proto)
|
|
365
|
+
|
|
366
|
+
if len(options.secrets) > 0:
|
|
367
|
+
new_info.secrets = [repr(s) for s in options.secrets]
|
|
368
|
+
|
|
369
|
+
if options.routing_region:
|
|
370
|
+
new_info.routing_region = options.routing_region
|
|
371
|
+
|
|
372
|
+
return new_info
|
|
@@ -16,6 +16,7 @@ from google.protobuf.message import Message
|
|
|
16
16
|
from grpclib import Status
|
|
17
17
|
from synchronicity.combined_types import MethodWithAio
|
|
18
18
|
|
|
19
|
+
from modal.types import CloudBucketMountInfo, FunctionInfo, HttpInfo, VolumeMountInfo
|
|
19
20
|
from modal_proto import api_pb2
|
|
20
21
|
from modal_proto.modal_api_grpc import ModalClientModal
|
|
21
22
|
|
|
@@ -56,6 +57,7 @@ from ._utils.function_utils import (
|
|
|
56
57
|
_process_result,
|
|
57
58
|
_stream_function_call_data,
|
|
58
59
|
get_function_type,
|
|
60
|
+
get_schedule_str,
|
|
59
61
|
is_async,
|
|
60
62
|
normalize_fractional_target_concurrency,
|
|
61
63
|
parse_gpu_config,
|
|
@@ -636,6 +638,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
636
638
|
_options: _FunctionOptions
|
|
637
639
|
_base_function: "_Function | None" = None
|
|
638
640
|
_app_id: str | None = None
|
|
641
|
+
_function_info: FunctionInfo | None = None
|
|
639
642
|
|
|
640
643
|
async def _get_log_query_data(self) -> _LogQueryData:
|
|
641
644
|
await self.hydrate()
|
|
@@ -1158,6 +1161,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1158
1161
|
serve_mounts = {m for m in all_mounts if m.is_local()}
|
|
1159
1162
|
serve_mounts |= image._serve_mounts
|
|
1160
1163
|
obj._serve_mounts = frozenset(serve_mounts)
|
|
1164
|
+
obj._function_info = FunctionInfo._from_function_proto(response.function_data)
|
|
1165
|
+
|
|
1161
1166
|
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
1162
1167
|
|
|
1163
1168
|
rep = f"Function({tag})"
|
|
@@ -1193,6 +1198,36 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1193
1198
|
obj._build_args["cloud"] = repr(cloud)
|
|
1194
1199
|
obj._build_args["scheduler_placement"] = repr(scheduler_placement)
|
|
1195
1200
|
|
|
1201
|
+
obj._function_info = FunctionInfo(
|
|
1202
|
+
cpu=cpu,
|
|
1203
|
+
memory_mib=memory,
|
|
1204
|
+
gpus=[(parsed.count, parsed.gpu_type) for parsed in (parse_gpu_config(g) for g in gpus)],
|
|
1205
|
+
ephemeral_disk_mib=ephemeral_disk,
|
|
1206
|
+
nonpreemptible=nonpreemptible,
|
|
1207
|
+
regions=None if region is None else [region] if isinstance(region, str) else list(region),
|
|
1208
|
+
routing_region=routing_region,
|
|
1209
|
+
cloud=cloud,
|
|
1210
|
+
schedule=None if schedule is None else get_schedule_str(schedule.proto_message),
|
|
1211
|
+
volumes={
|
|
1212
|
+
mount_path: VolumeMountInfo(vol._name, vol._object_id, False, None)
|
|
1213
|
+
if vol._mount_options is None
|
|
1214
|
+
else VolumeMountInfo(
|
|
1215
|
+
vol._name,
|
|
1216
|
+
vol._object_id,
|
|
1217
|
+
vol._mount_options.read_only,
|
|
1218
|
+
vol._mount_options.sub_path,
|
|
1219
|
+
)
|
|
1220
|
+
for mount_path, vol in validated_volumes_no_cloud_buckets
|
|
1221
|
+
},
|
|
1222
|
+
cloud_bucket_mounts={
|
|
1223
|
+
cbm.mount_path: CloudBucketMountInfo._from_proto(cbm)
|
|
1224
|
+
for cbm in cloud_bucket_mounts_to_proto(cloud_bucket_mounts, include_secrets=False)[0]
|
|
1225
|
+
},
|
|
1226
|
+
# We don't have any secret IDs yet as we are dehydrated, so we return the reprs instead
|
|
1227
|
+
secrets=[repr(s) for s in secrets],
|
|
1228
|
+
http_info=HttpInfo._from_proto(http_config) if http_config else None,
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1196
1231
|
return obj
|
|
1197
1232
|
|
|
1198
1233
|
from_local = with_deprecation_warning(
|
|
@@ -1332,6 +1367,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1332
1367
|
print_server_warnings(response.server_warnings)
|
|
1333
1368
|
|
|
1334
1369
|
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
1370
|
+
self._function_info = FunctionInfo._from_function_proto(response.function)
|
|
1335
1371
|
|
|
1336
1372
|
environment_rep = (
|
|
1337
1373
|
f", environment_name={load_context_overrides.environment_name!r}"
|
|
@@ -1424,14 +1460,44 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1424
1460
|
# Deprecated soon, only for backwards compatibility
|
|
1425
1461
|
return self.app
|
|
1426
1462
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1463
|
+
async def info(self, *, refresh: bool = False) -> FunctionInfo:
|
|
1464
|
+
"""Get an overview of a Function's resource requests, associated mounts, etc.
|
|
1465
|
+
|
|
1466
|
+
This method performs a network request to populate this information if the Function handle is
|
|
1467
|
+
a remote lookup whose information has not yet been fetched (e.g. from `Function.from_name(...)`),
|
|
1468
|
+
or if `refresh=True`.
|
|
1469
|
+
|
|
1470
|
+
Args:
|
|
1471
|
+
refresh: Always perform a network request. Pass `refresh=True` to ensure that this method
|
|
1472
|
+
returns the most up to date information.
|
|
1473
|
+
|
|
1474
|
+
Returns:
|
|
1475
|
+
This returns a [`modal.types.FunctionInfo`](https://modal.com/docs/sdk/py/latest/types#FunctionInfo)
|
|
1476
|
+
dataclass.
|
|
1477
|
+
"""
|
|
1478
|
+
|
|
1479
|
+
if self._function_info is not None and not refresh:
|
|
1480
|
+
return self._function_info
|
|
1481
|
+
|
|
1482
|
+
# For some constructors, e.g. Function.from_name(), hydration will set self._function_info
|
|
1483
|
+
if not self._is_hydrated:
|
|
1484
|
+
await self.hydrate()
|
|
1485
|
+
|
|
1486
|
+
# Other constructors, such as `Function._new_hydrated()`, don't set self._function_info on
|
|
1487
|
+
# hydration, so we have to do an RPC to get that information
|
|
1488
|
+
if refresh or self._function_info is None:
|
|
1489
|
+
# Accessing `.client` or `.object_id` here can't throw since we only get to this branch on a
|
|
1490
|
+
# hydrated handle
|
|
1491
|
+
response = await self.client.stub.FunctionGetById(
|
|
1492
|
+
api_pb2.FunctionGetByIdRequest(
|
|
1493
|
+
function_id=self.object_id,
|
|
1494
|
+
)
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
self._function_info = FunctionInfo._from_function_proto(response.function)
|
|
1498
|
+
|
|
1499
|
+
assert self._function_info is not None
|
|
1500
|
+
return self._function_info._get_copy()
|
|
1435
1501
|
|
|
1436
1502
|
@property
|
|
1437
1503
|
def _source_info_(self) -> FunctionSourceInfo:
|
|
@@ -358,6 +358,7 @@ class _Object:
|
|
|
358
358
|
|
|
359
359
|
*Added in v0.72.39*: This method replaces the deprecated `.resolve()` method.
|
|
360
360
|
"""
|
|
361
|
+
|
|
361
362
|
# TODO: add deprecation for the client argument here - should be added in constructors instead
|
|
362
363
|
if self._is_hydrated:
|
|
363
364
|
if self.client._snapshotted and not self._is_rehydrated:
|
|
@@ -388,4 +389,5 @@ class _Object:
|
|
|
388
389
|
root_load_context = LoadContext(client=client, task_context=tc)
|
|
389
390
|
with suppress_tb_frame(): # skip this frame by default
|
|
390
391
|
await resolver.load(self, root_load_context)
|
|
392
|
+
|
|
391
393
|
return self
|
|
@@ -668,3 +668,32 @@ def parse_gpu_config(value: str | None) -> api_pb2.GPUConfig:
|
|
|
668
668
|
return api_pb2.GPUConfig(gpu_type=gpu_type, count=count)
|
|
669
669
|
else:
|
|
670
670
|
raise InvalidError(f"Invalid GPU config: {value!r}. Value must be a string or `None`")
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def get_schedule_str(schedule: api_pb2.Schedule) -> str | None:
|
|
674
|
+
if schedule.WhichOneof("schedule_oneof") is None:
|
|
675
|
+
return None
|
|
676
|
+
|
|
677
|
+
if schedule.WhichOneof("schedule_oneof") == "cron":
|
|
678
|
+
cron = schedule.cron
|
|
679
|
+
return f"Cron({cron.cron_string!r}, {cron.timezone})"
|
|
680
|
+
|
|
681
|
+
period = schedule.period
|
|
682
|
+
|
|
683
|
+
period_args = []
|
|
684
|
+
if period.years > 0:
|
|
685
|
+
period_args.append(f"years={period.years}")
|
|
686
|
+
if period.months > 0:
|
|
687
|
+
period_args.append(f"months={period.months}")
|
|
688
|
+
if period.weeks > 0:
|
|
689
|
+
period_args.append(f"weeks={period.weeks}")
|
|
690
|
+
if period.days > 0:
|
|
691
|
+
period_args.append(f"days={period.days}")
|
|
692
|
+
if period.hours > 0:
|
|
693
|
+
period_args.append(f"hours={period.hours}")
|
|
694
|
+
if period.minutes > 0:
|
|
695
|
+
period_args.append(f"minutes={period.minutes}")
|
|
696
|
+
if period.seconds > 0:
|
|
697
|
+
period_args.append(f"seconds={period.seconds}")
|
|
698
|
+
|
|
699
|
+
return f"Period({', '.join(period_args)})"
|
|
@@ -272,18 +272,34 @@ class CustomProtoStatusDetailsCodec(StatusDetailsCodecBase):
|
|
|
272
272
|
custom_detail_codec = CustomProtoStatusDetailsCodec()
|
|
273
273
|
|
|
274
274
|
|
|
275
|
-
def create_channel_config() -> grpclib.config.Configuration:
|
|
275
|
+
def create_channel_config(*, sustained_keepalive: bool = False) -> grpclib.config.Configuration:
|
|
276
276
|
"""Shared grpclib channel settings for Modal client connections.
|
|
277
277
|
|
|
278
278
|
HTTP/2 keepalive probes keep idle transports warm through stateful
|
|
279
279
|
middleboxes and surface dead connections before retries reuse them.
|
|
280
|
+
|
|
281
|
+
`sustained_keepalive` clears the two grpclib defaults such that a probe
|
|
282
|
+
actually goes out every `_keepalive_time`, for as long as the connection is
|
|
283
|
+
open.
|
|
280
284
|
"""
|
|
285
|
+
ping_throttle_overrides: dict[str, Any] = {}
|
|
286
|
+
if sustained_keepalive:
|
|
287
|
+
ping_throttle_overrides = {
|
|
288
|
+
# Default 2: probing stops after two probes with no request sent in
|
|
289
|
+
# between. Requests reset the count, acks don't. 0 = no cap.
|
|
290
|
+
"_http2_max_pings_without_data": 0,
|
|
291
|
+
# Default 300s floor between probes, applied regardless of data
|
|
292
|
+
# despite the name, so it has to stay under `_keepalive_time`.
|
|
293
|
+
"_http2_min_sent_ping_interval_without_data": GRPC_KEEPALIVE_TIME_SECS / 2,
|
|
294
|
+
}
|
|
295
|
+
|
|
281
296
|
return grpclib.config.Configuration(
|
|
282
297
|
_keepalive_time=GRPC_KEEPALIVE_TIME_SECS,
|
|
283
298
|
_keepalive_timeout=GRPC_KEEPALIVE_TIMEOUT_SECS,
|
|
284
299
|
_keepalive_permit_without_calls=True,
|
|
285
300
|
http2_connection_window_size=GRPC_WINDOW_SIZE,
|
|
286
301
|
http2_stream_window_size=GRPC_WINDOW_SIZE,
|
|
302
|
+
**ping_throttle_overrides,
|
|
287
303
|
)
|
|
288
304
|
|
|
289
305
|
|
|
@@ -251,7 +251,7 @@ class TaskCommandRouterClient:
|
|
|
251
251
|
host,
|
|
252
252
|
port,
|
|
253
253
|
ssl=ssl_context,
|
|
254
|
-
config=create_channel_config(),
|
|
254
|
+
config=create_channel_config(sustained_keepalive=True),
|
|
255
255
|
closed_error_message="Unable to perform operation on a detached sandbox",
|
|
256
256
|
)
|
|
257
257
|
|
|
@@ -56,6 +56,7 @@ from .retries import Retries
|
|
|
56
56
|
from .running_app import RunningApp
|
|
57
57
|
from .schedule import Schedule
|
|
58
58
|
from .secret import _Secret
|
|
59
|
+
from .types import AppInfo
|
|
59
60
|
from .volume import _Volume
|
|
60
61
|
|
|
61
62
|
_default_image: _Image = _Image.debian_slim()
|
|
@@ -182,6 +183,7 @@ class _App:
|
|
|
182
183
|
# Metadata for loading objects within this app
|
|
183
184
|
# passed by reference to functions and classes so it can be updated by run()/deploy()
|
|
184
185
|
_root_load_context: LoadContext
|
|
186
|
+
_info: AppInfo | None
|
|
185
187
|
|
|
186
188
|
@property
|
|
187
189
|
def _local_state(self) -> _LocalAppState:
|
|
@@ -366,6 +368,7 @@ class _App:
|
|
|
366
368
|
app._client = client
|
|
367
369
|
app._root_load_context = LoadContext(client=client, environment_name=environment_name, app_id=response.app_id)
|
|
368
370
|
app._running_app = RunningApp(response.app_id, interactive=False)
|
|
371
|
+
app._info = AppInfo._from_proto(response.handle_metadata, response.app_id)
|
|
369
372
|
return app
|
|
370
373
|
|
|
371
374
|
async def get_dashboard_url(self) -> str:
|
|
@@ -1655,5 +1658,37 @@ class _App:
|
|
|
1655
1658
|
|
|
1656
1659
|
return _AppLogsManager(self)
|
|
1657
1660
|
|
|
1661
|
+
async def info(self, refresh: bool = False) -> AppInfo:
|
|
1662
|
+
"""Return information for a modal `App`.
|
|
1663
|
+
|
|
1664
|
+
The information returned includes the App's ID, member functions and servers,
|
|
1665
|
+
as well as basic lifecycle information, e.g. who created the app and when.
|
|
1666
|
+
|
|
1667
|
+
Args:
|
|
1668
|
+
refresh: Whether to fetch the latest info. By default, false, so
|
|
1669
|
+
the info corresponds the App state at the time of the previous lookup.
|
|
1670
|
+
|
|
1671
|
+
See also:
|
|
1672
|
+
- [`AppInfo`](https://modal.com/docs/sdk/py/latest/types#appinfo)
|
|
1673
|
+
|
|
1674
|
+
Returns:
|
|
1675
|
+
`AppInfo` object.
|
|
1676
|
+
"""
|
|
1677
|
+
if hasattr(self, "_info") and self._info and not refresh:
|
|
1678
|
+
return AppInfo(
|
|
1679
|
+
app_id=self._info.app_id,
|
|
1680
|
+
description=self._info.description,
|
|
1681
|
+
functions=self._info.functions,
|
|
1682
|
+
servers=self._info.servers,
|
|
1683
|
+
lifecycle=self._info.lifecycle,
|
|
1684
|
+
)
|
|
1685
|
+
if not self._app_id:
|
|
1686
|
+
raise InvalidError("`app.info` requires a running or stopped app.")
|
|
1687
|
+
request = api_pb2.AppGetInfoRequest(app_id=self._app_id)
|
|
1688
|
+
client = self._client or await _Client.from_env()
|
|
1689
|
+
resp = await client.stub.AppGetInfo(request)
|
|
1690
|
+
self._info = AppInfo._from_proto(resp.info, self._app_id)
|
|
1691
|
+
return self._info
|
|
1692
|
+
|
|
1658
1693
|
|
|
1659
1694
|
App = synchronize_api(_App)
|
|
@@ -20,6 +20,7 @@ import modal.running_app
|
|
|
20
20
|
import modal.schedule
|
|
21
21
|
import modal.secret
|
|
22
22
|
import modal.server
|
|
23
|
+
import modal.types
|
|
23
24
|
import modal.volume
|
|
24
25
|
import pathlib
|
|
25
26
|
import synchronicity.combined_types
|
|
@@ -155,6 +156,7 @@ class _App:
|
|
|
155
156
|
_running_app: typing.Optional[modal.running_app.RunningApp]
|
|
156
157
|
_client: typing.Optional[modal.client._Client]
|
|
157
158
|
_root_load_context: modal._load_context.LoadContext
|
|
159
|
+
_info: typing.Optional[modal.types.AppInfo]
|
|
158
160
|
|
|
159
161
|
@property
|
|
160
162
|
def _local_state(self) -> _LocalAppState:
|
|
@@ -940,6 +942,24 @@ class _App:
|
|
|
940
942
|
"""
|
|
941
943
|
...
|
|
942
944
|
|
|
945
|
+
async def info(self, refresh: bool = False) -> modal.types.AppInfo:
|
|
946
|
+
"""Return information for a modal `App`.
|
|
947
|
+
|
|
948
|
+
The information returned includes the App's ID, member functions and servers,
|
|
949
|
+
as well as basic lifecycle information, e.g. who created the app and when.
|
|
950
|
+
|
|
951
|
+
Args:
|
|
952
|
+
refresh: Whether to fetch the latest info. By default, false, so
|
|
953
|
+
the info corresponds the App state at the time of the previous lookup.
|
|
954
|
+
|
|
955
|
+
See also:
|
|
956
|
+
- [`AppInfo`](https://modal.com/docs/sdk/py/latest/types#appinfo)
|
|
957
|
+
|
|
958
|
+
Returns:
|
|
959
|
+
`AppInfo` object.
|
|
960
|
+
"""
|
|
961
|
+
...
|
|
962
|
+
|
|
943
963
|
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
|
944
964
|
|
|
945
965
|
class App:
|
|
@@ -983,6 +1003,7 @@ class App:
|
|
|
983
1003
|
_running_app: typing.Optional[modal.running_app.RunningApp]
|
|
984
1004
|
_client: typing.Optional[modal.client.Client]
|
|
985
1005
|
_root_load_context: modal._load_context.LoadContext
|
|
1006
|
+
_info: typing.Optional[modal.types.AppInfo]
|
|
986
1007
|
|
|
987
1008
|
def __init__(
|
|
988
1009
|
self,
|
|
@@ -2012,4 +2033,43 @@ class App:
|
|
|
2012
2033
|
"""
|
|
2013
2034
|
...
|
|
2014
2035
|
|
|
2036
|
+
class __info_spec(typing_extensions.Protocol):
|
|
2037
|
+
def __call__(self, /, refresh: bool = False) -> modal.types.AppInfo:
|
|
2038
|
+
"""Return information for a modal `App`.
|
|
2039
|
+
|
|
2040
|
+
The information returned includes the App's ID, member functions and servers,
|
|
2041
|
+
as well as basic lifecycle information, e.g. who created the app and when.
|
|
2042
|
+
|
|
2043
|
+
Args:
|
|
2044
|
+
refresh: Whether to fetch the latest info. By default, false, so
|
|
2045
|
+
the info corresponds the App state at the time of the previous lookup.
|
|
2046
|
+
|
|
2047
|
+
See also:
|
|
2048
|
+
- [`AppInfo`](https://modal.com/docs/sdk/py/latest/types#appinfo)
|
|
2049
|
+
|
|
2050
|
+
Returns:
|
|
2051
|
+
`AppInfo` object.
|
|
2052
|
+
"""
|
|
2053
|
+
...
|
|
2054
|
+
|
|
2055
|
+
async def aio(self, /, refresh: bool = False) -> modal.types.AppInfo:
|
|
2056
|
+
"""Return information for a modal `App`.
|
|
2057
|
+
|
|
2058
|
+
The information returned includes the App's ID, member functions and servers,
|
|
2059
|
+
as well as basic lifecycle information, e.g. who created the app and when.
|
|
2060
|
+
|
|
2061
|
+
Args:
|
|
2062
|
+
refresh: Whether to fetch the latest info. By default, false, so
|
|
2063
|
+
the info corresponds the App state at the time of the previous lookup.
|
|
2064
|
+
|
|
2065
|
+
See also:
|
|
2066
|
+
- [`AppInfo`](https://modal.com/docs/sdk/py/latest/types#appinfo)
|
|
2067
|
+
|
|
2068
|
+
Returns:
|
|
2069
|
+
`AppInfo` object.
|
|
2070
|
+
"""
|
|
2071
|
+
...
|
|
2072
|
+
|
|
2073
|
+
info: __info_spec
|
|
2074
|
+
|
|
2015
2075
|
_default_image: modal._image._Image
|
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
import time
|
|
5
5
|
import warnings
|
|
6
6
|
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from json import dumps
|
|
7
8
|
from typing import Literal, get_args
|
|
8
9
|
|
|
9
10
|
import click
|
|
@@ -709,3 +710,79 @@ async def dashboard(
|
|
|
709
710
|
app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
|
|
710
711
|
url = f"https://modal.com/id/{app_id}"
|
|
711
712
|
open_url_and_display(url, "App dashboard")
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
@app_cli.command("info", no_args_is_help=True)
|
|
716
|
+
@click.argument("app_identifier")
|
|
717
|
+
@env_option
|
|
718
|
+
@click.option("--json", is_flag=True, default=False)
|
|
719
|
+
@synchronizer.create_blocking
|
|
720
|
+
async def info(app_identifier: str, *, env: str | None = None, json: bool = False):
|
|
721
|
+
"""Show an App's lifecycle, Functions, and Servers.
|
|
722
|
+
|
|
723
|
+
Examples:
|
|
724
|
+
|
|
725
|
+
Get info based on an App ID:
|
|
726
|
+
|
|
727
|
+
```
|
|
728
|
+
modal app info ap-123456
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Get info for a currently deployed App based on its name:
|
|
732
|
+
|
|
733
|
+
```
|
|
734
|
+
modal app info my-app
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
"""
|
|
738
|
+
env = ensure_env(env)
|
|
739
|
+
client = await _Client.from_env()
|
|
740
|
+
app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
|
|
741
|
+
request = api_pb2.AppGetInfoRequest(app_id=app_id)
|
|
742
|
+
resp: api_pb2.AppGetInfoResponse = await client.stub.AppGetInfo(request)
|
|
743
|
+
app_info = resp.info
|
|
744
|
+
lifecycle = app_info.lifecycle
|
|
745
|
+
output = OutputManager.get()
|
|
746
|
+
state = APP_STATE_TO_MESSAGE.get(lifecycle.app_state, Text("unknown", style="gray"))
|
|
747
|
+
|
|
748
|
+
if json:
|
|
749
|
+
output.print_json(
|
|
750
|
+
dumps(
|
|
751
|
+
{
|
|
752
|
+
"description": app_info.description,
|
|
753
|
+
"app_id": app_info.app_id,
|
|
754
|
+
"lifecycle": {
|
|
755
|
+
"state": state.plain,
|
|
756
|
+
"created_at": timestamp_to_localized_str(lifecycle.created_at, json),
|
|
757
|
+
"created_by": lifecycle.created_by or None,
|
|
758
|
+
"deployed_at": timestamp_to_localized_str(lifecycle.deployed_at, json),
|
|
759
|
+
"deployed_by": lifecycle.deployed_by or None,
|
|
760
|
+
"version": lifecycle.version or None,
|
|
761
|
+
"stopped_at": timestamp_to_localized_str(lifecycle.stopped_at, json),
|
|
762
|
+
"stopped_by": lifecycle.stopped_by or None,
|
|
763
|
+
},
|
|
764
|
+
"functions": dict(sorted(app_info.functions.items())),
|
|
765
|
+
"servers": dict(sorted(app_info.servers.items())),
|
|
766
|
+
}
|
|
767
|
+
)
|
|
768
|
+
)
|
|
769
|
+
return
|
|
770
|
+
|
|
771
|
+
output.print(
|
|
772
|
+
Text.assemble("App: ", (app_info.app_id, "bold"), " - ", (app_info.description, "bold"), " (", state, ")")
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
if app_info.functions:
|
|
776
|
+
display_table(["Name", "Function ID"], sorted(app_info.functions.items()), title="Functions")
|
|
777
|
+
if app_info.servers:
|
|
778
|
+
display_table(["Name", "Function ID"], sorted(app_info.servers.items()), title="Servers")
|
|
779
|
+
|
|
780
|
+
events: list[list[Text | str | None]] = [
|
|
781
|
+
["Created", timestamp_to_localized_str(lifecycle.created_at, json), lifecycle.created_by]
|
|
782
|
+
]
|
|
783
|
+
if lifecycle.deployed_at:
|
|
784
|
+
label = f"Deployed (v{lifecycle.version})" if lifecycle.version else "Deployed"
|
|
785
|
+
events.append([label, timestamp_to_localized_str(lifecycle.deployed_at, json), lifecycle.deployed_by])
|
|
786
|
+
if lifecycle.stopped_at:
|
|
787
|
+
events.append(["Stopped", timestamp_to_localized_str(lifecycle.stopped_at, json), lifecycle.stopped_by])
|
|
788
|
+
display_table(["Event", "Time", "By"], events, title="Lifecycle")
|
|
@@ -77,7 +77,7 @@ class _Client:
|
|
|
77
77
|
server_url: str,
|
|
78
78
|
client_type: int,
|
|
79
79
|
credentials: typing.Optional[tuple[str, str]],
|
|
80
|
-
version: str = "1.5.6.
|
|
80
|
+
version: str = "1.5.6.dev7",
|
|
81
81
|
*,
|
|
82
82
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
83
83
|
):
|
|
@@ -277,7 +277,7 @@ class Client:
|
|
|
277
277
|
server_url: str,
|
|
278
278
|
client_type: int,
|
|
279
279
|
credentials: typing.Optional[tuple[str, str]],
|
|
280
|
-
version: str = "1.5.6.
|
|
280
|
+
version: str = "1.5.6.dev7",
|
|
281
281
|
*,
|
|
282
282
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
283
283
|
):
|
|
@@ -122,7 +122,9 @@ class _CloudBucketMount:
|
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
def cloud_bucket_mounts_to_proto(
|
|
125
|
-
mounts: Sequence[tuple[str, _CloudBucketMount]],
|
|
125
|
+
mounts: Sequence[tuple[str, _CloudBucketMount]],
|
|
126
|
+
split_ephemeral_credentials: bool = False,
|
|
127
|
+
include_secrets: bool = True,
|
|
126
128
|
) -> tuple[list[api_pb2.CloudBucketMount], dict[str, api_pb2.StringMap]]:
|
|
127
129
|
"""mdmd:hidden
|
|
128
130
|
Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server.
|
|
@@ -158,11 +160,11 @@ def cloud_bucket_mounts_to_proto(
|
|
|
158
160
|
key_prefix = mount.key_prefix
|
|
159
161
|
|
|
160
162
|
credentials_secret_id = ""
|
|
161
|
-
if secret := mount.secret:
|
|
163
|
+
if include_secrets and (secret := mount.secret):
|
|
162
164
|
if split_ephemeral_credentials and secret._load_env_dict:
|
|
163
165
|
env_dict = secret._load_env_dict()
|
|
164
|
-
|
|
165
|
-
cloud_bucket_credentials[path] =
|
|
166
|
+
credentials = api_pb2.StringMap(contents=env_dict)
|
|
167
|
+
cloud_bucket_credentials[path] = credentials
|
|
166
168
|
else:
|
|
167
169
|
credentials_secret_id = secret.object_id
|
|
168
170
|
|
|
@@ -126,7 +126,9 @@ class _CloudBucketMount:
|
|
|
126
126
|
...
|
|
127
127
|
|
|
128
128
|
def cloud_bucket_mounts_to_proto(
|
|
129
|
-
mounts: collections.abc.Sequence[tuple[str, _CloudBucketMount]],
|
|
129
|
+
mounts: collections.abc.Sequence[tuple[str, _CloudBucketMount]],
|
|
130
|
+
split_ephemeral_credentials: bool = False,
|
|
131
|
+
include_secrets: bool = True,
|
|
130
132
|
) -> tuple[list[modal_proto.api_pb2.CloudBucketMount], dict[str, modal_proto.api_pb2.StringMap]]:
|
|
131
133
|
"""mdmd:hidden
|
|
132
134
|
Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server.
|