modal 1.5.2.dev22__tar.gz → 1.5.3__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.2.dev22 → modal-1.5.3}/PKG-INFO +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_clustered_functions.py +11 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_clustered_functions.pyi +6 -2
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_container_entrypoint.py +2 -4
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_environments.py +155 -81
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_function_variants.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_functions.py +90 -21
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_grpc_client.py +1 -1
- modal-1.5.3/modal/_logs_manager.py +674 -0
- modal-1.5.3/modal/_logs_manager.pyi +1063 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_object.py +5 -4
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_output/rich.py +3 -3
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_partial_function.py +15 -3
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_resources.py +4 -4
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/container_io_manager.py +6 -30
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/container_io_manager.pyi +5 -16
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/user_code_imports.py +2 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_server.py +21 -1
- modal-1.5.3/modal/_supports_logs.py +32 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_traceback.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/async_utils.py +5 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/blob_utils.py +1 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/bytes_io_segment_payload.py +2 -1
- modal-1.5.3/modal/_utils/display_utils.py +15 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/grpc_utils.py +20 -5
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/pattern_utils.py +2 -2
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/task_command_router_client.py +23 -34
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/time_utils.py +106 -14
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_workspace.py +78 -16
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/app.py +15 -3
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/app.pyi +8 -4
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/billing.py +121 -17
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/changelog.py +7 -7
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/cluster.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/config.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/dashboard.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/endpoint.py +23 -3
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/environment.py +215 -54
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/launch.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/network_file_system.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/profile.py +3 -3
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/shell.py +23 -4
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/client.py +14 -8
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/client.pyi +15 -21
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cloud_bucket_mount.py +18 -5
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cloud_bucket_mount.pyi +2 -2
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cls.py +5 -2
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cls.pyi +4 -4
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/config.py +30 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/environments.py +3 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/environments.pyi +152 -66
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/exception.py +5 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/experimental/__init__.py +11 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/file_pattern_matcher.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/functions.pyi +91 -30
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/mount.py +89 -31
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/mount.pyi +18 -6
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/parallel_map.py +56 -51
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/partial_function.pyi +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/runner.py +31 -9
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/runner.pyi +8 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/sandbox.py +239 -61
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/sandbox.pyi +251 -46
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/sandbox_fs.py +1 -2
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/sandbox_fs.pyi +2 -6
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/schedule.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/secret.py +11 -2
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/secret.pyi +18 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/server.pyi +24 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/serving.py +3 -3
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/snapshot.py +17 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/snapshot.pyi +22 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/types.py +81 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/workspace.pyi +95 -20
- {modal-1.5.2.dev22 → modal-1.5.3}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/modal.egg-info/SOURCES.txt +5 -0
- modal-1.5.3/modal_docs/gen_reference_docs.py +167 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_docs/mdmd/mdmd.py +67 -46
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_docs/mdmd/types.py +1 -0
- modal-1.5.3/modal_docs/reference_intro.md +115 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/api_grpc.py +80 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/api_pb2.py +1317 -1125
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/api_pb2.pyi +447 -56
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/api_pb2_grpc.py +165 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/api_pb2_grpc.pyi +50 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/modal_api_grpc.py +5 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/task_command_router_grpc.py +16 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/task_command_router_pb2.py +108 -88
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/task_command_router_pb2.pyi +47 -2
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/task_command_router_pb2_grpc.py +37 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/task_command_router_pb2_grpc.pyi +20 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_version/__init__.py +1 -1
- {modal-1.5.2.dev22 → modal-1.5.3}/pyproject.toml +49 -2
- modal-1.5.2.dev22/modal_docs/gen_reference_docs.py +0 -361
- {modal-1.5.2.dev22 → modal-1.5.3}/LICENSE +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/README.md +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/__main__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_billing.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_image.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_ipython.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_load_context.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_location.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_logs.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_output/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_output/manager.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_output/pty.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_output/status.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_resolver.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/asgi.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/execution_context.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/task_lifecycle_manager.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/telemetry.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_serialization.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_tunnel.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_tunnel.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_type_manager.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/app_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/browser_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/curl_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/deprecation.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/docker_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/function_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/git_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/hash_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/http_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/logger.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/mount_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/name_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/package_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/proxy_support.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_utils/shell_utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_vendor/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_vendor/tblib.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_vendor/version.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/_watcher.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/billing.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/billing.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/builder/2023.12.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/builder/2024.04.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/builder/2024.10.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/builder/2025.06.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/builder/README.md +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/builder/base-images.json +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/call_graph.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/_download.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/_help.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/_traceback.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/app.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/bootstrap.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/container.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/curl.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/dict.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/entry_point.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/image.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/import_refs.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/logo.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/programs/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/programs/vscode.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/queues.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/run.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/secret.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/selector.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/skills.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/token.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/utils.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/volume.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/cli/workspace.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/container_process.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/container_process.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/dict.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/dict.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/experimental/flash.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/experimental/flash.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/experimental/ipython.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/file_io.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/file_io.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/functions.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/image.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/image.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/io_streams.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/io_streams.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/network_file_system.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/network_file_system.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/object.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/object.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/output.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/parallel_map.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/partial_function.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/proxy.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/proxy.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/py.typed +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/queue.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/queue.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/retries.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/running_app.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/scheduler_placement.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/server.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/serving.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/skills/modal/SKILL.md +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/stream_type.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/token_flow.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/token_flow.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/volume.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/volume.pyi +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal/workspace.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal.egg-info/requires.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal.egg-info/top_level.txt +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_docs/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/__init__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_proto/py.typed +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/modal_version/__main__.py +0 -0
- {modal-1.5.2.dev22 → modal-1.5.3}/setup.cfg +0 -0
|
@@ -15,6 +15,7 @@ class ClusterInfo:
|
|
|
15
15
|
cluster_id: str
|
|
16
16
|
container_ips: list[str]
|
|
17
17
|
container_ipv4_ips: list[str]
|
|
18
|
+
fabric_ids: list[str]
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
cluster_info: ClusterInfo | None = None
|
|
@@ -29,6 +30,14 @@ def get_cluster_info() -> ClusterInfo:
|
|
|
29
30
|
return cluster_info
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def get_fabric_peers() -> list[int]:
|
|
34
|
+
info = get_cluster_info()
|
|
35
|
+
if info.rank >= len(info.fabric_ids) or not info.fabric_ids[info.rank]:
|
|
36
|
+
return [info.rank]
|
|
37
|
+
own_fabric_id = info.fabric_ids[info.rank]
|
|
38
|
+
return [rank for rank, fabric_id in enumerate(info.fabric_ids) if fabric_id == own_fabric_id]
|
|
39
|
+
|
|
40
|
+
|
|
32
41
|
async def _initialize_clustered_function(client: _Client, task_id: str, world_size: int):
|
|
33
42
|
global cluster_info
|
|
34
43
|
|
|
@@ -70,6 +79,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
|
|
|
70
79
|
cluster_id=resp.cluster_id,
|
|
71
80
|
container_ips=resp.container_ips,
|
|
72
81
|
container_ipv4_ips=resp.container_ipv4_ips,
|
|
82
|
+
fabric_ids=resp.fabric_ids,
|
|
73
83
|
)
|
|
74
84
|
else:
|
|
75
85
|
cluster_info = ClusterInfo(
|
|
@@ -77,6 +87,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
|
|
|
77
87
|
cluster_id="", # No cluster ID for single-node # TODO(irfansharif): Is this right?
|
|
78
88
|
container_ips=[container_ip],
|
|
79
89
|
container_ipv4_ips=[], # No IPv4 IPs for single-node
|
|
90
|
+
fabric_ids=[],
|
|
80
91
|
)
|
|
81
92
|
|
|
82
93
|
|
|
@@ -3,14 +3,17 @@ import types
|
|
|
3
3
|
import typing_extensions
|
|
4
4
|
|
|
5
5
|
class ClusterInfo:
|
|
6
|
-
"""ClusterInfo(rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str])"""
|
|
6
|
+
"""ClusterInfo(rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str], fabric_ids: list[str])"""
|
|
7
7
|
|
|
8
8
|
rank: int
|
|
9
9
|
cluster_id: str
|
|
10
10
|
container_ips: list[str]
|
|
11
11
|
container_ipv4_ips: list[str]
|
|
12
|
+
fabric_ids: list[str]
|
|
12
13
|
|
|
13
|
-
def __init__(
|
|
14
|
+
def __init__(
|
|
15
|
+
self, rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str], fabric_ids: list[str]
|
|
16
|
+
) -> None:
|
|
14
17
|
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
15
18
|
...
|
|
16
19
|
|
|
@@ -23,6 +26,7 @@ class ClusterInfo:
|
|
|
23
26
|
...
|
|
24
27
|
|
|
25
28
|
def get_cluster_info() -> ClusterInfo: ...
|
|
29
|
+
def get_fabric_peers() -> list[int]: ...
|
|
26
30
|
async def _initialize_clustered_function(client: modal.client._Client, task_id: str, world_size: int): ...
|
|
27
31
|
|
|
28
32
|
class __initialize_clustered_function_spec(typing_extensions.Protocol):
|
|
@@ -145,7 +145,7 @@ def call_function(
|
|
|
145
145
|
item_count += 1
|
|
146
146
|
|
|
147
147
|
await container_io_manager._send_outputs.aio(
|
|
148
|
-
|
|
148
|
+
io_context.output_items_generator_done(started_at, item_count)
|
|
149
149
|
)
|
|
150
150
|
else:
|
|
151
151
|
value = await io_context.call_function_async()
|
|
@@ -182,9 +182,7 @@ def call_function(
|
|
|
182
182
|
container_io_manager._queue_put(generator_queue, value)
|
|
183
183
|
item_count += 1
|
|
184
184
|
|
|
185
|
-
container_io_manager._send_outputs(
|
|
186
|
-
started_at, io_context.output_items_generator_done(started_at, item_count)
|
|
187
|
-
)
|
|
185
|
+
container_io_manager._send_outputs(io_context.output_items_generator_done(started_at, item_count))
|
|
188
186
|
else:
|
|
189
187
|
values = io_context.call_function_sync()
|
|
190
188
|
container_io_manager.push_outputs(io_context, started_at, values)
|
|
@@ -16,11 +16,13 @@ from modal_proto import api_pb2
|
|
|
16
16
|
from ._load_context import LoadContext
|
|
17
17
|
from ._object import _Object
|
|
18
18
|
from ._resolver import Resolver
|
|
19
|
+
from ._utils.deprecation import deprecation_warning
|
|
19
20
|
from ._utils.name_utils import check_environment_name
|
|
21
|
+
from ._utils.time_utils import is_utc_month_aligned, parse_billing_cycle
|
|
20
22
|
from .client import _Client
|
|
21
23
|
from .config import config, logger
|
|
22
24
|
from .exception import InvalidError, WorkspaceManagementError
|
|
23
|
-
from .types import BillingReportItem
|
|
25
|
+
from .types import BillingReportItem, EnvironmentBillingSummary
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
@dataclass(frozen=True)
|
|
@@ -103,63 +105,38 @@ class _EnvironmentManager:
|
|
|
103
105
|
await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
case "viewer":
|
|
109
|
-
return api_pb2.ENVIRONMENT_ROLE_VIEWER
|
|
110
|
-
case "contributor":
|
|
111
|
-
return api_pb2.ENVIRONMENT_ROLE_CONTRIBUTOR
|
|
112
|
-
case _:
|
|
113
|
-
raise InvalidError(f"Invalid Environment role: {role!r} (expected 'viewer' or 'contributor')")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _role_from_proto(proto_value: int) -> str:
|
|
117
|
-
match proto_value:
|
|
118
|
-
case api_pb2.ENVIRONMENT_ROLE_VIEWER:
|
|
119
|
-
return "viewer"
|
|
120
|
-
case api_pb2.ENVIRONMENT_ROLE_CONTRIBUTOR:
|
|
121
|
-
return "contributor"
|
|
122
|
-
case _:
|
|
123
|
-
raise ValueError(f"Unknown environment role: {proto_value}")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class _EnvironmentMembersManager:
|
|
127
|
-
"""mdmd:namespace
|
|
128
|
-
Namespace with methods for managing the membership of a restricted Environment.
|
|
129
|
-
|
|
130
|
-
See https://modal.com/docs/guide/rbac for more information on restricted Environments.
|
|
131
|
-
"""
|
|
108
|
+
class _EnvironmentRolesManager:
|
|
109
|
+
"""mdmd:namespace"""
|
|
132
110
|
|
|
133
111
|
def __init__(self, environment: "_Environment"):
|
|
134
112
|
"""mdmd:hidden"""
|
|
135
113
|
self._environment = environment
|
|
136
114
|
|
|
137
115
|
async def list(self) -> dict[Literal["users", "service_users"], dict[str, str]]:
|
|
138
|
-
"""
|
|
116
|
+
"""Enumerate the Environment Role for each user and service user in the workspace.
|
|
139
117
|
|
|
140
118
|
**Examples:**
|
|
141
119
|
|
|
142
120
|
```python notest
|
|
143
|
-
|
|
144
|
-
print(
|
|
121
|
+
roles = modal.Environment.from_name("my-env").roles.list()
|
|
122
|
+
print(roles)
|
|
145
123
|
# {
|
|
146
|
-
# "users": {"alice": "contributor", "bob": "viewer"},
|
|
147
|
-
# "service_users": {"alice-bot": "contributor"},
|
|
124
|
+
# "users": {"alice": "contributor", "bob": "viewer", "carol": "contributor"},
|
|
125
|
+
# "service_users": {"alice-bot": "contributor", "ops-bot": "viewer", "ci-bot": "no-access"},
|
|
148
126
|
# }
|
|
149
127
|
```
|
|
150
128
|
"""
|
|
151
129
|
await self._environment.hydrate()
|
|
152
|
-
req = api_pb2.
|
|
153
|
-
resp = await self._environment.client.stub.
|
|
130
|
+
req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
|
|
131
|
+
resp = await self._environment.client.stub.EnvironmentGetRoles(req)
|
|
154
132
|
|
|
155
133
|
users: dict[str, str] = {}
|
|
156
134
|
service_users: dict[str, str] = {}
|
|
157
135
|
for principal in resp.principal_roles:
|
|
158
|
-
role = _role_from_proto(principal.role)
|
|
159
136
|
if principal.user_id:
|
|
160
|
-
users[principal.user_name] =
|
|
137
|
+
users[principal.user_name] = principal.role_str
|
|
161
138
|
elif principal.service_user_id:
|
|
162
|
-
service_users[principal.service_user_name] =
|
|
139
|
+
service_users[principal.service_user_name] = principal.role_str
|
|
163
140
|
|
|
164
141
|
return {"users": users, "service_users": service_users}
|
|
165
142
|
|
|
@@ -169,16 +146,17 @@ class _EnvironmentMembersManager:
|
|
|
169
146
|
users: Mapping[str, str] | None = None,
|
|
170
147
|
service_users: Mapping[str, str] | None = None,
|
|
171
148
|
) -> None:
|
|
172
|
-
"""
|
|
149
|
+
"""Update the Environment Role of users and service users.
|
|
173
150
|
|
|
174
|
-
Each
|
|
175
|
-
|
|
151
|
+
Each role is one of 'contributor', 'viewer', or 'no-access'. Service users can be
|
|
152
|
+
assigned a role on any Environment, while workspace members can only be assigned a
|
|
153
|
+
role on restricted Environments.
|
|
176
154
|
|
|
177
155
|
**Examples:**
|
|
178
156
|
|
|
179
157
|
```python notest
|
|
180
158
|
env = modal.Environment.from_name("my-restricted-env")
|
|
181
|
-
env.
|
|
159
|
+
env.roles.update(
|
|
182
160
|
users={"alice": "contributor", "bob": "viewer"},
|
|
183
161
|
service_users={"alice-bot": "contributor"},
|
|
184
162
|
)
|
|
@@ -188,13 +166,13 @@ class _EnvironmentMembersManager:
|
|
|
188
166
|
users = users or {}
|
|
189
167
|
service_users = service_users or {}
|
|
190
168
|
|
|
191
|
-
req = api_pb2.
|
|
192
|
-
resp = await self._environment.client.stub.
|
|
169
|
+
req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
|
|
170
|
+
resp = await self._environment.client.stub.EnvironmentGetRoles(req)
|
|
193
171
|
|
|
194
|
-
#
|
|
172
|
+
# EnvironmentGetRoles returns every workspace principal
|
|
195
173
|
user_name_to_id: dict[str, str] = {}
|
|
196
174
|
service_user_name_to_id: dict[str, str] = {}
|
|
197
|
-
for principal in
|
|
175
|
+
for principal in resp.principal_roles:
|
|
198
176
|
if principal.user_id:
|
|
199
177
|
user_name_to_id[principal.user_name] = principal.user_id
|
|
200
178
|
elif principal.service_user_id:
|
|
@@ -207,7 +185,7 @@ class _EnvironmentMembersManager:
|
|
|
207
185
|
requests[f"User {name!r}"] = api_pb2.EnvironmentRoleSetRequest(
|
|
208
186
|
environment_id=self._environment.object_id,
|
|
209
187
|
user_id=user_name_to_id[name],
|
|
210
|
-
|
|
188
|
+
role_str=role,
|
|
211
189
|
)
|
|
212
190
|
for name, role in service_users.items():
|
|
213
191
|
if name not in service_user_name_to_id:
|
|
@@ -215,35 +193,70 @@ class _EnvironmentMembersManager:
|
|
|
215
193
|
requests[f"Service user {name!r}"] = api_pb2.EnvironmentRoleSetRequest(
|
|
216
194
|
environment_id=self._environment.object_id,
|
|
217
195
|
service_user_id=service_user_name_to_id[name],
|
|
218
|
-
|
|
196
|
+
role_str=role,
|
|
219
197
|
)
|
|
220
198
|
|
|
221
199
|
await self._dispatch_role_updates(requests)
|
|
222
200
|
|
|
201
|
+
async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
|
|
202
|
+
"""Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
|
|
203
|
+
results = await asyncio.gather(
|
|
204
|
+
*(self._environment.client.stub.EnvironmentRoleSet(req) for req in requests.values()),
|
|
205
|
+
return_exceptions=True,
|
|
206
|
+
)
|
|
207
|
+
errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
|
|
208
|
+
if errors:
|
|
209
|
+
n = len(errors)
|
|
210
|
+
header = f"{n} error{'s' if n != 1 else ''} occurred while updating Environment roles:"
|
|
211
|
+
details = "\n".join(f" - {label}: {e}" for label, e in errors)
|
|
212
|
+
raise WorkspaceManagementError(f"{header}\n{details}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Deprecated alias for `_EnvironmentRolesManager`; each method warns and delegates.
|
|
216
|
+
class _EnvironmentMembersManager:
|
|
217
|
+
"""mdmd:hidden"""
|
|
218
|
+
|
|
219
|
+
def __init__(self, environment: "_Environment"):
|
|
220
|
+
"""mdmd:hidden"""
|
|
221
|
+
self._environment = environment
|
|
222
|
+
|
|
223
|
+
async def list(self) -> dict[Literal["users", "service_users"], dict[str, str]]:
|
|
224
|
+
deprecation_warning(
|
|
225
|
+
(2026, 7, 23),
|
|
226
|
+
"`Environment.members.list()` is deprecated; use `Environment.roles.list()` instead.",
|
|
227
|
+
)
|
|
228
|
+
return await _EnvironmentRolesManager(self._environment).list()
|
|
229
|
+
|
|
230
|
+
async def update(
|
|
231
|
+
self,
|
|
232
|
+
*,
|
|
233
|
+
users: Mapping[str, str] | None = None,
|
|
234
|
+
service_users: Mapping[str, str] | None = None,
|
|
235
|
+
) -> None:
|
|
236
|
+
deprecation_warning(
|
|
237
|
+
(2026, 7, 23),
|
|
238
|
+
"`Environment.members.update()` is deprecated; use `Environment.roles.update()` instead.",
|
|
239
|
+
)
|
|
240
|
+
await _EnvironmentRolesManager(self._environment).update(users=users, service_users=service_users)
|
|
241
|
+
|
|
223
242
|
async def remove(
|
|
224
243
|
self,
|
|
225
244
|
*,
|
|
226
245
|
users: Iterable[str] | None = None,
|
|
227
246
|
service_users: Iterable[str] | None = None,
|
|
228
247
|
) -> None:
|
|
229
|
-
"""Remove
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
env = modal.Environment.from_name("my-restricted-env")
|
|
235
|
-
env.members.remove(
|
|
236
|
-
users=["alice"],
|
|
237
|
-
service_users=["alice-bot"],
|
|
248
|
+
"""Remove the Environment Role of users and service users, reverting them to the default."""
|
|
249
|
+
deprecation_warning(
|
|
250
|
+
(2026, 7, 23),
|
|
251
|
+
"`Environment.members.remove()` is deprecated. Environment Roles are now explicit; "
|
|
252
|
+
"set a role (e.g. 'no-access') with `Environment.roles.update()` instead.",
|
|
238
253
|
)
|
|
239
|
-
```
|
|
240
|
-
"""
|
|
241
254
|
await self._environment.hydrate()
|
|
242
255
|
users = users or []
|
|
243
256
|
service_users = service_users or []
|
|
244
257
|
|
|
245
|
-
req = api_pb2.
|
|
246
|
-
resp = await self._environment.client.stub.
|
|
258
|
+
req = api_pb2.EnvironmentGetRolesRequest(environment_id=self._environment.object_id)
|
|
259
|
+
resp = await self._environment.client.stub.EnvironmentGetRoles(req)
|
|
247
260
|
|
|
248
261
|
user_name_to_id: dict[str, str] = {}
|
|
249
262
|
service_user_name_to_id: dict[str, str] = {}
|
|
@@ -256,7 +269,7 @@ class _EnvironmentMembersManager:
|
|
|
256
269
|
requests: dict[str, api_pb2.EnvironmentRoleSetRequest] = {}
|
|
257
270
|
for name in users:
|
|
258
271
|
if name not in user_name_to_id:
|
|
259
|
-
raise InvalidError(f"User {name!r}
|
|
272
|
+
raise InvalidError(f"User {name!r} not found in workspace")
|
|
260
273
|
requests[f"User {name!r}"] = api_pb2.EnvironmentRoleSetRequest(
|
|
261
274
|
environment_id=self._environment.object_id,
|
|
262
275
|
user_id=user_name_to_id[name],
|
|
@@ -264,27 +277,14 @@ class _EnvironmentMembersManager:
|
|
|
264
277
|
)
|
|
265
278
|
for name in service_users:
|
|
266
279
|
if name not in service_user_name_to_id:
|
|
267
|
-
raise InvalidError(f"Service user {name!r}
|
|
280
|
+
raise InvalidError(f"Service user {name!r} not found in workspace")
|
|
268
281
|
requests[f"Service user {name!r}"] = api_pb2.EnvironmentRoleSetRequest(
|
|
269
282
|
environment_id=self._environment.object_id,
|
|
270
283
|
service_user_id=service_user_name_to_id[name],
|
|
271
284
|
role=api_pb2.ENVIRONMENT_ROLE_UNSPECIFIED,
|
|
272
285
|
)
|
|
273
286
|
|
|
274
|
-
await self._dispatch_role_updates(requests)
|
|
275
|
-
|
|
276
|
-
async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
|
|
277
|
-
"""Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
|
|
278
|
-
results = await asyncio.gather(
|
|
279
|
-
*(self._environment.client.stub.EnvironmentRoleSet(req) for req in requests.values()),
|
|
280
|
-
return_exceptions=True,
|
|
281
|
-
)
|
|
282
|
-
errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
|
|
283
|
-
if errors:
|
|
284
|
-
n = len(errors)
|
|
285
|
-
header = f"{n} error{'s' if n != 1 else ''} occurred while updating Environment members:"
|
|
286
|
-
details = "\n".join(f" - {label}: {e}" for label, e in errors)
|
|
287
|
-
raise WorkspaceManagementError(f"{header}\n{details}")
|
|
287
|
+
await _EnvironmentRolesManager(self._environment)._dispatch_role_updates(requests)
|
|
288
288
|
|
|
289
289
|
|
|
290
290
|
class _Environment(_Object, type_prefix="en"):
|
|
@@ -307,8 +307,18 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
307
307
|
def objects(cls) -> _EnvironmentManager:
|
|
308
308
|
return _EnvironmentManager()
|
|
309
309
|
|
|
310
|
+
@property
|
|
311
|
+
def roles(self) -> "_EnvironmentRolesManager":
|
|
312
|
+
"""Namespace with methods for managing the Environment Roles of users and service users.
|
|
313
|
+
|
|
314
|
+
See https://modal.com/docs/guide/rbac for more information on Environment Roles.
|
|
315
|
+
"""
|
|
316
|
+
return _EnvironmentRolesManager(self)
|
|
317
|
+
|
|
310
318
|
@property
|
|
311
319
|
def members(self) -> "_EnvironmentMembersManager":
|
|
320
|
+
"""mdmd:hidden"""
|
|
321
|
+
# Deprecated alias for `Environment.roles`.
|
|
312
322
|
return _EnvironmentMembersManager(self)
|
|
313
323
|
|
|
314
324
|
# TODO(michael) Keeping this private for now until we decide what else should be in it
|
|
@@ -393,13 +403,12 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
393
403
|
|
|
394
404
|
@property
|
|
395
405
|
def billing(self) -> "_EnvironmentBillingManager":
|
|
406
|
+
"""Namespace for Environment billing APIs."""
|
|
396
407
|
return _EnvironmentBillingManager(self)
|
|
397
408
|
|
|
398
409
|
|
|
399
410
|
class _EnvironmentBillingManager:
|
|
400
|
-
"""mdmd:namespace
|
|
401
|
-
Namespace for Environment billing APIs
|
|
402
|
-
"""
|
|
411
|
+
"""mdmd:namespace"""
|
|
403
412
|
|
|
404
413
|
def __init__(self, environment: _Environment):
|
|
405
414
|
"""mdmd:ignore"""
|
|
@@ -436,7 +445,7 @@ class _EnvironmentBillingManager:
|
|
|
436
445
|
- [`modal environment billing report`](https://modal.com/docs/cli/latest/environment#modal-environment-billing-report):
|
|
437
446
|
An environment report CLI that has convenience features around relative time range queries
|
|
438
447
|
and JSON/CSV output.
|
|
439
|
-
- [`Workspace.billing.report()`](https://modal.com/docs/sdk/py/latest/
|
|
448
|
+
- [`Workspace.billing.report()`](https://modal.com/docs/sdk/py/latest/Workspace#billingreport):
|
|
440
449
|
An analogous report API for the entire Workspace.
|
|
441
450
|
|
|
442
451
|
"""
|
|
@@ -472,6 +481,71 @@ class _EnvironmentBillingManager:
|
|
|
472
481
|
async for pb_item in self._environment.client.stub.WorkspaceBillingReport.unary_stream(request)
|
|
473
482
|
]
|
|
474
483
|
|
|
484
|
+
async def summary(
|
|
485
|
+
self,
|
|
486
|
+
cycle: str | datetime | None = None, # Start of the summary, inclusive
|
|
487
|
+
) -> EnvironmentBillingSummary:
|
|
488
|
+
"""Return a summary of environment cost over a single billing cycle determined by `cycle`.
|
|
489
|
+
|
|
490
|
+
Unlike the analogous `Workspace.billing.summary()`, this API only emits metered cost
|
|
491
|
+
information. This is because billing adjustments due to credits, free storage, etc. are
|
|
492
|
+
applied at the Workspace level, and thus cannot be attributed to individual Environments.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
cycle: Start of the summary, inclusive. Must be the first of a month, and must be in UTC
|
|
496
|
+
or timezone-naive (interpreted as UTC). If provided as a string, it must either be
|
|
497
|
+
formatted as an ISO 8601 month (YYYY-MM), or must be one of the convenience spellings
|
|
498
|
+
"this month" or "last month". If not provided, `cycle` defaults to the first of the
|
|
499
|
+
current month (in which case a summary is generated for the current billing cycle).
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
A single `EnvironmentBillingSummary` dataclass containing the following fields:
|
|
503
|
+
- `metered_cost` representing cost before any adjustments, and
|
|
504
|
+
- `metered_cost_breakdown` containing a breakdown of that cost by the Modal resources
|
|
505
|
+
that generated it. The exact keys of this are subject to change as Modal's billing
|
|
506
|
+
model evolves.
|
|
507
|
+
|
|
508
|
+
All values are reported as `decimal.Decimal`s.
|
|
509
|
+
|
|
510
|
+
See also:
|
|
511
|
+
- [`modal environment billing summary`](https://modal.com/docs/cli/latest/billing#modal-environment-billing-summary):
|
|
512
|
+
An environment summary CLI that has convenience features around relative time range queries.
|
|
513
|
+
- [`Environment.billing.report()`](https://modal.com/docs/sdk/py/latest/Environment#billingreport):
|
|
514
|
+
An analogous report API that is scoped to a specific Environment.
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
if cycle is None:
|
|
518
|
+
cycle = datetime.now(timezone.utc).replace(
|
|
519
|
+
day=1,
|
|
520
|
+
hour=0,
|
|
521
|
+
minute=0,
|
|
522
|
+
second=0,
|
|
523
|
+
microsecond=0,
|
|
524
|
+
)
|
|
525
|
+
elif isinstance(cycle, str):
|
|
526
|
+
cycle = parse_billing_cycle(cycle)
|
|
527
|
+
|
|
528
|
+
if cycle.tzinfo is None:
|
|
529
|
+
cycle = cycle.replace(tzinfo=timezone.utc)
|
|
530
|
+
elif cycle.tzinfo != timezone.utc:
|
|
531
|
+
raise InvalidError("Timezone-aware 'cycle' parameter must be in UTC.")
|
|
532
|
+
|
|
533
|
+
if not is_utc_month_aligned(cycle):
|
|
534
|
+
raise InvalidError("Provided 'cycle' parameter must be the first of a month.")
|
|
535
|
+
|
|
536
|
+
if cycle > datetime.now(timezone.utc):
|
|
537
|
+
raise InvalidError("Provided 'cycle' parameter cannot be in the future.")
|
|
538
|
+
|
|
539
|
+
if not self._environment.is_hydrated:
|
|
540
|
+
await self._environment.hydrate()
|
|
541
|
+
|
|
542
|
+
request = api_pb2.EnvironmentBillingSummaryRequest(environment_id=self._environment.object_id)
|
|
543
|
+
request.start_timestamp.FromDatetime(cycle)
|
|
544
|
+
|
|
545
|
+
return EnvironmentBillingSummary._from_proto(
|
|
546
|
+
await self._environment.client.stub.EnvironmentBillingSummary(request)
|
|
547
|
+
)
|
|
548
|
+
|
|
475
549
|
|
|
476
550
|
ENVIRONMENT_CACHE: dict[str, _Environment] = {}
|
|
477
551
|
|
|
@@ -547,4 +621,4 @@ def ensure_env(environment_name: str | None = None) -> str:
|
|
|
547
621
|
if environment_name is not None:
|
|
548
622
|
config.override_locally("environment", environment_name)
|
|
549
623
|
|
|
550
|
-
return config.get("environment")
|
|
624
|
+
return config.get("environment") or ""
|
|
@@ -171,7 +171,7 @@ class _FunctionOptions:
|
|
|
171
171
|
replace_secret_ids=bool(self.secrets),
|
|
172
172
|
replace_volume_mounts=len(volume_mounts) > 0,
|
|
173
173
|
volume_mounts=volume_mounts,
|
|
174
|
-
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(self.cloud_bucket_mounts),
|
|
174
|
+
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(self.cloud_bucket_mounts)[0],
|
|
175
175
|
replace_cloud_bucket_mounts=bool(self.cloud_bucket_mounts),
|
|
176
176
|
resources=self.resources,
|
|
177
177
|
retry_policy=self.retry_policy,
|