modal 1.5.1.dev7__tar.gz → 1.5.1.dev8__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.dev8}/PKG-INFO +1 -1
- modal-1.5.1.dev8/modal/_billing.py +155 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_environments.py +76 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_grpc_client.py +3 -3
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/time_utils.py +23 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_workspace.py +75 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/billing.py +3 -2
- modal-1.5.1.dev8/modal/billing.pyi +184 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/billing.py +100 -69
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/environment.py +140 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/client.pyi +2 -2
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/environments.py +2 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/environments.pyi +36 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/functions.pyi +6 -6
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/workspace.py +2 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/workspace.pyi +34 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal.egg-info/SOURCES.txt +1 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/api_grpc.py +3 -3
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/api_pb2.py +1113 -1103
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/api_pb2.pyi +21 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/api_pb2_grpc.py +3 -3
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/api_pb2_grpc.pyi +2 -2
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_version/__init__.py +1 -1
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/pyproject.toml +1 -1
- modal-1.5.1.dev7/modal/_billing.py +0 -94
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/LICENSE +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/README.md +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/__main__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_clustered_functions.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_clustered_functions.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_container_entrypoint.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_function_variants.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_functions.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_image.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_ipython.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_load_context.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_location.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_logs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_object.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_output/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_output/manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_output/pty.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_output/rich.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_output/status.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_partial_function.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_resolver.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_resources.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_serialization.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_server.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_traceback.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_tunnel.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_tunnel.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_type_manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/async_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/blob_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/logger.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_vendor/version.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/_watcher.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/app.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/app.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/builder/README.md +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/builder/base-images.json +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/call_graph.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/_download.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/_help.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/_traceback.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/app.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/changelog.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/cluster.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/config.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/container.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/dashboard.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/dict.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/entry_point.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/image.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/import_refs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/launch.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/logo.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/network_file_system.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/profile.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/queues.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/run.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/secret.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/selector.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/shell.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/skills.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/token.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/utils.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cli/volume.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/client.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cls.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/cls.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/config.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/container_process.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/container_process.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/dict.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/dict.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/exception.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/experimental/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/experimental/flash.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/experimental/ipython.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/file_io.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/file_io.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/file_pattern_matcher.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/functions.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/image.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/image.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/io_streams.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/io_streams.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/mount.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/mount.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/network_file_system.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/network_file_system.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/object.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/object.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/output.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/parallel_map.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/parallel_map.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/partial_function.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/partial_function.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/proxy.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/proxy.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/py.typed +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/queue.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/queue.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/retries.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/runner.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/runner.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/running_app.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/sandbox.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/sandbox.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/sandbox_fs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/sandbox_fs.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/schedule.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/scheduler_placement.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/secret.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/secret.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/server.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/server.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/serving.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/serving.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/snapshot.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/snapshot.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/stream_type.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/token_flow.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/token_flow.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/volume.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal/volume.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_docs/mdmd/types.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/__init__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/py.typed +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/modal_version/__main__.py +0 -0
- {modal-1.5.1.dev7 → modal-1.5.1.dev8}/setup.cfg +0 -0
|
@@ -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(str(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/reference/cli/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,79 @@ 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/reference/cli/environment) CLI command.
|
|
432
|
+
The CLI has a few convenience features for generating reports across relative time ranges.
|
|
433
|
+
|
|
434
|
+
"""
|
|
435
|
+
+ BILLING_DOCSTRING
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if tag_names is None:
|
|
439
|
+
tag_names = []
|
|
440
|
+
|
|
441
|
+
if end is None:
|
|
442
|
+
end = datetime.now(timezone.utc)
|
|
443
|
+
|
|
444
|
+
if start.tzinfo is None:
|
|
445
|
+
start = start.replace(tzinfo=timezone.utc)
|
|
446
|
+
elif start.tzinfo != timezone.utc:
|
|
447
|
+
raise InvalidError("Timezone-aware 'start' parameter must be in UTC.")
|
|
448
|
+
|
|
449
|
+
if end.tzinfo is None:
|
|
450
|
+
end = end.replace(tzinfo=timezone.utc)
|
|
451
|
+
elif end.tzinfo != timezone.utc:
|
|
452
|
+
raise InvalidError("Timezone-aware 'end' parameter must be in UTC.")
|
|
453
|
+
|
|
454
|
+
if not self._environment.is_hydrated:
|
|
455
|
+
await self._environment.hydrate()
|
|
456
|
+
|
|
457
|
+
request = api_pb2.WorkspaceBillingReportRequest(
|
|
458
|
+
resolution=resolution,
|
|
459
|
+
tag_names=tag_names,
|
|
460
|
+
environment_ids=[self._environment.object_id],
|
|
461
|
+
)
|
|
462
|
+
request.start_timestamp.FromDatetime(start)
|
|
463
|
+
request.end_timestamp.FromDatetime(end)
|
|
464
|
+
|
|
465
|
+
return [
|
|
466
|
+
BillingReportItem._from_proto(pb_item)
|
|
467
|
+
async for pb_item in self._environment.client.stub.WorkspaceBillingReport.unary_stream(request)
|
|
468
|
+
]
|
|
469
|
+
|
|
395
470
|
|
|
396
471
|
ENVIRONMENT_CACHE: dict[str, _Environment] = {}
|
|
397
472
|
|
|
@@ -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:
|
|
@@ -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,75 @@ 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/reference/cli/billing) CLI command. The CLI
|
|
155
|
+
has a few convenience features for generating reports across relative time ranges.
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
+ BILLING_DOCSTRING
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if tag_names is None:
|
|
162
|
+
tag_names = []
|
|
163
|
+
|
|
164
|
+
if end is None:
|
|
165
|
+
end = datetime.now(timezone.utc)
|
|
166
|
+
|
|
167
|
+
if start.tzinfo is None:
|
|
168
|
+
start = start.replace(tzinfo=timezone.utc)
|
|
169
|
+
elif start.tzinfo != timezone.utc:
|
|
170
|
+
raise InvalidError("Timezone-aware 'start' parameter must be in UTC.")
|
|
171
|
+
|
|
172
|
+
if end.tzinfo is None:
|
|
173
|
+
end = end.replace(tzinfo=timezone.utc)
|
|
174
|
+
elif end.tzinfo != timezone.utc:
|
|
175
|
+
raise InvalidError("Timezone-aware 'end' parameter must be in UTC.")
|
|
176
|
+
|
|
177
|
+
if not self._workspace.is_hydrated:
|
|
178
|
+
await self._workspace.hydrate()
|
|
179
|
+
|
|
180
|
+
request = api_pb2.WorkspaceBillingReportRequest(
|
|
181
|
+
resolution=resolution,
|
|
182
|
+
tag_names=tag_names,
|
|
183
|
+
)
|
|
184
|
+
request.start_timestamp.FromDatetime(start)
|
|
185
|
+
request.end_timestamp.FromDatetime(end)
|
|
186
|
+
|
|
187
|
+
return [
|
|
188
|
+
BillingReportItem._from_proto(pb_item)
|
|
189
|
+
async for pb_item in self._workspace.client.stub.WorkspaceBillingReport.unary_stream(request)
|
|
190
|
+
]
|
|
@@ -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
|
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import decimal
|
|
3
|
+
import modal._billing
|
|
4
|
+
import modal.client
|
|
5
|
+
import modal_proto.api_pb2
|
|
6
|
+
import typing
|
|
7
|
+
import typing_extensions
|
|
8
|
+
|
|
9
|
+
class BillingReportItem:
|
|
10
|
+
"""BillingReportItem(object_id: str, description: str, environment_name: str, interval_start: datetime.datetime, cost: decimal.Decimal, cost_by_resource: dict[str, decimal.Decimal], tags: dict[str, str])"""
|
|
11
|
+
|
|
12
|
+
object_id: str
|
|
13
|
+
description: str
|
|
14
|
+
environment_name: str
|
|
15
|
+
interval_start: datetime.datetime
|
|
16
|
+
cost: decimal.Decimal
|
|
17
|
+
cost_by_resource: dict[str, decimal.Decimal]
|
|
18
|
+
tags: dict[str, str]
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, key: str) -> typing.Any:
|
|
21
|
+
"""mdmd:ignore"""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def __setitem__(self, key: str, _: typing.Any):
|
|
25
|
+
"""mdmd:ignore"""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def keys(self) -> typing.Iterable[str]:
|
|
29
|
+
"""mdmd:ignore"""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def values(self) -> typing.Iterable[typing.Any]:
|
|
33
|
+
"""mdmd:ignore"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def items(self) -> typing.Iterable[tuple[str, typing.Any]]:
|
|
37
|
+
"""mdmd:ignore"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _from_proto(
|
|
42
|
+
cls, pb_item: modal_proto.api_pb2.WorkspaceBillingReportItem
|
|
43
|
+
) -> modal._billing.BillingReportItem: ...
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
object_id: str,
|
|
47
|
+
description: str,
|
|
48
|
+
environment_name: str,
|
|
49
|
+
interval_start: datetime.datetime,
|
|
50
|
+
cost: decimal.Decimal,
|
|
51
|
+
cost_by_resource: dict[str, decimal.Decimal],
|
|
52
|
+
tags: dict[str, str],
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def __repr__(self):
|
|
58
|
+
"""Return repr(self)."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def __eq__(self, other):
|
|
62
|
+
"""Return self==value."""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def __setattr__(self, name, value):
|
|
66
|
+
"""Implement setattr(self, name, value)."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def __delattr__(self, name):
|
|
70
|
+
"""Implement delattr(self, name)."""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def __hash__(self):
|
|
74
|
+
"""Return hash(self)."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
def __getstate__(self): ...
|
|
78
|
+
def __setstate__(self, state): ...
|
|
79
|
+
|
|
80
|
+
class WorkspaceBillingReportItem(typing.TypedDict):
|
|
81
|
+
"""dict() -> new empty dictionary
|
|
82
|
+
dict(mapping) -> new dictionary initialized from a mapping object's
|
|
83
|
+
(key, value) pairs
|
|
84
|
+
dict(iterable) -> new dictionary initialized as if via:
|
|
85
|
+
d = {}
|
|
86
|
+
for k, v in iterable:
|
|
87
|
+
d[k] = v
|
|
88
|
+
dict(**kwargs) -> new dictionary initialized with the name=value pairs
|
|
89
|
+
in the keyword argument list. For example: dict(one=1, two=2)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
object_id: str
|
|
93
|
+
description: str
|
|
94
|
+
environment_name: str
|
|
95
|
+
interval_start: datetime.datetime
|
|
96
|
+
cost: decimal.Decimal
|
|
97
|
+
tags: dict[str, str]
|
|
98
|
+
|
|
99
|
+
class __workspace_billing_report_spec(typing_extensions.Protocol):
|
|
100
|
+
def __call__(
|
|
101
|
+
self,
|
|
102
|
+
/,
|
|
103
|
+
*,
|
|
104
|
+
start: datetime.datetime,
|
|
105
|
+
end: typing.Optional[datetime.datetime] = None,
|
|
106
|
+
resolution: str = "d",
|
|
107
|
+
tag_names: typing.Optional[list[str]] = None,
|
|
108
|
+
client: typing.Optional[modal.client.Client] = None,
|
|
109
|
+
) -> list[modal._billing.WorkspaceBillingReportItem]:
|
|
110
|
+
"""Generate a tabular report of workspace usage by object and time.
|
|
111
|
+
|
|
112
|
+
The result will be a list of dictionaries for each interval (determined by `resolution`)
|
|
113
|
+
between the `start` and `end` limits. The dictionary represents a single Modal object
|
|
114
|
+
that billing can be attributed to (e.g., an App) along with metadata (including user-defined
|
|
115
|
+
tags) for identifying that object. The dictionary also contains a breakdown of the cost value
|
|
116
|
+
attributed to individual resources (for an App, this can be CPU, Memory, specific GPU types,
|
|
117
|
+
etc.). The specific resource types included in the breakdown are subject to change as
|
|
118
|
+
Modal's billing model evolves.
|
|
119
|
+
|
|
120
|
+
The `start` and `end` parameters are required to either have a UTC timezone or to be
|
|
121
|
+
timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
|
|
122
|
+
be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
|
|
123
|
+
parameters are partial: `start` will be rounded to the beginning of its interval, while
|
|
124
|
+
partial `end` intervals will be excluded.
|
|
125
|
+
|
|
126
|
+
Additional user-provided metadata can be included in the report if the objects have tags
|
|
127
|
+
and `tag_names` (i.e., keys) are specified in the request. Alternatively, pass `tag_names=["*"]`
|
|
128
|
+
to include all tags in the report. Note that tags will be attributed to the entire interval even
|
|
129
|
+
if they were added or removed at some point within it. If the tag name was not in use during an
|
|
130
|
+
interval, it will be absent from the tags dictionary in that output row.
|
|
131
|
+
|
|
132
|
+
In most cases, billing data will be available in the database that this API queries within
|
|
133
|
+
minutes, although there may be collection delays. If completeness is important for your use
|
|
134
|
+
case, we recommend leaving a buffer after the end of the query interval.
|
|
135
|
+
|
|
136
|
+
It's also possible to generate reports using the
|
|
137
|
+
[`modal billing report`](https://modal.com/docs/reference/cli/billing) CLI command. The CLI
|
|
138
|
+
has a few convenience features for generating reports across relative time ranges.
|
|
139
|
+
"""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
async def aio(
|
|
143
|
+
self,
|
|
144
|
+
/,
|
|
145
|
+
*,
|
|
146
|
+
start: datetime.datetime,
|
|
147
|
+
end: typing.Optional[datetime.datetime] = None,
|
|
148
|
+
resolution: str = "d",
|
|
149
|
+
tag_names: typing.Optional[list[str]] = None,
|
|
150
|
+
client: typing.Optional[modal.client.Client] = None,
|
|
151
|
+
) -> list[modal._billing.WorkspaceBillingReportItem]:
|
|
152
|
+
"""Generate a tabular report of workspace usage by object and time.
|
|
153
|
+
|
|
154
|
+
The result will be a list of dictionaries for each interval (determined by `resolution`)
|
|
155
|
+
between the `start` and `end` limits. The dictionary represents a single Modal object
|
|
156
|
+
that billing can be attributed to (e.g., an App) along with metadata (including user-defined
|
|
157
|
+
tags) for identifying that object. The dictionary also contains a breakdown of the cost value
|
|
158
|
+
attributed to individual resources (for an App, this can be CPU, Memory, specific GPU types,
|
|
159
|
+
etc.). The specific resource types included in the breakdown are subject to change as
|
|
160
|
+
Modal's billing model evolves.
|
|
161
|
+
|
|
162
|
+
The `start` and `end` parameters are required to either have a UTC timezone or to be
|
|
163
|
+
timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
|
|
164
|
+
be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
|
|
165
|
+
parameters are partial: `start` will be rounded to the beginning of its interval, while
|
|
166
|
+
partial `end` intervals will be excluded.
|
|
167
|
+
|
|
168
|
+
Additional user-provided metadata can be included in the report if the objects have tags
|
|
169
|
+
and `tag_names` (i.e., keys) are specified in the request. Alternatively, pass `tag_names=["*"]`
|
|
170
|
+
to include all tags in the report. Note that tags will be attributed to the entire interval even
|
|
171
|
+
if they were added or removed at some point within it. If the tag name was not in use during an
|
|
172
|
+
interval, it will be absent from the tags dictionary in that output row.
|
|
173
|
+
|
|
174
|
+
In most cases, billing data will be available in the database that this API queries within
|
|
175
|
+
minutes, although there may be collection delays. If completeness is important for your use
|
|
176
|
+
case, we recommend leaving a buffer after the end of the query interval.
|
|
177
|
+
|
|
178
|
+
It's also possible to generate reports using the
|
|
179
|
+
[`modal billing report`](https://modal.com/docs/reference/cli/billing) CLI command. The CLI
|
|
180
|
+
has a few convenience features for generating reports across relative time ranges.
|
|
181
|
+
"""
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
workspace_billing_report: __workspace_billing_report_spec
|