modal 1.5.1.dev7__tar.gz → 1.5.1.dev9__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.1.dev7 → modal-1.5.1.dev9}/PKG-INFO +2 -2
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/README.md +1 -1
- modal-1.5.1.dev9/modal/_billing.py +155 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_environments.py +77 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_functions.py +4 -4
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_grpc_client.py +3 -3
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_partial_function.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_server.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/task_command_router_client.py +27 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/time_utils.py +23 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_workspace.py +76 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/app.py +6 -3
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/app.pyi +10 -6
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/billing.py +3 -2
- modal-1.5.1.dev9/modal/billing.pyi +184 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/_help.py +3 -3
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/billing.py +100 -69
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/changelog.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/config.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/environment.py +141 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/shell.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/client.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/client.pyi +2 -2
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cls.py +2 -2
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cls.pyi +4 -4
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/environments.py +2 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/environments.pyi +36 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/exception.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/functions.pyi +14 -14
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/partial_function.pyi +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/runner.py +16 -12
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/sandbox.py +93 -26
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/sandbox.pyi +78 -15
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/schedule.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/server.pyi +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/workspace.py +2 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/workspace.pyi +34 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal.egg-info/PKG-INFO +2 -2
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal.egg-info/SOURCES.txt +1 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/gen_cli_docs.py +48 -2
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/api_grpc.py +19 -3
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/api_pb2.py +1146 -1103
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/api_pb2.pyi +83 -5
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/api_pb2_grpc.py +36 -3
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/api_pb2_grpc.pyi +12 -2
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_version/__init__.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/pyproject.toml +1 -1
- modal-1.5.1.dev7/modal/_billing.py +0 -94
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/LICENSE +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/__main__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_clustered_functions.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_function_variants.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_image.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_ipython.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_load_context.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_location.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_logs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_object.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_output/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_output/manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_output/pty.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_output/rich.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_output/status.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_resolver.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_resources.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_serialization.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_traceback.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_tunnel.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_tunnel.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_type_manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/logger.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_vendor/version.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/_watcher.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/builder/README.md +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/builder/base-images.json +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/call_graph.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/_download.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/_traceback.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/app.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/cluster.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/container.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/dashboard.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/dict.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/entry_point.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/image.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/import_refs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/launch.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/logo.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/profile.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/queues.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/run.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/secret.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/selector.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/skills.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/token.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cli/volume.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/config.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/container_process.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/container_process.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/dict.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/dict.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/experimental/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/experimental/flash.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/experimental/ipython.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/file_io.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/file_io.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/functions.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/image.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/image.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/io_streams.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/io_streams.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/mount.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/mount.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/network_file_system.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/network_file_system.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/object.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/object.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/output.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/parallel_map.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/parallel_map.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/partial_function.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/proxy.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/proxy.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/py.typed +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/queue.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/queue.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/retries.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/runner.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/running_app.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/sandbox_fs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/scheduler_placement.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/secret.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/secret.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/server.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/serving.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/serving.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/snapshot.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/snapshot.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/stream_type.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/token_flow.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/token_flow.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/volume.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal/volume.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/py.typed +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/modal_version/__main__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.5.1.
|
|
3
|
+
Version: 1.5.1.dev9
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -45,7 +45,7 @@ access to serverless cloud compute from Python scripts on your local computer.
|
|
|
45
45
|
See the [online documentation](https://modal.com/docs/guide) for many
|
|
46
46
|
[example applications](https://modal.com/docs/examples),
|
|
47
47
|
a [user guide](https://modal.com/docs/guide), and the detailed
|
|
48
|
-
[API reference](https://modal.com/docs/
|
|
48
|
+
[API reference](https://modal.com/docs/sdk/py/latest).
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
@@ -12,7 +12,7 @@ access to serverless cloud compute from Python scripts on your local computer.
|
|
|
12
12
|
See the [online documentation](https://modal.com/docs/guide) for many
|
|
13
13
|
[example applications](https://modal.com/docs/examples),
|
|
14
14
|
a [user guide](https://modal.com/docs/guide), and the detailed
|
|
15
|
-
[API reference](https://modal.com/docs/
|
|
15
|
+
[API reference](https://modal.com/docs/sdk/py/latest).
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Copyright Modal Labs 2025
|
|
2
|
+
from dataclasses import FrozenInstanceError, dataclass
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any, Iterable, TypedDict
|
|
6
|
+
|
|
7
|
+
from modal_proto import api_pb2
|
|
8
|
+
|
|
9
|
+
from ._utils.deprecation import deprecation_warning
|
|
10
|
+
from .client import _Client
|
|
11
|
+
|
|
12
|
+
BILLING_DOCSTRING = """
|
|
13
|
+
The `start` and `end` parameters are required to either have a UTC timezone or to be
|
|
14
|
+
timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
|
|
15
|
+
be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
|
|
16
|
+
parameters are partial: `start` will be rounded to the beginning of its interval, while
|
|
17
|
+
partial `end` intervals will be excluded.
|
|
18
|
+
|
|
19
|
+
Additional user-provided metadata can be included in the report if the objects have tags
|
|
20
|
+
and `tag_names` (i.e., keys) are specified in the request. Alternatively, pass `tag_names=["*"]`
|
|
21
|
+
to include all tags in the report. Note that tags will be attributed to the entire interval even
|
|
22
|
+
if they were added or removed at some point within it. If the tag name was not in use during an
|
|
23
|
+
interval, it will be absent from the tags dictionary in that output row.
|
|
24
|
+
|
|
25
|
+
In most cases, billing data will be available in the database that this API queries within
|
|
26
|
+
minutes, although there may be collection delays. If completeness is important for your use
|
|
27
|
+
case, we recommend leaving a buffer after the end of the query interval.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WorkspaceBillingReportItem(TypedDict):
|
|
32
|
+
object_id: str
|
|
33
|
+
description: str
|
|
34
|
+
environment_name: str
|
|
35
|
+
interval_start: datetime
|
|
36
|
+
cost: Decimal
|
|
37
|
+
tags: dict[str, str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True, frozen=True)
|
|
41
|
+
class BillingReportItem:
|
|
42
|
+
object_id: str
|
|
43
|
+
description: str
|
|
44
|
+
environment_name: str
|
|
45
|
+
interval_start: datetime
|
|
46
|
+
cost: Decimal
|
|
47
|
+
cost_by_resource: dict[str, Decimal]
|
|
48
|
+
tags: dict[str, str]
|
|
49
|
+
|
|
50
|
+
def __getitem__(self, key: str) -> Any:
|
|
51
|
+
"""mdmd:ignore"""
|
|
52
|
+
if key not in self.__slots__:
|
|
53
|
+
raise KeyError(key)
|
|
54
|
+
|
|
55
|
+
return getattr(self, key)
|
|
56
|
+
|
|
57
|
+
def __setitem__(self, key: str, _: Any):
|
|
58
|
+
"""mdmd:ignore"""
|
|
59
|
+
raise FrozenInstanceError(f"cannot assign to field {key!r}")
|
|
60
|
+
|
|
61
|
+
def keys(self) -> Iterable[str]:
|
|
62
|
+
"""mdmd:ignore"""
|
|
63
|
+
yield from self.__slots__
|
|
64
|
+
|
|
65
|
+
def values(self) -> Iterable[Any]:
|
|
66
|
+
"""mdmd:ignore"""
|
|
67
|
+
for k in self.__slots__:
|
|
68
|
+
yield getattr(self, k)
|
|
69
|
+
|
|
70
|
+
def items(self) -> Iterable[tuple[str, Any]]:
|
|
71
|
+
"""mdmd:ignore"""
|
|
72
|
+
for k in self.__slots__:
|
|
73
|
+
yield k, getattr(self, k)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def _from_proto(cls, pb_item: api_pb2.WorkspaceBillingReportItem) -> "BillingReportItem":
|
|
77
|
+
return cls(
|
|
78
|
+
object_id=pb_item.object_id,
|
|
79
|
+
description=pb_item.description,
|
|
80
|
+
environment_name=pb_item.environment_name,
|
|
81
|
+
interval_start=pb_item.interval.ToDatetime().replace(tzinfo=timezone.utc),
|
|
82
|
+
cost=Decimal(pb_item.cost),
|
|
83
|
+
cost_by_resource={k: Decimal(v) for k, v in pb_item.cost_by_resource.items()},
|
|
84
|
+
tags=dict(pb_item.tags),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def _workspace_billing_report(
|
|
89
|
+
*,
|
|
90
|
+
start: datetime, # Start of the report, inclusive
|
|
91
|
+
end: datetime | None = None, # End of the report, exclusive
|
|
92
|
+
resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
|
|
93
|
+
tag_names: list[str] | None = None, # Optional additional metadata to include
|
|
94
|
+
client: _Client | None = None,
|
|
95
|
+
) -> list[WorkspaceBillingReportItem]:
|
|
96
|
+
"""Generate a tabular report of workspace usage by object and time.
|
|
97
|
+
|
|
98
|
+
The result will be a list of dictionaries for each interval (determined by `resolution`)
|
|
99
|
+
between the `start` and `end` limits. The dictionary represents a single Modal object
|
|
100
|
+
that billing can be attributed to (e.g., an App) along with metadata (including user-defined
|
|
101
|
+
tags) for identifying that object. The dictionary also contains a breakdown of the cost value
|
|
102
|
+
attributed to individual resources (for an App, this can be CPU, Memory, specific GPU types,
|
|
103
|
+
etc.). The specific resource types included in the breakdown are subject to change as
|
|
104
|
+
Modal's billing model evolves.
|
|
105
|
+
|
|
106
|
+
The `start` and `end` parameters are required to either have a UTC timezone or to be
|
|
107
|
+
timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
|
|
108
|
+
be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
|
|
109
|
+
parameters are partial: `start` will be rounded to the beginning of its interval, while
|
|
110
|
+
partial `end` intervals will be excluded.
|
|
111
|
+
|
|
112
|
+
Additional user-provided metadata can be included in the report if the objects have tags
|
|
113
|
+
and `tag_names` (i.e., keys) are specified in the request. Alternatively, pass `tag_names=["*"]`
|
|
114
|
+
to include all tags in the report. Note that tags will be attributed to the entire interval even
|
|
115
|
+
if they were added or removed at some point within it. If the tag name was not in use during an
|
|
116
|
+
interval, it will be absent from the tags dictionary in that output row.
|
|
117
|
+
|
|
118
|
+
In most cases, billing data will be available in the database that this API queries within
|
|
119
|
+
minutes, although there may be collection delays. If completeness is important for your use
|
|
120
|
+
case, we recommend leaving a buffer after the end of the query interval.
|
|
121
|
+
|
|
122
|
+
It's also possible to generate reports using the
|
|
123
|
+
[`modal billing report`](https://modal.com/docs/cli/latest/billing) CLI command. The CLI
|
|
124
|
+
has a few convenience features for generating reports across relative time ranges.
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
deprecation_warning(
|
|
129
|
+
(2026, 6, 18),
|
|
130
|
+
"`workspace_billing_report()` is deprecated. Use `Workspace.billing.report()` instead.",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
from ._workspace import _Workspace
|
|
134
|
+
|
|
135
|
+
data = await _Workspace.from_context(client=client).billing.report(
|
|
136
|
+
start=start,
|
|
137
|
+
end=end,
|
|
138
|
+
resolution=resolution,
|
|
139
|
+
tag_names=tag_names,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
res: list[WorkspaceBillingReportItem] = []
|
|
143
|
+
for datum in data:
|
|
144
|
+
item: WorkspaceBillingReportItem = {
|
|
145
|
+
"object_id": datum.object_id,
|
|
146
|
+
"description": datum.description,
|
|
147
|
+
"environment_name": datum.environment_name,
|
|
148
|
+
"interval_start": datum.interval_start,
|
|
149
|
+
"cost": Decimal(datum.cost),
|
|
150
|
+
"tags": dict(datum.tags),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
res.append(item)
|
|
154
|
+
|
|
155
|
+
return res
|
|
@@ -3,6 +3,7 @@ import asyncio
|
|
|
3
3
|
import builtins
|
|
4
4
|
from collections.abc import Iterable, Mapping
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
6
7
|
from typing import Literal
|
|
7
8
|
|
|
8
9
|
from google.protobuf.empty_pb2 import Empty
|
|
@@ -12,6 +13,7 @@ from synchronicity import classproperty
|
|
|
12
13
|
|
|
13
14
|
from modal_proto import api_pb2
|
|
14
15
|
|
|
16
|
+
from ._billing import BILLING_DOCSTRING, BillingReportItem
|
|
15
17
|
from ._load_context import LoadContext
|
|
16
18
|
from ._object import _Object
|
|
17
19
|
from ._resolver import Resolver
|
|
@@ -318,7 +320,7 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
318
320
|
# def settings(self) -> EnvironmentSettings:
|
|
319
321
|
# return self._settings
|
|
320
322
|
|
|
321
|
-
def _hydrate_metadata(self, metadata: Message):
|
|
323
|
+
def _hydrate_metadata(self, metadata: Message | None):
|
|
322
324
|
# Overridden concrete implementation of base class method
|
|
323
325
|
assert metadata and isinstance(metadata, api_pb2.EnvironmentMetadata)
|
|
324
326
|
self._name = metadata.name or None
|
|
@@ -392,6 +394,80 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
392
394
|
client=client,
|
|
393
395
|
)
|
|
394
396
|
|
|
397
|
+
@property
|
|
398
|
+
def billing(self) -> "_EnvironmentBillingManager":
|
|
399
|
+
return _EnvironmentBillingManager(self)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class _EnvironmentBillingManager:
|
|
403
|
+
"""mdmd:namespace
|
|
404
|
+
Namespace for Environment billing APIs
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
def __init__(self, environment: _Environment):
|
|
408
|
+
"""mdmd:ignore"""
|
|
409
|
+
self._environment = environment
|
|
410
|
+
|
|
411
|
+
async def report(
|
|
412
|
+
self,
|
|
413
|
+
*,
|
|
414
|
+
start: datetime, # Start of the report, inclusive
|
|
415
|
+
end: datetime | None = None, # End of the report, exclusive
|
|
416
|
+
resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
|
|
417
|
+
tag_names: list[str] | None = None, # Optional additional metadata to include
|
|
418
|
+
) -> list[BillingReportItem]:
|
|
419
|
+
(
|
|
420
|
+
"""Return a report of workspace usage by object and time, scoped to the calling Environment.
|
|
421
|
+
|
|
422
|
+
The result will be a list of dataclasses for each interval (determined by `resolution`)
|
|
423
|
+
between the `start` and `end` limits. Each item represents a single (Modal object, time interval)
|
|
424
|
+
pair that billing can be attributed to (e.g., an App) along with metadata (including user-defined
|
|
425
|
+
tags) to identify that object. The dataclass also contains a breakdown of the cost value
|
|
426
|
+
attributed to individual resources (for an App, this can be CPU, Memory, specific GPU types,
|
|
427
|
+
etc.). The specific resource types included in the breakdown are subject to change as
|
|
428
|
+
Modal's billing model evolves.
|
|
429
|
+
|
|
430
|
+
It's also possible to generate reports using the
|
|
431
|
+
[`modal environment billing report`](https://modal.com/docs/cli/latest/environment#modal-environment-billing-report)
|
|
432
|
+
CLI command. The CLI has a few convenience features for generating
|
|
433
|
+
reports across relative time ranges.
|
|
434
|
+
|
|
435
|
+
"""
|
|
436
|
+
+ BILLING_DOCSTRING
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if tag_names is None:
|
|
440
|
+
tag_names = []
|
|
441
|
+
|
|
442
|
+
if end is None:
|
|
443
|
+
end = datetime.now(timezone.utc)
|
|
444
|
+
|
|
445
|
+
if start.tzinfo is None:
|
|
446
|
+
start = start.replace(tzinfo=timezone.utc)
|
|
447
|
+
elif start.tzinfo != timezone.utc:
|
|
448
|
+
raise InvalidError("Timezone-aware 'start' parameter must be in UTC.")
|
|
449
|
+
|
|
450
|
+
if end.tzinfo is None:
|
|
451
|
+
end = end.replace(tzinfo=timezone.utc)
|
|
452
|
+
elif end.tzinfo != timezone.utc:
|
|
453
|
+
raise InvalidError("Timezone-aware 'end' parameter must be in UTC.")
|
|
454
|
+
|
|
455
|
+
if not self._environment.is_hydrated:
|
|
456
|
+
await self._environment.hydrate()
|
|
457
|
+
|
|
458
|
+
request = api_pb2.WorkspaceBillingReportRequest(
|
|
459
|
+
resolution=resolution,
|
|
460
|
+
tag_names=tag_names,
|
|
461
|
+
environment_ids=[self._environment.object_id],
|
|
462
|
+
)
|
|
463
|
+
request.start_timestamp.FromDatetime(start)
|
|
464
|
+
request.end_timestamp.FromDatetime(end)
|
|
465
|
+
|
|
466
|
+
return [
|
|
467
|
+
BillingReportItem._from_proto(pb_item)
|
|
468
|
+
async for pb_item in self._environment.client.stub.WorkspaceBillingReport.unary_stream(request)
|
|
469
|
+
]
|
|
470
|
+
|
|
395
471
|
|
|
396
472
|
ENVIRONMENT_CACHE: dict[str, _Environment] = {}
|
|
397
473
|
|
|
@@ -1912,9 +1912,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1912
1912
|
**kwargs: Keyword arguments forwarded to the remote function.
|
|
1913
1913
|
|
|
1914
1914
|
Returns:
|
|
1915
|
-
A [`modal.FunctionCall`](https://modal.com/docs/
|
|
1915
|
+
A [`modal.FunctionCall`](https://modal.com/docs/sdk/py/latest/modal.FunctionCall) object
|
|
1916
1916
|
that can later be polled or waited for using
|
|
1917
|
-
[`.get(timeout=...)`](https://modal.com/docs/
|
|
1917
|
+
[`.get(timeout=...)`](https://modal.com/docs/sdk/py/latest/modal.FunctionCall#get).
|
|
1918
1918
|
"""
|
|
1919
1919
|
self._check_no_web_url("spawn")
|
|
1920
1920
|
if self._is_generator:
|
|
@@ -2065,7 +2065,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2065
2065
|
"""Returns a structure representing the call graph from a given root
|
|
2066
2066
|
call ID, along with the status of execution for each node.
|
|
2067
2067
|
|
|
2068
|
-
See [`modal.call_graph`](https://modal.com/docs/
|
|
2068
|
+
See [`modal.call_graph`](https://modal.com/docs/sdk/py/latest/modal.call_graph) reference page
|
|
2069
2069
|
for documentation on the structure of the returned `InputInfo` items.
|
|
2070
2070
|
|
|
2071
2071
|
Returns:
|
|
@@ -2083,7 +2083,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2083
2083
|
terminate_containers: bool = False,
|
|
2084
2084
|
):
|
|
2085
2085
|
"""Cancels the function call, which will stop its execution and mark its inputs as
|
|
2086
|
-
[`TERMINATED`](https://modal.com/docs/
|
|
2086
|
+
[`TERMINATED`](https://modal.com/docs/sdk/py/latest/modal.call_graph#modalcall_graphinputstatus).
|
|
2087
2087
|
|
|
2088
2088
|
If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
|
|
2089
2089
|
causing any non-cancelled inputs on those containers to be rescheduled in new containers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
2
|
from collections.abc import Collection, Mapping
|
|
3
|
-
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload
|
|
3
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Generic, Literal, TypeVar, overload
|
|
4
4
|
|
|
5
5
|
import grpclib.client
|
|
6
6
|
from google.protobuf.message import Message
|
|
@@ -178,9 +178,9 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
|
|
|
178
178
|
|
|
179
179
|
async def unary_stream(
|
|
180
180
|
self,
|
|
181
|
-
request,
|
|
181
|
+
request: RequestType,
|
|
182
182
|
metadata: Any | None = None,
|
|
183
|
-
):
|
|
183
|
+
) -> AsyncGenerator[ResponseType, None]:
|
|
184
184
|
from .client import _Client
|
|
185
185
|
|
|
186
186
|
if self.client._snapshotted:
|
|
@@ -477,7 +477,7 @@ def _wsgi_app(
|
|
|
477
477
|
Web Server Gateway Interface (WSGI) is a standard for synchronous Python web apps.
|
|
478
478
|
It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
|
|
479
479
|
which is compatible with ASGI and supports additional functionality such as web sockets.
|
|
480
|
-
Modal supports ASGI via [`asgi_app`](https://modal.com/docs/
|
|
480
|
+
Modal supports ASGI via [`asgi_app`](https://modal.com/docs/sdk/py/latest/modal.asgi_app).
|
|
481
481
|
|
|
482
482
|
Examples:
|
|
483
483
|
```python
|
|
@@ -49,7 +49,7 @@ class _Server:
|
|
|
49
49
|
See [lifecycle hooks](https://modal.com/docs/guide/lifecycle-functions) for more information.
|
|
50
50
|
|
|
51
51
|
Generally, you will not construct a Server directly.
|
|
52
|
-
Instead, use the [`@app.server()`](https://modal.com/docs/
|
|
52
|
+
Instead, use the [`@app.server()`](https://modal.com/docs/sdk/py/latest/modal.App#server) decorator.
|
|
53
53
|
|
|
54
54
|
```python notest
|
|
55
55
|
@app.server(port=8000, routing_region="us-east")
|
|
@@ -491,6 +491,27 @@ class TaskCommandRouterClient:
|
|
|
491
491
|
lambda: self._call_with_auth_retry(self._stub.SandboxStdinWriteV2, request)
|
|
492
492
|
)
|
|
493
493
|
|
|
494
|
+
async def sandbox_wait_until_ready(self, task_id: str, timeout: float) -> sr_pb2.SandboxWaitUntilReadyTcrResponse:
|
|
495
|
+
"""Wait until the sandbox's readiness probe reports ready.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
task_id: The task ID hosting the sandbox.
|
|
499
|
+
timeout: Maximum time in seconds for the worker to wait.
|
|
500
|
+
Raises:
|
|
501
|
+
TimeoutError: If the sandbox does not become ready within `timeout`.
|
|
502
|
+
"""
|
|
503
|
+
request = sr_pb2.SandboxWaitUntilReadyTcrRequest(task_id=task_id, timeout=timeout)
|
|
504
|
+
with grpc_error_converter():
|
|
505
|
+
try:
|
|
506
|
+
return await asyncio.wait_for(
|
|
507
|
+
call_with_retries_on_transient_errors(
|
|
508
|
+
lambda: self._call_with_auth_retry(self._stub.SandboxWaitUntilReady, request, timeout=timeout),
|
|
509
|
+
),
|
|
510
|
+
timeout=timeout,
|
|
511
|
+
)
|
|
512
|
+
except asyncio.TimeoutError:
|
|
513
|
+
raise ModalTimeoutError("Timeout expired")
|
|
514
|
+
|
|
494
515
|
async def exec_poll(self, task_id: str, exec_id: str, deadline: float | None = None) -> sr_pb2.TaskExecPollResponse:
|
|
495
516
|
"""Poll for the exit status of an exec'd command, properly retrying on transient errors.
|
|
496
517
|
|
|
@@ -784,6 +805,12 @@ class TaskCommandRouterClient:
|
|
|
784
805
|
lambda: self._call_with_auth_retry(self._stub.TaskUnmountDirectory, request)
|
|
785
806
|
)
|
|
786
807
|
|
|
808
|
+
async def set_network_access(self, request: sr_pb2.TaskSetNetworkAccessRequest):
|
|
809
|
+
with grpc_error_converter():
|
|
810
|
+
return await call_with_retries_on_transient_errors(
|
|
811
|
+
lambda: self._call_with_auth_retry(self._stub.TaskSetNetworkAccess, request)
|
|
812
|
+
)
|
|
813
|
+
|
|
787
814
|
async def _snapshot_with_deadline(self, rpc, request, *, timeout: float, **kwargs):
|
|
788
815
|
# helper method for snapshot_directory and snapshot_filesystem to handle grpc
|
|
789
816
|
# deadlines in a consistent way, converting any error to TimeoutError after passing
|
|
@@ -256,3 +256,26 @@ def timestamp_to_localized_str(ts: float, isotz: bool = True) -> str | None:
|
|
|
256
256
|
return f"{dt:%Y-%m-%d %H:%M %Z}"
|
|
257
257
|
else:
|
|
258
258
|
return None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def format_interval(
|
|
262
|
+
dt: datetime,
|
|
263
|
+
*,
|
|
264
|
+
tz: tzinfo | None = None,
|
|
265
|
+
isoformat: bool = False,
|
|
266
|
+
show_date_only: bool = False,
|
|
267
|
+
) -> str:
|
|
268
|
+
if tz is not None:
|
|
269
|
+
dt = dt.astimezone(tz)
|
|
270
|
+
else:
|
|
271
|
+
# Strip UTC tzinfo so isoformat() doesn't append +00:00
|
|
272
|
+
dt = dt.replace(tzinfo=None)
|
|
273
|
+
if isoformat:
|
|
274
|
+
# Full ISO format for machine-readable output
|
|
275
|
+
return dt.isoformat()
|
|
276
|
+
elif show_date_only:
|
|
277
|
+
# Just the date for daily resolution
|
|
278
|
+
return dt.strftime("%Y-%m-%d")
|
|
279
|
+
else:
|
|
280
|
+
# Date and time without seconds for hourly resolution
|
|
281
|
+
return dt.strftime("%Y-%m-%dT%H:%M")
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
2
|
import builtins
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
from typing import Literal, Optional
|
|
6
6
|
|
|
7
7
|
from google.protobuf.empty_pb2 import Empty
|
|
8
8
|
|
|
9
|
+
from modal.exception import InvalidError
|
|
9
10
|
from modal_proto import api_pb2
|
|
10
11
|
|
|
12
|
+
from ._billing import BILLING_DOCSTRING, BillingReportItem
|
|
11
13
|
from ._load_context import LoadContext
|
|
12
14
|
from ._object import _Object
|
|
13
15
|
from ._resolver import Resolver
|
|
@@ -114,3 +116,76 @@ class _Workspace(_Object, type_prefix="ac"):
|
|
|
114
116
|
hydrate_lazily=True,
|
|
115
117
|
load_context_overrides=LoadContext(client=client),
|
|
116
118
|
)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def billing(self) -> "_WorkspaceBillingManager":
|
|
122
|
+
return _WorkspaceBillingManager(self)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _WorkspaceBillingManager:
|
|
126
|
+
"""mdmd:namespace
|
|
127
|
+
Namespace for Workspace billing APIs
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, workspace: _Workspace):
|
|
131
|
+
"""mdmd:hidden"""
|
|
132
|
+
self._workspace = workspace
|
|
133
|
+
|
|
134
|
+
async def report(
|
|
135
|
+
self,
|
|
136
|
+
*,
|
|
137
|
+
start: datetime, # Start of the report, inclusive
|
|
138
|
+
end: datetime | None = None, # End of the report, exclusive
|
|
139
|
+
resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
|
|
140
|
+
tag_names: list[str] | None = None, # Optional additional metadata to include
|
|
141
|
+
) -> list[BillingReportItem]:
|
|
142
|
+
(
|
|
143
|
+
"""Return a report of workspace usage by object and time.
|
|
144
|
+
|
|
145
|
+
The result will be a list of dataclasses for each interval (determined by `resolution`)
|
|
146
|
+
between the `start` and `end` limits. Each item represents a single (Modal object, time interval)
|
|
147
|
+
pair that billing can be attributed to (e.g., an App) along with metadata (including user-defined
|
|
148
|
+
tags) to identify that object. The dataclass also contains a breakdown of the cost value
|
|
149
|
+
attributed to individual resources (for an App, this can be CPU, Memory, specific GPU types,
|
|
150
|
+
etc.). The specific resource types included in the breakdown are subject to change as
|
|
151
|
+
Modal's billing model evolves.
|
|
152
|
+
|
|
153
|
+
It's also possible to generate reports using the
|
|
154
|
+
[`modal billing report`](https://modal.com/docs/cli/latest/billing#modal-billing-report)
|
|
155
|
+
CLI command. The CLI has a few convenience features for generating reports across relative
|
|
156
|
+
time ranges.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
+ BILLING_DOCSTRING
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if tag_names is None:
|
|
163
|
+
tag_names = []
|
|
164
|
+
|
|
165
|
+
if end is None:
|
|
166
|
+
end = datetime.now(timezone.utc)
|
|
167
|
+
|
|
168
|
+
if start.tzinfo is None:
|
|
169
|
+
start = start.replace(tzinfo=timezone.utc)
|
|
170
|
+
elif start.tzinfo != timezone.utc:
|
|
171
|
+
raise InvalidError("Timezone-aware 'start' parameter must be in UTC.")
|
|
172
|
+
|
|
173
|
+
if end.tzinfo is None:
|
|
174
|
+
end = end.replace(tzinfo=timezone.utc)
|
|
175
|
+
elif end.tzinfo != timezone.utc:
|
|
176
|
+
raise InvalidError("Timezone-aware 'end' parameter must be in UTC.")
|
|
177
|
+
|
|
178
|
+
if not self._workspace.is_hydrated:
|
|
179
|
+
await self._workspace.hydrate()
|
|
180
|
+
|
|
181
|
+
request = api_pb2.WorkspaceBillingReportRequest(
|
|
182
|
+
resolution=resolution,
|
|
183
|
+
tag_names=tag_names,
|
|
184
|
+
)
|
|
185
|
+
request.start_timestamp.FromDatetime(start)
|
|
186
|
+
request.end_timestamp.FromDatetime(end)
|
|
187
|
+
|
|
188
|
+
return [
|
|
189
|
+
BillingReportItem._from_proto(pb_item)
|
|
190
|
+
async for pb_item in self._workspace.client.stub.WorkspaceBillingReport.unary_stream(request)
|
|
191
|
+
]
|
|
@@ -710,7 +710,7 @@ class _App:
|
|
|
710
710
|
Modal functions can also be used as CLI entrypoints, but unlike `local_entrypoint`,
|
|
711
711
|
those functions are executed remotely directly.
|
|
712
712
|
|
|
713
|
-
Note that an explicit [`app.run()`](https://modal.com/docs/
|
|
713
|
+
Note that an explicit [`app.run()`](https://modal.com/docs/sdk/py/latest/modal.App#run) is not needed, as an
|
|
714
714
|
[app](https://modal.com/docs/guide/apps) is automatically created for you.
|
|
715
715
|
|
|
716
716
|
Args:
|
|
@@ -1066,7 +1066,7 @@ class _App:
|
|
|
1066
1066
|
max_inputs: int | None = None,
|
|
1067
1067
|
) -> Callable[[CLS_T | _PartialFunction], CLS_T]:
|
|
1068
1068
|
"""
|
|
1069
|
-
Decorator to register a new Modal [Cls](https://modal.com/docs/
|
|
1069
|
+
Decorator to register a new Modal [Cls](https://modal.com/docs/sdk/py/latest/modal.Cls) with this App.
|
|
1070
1070
|
|
|
1071
1071
|
Args:
|
|
1072
1072
|
image: The image to run as the container for the class service.
|
|
@@ -1290,6 +1290,7 @@ class _App:
|
|
|
1290
1290
|
scaleup_window: int | None = None, # Stabilization window (seconds) of sustained demand before scaling up
|
|
1291
1291
|
scaledown_window: int | None = None, # Max idle time before scaling down (seconds)
|
|
1292
1292
|
proxy: _Proxy | None = None, # Modal Proxy to use in front of this server
|
|
1293
|
+
unauthenticated: bool = False, # Whether the endpoint requires proxy authentication, required by default.
|
|
1293
1294
|
port: int = 8000, # Port the HTTP server listens on
|
|
1294
1295
|
startup_timeout: int = 30, # Maximum startup time in seconds
|
|
1295
1296
|
exit_grace_period: int = 0, # Grace period for in-flight requests on shutdown
|
|
@@ -1334,10 +1335,11 @@ class _App:
|
|
|
1334
1335
|
scaledown_window: Max idle time before scaling down (seconds).
|
|
1335
1336
|
proxy: Modal Proxy to use in front of this server.
|
|
1336
1337
|
port: Port the HTTP server listens on.
|
|
1338
|
+
unauthenticated: Whether the endpoint requires proxy authentication, required by default.
|
|
1337
1339
|
startup_timeout: Maximum startup time in seconds.
|
|
1338
1340
|
exit_grace_period: Grace period for in-flight requests on shutdown.
|
|
1339
1341
|
routing_region: Region to route Server requests through.
|
|
1340
|
-
h2_enabled: Enable HTTP/2
|
|
1342
|
+
h2_enabled: Enable HTTP/2.
|
|
1341
1343
|
target_concurrency: Target concurrency for the server; 0 disables autoscaling.
|
|
1342
1344
|
cloud: Cloud provider (aws, gcp, oci, auto).
|
|
1343
1345
|
region: Region(s) to run on.
|
|
@@ -1385,6 +1387,7 @@ class _App:
|
|
|
1385
1387
|
startup_timeout=startup_timeout,
|
|
1386
1388
|
exit_grace_period=exit_grace_period,
|
|
1387
1389
|
h2_enabled=h2_enabled,
|
|
1390
|
+
unauthenticated=unauthenticated,
|
|
1388
1391
|
)
|
|
1389
1392
|
|
|
1390
1393
|
# Build secrets list
|
|
@@ -494,7 +494,7 @@ class _App:
|
|
|
494
494
|
Modal functions can also be used as CLI entrypoints, but unlike `local_entrypoint`,
|
|
495
495
|
those functions are executed remotely directly.
|
|
496
496
|
|
|
497
|
-
Note that an explicit [`app.run()`](https://modal.com/docs/
|
|
497
|
+
Note that an explicit [`app.run()`](https://modal.com/docs/sdk/py/latest/modal.App#run) is not needed, as an
|
|
498
498
|
[app](https://modal.com/docs/guide/apps) is automatically created for you.
|
|
499
499
|
|
|
500
500
|
Args:
|
|
@@ -683,7 +683,7 @@ class _App:
|
|
|
683
683
|
_experimental_restrict_output: bool = False,
|
|
684
684
|
max_inputs: typing.Optional[int] = None,
|
|
685
685
|
) -> collections.abc.Callable[[typing.Union[CLS_T, modal._partial_function._PartialFunction]], CLS_T]:
|
|
686
|
-
"""Decorator to register a new Modal [Cls](https://modal.com/docs/
|
|
686
|
+
"""Decorator to register a new Modal [Cls](https://modal.com/docs/sdk/py/latest/modal.Cls) with this App.
|
|
687
687
|
|
|
688
688
|
Args:
|
|
689
689
|
image: The image to run as the container for the class service.
|
|
@@ -751,6 +751,7 @@ class _App:
|
|
|
751
751
|
scaleup_window: typing.Optional[int] = None,
|
|
752
752
|
scaledown_window: typing.Optional[int] = None,
|
|
753
753
|
proxy: typing.Optional[modal.proxy._Proxy] = None,
|
|
754
|
+
unauthenticated: bool = False,
|
|
754
755
|
port: int = 8000,
|
|
755
756
|
startup_timeout: int = 30,
|
|
756
757
|
exit_grace_period: int = 0,
|
|
@@ -795,10 +796,11 @@ class _App:
|
|
|
795
796
|
scaledown_window: Max idle time before scaling down (seconds).
|
|
796
797
|
proxy: Modal Proxy to use in front of this server.
|
|
797
798
|
port: Port the HTTP server listens on.
|
|
799
|
+
unauthenticated: Whether the endpoint requires proxy authentication, required by default.
|
|
798
800
|
startup_timeout: Maximum startup time in seconds.
|
|
799
801
|
exit_grace_period: Grace period for in-flight requests on shutdown.
|
|
800
802
|
routing_region: Region to route Server requests through.
|
|
801
|
-
h2_enabled: Enable HTTP/2
|
|
803
|
+
h2_enabled: Enable HTTP/2.
|
|
802
804
|
target_concurrency: Target concurrency for the server; 0 disables autoscaling.
|
|
803
805
|
cloud: Cloud provider (aws, gcp, oci, auto).
|
|
804
806
|
region: Region(s) to run on.
|
|
@@ -1495,7 +1497,7 @@ class App:
|
|
|
1495
1497
|
Modal functions can also be used as CLI entrypoints, but unlike `local_entrypoint`,
|
|
1496
1498
|
those functions are executed remotely directly.
|
|
1497
1499
|
|
|
1498
|
-
Note that an explicit [`app.run()`](https://modal.com/docs/
|
|
1500
|
+
Note that an explicit [`app.run()`](https://modal.com/docs/sdk/py/latest/modal.App#run) is not needed, as an
|
|
1499
1501
|
[app](https://modal.com/docs/guide/apps) is automatically created for you.
|
|
1500
1502
|
|
|
1501
1503
|
Args:
|
|
@@ -1684,7 +1686,7 @@ class App:
|
|
|
1684
1686
|
_experimental_restrict_output: bool = False,
|
|
1685
1687
|
max_inputs: typing.Optional[int] = None,
|
|
1686
1688
|
) -> collections.abc.Callable[[typing.Union[CLS_T, modal.partial_function.PartialFunction]], CLS_T]:
|
|
1687
|
-
"""Decorator to register a new Modal [Cls](https://modal.com/docs/
|
|
1689
|
+
"""Decorator to register a new Modal [Cls](https://modal.com/docs/sdk/py/latest/modal.Cls) with this App.
|
|
1688
1690
|
|
|
1689
1691
|
Args:
|
|
1690
1692
|
image: The image to run as the container for the class service.
|
|
@@ -1752,6 +1754,7 @@ class App:
|
|
|
1752
1754
|
scaleup_window: typing.Optional[int] = None,
|
|
1753
1755
|
scaledown_window: typing.Optional[int] = None,
|
|
1754
1756
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
1757
|
+
unauthenticated: bool = False,
|
|
1755
1758
|
port: int = 8000,
|
|
1756
1759
|
startup_timeout: int = 30,
|
|
1757
1760
|
exit_grace_period: int = 0,
|
|
@@ -1794,10 +1797,11 @@ class App:
|
|
|
1794
1797
|
scaledown_window: Max idle time before scaling down (seconds).
|
|
1795
1798
|
proxy: Modal Proxy to use in front of this server.
|
|
1796
1799
|
port: Port the HTTP server listens on.
|
|
1800
|
+
unauthenticated: Whether the endpoint requires proxy authentication, required by default.
|
|
1797
1801
|
startup_timeout: Maximum startup time in seconds.
|
|
1798
1802
|
exit_grace_period: Grace period for in-flight requests on shutdown.
|
|
1799
1803
|
routing_region: Region to route Server requests through.
|
|
1800
|
-
h2_enabled: Enable HTTP/2
|
|
1804
|
+
h2_enabled: Enable HTTP/2.
|
|
1801
1805
|
target_concurrency: Target concurrency for the server; 0 disables autoscaling.
|
|
1802
1806
|
cloud: Cloud provider (aws, gcp, oci, auto).
|
|
1803
1807
|
region: Region(s) to run on.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
|
-
from ._billing import WorkspaceBillingReportItem, _workspace_billing_report
|
|
2
|
+
from ._billing import BillingReportItem, WorkspaceBillingReportItem, _workspace_billing_report
|
|
3
3
|
from ._utils.async_utils import synchronize_api
|
|
4
4
|
|
|
5
|
-
workspace_billing_report = synchronize_api(_workspace_billing_report)
|
|
5
|
+
workspace_billing_report = synchronize_api(_workspace_billing_report, target_module=__name__)
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"workspace_billing_report",
|
|
9
9
|
"WorkspaceBillingReportItem",
|
|
10
|
+
"BillingReportItem",
|
|
10
11
|
]
|