modal 1.6.4.dev9__tar.gz → 1.6.4.dev11__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.dev9 → modal-1.6.4.dev11}/PKG-INFO +1 -1
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/__init__.py +11 -0
- modal-1.6.4.dev11/modal/_audit_logs/__init__.py +1 -0
- modal-1.6.4.dev11/modal/_audit_logs/_rpcs.py +66 -0
- modal-1.6.4.dev11/modal/_audit_logs/count.py +17 -0
- modal-1.6.4.dev11/modal/_audit_logs/list.py +83 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_function_variants.py +1 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_functions.py +39 -4
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_image.py +24 -6
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_type_manager.py +7 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/name_utils.py +8 -1
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/profile.py +40 -29
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/shell.py +1 -1
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/token.py +83 -14
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/client.py +1 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/client.pyi +2 -2
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/config.py +1 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/functions.pyi +7 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/token_flow.py +32 -2
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/token_flow.pyi +2 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/SOURCES.txt +4 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_grpc.py +48 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_pb2.py +1832 -1633
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_pb2.pyi +447 -14
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_pb2_grpc.py +101 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/api_pb2_grpc.pyi +34 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/modal_api_grpc.py +3 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_version/__init__.py +1 -1
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/pyproject.toml +1 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/LICENSE +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/README.md +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/__main__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_billing.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_cluster.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_cluster.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_container_entrypoint.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_environments.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_grpc_client.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_ipython.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_load_context.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_location.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_logs.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_logs_manager.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_logs_manager.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_metrics.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_metrics.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_object.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_outbound_policy.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_outbound_policy.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/manager.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/pty.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/rich.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_output/status.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_partial_function.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_resolver.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_resources.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/asgi.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/execution_context.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/telemetry.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_serialization.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_server.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_server.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_supports_logs.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_traceback.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_tunnel.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_tunnel.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/app_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/async_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/blob_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/browser_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/curl_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/deprecation.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/display_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/docker_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/function_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/git_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/hash_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/http_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/idle_countdown.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/logger.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/mount_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/oauth.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/package_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/proxy_support.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/shell_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_utils/time_utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/tblib.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_vendor/version.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_watcher.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/_workspace.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/app.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/app.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/billing.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/billing.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2023.12.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2024.04.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2024.10.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2025.06.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/2026.10.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/README.md +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/builder/base-images.json +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/call_graph.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_download.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_help.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_logs.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_stats.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/_traceback.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/app.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/billing.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/changelog.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/config.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/container.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/curl.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/dashboard.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/dict.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/endpoint.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/entry_point.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/environment.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/function.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/image.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/import_refs.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/logo.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/network_file_system.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/queues.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/run.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/secret.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/selector.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/server.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/skills.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/utils.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/volume.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cli/workspace.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cls.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cls.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cluster.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/cluster.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/container_process.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/container_process.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/dict.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/dict.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/environments.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/environments.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/exception.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/_flash_container_entry.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/flash.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/flash.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/flash_server.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/flash_server.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/experimental/ipython.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/file_pattern_matcher.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/functions.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/image.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/image.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/io_streams.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/io_streams.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/mount.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/mount.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/network_file_system.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/network_file_system.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/object.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/object.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/output.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/parallel_map.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/parallel_map.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/partial_function.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/partial_function.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/proxy.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/proxy.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/py.typed +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/queue.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/queue.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/retries.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/runner.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/runner.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/running_app.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_common.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_filesystem.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_impl_persistent.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_impl_v1.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_impl_v2.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_sandbox.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_sidecar.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_snapshot.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/_task_command_router_client.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/sandbox.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/sandbox/sandbox.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/schedule.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/scheduler_placement.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/secret.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/secret.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/server.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/server.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/serving.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/serving.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/stream_type.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/types.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/volume.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/volume.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/workspace.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal/workspace.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/requires.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal.egg-info/top_level.txt +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/mdmd/types.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_docs/reference_intro.md +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/__init__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/py.typed +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/modal_version/__main__.py +0 -0
- {modal-1.6.4.dev9 → modal-1.6.4.dev11}/setup.cfg +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
|
+
import os
|
|
2
3
|
import sys
|
|
3
4
|
|
|
4
5
|
if sys.version_info[:2] < (3, 10):
|
|
@@ -59,6 +60,16 @@ except Exception:
|
|
|
59
60
|
print()
|
|
60
61
|
raise
|
|
61
62
|
|
|
63
|
+
if sys.version_info[:2] == (3, 10) and os.environ.get("MODAL_IS_REMOTE") != "1":
|
|
64
|
+
from ._utils.deprecation import deprecation_warning
|
|
65
|
+
|
|
66
|
+
deprecation_warning(
|
|
67
|
+
(2026, 11, 4),
|
|
68
|
+
"Python 3.10 is EOL and Modal will not support it in version 1.7.0. "
|
|
69
|
+
"Please upgrade your local Python to 3.11 or later.",
|
|
70
|
+
show_source=False,
|
|
71
|
+
)
|
|
72
|
+
|
|
62
73
|
|
|
63
74
|
__all__ = [
|
|
64
75
|
"__version__",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
"""The audit log RPCs, one call each, retried with backoff. Ordering and batching live in list.py."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
8
|
+
|
|
9
|
+
from modal._utils.grpc_utils import Retry
|
|
10
|
+
from modal_proto import api_pb2
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from modal.client import _Client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_Resp = TypeVar("_Resp")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_MAX_RETRIES = 4
|
|
20
|
+
_PLAN_MAX_RETRIES = 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _call(
|
|
24
|
+
rpc: Callable[..., Awaitable[_Resp]], req: object, *, timeout: float, max_retries: int = _MAX_RETRIES
|
|
25
|
+
) -> _Resp:
|
|
26
|
+
"""Call with backoff on timeouts and transient errors; rate and resource limits wait as the server says."""
|
|
27
|
+
retry = Retry(base_delay=1.0, delay_factor=2.0, max_delay=8.0, max_retries=max_retries, attempt_timeout=timeout)
|
|
28
|
+
return await rpc(req, retry=retry)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def get_settings(client: _Client) -> api_pb2.AuditLogSettingsGetResponse:
|
|
32
|
+
return await client._stub.AuditLogSettingsGet(api_pb2.AuditLogSettingsGetRequest())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def count_entries(
|
|
36
|
+
client: _Client, q: str, oldest_ms: int, newest_ms: int, config: api_pb2.AuditLogSettingsGetResponse
|
|
37
|
+
) -> int:
|
|
38
|
+
req = api_pb2.AuditLogCountRequest(q=q, oldest_ms=oldest_ms, newest_ms=newest_ms)
|
|
39
|
+
resp = await _call(client._stub.AuditLogCount, req, timeout=config.request_timeout_secs)
|
|
40
|
+
return resp.count
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def list_page(
|
|
44
|
+
client: _Client,
|
|
45
|
+
q: str,
|
|
46
|
+
oldest_ms: int,
|
|
47
|
+
newest_ms: int,
|
|
48
|
+
limit: int,
|
|
49
|
+
config: api_pb2.AuditLogSettingsGetResponse,
|
|
50
|
+
newest_cursor: api_pb2.AuditLogCursor | None = None,
|
|
51
|
+
oldest_cursor: api_pb2.AuditLogCursor | None = None,
|
|
52
|
+
) -> api_pb2.AuditLogListResponse:
|
|
53
|
+
req = api_pb2.AuditLogListRequest(q=q, oldest_ms=oldest_ms, newest_ms=newest_ms, limit=limit)
|
|
54
|
+
if newest_cursor is not None:
|
|
55
|
+
req.newest_cursor.CopyFrom(newest_cursor)
|
|
56
|
+
if oldest_cursor is not None:
|
|
57
|
+
req.oldest_cursor.CopyFrom(oldest_cursor)
|
|
58
|
+
return await _call(client._stub.AuditLogList, req, timeout=config.request_timeout_secs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def plan_buckets(
|
|
62
|
+
client: _Client, q: str, oldest_ms: int, newest_ms: int, config: api_pb2.AuditLogSettingsGetResponse
|
|
63
|
+
) -> list[api_pb2.AuditLogCursor]:
|
|
64
|
+
req = api_pb2.AuditLogBucketsRequest(q=q, oldest_ms=oldest_ms, newest_ms=newest_ms, bucket_size=config.bucket_size)
|
|
65
|
+
resp = await _call(client._stub.AuditLogPlan, req, timeout=config.plan_timeout_secs, max_retries=_PLAN_MAX_RETRIES)
|
|
66
|
+
return list(resp.cursors)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from modal._audit_logs import _rpcs
|
|
7
|
+
from modal_proto import api_pb2
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from modal.client import _Client
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def count_audit_logs(
|
|
14
|
+
client: _Client, q: str, oldest_ms: int, newest_ms: int, config: api_pb2.AuditLogSettingsGetResponse
|
|
15
|
+
) -> int:
|
|
16
|
+
"""Count the entries in [oldest_ms, newest_ms) that match the query."""
|
|
17
|
+
return await _rpcs.count_entries(client, q, oldest_ms, newest_ms, config)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from contextlib import aclosing
|
|
6
|
+
|
|
7
|
+
from modal._audit_logs import _rpcs
|
|
8
|
+
from modal._utils.async_utils import async_map_ordered
|
|
9
|
+
from modal.client import _Client
|
|
10
|
+
from modal_proto import api_pb2
|
|
11
|
+
|
|
12
|
+
# (newest_cursor, oldest_cursor); None where the bucket is open-ended.
|
|
13
|
+
Bucket = tuple[api_pb2.AuditLogCursor | None, api_pb2.AuditLogCursor | None]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def get_bucket_cursors(
|
|
17
|
+
client: _Client, q: str, oldest_ms: int, newest_ms: int, config: api_pb2.AuditLogSettingsGetResponse
|
|
18
|
+
) -> list[Bucket]:
|
|
19
|
+
"""Split the range into buckets, as (newest_cursor, oldest_cursor) pairs ordered oldest to newest.
|
|
20
|
+
|
|
21
|
+
The first bucket has no oldest cursor and the last has no newest cursor.
|
|
22
|
+
"""
|
|
23
|
+
cursors = await _rpcs.plan_buckets(client, q, oldest_ms, newest_ms, config)
|
|
24
|
+
return list(zip([None, *cursors], [*cursors, None]))[::-1]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _fetch_bucket(
|
|
28
|
+
client: _Client,
|
|
29
|
+
q: str,
|
|
30
|
+
oldest_ms: int,
|
|
31
|
+
newest_ms: int,
|
|
32
|
+
config: api_pb2.AuditLogSettingsGetResponse,
|
|
33
|
+
bucket: Bucket,
|
|
34
|
+
) -> list[api_pb2.AuditLogEntry]:
|
|
35
|
+
"""Read one bucket's entries, oldest to newest, using pagination"""
|
|
36
|
+
newest_cursor, oldest_cursor = bucket
|
|
37
|
+
entries: list[api_pb2.AuditLogEntry] = []
|
|
38
|
+
while True:
|
|
39
|
+
resp = await _rpcs.list_page(
|
|
40
|
+
client, q, oldest_ms, newest_ms, config.page_size, config, newest_cursor, oldest_cursor
|
|
41
|
+
)
|
|
42
|
+
entries.extend(resp.items)
|
|
43
|
+
# Skips the empty request a full last page would otherwise trigger; an unset next_cursor also ends the loop.
|
|
44
|
+
if oldest_cursor is not None and resp.items and resp.items[-1].id == oldest_cursor.id:
|
|
45
|
+
break
|
|
46
|
+
if not resp.HasField("next_cursor"):
|
|
47
|
+
break
|
|
48
|
+
newest_cursor = resp.next_cursor
|
|
49
|
+
entries.reverse()
|
|
50
|
+
return entries
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def fetch_audit_logs(
|
|
54
|
+
q: str,
|
|
55
|
+
oldest_ms: int,
|
|
56
|
+
newest_ms: int,
|
|
57
|
+
*,
|
|
58
|
+
client: _Client | None = None,
|
|
59
|
+
config: api_pb2.AuditLogSettingsGetResponse,
|
|
60
|
+
) -> AsyncGenerator[api_pb2.AuditLogEntry]:
|
|
61
|
+
"""Yield the range's entries from the oldest to the newest, one bucket at a time.
|
|
62
|
+
|
|
63
|
+
At most `max_concurrent_requests` buckets are fetched at once, and at most `max_buffered_buckets` are fetched
|
|
64
|
+
or held at once. A new bucket is admitted only after the consumer has taken all of a bucket's entries, so a slow
|
|
65
|
+
consumer or a stalled request cannot grow memory.
|
|
66
|
+
If any request fails after its attempts, the remaining requests are cancelled and the error is raised.
|
|
67
|
+
"""
|
|
68
|
+
if client is None:
|
|
69
|
+
client = await _Client.from_env()
|
|
70
|
+
buckets = await get_bucket_cursors(client, q, oldest_ms, newest_ms, config)
|
|
71
|
+
|
|
72
|
+
async def bucket_stream() -> AsyncGenerator[Bucket]:
|
|
73
|
+
for bucket in buckets:
|
|
74
|
+
yield bucket
|
|
75
|
+
|
|
76
|
+
async def fetch(bucket: Bucket) -> list[api_pb2.AuditLogEntry]:
|
|
77
|
+
return await _fetch_bucket(client, q, oldest_ms, newest_ms, config, bucket)
|
|
78
|
+
|
|
79
|
+
fetched = async_map_ordered(bucket_stream(), fetch, config.max_concurrent_requests, config.max_buffered_buckets)
|
|
80
|
+
async with aclosing(fetched) as stream:
|
|
81
|
+
async for entries in stream:
|
|
82
|
+
for entry in entries:
|
|
83
|
+
yield entry
|
|
@@ -408,6 +408,7 @@ def _make_function_variant(
|
|
|
408
408
|
if not base_function._is_hydrated:
|
|
409
409
|
await base_function.hydrate(load_context.client)
|
|
410
410
|
|
|
411
|
+
base_function._check_not_observability_only()
|
|
411
412
|
assert base_function._client and base_function._client._stub
|
|
412
413
|
|
|
413
414
|
if parameter_schema is None:
|
|
@@ -119,6 +119,15 @@ if TYPE_CHECKING:
|
|
|
119
119
|
import modal.functions
|
|
120
120
|
|
|
121
121
|
MAX_INTERNAL_FAILURE_COUNT = 8
|
|
122
|
+
_OBSERVABILITY_ONLY_APIS = (
|
|
123
|
+
".logs",
|
|
124
|
+
".metrics",
|
|
125
|
+
".info()",
|
|
126
|
+
".stats()",
|
|
127
|
+
".get_current_stats()",
|
|
128
|
+
".get_web_url()",
|
|
129
|
+
".autoscaler.settings()",
|
|
130
|
+
)
|
|
122
131
|
TERMINAL_STATUSES = (
|
|
123
132
|
api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
|
|
124
133
|
api_pb2.GenericResult.GENERIC_STATUS_TERMINATED,
|
|
@@ -651,6 +660,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
651
660
|
_base_function: "_Function | None" = None
|
|
652
661
|
_app_id: str | None = None
|
|
653
662
|
_function_info: FunctionInfo | None = None
|
|
663
|
+
_is_observability_only: bool = False
|
|
654
664
|
|
|
655
665
|
async def _get_log_query_data(self) -> _LogQueryData:
|
|
656
666
|
await self.hydrate()
|
|
@@ -1398,6 +1408,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1398
1408
|
|
|
1399
1409
|
"""
|
|
1400
1410
|
|
|
1411
|
+
self._check_not_observability_only()
|
|
1401
1412
|
deprecation_warning(
|
|
1402
1413
|
(2026, 10, 16),
|
|
1403
1414
|
"`Function.update_autoscaler()` is deprecated and will be removed in a future version "
|
|
@@ -1485,16 +1496,15 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1485
1496
|
# refine the error message
|
|
1486
1497
|
msg = f"Lookup failed for {called_from} '{function_id}': {exc}."
|
|
1487
1498
|
raise NotFoundError(msg) from None
|
|
1488
|
-
if called_from == "Function" and
|
|
1489
|
-
|
|
1490
|
-
raise InvalidError(f"{function_id} is a Server. Use \n ``modal.Server.from_id('{function_id}')``")
|
|
1491
|
-
raise InvalidError(f"{function_id} is a Cls and cannot be loaded with Function.from_id().")
|
|
1499
|
+
if called_from == "Function" and response.function.is_server:
|
|
1500
|
+
raise InvalidError(f"{function_id} is a Server. Use \n ``modal.Server.from_id('{function_id}')``")
|
|
1492
1501
|
if called_from == "Server" and not response.function.is_server:
|
|
1493
1502
|
if response.function.is_class:
|
|
1494
1503
|
raise InvalidError(f"{function_id} is a Cls and cannot be loaded with Server.from_id().")
|
|
1495
1504
|
raise InvalidError(f"{function_id} is a Function. Use \n ``modal.Function.from_id('{function_id}')``")
|
|
1496
1505
|
self._hydrate(function_id, load_context.client, response.handle_metadata)
|
|
1497
1506
|
self._function_info = FunctionInfo._from_function_proto(response.function)
|
|
1507
|
+
self._is_observability_only = called_from == "Function" and response.function.is_class
|
|
1498
1508
|
|
|
1499
1509
|
rep = f"modal.Function.from_id('{function_id}')"
|
|
1500
1510
|
return cls._from_loader(
|
|
@@ -1514,6 +1524,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1514
1524
|
object with metadata from Modal servers until the first
|
|
1515
1525
|
time it is actually used.
|
|
1516
1526
|
|
|
1527
|
+
If the ID belongs to a Cls, the Function only supports the following APIs:
|
|
1528
|
+
`.logs`, `.metrics`, `.info()`, `.stats()`, `.get_current_stats()`,
|
|
1529
|
+
`.get_web_url()` and `.autoscaler.settings()`. Use `modal.Cls.from_name(...)`
|
|
1530
|
+
to invoke all other methods.
|
|
1531
|
+
|
|
1517
1532
|
Args:
|
|
1518
1533
|
function_id: ID of the function.
|
|
1519
1534
|
client: Modal client to use; defaults to `Client.from_env()` when omitted.
|
|
@@ -1744,6 +1759,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1744
1759
|
+ f"or call it locally: {self._function_name}.local()"
|
|
1745
1760
|
)
|
|
1746
1761
|
|
|
1762
|
+
def _check_not_observability_only(self):
|
|
1763
|
+
if self._is_observability_only:
|
|
1764
|
+
assert self._function_name
|
|
1765
|
+
class_name = self._function_name.removesuffix(".*")
|
|
1766
|
+
apis = [f"`{name}`" for name in _OBSERVABILITY_ONLY_APIS]
|
|
1767
|
+
allowed_apis = ", ".join(apis[:-1]) + " and " + apis[-1]
|
|
1768
|
+
raise InvalidError(
|
|
1769
|
+
f"`{self.object_id}` belongs to the Cls `{class_name}`. A Function loaded with "
|
|
1770
|
+
f"`Function.from_id` only supports {allowed_apis}. "
|
|
1771
|
+
"Use `modal.Cls.from_name(...)` to invoke all other methods."
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1747
1774
|
@live_method
|
|
1748
1775
|
async def get_web_url(self) -> str | None:
|
|
1749
1776
|
"""URL for addressing a Web Function via HTTP.
|
|
@@ -1779,6 +1806,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1779
1806
|
config_method_name: str,
|
|
1780
1807
|
) -> "_Function[P, ReturnType, OriginalReturnType]":
|
|
1781
1808
|
base_function = self._base_function or self
|
|
1809
|
+
base_function._check_not_observability_only()
|
|
1782
1810
|
combined_options = self._options.merge_options(new_options)
|
|
1783
1811
|
|
|
1784
1812
|
# For "True Functions" (i.e. not the Function inside a Cls), we will use
|
|
@@ -1941,6 +1969,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1941
1969
|
_SynchronizedQueue is used instead of asyncio.Queue so that the main thread can put
|
|
1942
1970
|
items in the queue safely.
|
|
1943
1971
|
"""
|
|
1972
|
+
self._check_not_observability_only()
|
|
1944
1973
|
self._check_no_web_url("map")
|
|
1945
1974
|
if self._is_generator:
|
|
1946
1975
|
raise InvalidError("A generator function cannot be called with `.map(...)`.")
|
|
@@ -1978,6 +2007,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1978
2007
|
|
|
1979
2008
|
@live_method
|
|
1980
2009
|
async def _spawn_map(self, input_queue: _SynchronizedQueue) -> "_FunctionCall[ReturnType]":
|
|
2010
|
+
self._check_not_observability_only()
|
|
1981
2011
|
self._check_no_web_url("spawn_map")
|
|
1982
2012
|
if self._is_generator:
|
|
1983
2013
|
raise InvalidError("A generator function cannot be called with `.spawn_map(...)`.")
|
|
@@ -1995,6 +2025,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1995
2025
|
return fc
|
|
1996
2026
|
|
|
1997
2027
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
|
2028
|
+
self._check_not_observability_only()
|
|
1998
2029
|
invocation: _Invocation | _InputPlaneInvocation
|
|
1999
2030
|
if self._input_plane_url:
|
|
2000
2031
|
invocation = await _InputPlaneInvocation.create(
|
|
@@ -2024,6 +2055,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
2024
2055
|
function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType",
|
|
2025
2056
|
from_spawn_map: bool = False,
|
|
2026
2057
|
) -> _Invocation:
|
|
2058
|
+
self._check_not_observability_only()
|
|
2027
2059
|
return await _Invocation.create(
|
|
2028
2060
|
self,
|
|
2029
2061
|
args,
|
|
@@ -2037,6 +2069,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
2037
2069
|
@live_method_gen
|
|
2038
2070
|
@synchronizer.no_input_translation
|
|
2039
2071
|
async def _call_generator(self, args, kwargs):
|
|
2072
|
+
self._check_not_observability_only()
|
|
2040
2073
|
invocation: _Invocation | _InputPlaneInvocation
|
|
2041
2074
|
if self._input_plane_url:
|
|
2042
2075
|
invocation = await _InputPlaneInvocation.create(
|
|
@@ -2093,6 +2126,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
2093
2126
|
Values produced by the remote generator.
|
|
2094
2127
|
"""
|
|
2095
2128
|
# TODO: Generics/TypeVars
|
|
2129
|
+
self._check_not_observability_only()
|
|
2096
2130
|
self._check_no_web_url("remote_gen")
|
|
2097
2131
|
|
|
2098
2132
|
if not self._is_generator:
|
|
@@ -2669,6 +2703,7 @@ class _FunctionAutoscalerManager:
|
|
|
2669
2703
|
if not self._function._is_hydrated:
|
|
2670
2704
|
await self._function.hydrate()
|
|
2671
2705
|
|
|
2706
|
+
self._function._check_not_observability_only()
|
|
2672
2707
|
settings: api_pb2.AutoscalerSettings = api_pb2.AutoscalerSettings()
|
|
2673
2708
|
|
|
2674
2709
|
if min_containers != UNSET_PARAMETER:
|
|
@@ -37,6 +37,7 @@ from ._serialization import get_preferred_payload_format, serialize
|
|
|
37
37
|
from ._supports_logs import _ImageLogQueryData
|
|
38
38
|
from ._utils.async_utils import TaskContext, deprecate_aio_usage, synchronizer
|
|
39
39
|
from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
|
40
|
+
from ._utils.deprecation import deprecation_warning
|
|
40
41
|
from ._utils.docker_utils import (
|
|
41
42
|
extract_copy_command_patterns,
|
|
42
43
|
find_dockerignore_file,
|
|
@@ -87,6 +88,23 @@ SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
|
|
|
87
88
|
"2023.12": ["3.10", "3.11", "3.12"],
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
|
|
92
|
+
def _uses_python_310(metadata: api_pb2.ImageMetadata | None) -> bool:
|
|
93
|
+
return metadata is not None and bool(re.match(r"Python 3\.10\.", metadata.python_version_info))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _warn_if_python_310(image_id: str, metadata: api_pb2.ImageMetadata | None) -> None:
|
|
97
|
+
if _uses_python_310(metadata):
|
|
98
|
+
deprecation_warning(
|
|
99
|
+
(2026, 11, 4),
|
|
100
|
+
f"Image {image_id} uses Python 3.10, which is EOL. Modal 1.7.0 and later will not run or deploy apps "
|
|
101
|
+
"that use Images with Python 3.10; apps already deployed with older clients keep working. "
|
|
102
|
+
"Please update the Image to use Python 3.11 or later "
|
|
103
|
+
"(e.g. via `python_version=` or `add_python=`).",
|
|
104
|
+
show_source=False,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
90
108
|
LOCAL_REQUIREMENTS_DIR = Path(__file__).parent / "builder"
|
|
91
109
|
CONTAINER_REQUIREMENTS_PATH = "/modal_requirements.txt"
|
|
92
110
|
|
|
@@ -877,6 +895,8 @@ class _Image(_Object, type_prefix="im"):
|
|
|
877
895
|
raise ImageBuildError("Unknown status %s!" % result.status, image_id)
|
|
878
896
|
|
|
879
897
|
self._hydrate(image_id, load_context.client, metadata)
|
|
898
|
+
if not any(_uses_python_310(base._metadata) for base in base_images.values()):
|
|
899
|
+
_warn_if_python_310(image_id, metadata)
|
|
880
900
|
local_mounts: set[_Mount] = set()
|
|
881
901
|
for base in base_images.values():
|
|
882
902
|
local_mounts |= base._serve_mounts
|
|
@@ -1098,6 +1118,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1098
1118
|
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
1099
1119
|
resp = await load_context.client._stub.ImageFromId(api_pb2.ImageFromIdRequest(image_id=image_id))
|
|
1100
1120
|
self._hydrate(resp.image_id, load_context.client, resp.metadata)
|
|
1121
|
+
_warn_if_python_310(resp.image_id, resp.metadata)
|
|
1101
1122
|
|
|
1102
1123
|
rep = f"Image.from_id({image_id!r})"
|
|
1103
1124
|
|
|
@@ -2064,8 +2085,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2064
2085
|
if not isinstance(entrypoint_commands, list) or not all(isinstance(x, str) for x in entrypoint_commands):
|
|
2065
2086
|
raise InvalidError("entrypoint_commands must be a list of strings.")
|
|
2066
2087
|
args = _flatten_str_args("entrypoint", "entrypoint_commands", entrypoint_commands)
|
|
2067
|
-
|
|
2068
|
-
dockerfile_cmd = f"ENTRYPOINT [{args_str}]"
|
|
2088
|
+
dockerfile_cmd = f"ENTRYPOINT {json.dumps(args, ensure_ascii=False)}"
|
|
2069
2089
|
|
|
2070
2090
|
return self.dockerfile_commands(dockerfile_cmd)
|
|
2071
2091
|
|
|
@@ -2085,8 +2105,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2085
2105
|
if not isinstance(shell_commands, list) or not all(isinstance(x, str) for x in shell_commands):
|
|
2086
2106
|
raise InvalidError("shell_commands must be a list of strings.")
|
|
2087
2107
|
args = _flatten_str_args("shell", "shell_commands", shell_commands)
|
|
2088
|
-
|
|
2089
|
-
dockerfile_cmd = f"SHELL [{args_str}]"
|
|
2108
|
+
dockerfile_cmd = f"SHELL {json.dumps(args, ensure_ascii=False)}"
|
|
2090
2109
|
|
|
2091
2110
|
return self.dockerfile_commands(dockerfile_cmd)
|
|
2092
2111
|
|
|
@@ -3076,8 +3095,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
3076
3095
|
raise InvalidError("Image CMD must be a list of strings.")
|
|
3077
3096
|
|
|
3078
3097
|
cmd_args = _flatten_str_args("cmd", "cmd", cmd)
|
|
3079
|
-
|
|
3080
|
-
dockerfile_cmd = f"CMD [{cmd_str}]"
|
|
3098
|
+
dockerfile_cmd = f"CMD {json.dumps(cmd_args, ensure_ascii=False)}"
|
|
3081
3099
|
|
|
3082
3100
|
return self.dockerfile_commands(dockerfile_cmd)
|
|
3083
3101
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
|
+
import types
|
|
2
3
|
import typing
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
@@ -186,6 +187,12 @@ class SchemaRegistry:
|
|
|
186
187
|
|
|
187
188
|
def get_proto_generic_type(self, declared_type: type):
|
|
188
189
|
if origin := typing_extensions.get_origin(declared_type):
|
|
190
|
+
if origin in (typing.Union, types.UnionType):
|
|
191
|
+
args = [arg for arg in typing_extensions.get_args(declared_type) if arg is not type(None)]
|
|
192
|
+
if len(args) == 1:
|
|
193
|
+
return self.get_proto_generic_type(args[0])
|
|
194
|
+
return unknown_type_schema(declared_type)
|
|
195
|
+
|
|
189
196
|
base_type = origin
|
|
190
197
|
else:
|
|
191
198
|
base_type = declared_type
|
|
@@ -16,6 +16,13 @@ def replace_invalid_subdomain_chars(label: str) -> str:
|
|
|
16
16
|
return re.sub("[^a-z0-9-]", "-", label.lower())
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
app_id_regex = re.compile("^ap-[a-zA-Z0-9]{22}$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_app_id(identifier: str) -> bool:
|
|
23
|
+
return app_id_regex.match(identifier) is not None
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
def is_valid_object_name(name: str) -> bool:
|
|
20
27
|
return (
|
|
21
28
|
# Limit object name length
|
|
@@ -23,7 +30,7 @@ def is_valid_object_name(name: str) -> bool:
|
|
|
23
30
|
# Limit character set
|
|
24
31
|
and re.match("^[a-zA-Z0-9-_.]+$", name) is not None
|
|
25
32
|
# Avoid collisions with App IDs
|
|
26
|
-
and
|
|
33
|
+
and not is_app_id(name)
|
|
27
34
|
)
|
|
28
35
|
|
|
29
36
|
|
|
@@ -5,11 +5,12 @@ import os
|
|
|
5
5
|
from contextlib import suppress
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
|
-
from rich.
|
|
9
|
-
from rich.
|
|
8
|
+
from rich.box import SIMPLE_HEAD
|
|
9
|
+
from rich.text import Text
|
|
10
10
|
|
|
11
11
|
from modal import config as config_module
|
|
12
12
|
from modal._utils.async_utils import synchronizer
|
|
13
|
+
from modal.cli.utils import display_table
|
|
13
14
|
from modal.config import (
|
|
14
15
|
Config,
|
|
15
16
|
_lookup_workspace,
|
|
@@ -28,7 +29,7 @@ profile_cli = ModalGroup(name="profile", help="Switch between Modal profiles.")
|
|
|
28
29
|
@profile_cli.command("list", help="Show all Modal profiles and highlight the active one.")
|
|
29
30
|
@click.option("--json", is_flag=True, default=False)
|
|
30
31
|
@synchronizer.create_blocking
|
|
31
|
-
async def list_(json: bool
|
|
32
|
+
async def list_(json: bool = False):
|
|
32
33
|
config = Config()
|
|
33
34
|
profiles = config_profiles()
|
|
34
35
|
lookup_coros = [
|
|
@@ -44,22 +45,6 @@ async def list_(json: bool | None = False):
|
|
|
44
45
|
]
|
|
45
46
|
responses = await asyncio.gather(*lookup_coros, return_exceptions=True)
|
|
46
47
|
|
|
47
|
-
rows = []
|
|
48
|
-
for profile, resp in zip(profiles, responses):
|
|
49
|
-
active = profile == config_module._profile
|
|
50
|
-
if isinstance(resp, AuthError):
|
|
51
|
-
workspace = "Unknown (authentication failure)"
|
|
52
|
-
elif isinstance(resp, TimeoutError):
|
|
53
|
-
workspace = "Unknown (timed out)"
|
|
54
|
-
elif isinstance(resp, Exception):
|
|
55
|
-
# Catch-all for other exceptions, like incorrect server url
|
|
56
|
-
workspace = "Unknown (profile misconfigured)"
|
|
57
|
-
else:
|
|
58
|
-
assert not isinstance(resp, BaseException)
|
|
59
|
-
workspace = resp.username
|
|
60
|
-
content = ["•" if active else "", profile, workspace]
|
|
61
|
-
rows.append((active, content))
|
|
62
|
-
|
|
63
48
|
env_based_workspace: str | None = None
|
|
64
49
|
credential_env_vars = (
|
|
65
50
|
"MODAL_TOKEN_ID",
|
|
@@ -87,18 +72,44 @@ async def list_(json: bool | None = False):
|
|
|
87
72
|
|
|
88
73
|
output = OutputManager.get()
|
|
89
74
|
highlight = "bold green" if env_based_workspace is None else "yellow"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
json_data.append({"name": content[1], "workspace": content[2], "active": active})
|
|
94
|
-
output.print(JSON.from_data(json_data))
|
|
75
|
+
|
|
76
|
+
if not json:
|
|
77
|
+
table_columns = [" Profile", "Workspace"]
|
|
95
78
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
table_columns = ["name", "workspace", "active"]
|
|
80
|
+
|
|
81
|
+
table_rows: list[list[str | Text | bool]] = []
|
|
82
|
+
for profile, resp in zip(profiles, responses):
|
|
83
|
+
active = profile == config_module._profile
|
|
84
|
+
|
|
85
|
+
if isinstance(resp, AuthError):
|
|
86
|
+
workspace = "Unknown (authentication failure)"
|
|
87
|
+
elif isinstance(resp, TimeoutError):
|
|
88
|
+
workspace = "Unknown (timed out)"
|
|
89
|
+
elif isinstance(resp, Exception):
|
|
90
|
+
# Catch-all for other exceptions, like incorrect server url
|
|
91
|
+
workspace = "Unknown (profile misconfigured)"
|
|
92
|
+
else:
|
|
93
|
+
assert not isinstance(resp, BaseException)
|
|
94
|
+
workspace = resp.username
|
|
95
|
+
|
|
96
|
+
if json:
|
|
97
|
+
table_rows.append([profile, workspace, active])
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
rendered_profile = f"• {profile}" if active else f" {profile}"
|
|
101
|
+
style = highlight if active else "dim"
|
|
102
|
+
|
|
103
|
+
table_rows.append(
|
|
104
|
+
[
|
|
105
|
+
Text(rendered_profile, style=style),
|
|
106
|
+
Text(workspace, style=style),
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
display_table(table_columns, table_rows, json=json, table_box=SIMPLE_HEAD)
|
|
100
111
|
|
|
101
|
-
if env_based_workspace is not None:
|
|
112
|
+
if not json and env_based_workspace is not None:
|
|
102
113
|
output.print(
|
|
103
114
|
f"[yellow]Using [bold]{env_based_workspace}[/bold] workspace based on environment variables[/yellow]"
|
|
104
115
|
)
|
|
@@ -448,7 +448,7 @@ def shell(
|
|
|
448
448
|
app = App("modal shell")
|
|
449
449
|
|
|
450
450
|
# NB: invoking under bash makes --cmd a lot more flexible.
|
|
451
|
-
cmds =
|
|
451
|
+
cmds = ["/bin/bash", "-c", cmd]
|
|
452
452
|
timeout = 3600
|
|
453
453
|
parsed_experimental_options = _parse_experimental_options(experimental_options)
|
|
454
454
|
|