modal 1.4.4.dev15__tar.gz → 1.4.4.dev16__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.4.4.dev15 → modal-1.4.4.dev16}/PKG-INFO +1 -1
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_container_entrypoint.py +158 -95
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/entry_point.py +2 -0
- modal-1.4.4.dev16/modal/cli/skills.py +310 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/client.pyi +2 -2
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/functions.pyi +6 -6
- modal-1.4.4.dev16/modal/skills/modal/SKILL.md +86 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal.egg-info/SOURCES.txt +2 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/api_pb2.py +786 -786
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/api_pb2.pyi +25 -3
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_version/__init__.py +1 -1
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/pyproject.toml +1 -1
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/LICENSE +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/README.md +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/__main__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_billing.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_clustered_functions.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_environments.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_function_variants.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_functions.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_grpc_client.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_image.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_ipython.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_load_context.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_location.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_logs.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_object.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_output/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_output/manager.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_output/pty.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_output/rich.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_output/status.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_partial_function.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_resolver.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_resources.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_serialization.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_server.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_traceback.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_tunnel.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_tunnel.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_type_manager.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/logger.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_vendor/version.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_watcher.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/_workspace.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/app.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/app.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/billing.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/builder/README.md +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/builder/base-images.json +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/call_graph.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/_download.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/_help.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/_traceback.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/app.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/billing.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/bootstrap.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/changelog.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/cluster.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/config.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/container.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/dashboard.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/dict.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/environment.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/import_refs.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/launch.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/logo.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/profile.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/queues.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/run.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/secret.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/selector.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/shell.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/token.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/utils.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cli/volume.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/client.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cls.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/cls.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/config.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/container_process.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/container_process.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/dict.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/dict.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/environments.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/environments.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/exception.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/experimental/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/experimental/flash.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/experimental/ipython.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/file_io.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/file_io.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/functions.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/image.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/image.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/io_streams.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/io_streams.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/mount.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/mount.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/network_file_system.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/network_file_system.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/object.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/object.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/output.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/parallel_map.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/parallel_map.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/partial_function.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/partial_function.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/proxy.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/proxy.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/py.typed +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/queue.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/queue.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/retries.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/runner.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/runner.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/running_app.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/sandbox.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/sandbox.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/sandbox_fs.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/schedule.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/scheduler_placement.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/secret.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/secret.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/server.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/server.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/serving.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/serving.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/snapshot.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/snapshot.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/stream_type.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/token_flow.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/token_flow.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/volume.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/volume.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/workspace.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal/workspace.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_docs/mdmd/types.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/__init__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/api_grpc.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/py.typed +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/modal_version/__main__.py +0 -0
- {modal-1.4.4.dev15 → modal-1.4.4.dev16}/setup.cfg +0 -0
|
@@ -14,7 +14,6 @@ if telemetry_socket:
|
|
|
14
14
|
from ._runtime.telemetry import instrument_imports
|
|
15
15
|
|
|
16
16
|
instrument_imports(telemetry_socket)
|
|
17
|
-
|
|
18
17
|
import asyncio
|
|
19
18
|
import queue
|
|
20
19
|
import signal
|
|
@@ -41,10 +40,11 @@ from ._runtime.container_io_manager import (
|
|
|
41
40
|
ContainerIOManager,
|
|
42
41
|
IOContext,
|
|
43
42
|
)
|
|
44
|
-
from ._runtime.task_lifecycle_manager import UserException
|
|
43
|
+
from ._runtime.task_lifecycle_manager import TaskLifecycleManager, UserException
|
|
45
44
|
|
|
46
45
|
if TYPE_CHECKING:
|
|
47
46
|
import modal._runtime.container_io_manager
|
|
47
|
+
import modal._runtime.task_lifecycle_manager
|
|
48
48
|
import modal._runtime.user_code_imports
|
|
49
49
|
|
|
50
50
|
|
|
@@ -287,12 +287,146 @@ def get_serialized_user_class_and_function(
|
|
|
287
287
|
return ser_usr_cls, ser_fun
|
|
288
288
|
|
|
289
289
|
|
|
290
|
-
def
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
def get_server_service(
|
|
291
|
+
container_args: api_pb2.ContainerArguments, function_def: api_pb2.Function, _client: _Client, client: Client
|
|
292
|
+
) -> Service:
|
|
293
|
+
ser_usr_cls, ser_fun = get_serialized_user_class_and_function(function_def, _client)
|
|
294
|
+
service_base_function_id = container_args.app_layout.function_ids[function_def.function_name]
|
|
295
|
+
service_function_hydration_data = [
|
|
296
|
+
o for o in container_args.app_layout.objects if o.object_id == service_base_function_id
|
|
297
|
+
][0]
|
|
298
|
+
service = import_server_service(
|
|
299
|
+
function_def,
|
|
300
|
+
service_function_hydration_data,
|
|
301
|
+
client,
|
|
302
|
+
ser_usr_cls,
|
|
303
|
+
)
|
|
304
|
+
return service
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def get_class_service(
|
|
308
|
+
container_args: api_pb2.ContainerArguments, function_def: api_pb2.Function, _client: _Client, client: Client
|
|
309
|
+
) -> Service:
|
|
310
|
+
ser_usr_cls, ser_fun = get_serialized_user_class_and_function(function_def, _client)
|
|
311
|
+
if container_args.serialized_params:
|
|
312
|
+
param_args, param_kwargs = deserialize_params(container_args.serialized_params, function_def, _client)
|
|
313
|
+
else:
|
|
314
|
+
param_args = ()
|
|
315
|
+
param_kwargs = {}
|
|
316
|
+
# this is a bit ugly - match the function and class based on function name to get metadata
|
|
317
|
+
# This metadata is required in order to hydrate the class in case it's not globally
|
|
318
|
+
# decorated (or serialized)
|
|
319
|
+
service_base_function_id = container_args.app_layout.function_ids[function_def.function_name]
|
|
320
|
+
service_function_hydration_data = [
|
|
321
|
+
o for o in container_args.app_layout.objects if o.object_id == service_base_function_id
|
|
322
|
+
][0]
|
|
323
|
+
class_id = container_args.app_layout.class_ids[function_def.function_name.removesuffix(".*")]
|
|
324
|
+
|
|
325
|
+
service = import_class_service(
|
|
326
|
+
function_def,
|
|
327
|
+
service_function_hydration_data,
|
|
328
|
+
class_id,
|
|
329
|
+
client,
|
|
330
|
+
ser_usr_cls,
|
|
331
|
+
param_args,
|
|
332
|
+
param_kwargs,
|
|
333
|
+
)
|
|
334
|
+
return service
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def get_function_service(function_def: api_pb2.Function, _client: _Client) -> Service:
|
|
338
|
+
ser_usr_cls, ser_fun = get_serialized_user_class_and_function(function_def, _client)
|
|
339
|
+
assert ser_usr_cls is None
|
|
340
|
+
service = import_single_function_service(
|
|
341
|
+
function_def,
|
|
342
|
+
ser_fun,
|
|
343
|
+
)
|
|
344
|
+
return service
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def hydrate_function(
|
|
348
|
+
container_args: api_pb2.ContainerArguments,
|
|
349
|
+
task_lifecycle_manager: TaskLifecycleManager,
|
|
350
|
+
function_def: api_pb2.Function,
|
|
351
|
+
_client: _Client,
|
|
352
|
+
client: Client,
|
|
353
|
+
) -> Service:
|
|
294
354
|
active_app: _App
|
|
295
355
|
service: Service
|
|
356
|
+
with task_lifecycle_manager.handle_task_lifecycle_exception():
|
|
357
|
+
with execution_context._import_context():
|
|
358
|
+
if function_def.is_server:
|
|
359
|
+
service = get_server_service(container_args, function_def, _client, client)
|
|
360
|
+
elif function_def.is_class:
|
|
361
|
+
service = get_class_service(container_args, function_def, _client, client)
|
|
362
|
+
else:
|
|
363
|
+
service = get_function_service(function_def, _client)
|
|
364
|
+
|
|
365
|
+
active_app = service.app
|
|
366
|
+
|
|
367
|
+
# Get ids and metadata for objects (primarily functions and classes) on the app
|
|
368
|
+
container_app: RunningApp = running_app_from_layout(container_args.app_id, container_args.app_layout)
|
|
369
|
+
|
|
370
|
+
# Initialize objects on the app.
|
|
371
|
+
# This is basically only functions and classes - anything else is deprecated and will be unsupported soon
|
|
372
|
+
app: App = cast(App, synchronizer._translate_out(active_app))
|
|
373
|
+
app._init_container(client, container_app)
|
|
374
|
+
|
|
375
|
+
# Hydrate all function dependencies.
|
|
376
|
+
# TODO(erikbern): we an remove this once we
|
|
377
|
+
# 1. Enable lazy hydration for all objects
|
|
378
|
+
# 2. Fully deprecate .new() objects
|
|
379
|
+
if service.service_deps is not None: # this is not set for serialized or non-global scope functions
|
|
380
|
+
dep_object_ids: list[str] = [dep.object_id for dep in function_def.object_dependencies]
|
|
381
|
+
if len(service.service_deps) != len(dep_object_ids):
|
|
382
|
+
raise ExecutionError(
|
|
383
|
+
f"Function has {len(service.service_deps)} dependencies"
|
|
384
|
+
f" but container got {len(dep_object_ids)} object ids.\n"
|
|
385
|
+
f"Code deps: {service.service_deps}\n"
|
|
386
|
+
f"Object ids: {dep_object_ids}\n"
|
|
387
|
+
"\n"
|
|
388
|
+
"This can happen if you are defining Modal objects under a conditional statement "
|
|
389
|
+
"that evaluates differently in the local and remote environments."
|
|
390
|
+
)
|
|
391
|
+
for object_id, obj in zip(dep_object_ids, service.service_deps):
|
|
392
|
+
metadata: Message | None = container_app.object_handle_metadata[object_id]
|
|
393
|
+
obj._hydrate(object_id, _client, metadata)
|
|
394
|
+
|
|
395
|
+
return service
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def run_server(container_args: api_pb2.ContainerArguments, client: Client):
|
|
399
|
+
function_def = container_args.function_def
|
|
400
|
+
_client: _Client = cast(_Client, synchronizer._translate_in(client)) # TODO(erikbern): ugly
|
|
401
|
+
task_lifecycle_manager = TaskLifecycleManager(
|
|
402
|
+
container_args.task_id,
|
|
403
|
+
container_args.function_id,
|
|
404
|
+
container_args.function_def,
|
|
405
|
+
container_args.checkpoint_id or None,
|
|
406
|
+
client,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
service: Service = hydrate_function(container_args, task_lifecycle_manager, function_def, _client, client)
|
|
410
|
+
# Initialize clustered functions.
|
|
411
|
+
if function_def._experimental_group_size > 0:
|
|
412
|
+
initialize_clustered_function(
|
|
413
|
+
client,
|
|
414
|
+
container_args.task_id,
|
|
415
|
+
function_def._experimental_group_size,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
with UserCodeEventLoop() as event_loop:
|
|
419
|
+
with service.lifecycle_context(
|
|
420
|
+
event_loop,
|
|
421
|
+
task_lifecycle_manager=task_lifecycle_manager,
|
|
422
|
+
):
|
|
423
|
+
call_server(event_loop)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def run_function(container_args: api_pb2.ContainerArguments, client: Client):
|
|
427
|
+
# This is a bit weird but we need both the blocking and async versions of ContainerIOManager.
|
|
428
|
+
# At some point, we should fix that by having built-in support for running "user code"
|
|
429
|
+
container_io_manager: Any = ContainerIOManager(container_args, client)
|
|
296
430
|
function_def = container_args.function_def
|
|
297
431
|
# The worker sets this flag to "1" for snapshot and restore tasks. Otherwise, this flag is unset,
|
|
298
432
|
# in which case snapshots should be disabled.
|
|
@@ -306,92 +440,17 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
|
306
440
|
container_io_manager.hello()
|
|
307
441
|
|
|
308
442
|
with container_io_manager.heartbeats(is_snapshotting_function), UserCodeEventLoop() as event_loop:
|
|
309
|
-
# If this is a serialized function, fetch the definition from the server
|
|
310
|
-
ser_usr_cls, ser_fun = get_serialized_user_class_and_function(function_def, _client)
|
|
311
|
-
|
|
312
443
|
# Initialize the function, importing user code.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
service_function_hydration_data = [
|
|
324
|
-
o for o in container_args.app_layout.objects if o.object_id == service_base_function_id
|
|
325
|
-
][0]
|
|
326
|
-
service = import_server_service(
|
|
327
|
-
function_def,
|
|
328
|
-
service_function_hydration_data,
|
|
329
|
-
client,
|
|
330
|
-
ser_usr_cls,
|
|
331
|
-
)
|
|
332
|
-
elif function_def.is_class:
|
|
333
|
-
# this is a bit ugly - match the function and class based on function name to get metadata
|
|
334
|
-
# This metadata is required in order to hydrate the class in case it's not globally
|
|
335
|
-
# decorated (or serialized)
|
|
336
|
-
service_base_function_id = container_args.app_layout.function_ids[function_def.function_name]
|
|
337
|
-
service_function_hydration_data = [
|
|
338
|
-
o for o in container_args.app_layout.objects if o.object_id == service_base_function_id
|
|
339
|
-
][0]
|
|
340
|
-
class_id = container_args.app_layout.class_ids[function_def.function_name.removesuffix(".*")]
|
|
341
|
-
|
|
342
|
-
service = import_class_service(
|
|
343
|
-
function_def,
|
|
344
|
-
service_function_hydration_data,
|
|
345
|
-
class_id,
|
|
346
|
-
client,
|
|
347
|
-
ser_usr_cls,
|
|
348
|
-
param_args,
|
|
349
|
-
param_kwargs,
|
|
350
|
-
)
|
|
351
|
-
else:
|
|
352
|
-
assert ser_usr_cls is None
|
|
353
|
-
service = import_single_function_service(
|
|
354
|
-
function_def,
|
|
355
|
-
ser_fun,
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
active_app = service.app
|
|
359
|
-
|
|
360
|
-
if function_def.pty_info.pty_type == api_pb2.PTYInfo.PTY_TYPE_SHELL:
|
|
361
|
-
# Concurrency and batching doesn't apply for `modal shell`.
|
|
362
|
-
batch_max_size = 0
|
|
363
|
-
batch_wait_ms = 0
|
|
364
|
-
else:
|
|
365
|
-
batch_max_size = function_def.batch_max_size or 0
|
|
366
|
-
batch_wait_ms = function_def.batch_linger_ms or 0
|
|
367
|
-
|
|
368
|
-
# Get ids and metadata for objects (primarily functions and classes) on the app
|
|
369
|
-
container_app: RunningApp = running_app_from_layout(container_args.app_id, container_args.app_layout)
|
|
370
|
-
|
|
371
|
-
# Initialize objects on the app.
|
|
372
|
-
# This is basically only functions and classes - anything else is deprecated and will be unsupported soon
|
|
373
|
-
app: App = cast(App, synchronizer._translate_out(active_app))
|
|
374
|
-
app._init_container(client, container_app)
|
|
375
|
-
|
|
376
|
-
# Hydrate all function dependencies.
|
|
377
|
-
# TODO(erikbern): we an remove this once we
|
|
378
|
-
# 1. Enable lazy hydration for all objects
|
|
379
|
-
# 2. Fully deprecate .new() objects
|
|
380
|
-
if service.service_deps is not None: # this is not set for serialized or non-global scope functions
|
|
381
|
-
dep_object_ids: list[str] = [dep.object_id for dep in function_def.object_dependencies]
|
|
382
|
-
if len(service.service_deps) != len(dep_object_ids):
|
|
383
|
-
raise ExecutionError(
|
|
384
|
-
f"Function has {len(service.service_deps)} dependencies"
|
|
385
|
-
f" but container got {len(dep_object_ids)} object ids.\n"
|
|
386
|
-
f"Code deps: {service.service_deps}\n"
|
|
387
|
-
f"Object ids: {dep_object_ids}\n"
|
|
388
|
-
"\n"
|
|
389
|
-
"This can happen if you are defining Modal objects under a conditional statement "
|
|
390
|
-
"that evaluates differently in the local and remote environments."
|
|
391
|
-
)
|
|
392
|
-
for object_id, obj in zip(dep_object_ids, service.service_deps):
|
|
393
|
-
metadata: Message | None = container_app.object_handle_metadata[object_id]
|
|
394
|
-
obj._hydrate(object_id, _client, metadata)
|
|
444
|
+
service: Service = hydrate_function(
|
|
445
|
+
container_args, container_io_manager.get_task_lifecycle_manager(), function_def, _client, client
|
|
446
|
+
)
|
|
447
|
+
if function_def.pty_info.pty_type == api_pb2.PTYInfo.PTY_TYPE_SHELL:
|
|
448
|
+
# Concurrency and batching doesn't apply for `modal shell`.
|
|
449
|
+
batch_max_size = 0
|
|
450
|
+
batch_wait_ms = 0
|
|
451
|
+
else:
|
|
452
|
+
batch_max_size = function_def.batch_max_size or 0
|
|
453
|
+
batch_wait_ms = function_def.batch_linger_ms or 0
|
|
395
454
|
|
|
396
455
|
# Initialize clustered functions.
|
|
397
456
|
if function_def._experimental_group_size > 0:
|
|
@@ -407,10 +466,14 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
|
407
466
|
) as finalized_functions:
|
|
408
467
|
# This context managers handles pre/post snapshot lifecycle of the user code,
|
|
409
468
|
# finalized functions, ASGI/WSGI lifespan, and volume commit.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
469
|
+
call_function(event_loop, container_io_manager, finalized_functions, batch_max_size, batch_wait_ms)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
473
|
+
if container_args.function_def.is_server:
|
|
474
|
+
run_server(container_args, client)
|
|
475
|
+
else:
|
|
476
|
+
run_function(container_args, client)
|
|
414
477
|
|
|
415
478
|
|
|
416
479
|
if __name__ == "__main__":
|
|
@@ -25,6 +25,7 @@ from .network_file_system import nfs_cli
|
|
|
25
25
|
from .profile import profile_cli
|
|
26
26
|
from .queues import queue_cli
|
|
27
27
|
from .secret import secret_cli
|
|
28
|
+
from .skills import skills_cli
|
|
28
29
|
from .token import _new_token, token_cli
|
|
29
30
|
from .volume import volume_cli
|
|
30
31
|
|
|
@@ -117,6 +118,7 @@ entrypoint_cli.add_command(bootstrap, panel="Onboarding")
|
|
|
117
118
|
entrypoint_cli.add_command(config_cli, panel="Configuration")
|
|
118
119
|
entrypoint_cli.add_command(environment_cli, panel="Configuration")
|
|
119
120
|
entrypoint_cli.add_command(profile_cli, panel="Configuration")
|
|
121
|
+
entrypoint_cli.add_command(skills_cli, panel="Configuration")
|
|
120
122
|
entrypoint_cli.add_command(token_cli, panel="Configuration")
|
|
121
123
|
|
|
122
124
|
entrypoint_cli.add_command(billing_cli, panel="Observability")
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
from importlib import resources
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from modal.cli.utils import confirm_or_suggest_yes, yes_option
|
|
14
|
+
from modal.output import OutputManager
|
|
15
|
+
from modal_version import __version__
|
|
16
|
+
|
|
17
|
+
from ._help import ModalGroup
|
|
18
|
+
from .logo import GREEN, print_logo
|
|
19
|
+
|
|
20
|
+
skills_cli = ModalGroup(name="skills", help="Install and update Modal's agent skills.")
|
|
21
|
+
|
|
22
|
+
_SKILL_RESOURCE = "skills/modal/SKILL.md"
|
|
23
|
+
_SKILL_NAME = "modal"
|
|
24
|
+
_LLMS_TXT_URL = "https://modal.com/llms.txt"
|
|
25
|
+
_DOCS_DOWNLOAD_TIMEOUT = 5.0
|
|
26
|
+
_DOCS_DOWNLOAD_CONCURRENCY = 20
|
|
27
|
+
# The slug allows `/` so nested doc paths (e.g. guide/functions/dynamic-config.md) are
|
|
28
|
+
# matched; `_doc_resource_path` guards against `..`/absolute-path traversal.
|
|
29
|
+
_DOC_URL_RE = re.compile(r"https://modal\.com/docs/(examples|guide)/[A-Za-z0-9._~%+/-]+\.md")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _target_dir(*, claude: bool, global_install: bool) -> Path:
|
|
33
|
+
base = Path.home() if global_install else Path.cwd()
|
|
34
|
+
agent_dir = ".claude" if claude else ".agents"
|
|
35
|
+
return base / agent_dir / "skills" / _SKILL_NAME
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _skill_resource():
|
|
39
|
+
return resources.files("modal").joinpath(_SKILL_RESOURCE)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _skill_content_for_install() -> str:
|
|
43
|
+
content = _skill_resource().read_text()
|
|
44
|
+
lines = content.splitlines(keepends=True)
|
|
45
|
+
|
|
46
|
+
closing_idx = next(idx for idx, line in enumerate(lines[1:], start=1) if line.strip() == "---")
|
|
47
|
+
|
|
48
|
+
# Record the SDK version under the frontmatter `metadata` map, where the Agent
|
|
49
|
+
# Skills spec expects non-standard fields (rather than as a top-level key).
|
|
50
|
+
version_line = f' version: "{__version__}"\n'
|
|
51
|
+
metadata_idx = next((idx for idx in range(1, closing_idx) if lines[idx].rstrip() == "metadata:"), None)
|
|
52
|
+
if metadata_idx is None:
|
|
53
|
+
lines.insert(closing_idx, "metadata:\n")
|
|
54
|
+
lines.insert(closing_idx + 1, version_line)
|
|
55
|
+
return "".join(lines)
|
|
56
|
+
|
|
57
|
+
for idx in range(metadata_idx + 1, closing_idx):
|
|
58
|
+
if lines[idx].strip() and not lines[idx][:1].isspace():
|
|
59
|
+
break # reached the next top-level key; the metadata map has no version
|
|
60
|
+
if lines[idx].strip().startswith("version:"):
|
|
61
|
+
lines[idx] = version_line
|
|
62
|
+
return "".join(lines)
|
|
63
|
+
lines.insert(metadata_idx + 1, version_line)
|
|
64
|
+
return "".join(lines)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _target_has_modal_skill(target_dir: Path) -> bool:
|
|
68
|
+
try:
|
|
69
|
+
lines = (target_dir / "SKILL.md").read_text().splitlines()
|
|
70
|
+
except OSError:
|
|
71
|
+
return False
|
|
72
|
+
if not lines or lines[0].strip() != "---":
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
for line in lines[1:]:
|
|
76
|
+
if line.strip() == "---":
|
|
77
|
+
return False
|
|
78
|
+
if line.strip() == f"name: {_SKILL_NAME}":
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _extract_doc_urls(llms_txt: str) -> list[str]:
|
|
84
|
+
urls = []
|
|
85
|
+
seen = set()
|
|
86
|
+
for match in _DOC_URL_RE.finditer(llms_txt):
|
|
87
|
+
url = match.group(0)
|
|
88
|
+
if url not in seen:
|
|
89
|
+
seen.add(url)
|
|
90
|
+
urls.append(url)
|
|
91
|
+
return urls
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _doc_resource_path(target_dir: Path, url: str) -> Path:
|
|
95
|
+
path = urlparse(url).path
|
|
96
|
+
examples_prefix = "/docs/examples/"
|
|
97
|
+
guide_prefix = "/docs/guide/"
|
|
98
|
+
if path.startswith(examples_prefix) and path.endswith(".md"):
|
|
99
|
+
rel_path = Path("examples") / path[len(examples_prefix) :]
|
|
100
|
+
elif path.startswith(guide_prefix) and path.endswith(".md"):
|
|
101
|
+
rel_path = Path("guide") / path[len(guide_prefix) :]
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError(f"Unexpected Modal documentation URL: {url}")
|
|
104
|
+
|
|
105
|
+
if rel_path.is_absolute() or ".." in rel_path.parts:
|
|
106
|
+
raise ValueError(f"Unexpected Modal documentation URL: {url}")
|
|
107
|
+
return target_dir / "references" / rel_path
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _llms_section_body(llms_txt: str, heading: str) -> str | None:
|
|
111
|
+
"""Return the body of a `## {heading}` section of llms.txt, or None if absent."""
|
|
112
|
+
lines = llms_txt.splitlines(keepends=True)
|
|
113
|
+
start = next((idx + 1 for idx, line in enumerate(lines) if line.strip() == f"## {heading}"), None)
|
|
114
|
+
if start is None:
|
|
115
|
+
return None
|
|
116
|
+
end = next((idx for idx in range(start, len(lines)) if lines[idx].startswith("## ")), len(lines))
|
|
117
|
+
return "".join(lines[start:end]).strip("\n")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _write_section_index(target_dir: Path, subdir: str, llms_txt: str) -> None:
|
|
121
|
+
"""Write references/{subdir}/index.md from the matching llms.txt section.
|
|
122
|
+
|
|
123
|
+
The section preserves llms.txt's category groupings and page titles, with links to
|
|
124
|
+
the pages we mirror locally rewritten to relative paths. Links to pages we don't
|
|
125
|
+
download (section roots, non-`.md` pages) are dropped.
|
|
126
|
+
"""
|
|
127
|
+
heading = subdir.capitalize()
|
|
128
|
+
body = _llms_section_body(llms_txt, heading)
|
|
129
|
+
if not body:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
def _localize(match: re.Match[str]) -> str:
|
|
133
|
+
url, section = match.group(0), match.group(1)
|
|
134
|
+
if section != subdir:
|
|
135
|
+
return url # link into another section; left absolute so it is dropped below
|
|
136
|
+
# Keep the path under the section so nested pages stay nested (functions/foo.md).
|
|
137
|
+
return "./" + url.split(f"/docs/{section}/", 1)[1]
|
|
138
|
+
|
|
139
|
+
localized = _DOC_URL_RE.sub(_localize, body)
|
|
140
|
+
# Drop entries still pointing at absolute URLs (i.e. pages we didn't mirror locally).
|
|
141
|
+
kept = [line for line in localized.splitlines() if "](http" not in line]
|
|
142
|
+
index_dir = target_dir / "references" / subdir
|
|
143
|
+
index_dir.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
(index_dir / "index.md").write_text(f"# {heading}\n\n" + "\n".join(kept).strip("\n") + "\n")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _replace_existing_target(target_dir: Path) -> None:
|
|
148
|
+
if target_dir.is_dir() and not target_dir.is_symlink():
|
|
149
|
+
shutil.rmtree(target_dir)
|
|
150
|
+
else:
|
|
151
|
+
target_dir.unlink()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _http_session():
|
|
155
|
+
import aiohttp
|
|
156
|
+
|
|
157
|
+
return aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=_DOCS_DOWNLOAD_TIMEOUT))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def _fetch_text(session, url: str) -> str:
|
|
161
|
+
async with session.get(url) as response:
|
|
162
|
+
response.raise_for_status()
|
|
163
|
+
return await response.text()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def _download_doc(session, target_dir: Path, url: str) -> Exception | None:
|
|
167
|
+
try:
|
|
168
|
+
content = await _fetch_text(session, url)
|
|
169
|
+
output_path = _doc_resource_path(target_dir, url)
|
|
170
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
output_path.write_text(content)
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
return exc
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def _download_docs(target_dir: Path) -> None:
|
|
178
|
+
async with _http_session() as session:
|
|
179
|
+
try:
|
|
180
|
+
llms_txt = await _fetch_text(session, _LLMS_TXT_URL)
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
suffix = f": {str(exc)}" if str(exc) else ""
|
|
183
|
+
click.echo(f"Warning: could not download Modal documentation{suffix}", err=True)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
urls = _extract_doc_urls(llms_txt)
|
|
187
|
+
semaphore = asyncio.Semaphore(_DOCS_DOWNLOAD_CONCURRENCY)
|
|
188
|
+
|
|
189
|
+
async def _download_with_limit(url: str) -> Exception | None:
|
|
190
|
+
async with semaphore:
|
|
191
|
+
return await _download_doc(session, target_dir, url)
|
|
192
|
+
|
|
193
|
+
failures = await asyncio.gather(*(_download_with_limit(url) for url in urls))
|
|
194
|
+
|
|
195
|
+
failed = [(url, failure) for url, failure in zip(urls, failures) if failure is not None]
|
|
196
|
+
failed_count = len(failed)
|
|
197
|
+
if failed_count:
|
|
198
|
+
failed_url, failure = failed[0]
|
|
199
|
+
click.echo(
|
|
200
|
+
f"Warning: failed to download {failed_count} of {len(urls)} Modal documentation resources. "
|
|
201
|
+
f"First failure: {failed_url}: {failure}",
|
|
202
|
+
err=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
for subdir in ("guide", "examples"):
|
|
206
|
+
_write_section_index(target_dir, subdir, llms_txt)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _generate_api_reference_docs(target_dir: Path) -> None:
|
|
210
|
+
"""Generate the API reference locally from the installed client into references/api/.
|
|
211
|
+
|
|
212
|
+
Introspecting the installed `modal` package keeps the reference in sync with the
|
|
213
|
+
user's SDK version and removes the network dependency for this section.
|
|
214
|
+
"""
|
|
215
|
+
import contextlib
|
|
216
|
+
import io
|
|
217
|
+
import warnings
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
from modal_docs.gen_reference_docs import run
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
click.echo(f"Warning: could not generate Modal API reference: {exc}", err=True)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
api_dir = target_dir / "references" / "api"
|
|
226
|
+
with tempfile.TemporaryDirectory() as raw_dir:
|
|
227
|
+
# TODO(michael): doc generation is very noisy by default; make it configurable
|
|
228
|
+
with (
|
|
229
|
+
warnings.catch_warnings(),
|
|
230
|
+
contextlib.redirect_stdout(io.StringIO()),
|
|
231
|
+
contextlib.redirect_stderr(io.StringIO()),
|
|
232
|
+
):
|
|
233
|
+
warnings.simplefilter("ignore")
|
|
234
|
+
run(raw_dir)
|
|
235
|
+
|
|
236
|
+
api_dir.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
for raw_file in Path(raw_dir).glob("*.md"):
|
|
238
|
+
content = raw_file.read_text()
|
|
239
|
+
# Strip the leading mdsvex `<script>` block; it is website-only boilerplate.
|
|
240
|
+
if content.startswith("<script>"):
|
|
241
|
+
content = content.partition("</script>\n")[2].lstrip("\n")
|
|
242
|
+
(api_dir / raw_file.name).write_text(content)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _install_or_update(*, claude: bool, global_install: bool, no_docs: bool, yes: bool, update: bool) -> None:
|
|
246
|
+
target_dir = _target_dir(claude=claude, global_install=global_install)
|
|
247
|
+
action = "Updating" if update else "Installing"
|
|
248
|
+
completed_action = "Updated" if update else "Installed"
|
|
249
|
+
output_manager = OutputManager.get()
|
|
250
|
+
|
|
251
|
+
target_exists = target_dir.exists() or target_dir.is_symlink()
|
|
252
|
+
if target_exists:
|
|
253
|
+
target_has_modal_skill = _target_has_modal_skill(target_dir)
|
|
254
|
+
if not yes and not (update and target_has_modal_skill):
|
|
255
|
+
confirm_or_suggest_yes(f"Replace existing content at {target_dir}?")
|
|
256
|
+
elif update and not yes:
|
|
257
|
+
confirm_or_suggest_yes(f"No existing Modal skill found at {target_dir}. Install it there?")
|
|
258
|
+
|
|
259
|
+
print_logo()
|
|
260
|
+
|
|
261
|
+
# Build the output in a tmp directory so final installation is atomic.
|
|
262
|
+
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
staging_dir = Path(tempfile.mkdtemp(dir=target_dir.parent, prefix=".modal-skill-"))
|
|
264
|
+
try:
|
|
265
|
+
with output_manager.status(f"{action} Modal skill..."):
|
|
266
|
+
(staging_dir / "SKILL.md").write_text(_skill_content_for_install())
|
|
267
|
+
if not no_docs:
|
|
268
|
+
with output_manager.status("Downloading Modal documentation..."):
|
|
269
|
+
asyncio.run(_download_docs(staging_dir))
|
|
270
|
+
_generate_api_reference_docs(staging_dir)
|
|
271
|
+
|
|
272
|
+
if target_exists:
|
|
273
|
+
_replace_existing_target(target_dir)
|
|
274
|
+
staging_dir.rename(target_dir)
|
|
275
|
+
finally:
|
|
276
|
+
if staging_dir.exists():
|
|
277
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
278
|
+
|
|
279
|
+
output_manager.print(f"[{GREEN}]✓[/{GREEN}] {completed_action} Modal skill to {target_dir}", highlight=False)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _install_options(func):
|
|
283
|
+
func = click.option("--claude", is_flag=True, default=False, help="Install to .claude/ rather than .agents/.")(func)
|
|
284
|
+
func = click.option(
|
|
285
|
+
"-g", "--global", "global_install", is_flag=True, default=False, help="Install in the user home directory."
|
|
286
|
+
)(func)
|
|
287
|
+
func = click.option(
|
|
288
|
+
"--no-docs",
|
|
289
|
+
is_flag=True,
|
|
290
|
+
default=False,
|
|
291
|
+
help="Skip downloading Modal documentation resources.",
|
|
292
|
+
)(func)
|
|
293
|
+
return yes_option(func)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@skills_cli.command("install", help="Install Modal skills.")
|
|
297
|
+
@_install_options
|
|
298
|
+
def install(*, claude: bool, global_install: bool, no_docs: bool, yes: bool):
|
|
299
|
+
_install_or_update(claude=claude, global_install=global_install, no_docs=no_docs, yes=yes, update=False)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@skills_cli.command("update", help="Update installed Modal skills.")
|
|
303
|
+
@_install_options
|
|
304
|
+
def update(*, claude: bool, global_install: bool, no_docs: bool, yes: bool):
|
|
305
|
+
_install_or_update(claude=claude, global_install=global_install, no_docs=no_docs, yes=yes, update=True)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@skills_cli.command("show", help="Print Modal skill content to the terminal.")
|
|
309
|
+
def show():
|
|
310
|
+
click.echo(_skill_content_for_install(), nl=False)
|
|
@@ -35,7 +35,7 @@ class _Client:
|
|
|
35
35
|
server_url: str,
|
|
36
36
|
client_type: int,
|
|
37
37
|
credentials: typing.Optional[tuple[str, str]],
|
|
38
|
-
version: str = "1.4.4.
|
|
38
|
+
version: str = "1.4.4.dev16",
|
|
39
39
|
):
|
|
40
40
|
"""mdmd:hidden
|
|
41
41
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -205,7 +205,7 @@ class Client:
|
|
|
205
205
|
server_url: str,
|
|
206
206
|
client_type: int,
|
|
207
207
|
credentials: typing.Optional[tuple[str, str]],
|
|
208
|
-
version: str = "1.4.4.
|
|
208
|
+
version: str = "1.4.4.dev16",
|
|
209
209
|
):
|
|
210
210
|
"""mdmd:hidden
|
|
211
211
|
The Modal client object is not intended to be instantiated directly by users.
|