modal 1.4.3.dev12__tar.gz → 1.4.3.dev14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/PKG-INFO +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/__init__.py +2 -0
- modal-1.4.3.dev14/modal/_environments.py +470 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_serialization.py +3 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/name_utils.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/app.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/cluster.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/container.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/dict.py +1 -1
- modal-1.4.3.dev14/modal/cli/environment.py +179 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/network_file_system.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/queues.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/run.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/secret.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/shell.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/volume.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/client.pyi +2 -2
- modal-1.4.3.dev14/modal/environments.py +31 -0
- modal-1.4.3.dev14/modal/environments.pyi +343 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/exception.py +4 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/image.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/runner.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal.egg-info/SOURCES.txt +1 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/task_command_router_pb2.py +16 -16
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/task_command_router_pb2.pyi +18 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_version/__init__.py +1 -1
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/pyproject.toml +0 -1
- modal-1.4.3.dev12/modal/cli/environment.py +0 -117
- modal-1.4.3.dev12/modal/environments.py +0 -164
- modal-1.4.3.dev12/modal/environments.pyi +0 -120
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/LICENSE +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/README.md +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/__main__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_billing.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_clustered_functions.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_functions.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_grpc_client.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_ipython.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_load_context.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_location.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_logs.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_object.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_output/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_output/manager.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_output/pty.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_output/rich.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_output/status.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_partial_function.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_resolver.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_resources.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_server.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_traceback.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_tunnel.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_tunnel.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_type_manager.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/logger.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/sandbox_fs_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_vendor/version.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/_watcher.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/app.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/app.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/billing.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/builder/README.md +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/builder/base-images.json +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/call_graph.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/_download.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/_help.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/_traceback.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/billing.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/bootstrap.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/changelog.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/config.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/dashboard.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/entry_point.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/import_refs.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/launch.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/logo.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/profile.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/selector.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/token.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cli/utils.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/client.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cls.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/cls.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/config.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/container_process.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/container_process.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/dict.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/dict.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/experimental/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/experimental/flash.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/experimental/ipython.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/file_io.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/file_io.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/functions.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/functions.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/image.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/io_streams.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/io_streams.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/mount.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/mount.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/network_file_system.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/network_file_system.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/object.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/object.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/output.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/parallel_map.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/parallel_map.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/partial_function.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/partial_function.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/proxy.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/proxy.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/py.typed +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/queue.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/queue.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/retries.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/runner.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/running_app.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/sandbox.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/sandbox.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/sandbox_fs.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/schedule.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/scheduler_placement.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/secret.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/secret.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/server.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/server.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/serving.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/serving.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/snapshot.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/snapshot.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/stream_type.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/token_flow.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/token_flow.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/volume.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal/volume.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal.egg-info/requires.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_docs/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/__init__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/api_grpc.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/api_pb2.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/py.typed +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/modal_version/__main__.py +0 -0
- {modal-1.4.3.dev12 → modal-1.4.3.dev14}/setup.cfg +0 -0
|
@@ -17,6 +17,7 @@ try:
|
|
|
17
17
|
from .cloud_bucket_mount import CloudBucketMount
|
|
18
18
|
from .cls import Cls, parameter
|
|
19
19
|
from .dict import Dict
|
|
20
|
+
from .environments import Environment
|
|
20
21
|
from .exception import Error
|
|
21
22
|
from .file_pattern_matcher import FilePatternMatcher
|
|
22
23
|
from .functions import Function, FunctionCall
|
|
@@ -61,6 +62,7 @@ __all__ = [
|
|
|
61
62
|
"Cls",
|
|
62
63
|
"Cron",
|
|
63
64
|
"Dict",
|
|
65
|
+
"Environment",
|
|
64
66
|
"Error",
|
|
65
67
|
"FilePatternMatcher",
|
|
66
68
|
"Function",
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Copyright Modal Labs 2023
|
|
2
|
+
import asyncio
|
|
3
|
+
import builtins
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
from google.protobuf.empty_pb2 import Empty
|
|
9
|
+
from google.protobuf.message import Message
|
|
10
|
+
from google.protobuf.wrappers_pb2 import StringValue
|
|
11
|
+
from synchronicity import classproperty
|
|
12
|
+
|
|
13
|
+
from modal_proto import api_pb2
|
|
14
|
+
|
|
15
|
+
from ._load_context import LoadContext
|
|
16
|
+
from ._object import _Object
|
|
17
|
+
from ._resolver import Resolver
|
|
18
|
+
from ._utils.name_utils import check_environment_name
|
|
19
|
+
from .client import _Client
|
|
20
|
+
from .config import config, logger
|
|
21
|
+
from .exception import InvalidError, WorkspaceManagementError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class EnvironmentSettings:
|
|
26
|
+
image_builder_version: str
|
|
27
|
+
webhook_suffix: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _EnvironmentManager:
|
|
31
|
+
"""Namespace with methods for managing Environment objects."""
|
|
32
|
+
|
|
33
|
+
async def create(
|
|
34
|
+
self,
|
|
35
|
+
name: str, # Name to use for the new Environment
|
|
36
|
+
*,
|
|
37
|
+
restricted: bool = False, # If True, enable RBAC restrictions on the Environment
|
|
38
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Create a new Environment.
|
|
41
|
+
|
|
42
|
+
**Examples:**
|
|
43
|
+
|
|
44
|
+
```python notest
|
|
45
|
+
modal.Environment.objects.create("my-environment")
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
check_environment_name(name)
|
|
49
|
+
client = await _Client.from_env() if client is None else client
|
|
50
|
+
await client.stub.EnvironmentCreate(api_pb2.EnvironmentCreateRequest(name=name, is_managed=restricted))
|
|
51
|
+
|
|
52
|
+
async def list(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
56
|
+
) -> builtins.list["_Environment"]:
|
|
57
|
+
"""Return a list of hydrated Environment objects.
|
|
58
|
+
|
|
59
|
+
**Examples:**
|
|
60
|
+
|
|
61
|
+
```python notest
|
|
62
|
+
environments = modal.Environment.objects.list()
|
|
63
|
+
print([e.name for e in environments])
|
|
64
|
+
```
|
|
65
|
+
"""
|
|
66
|
+
client = await _Client.from_env() if client is None else client
|
|
67
|
+
resp = await client.stub.EnvironmentList(Empty())
|
|
68
|
+
environments = []
|
|
69
|
+
for item in resp.items:
|
|
70
|
+
metadata = api_pb2.EnvironmentMetadata(
|
|
71
|
+
name=item.name,
|
|
72
|
+
settings=api_pb2.EnvironmentSettings(webhook_suffix=item.webhook_suffix),
|
|
73
|
+
)
|
|
74
|
+
env = _Environment._new_hydrated(
|
|
75
|
+
item.environment_id,
|
|
76
|
+
client,
|
|
77
|
+
metadata,
|
|
78
|
+
is_another_app=True,
|
|
79
|
+
rep=f"Environment.from_name({item.name!r})",
|
|
80
|
+
)
|
|
81
|
+
environments.append(env)
|
|
82
|
+
return environments
|
|
83
|
+
|
|
84
|
+
async def delete(
|
|
85
|
+
self,
|
|
86
|
+
name: str, # Name of the Environment to delete
|
|
87
|
+
*,
|
|
88
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Delete a named Environment.
|
|
91
|
+
|
|
92
|
+
Warning: This is irreversible and will transitively delete all objects in the Environment.
|
|
93
|
+
|
|
94
|
+
**Examples:**
|
|
95
|
+
|
|
96
|
+
```python notest
|
|
97
|
+
modal.Environment.objects.delete("my-environment")
|
|
98
|
+
```
|
|
99
|
+
"""
|
|
100
|
+
client = await _Client.from_env() if client is None else client
|
|
101
|
+
await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
MemberRole = Literal["viewer", "contributor"]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _role_to_proto(role: str) -> api_pb2.EnvironmentRole.ValueType:
|
|
108
|
+
match role:
|
|
109
|
+
case "viewer":
|
|
110
|
+
return api_pb2.ENVIRONMENT_ROLE_VIEWER
|
|
111
|
+
case "contributor":
|
|
112
|
+
return api_pb2.ENVIRONMENT_ROLE_CONTRIBUTOR
|
|
113
|
+
case _:
|
|
114
|
+
raise InvalidError(f"Invalid Environment role: {role!r} (expected 'viewer' or 'contributor')")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _role_from_proto(proto_value: int) -> MemberRole:
|
|
118
|
+
match proto_value:
|
|
119
|
+
case int(v) if v == api_pb2.ENVIRONMENT_ROLE_VIEWER:
|
|
120
|
+
return "viewer"
|
|
121
|
+
case int(v) if v == api_pb2.ENVIRONMENT_ROLE_CONTRIBUTOR:
|
|
122
|
+
return "contributor"
|
|
123
|
+
case _:
|
|
124
|
+
raise ValueError(f"Unknown environment role: {proto_value}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _EnvironmentMembersManager:
|
|
128
|
+
"""mdmd:namespace
|
|
129
|
+
Namespace with methods for managing the membership of a restricted Environment.
|
|
130
|
+
|
|
131
|
+
See https://modal.com/docs/guide/rbac for more information on restricted Environments.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, environment: "_Environment"):
|
|
135
|
+
"""mdmd:hidden"""
|
|
136
|
+
self._environment = environment
|
|
137
|
+
|
|
138
|
+
async def list(self) -> dict[Literal["users", "service_users"], dict[str, MemberRole]]:
|
|
139
|
+
"""Return the members of a restricted Environment with their roles.
|
|
140
|
+
|
|
141
|
+
**Examples:**
|
|
142
|
+
|
|
143
|
+
```python notest
|
|
144
|
+
members = modal.Environment.from_name("my-restricted-env").members.list()
|
|
145
|
+
print(members)
|
|
146
|
+
# {
|
|
147
|
+
# "users": {"alice": "contributor", "bob": "viewer"},
|
|
148
|
+
# "service_users": {"alice-bot": "contributor"},
|
|
149
|
+
# }
|
|
150
|
+
```
|
|
151
|
+
"""
|
|
152
|
+
await self._environment.hydrate()
|
|
153
|
+
req = api_pb2.EnvironmentGetManagedRequest(environment_id=self._environment.object_id)
|
|
154
|
+
resp = await self._environment.client.stub.EnvironmentGetManaged(req)
|
|
155
|
+
|
|
156
|
+
users: dict[str, MemberRole] = {}
|
|
157
|
+
service_users: dict[str, MemberRole] = {}
|
|
158
|
+
for principal in resp.principal_roles:
|
|
159
|
+
role = _role_from_proto(principal.role)
|
|
160
|
+
if principal.user_id:
|
|
161
|
+
users[principal.user_name] = role
|
|
162
|
+
elif principal.service_user_id:
|
|
163
|
+
service_users[principal.service_user_name] = role
|
|
164
|
+
|
|
165
|
+
return {"users": users, "service_users": service_users}
|
|
166
|
+
|
|
167
|
+
async def update(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
users: Optional[Mapping[str, MemberRole]] = None,
|
|
171
|
+
service_users: Optional[Mapping[str, MemberRole]] = None,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Add or modify roles for members of a restricted Environment.
|
|
174
|
+
|
|
175
|
+
Each user or service user will be added to the Environment if not currently a member;
|
|
176
|
+
if already a member, the user or service user's role will be updated.
|
|
177
|
+
|
|
178
|
+
**Examples:**
|
|
179
|
+
|
|
180
|
+
```python notest
|
|
181
|
+
env = modal.Environment.from_name("my-restricted-env")
|
|
182
|
+
env.members.update(
|
|
183
|
+
users={"alice": "contributor", "bob": "viewer"},
|
|
184
|
+
service_users={"alice-bot": "contributor"},
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
"""
|
|
188
|
+
await self._environment.hydrate()
|
|
189
|
+
users = users or {}
|
|
190
|
+
service_users = service_users or {}
|
|
191
|
+
|
|
192
|
+
req = api_pb2.EnvironmentGetManagedRequest(environment_id=self._environment.object_id)
|
|
193
|
+
resp = await self._environment.client.stub.EnvironmentGetManaged(req)
|
|
194
|
+
|
|
195
|
+
# Both current members and additional eligible workspace principals can be assigned a role
|
|
196
|
+
user_name_to_id: dict[str, str] = {}
|
|
197
|
+
service_user_name_to_id: dict[str, str] = {}
|
|
198
|
+
for principal in [*resp.principal_roles, *resp.additional_roles]:
|
|
199
|
+
if principal.user_id:
|
|
200
|
+
user_name_to_id[principal.user_name] = principal.user_id
|
|
201
|
+
elif principal.service_user_id:
|
|
202
|
+
service_user_name_to_id[principal.service_user_name] = principal.service_user_id
|
|
203
|
+
|
|
204
|
+
requests: dict[str, api_pb2.EnvironmentRoleSetRequest] = {}
|
|
205
|
+
for name, role in users.items():
|
|
206
|
+
if name not in user_name_to_id:
|
|
207
|
+
raise InvalidError(f"User {name!r} not found in workspace")
|
|
208
|
+
requests[f"User {name!r}"] = api_pb2.EnvironmentRoleSetRequest(
|
|
209
|
+
environment_id=self._environment.object_id,
|
|
210
|
+
user_id=user_name_to_id[name],
|
|
211
|
+
role=_role_to_proto(role),
|
|
212
|
+
)
|
|
213
|
+
for name, role in service_users.items():
|
|
214
|
+
if name not in service_user_name_to_id:
|
|
215
|
+
raise InvalidError(f"Service user {name!r} not found in workspace")
|
|
216
|
+
requests[f"Service user {name!r}"] = api_pb2.EnvironmentRoleSetRequest(
|
|
217
|
+
environment_id=self._environment.object_id,
|
|
218
|
+
service_user_id=service_user_name_to_id[name],
|
|
219
|
+
role=_role_to_proto(role),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
await self._dispatch_role_updates(requests)
|
|
223
|
+
|
|
224
|
+
async def remove(
|
|
225
|
+
self,
|
|
226
|
+
*,
|
|
227
|
+
users: Optional[Iterable[str]] = None,
|
|
228
|
+
service_users: Optional[Iterable[str]] = None,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Remove members from a restricted Environment.
|
|
231
|
+
|
|
232
|
+
**Examples:**
|
|
233
|
+
|
|
234
|
+
```python notest
|
|
235
|
+
env = modal.Environment.from_name("my-restricted-env")
|
|
236
|
+
env.members.remove(
|
|
237
|
+
users=["alice"],
|
|
238
|
+
service_users=["alice-bot"],
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
"""
|
|
242
|
+
await self._environment.hydrate()
|
|
243
|
+
users = users or []
|
|
244
|
+
service_users = service_users or []
|
|
245
|
+
|
|
246
|
+
req = api_pb2.EnvironmentGetManagedRequest(environment_id=self._environment.object_id)
|
|
247
|
+
resp = await self._environment.client.stub.EnvironmentGetManaged(req)
|
|
248
|
+
|
|
249
|
+
user_name_to_id: dict[str, str] = {}
|
|
250
|
+
service_user_name_to_id: dict[str, str] = {}
|
|
251
|
+
for principal in resp.principal_roles:
|
|
252
|
+
if principal.user_id:
|
|
253
|
+
user_name_to_id[principal.user_name] = principal.user_id
|
|
254
|
+
elif principal.service_user_id:
|
|
255
|
+
service_user_name_to_id[principal.service_user_name] = principal.service_user_id
|
|
256
|
+
|
|
257
|
+
requests: dict[str, api_pb2.EnvironmentRoleSetRequest] = {}
|
|
258
|
+
for name in users:
|
|
259
|
+
if name not in user_name_to_id:
|
|
260
|
+
raise InvalidError(f"User {name!r} is not a member of this Environment")
|
|
261
|
+
requests[f"User {name!r}"] = api_pb2.EnvironmentRoleSetRequest(
|
|
262
|
+
environment_id=self._environment.object_id,
|
|
263
|
+
user_id=user_name_to_id[name],
|
|
264
|
+
role=api_pb2.ENVIRONMENT_ROLE_UNSPECIFIED,
|
|
265
|
+
)
|
|
266
|
+
for name in service_users:
|
|
267
|
+
if name not in service_user_name_to_id:
|
|
268
|
+
raise InvalidError(f"Service user {name!r} is not a member of this Environment")
|
|
269
|
+
requests[f"Service user {name!r}"] = api_pb2.EnvironmentRoleSetRequest(
|
|
270
|
+
environment_id=self._environment.object_id,
|
|
271
|
+
service_user_id=service_user_name_to_id[name],
|
|
272
|
+
role=api_pb2.ENVIRONMENT_ROLE_UNSPECIFIED,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
await self._dispatch_role_updates(requests)
|
|
276
|
+
|
|
277
|
+
async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
|
|
278
|
+
"""Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
|
|
279
|
+
results = await asyncio.gather(
|
|
280
|
+
*(self._environment.client.stub.EnvironmentRoleSet(req) for req in requests.values()),
|
|
281
|
+
return_exceptions=True,
|
|
282
|
+
)
|
|
283
|
+
errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
|
|
284
|
+
if errors:
|
|
285
|
+
n = len(errors)
|
|
286
|
+
header = f"{n} error{'s' if n != 1 else ''} occurred while updating Environment members:"
|
|
287
|
+
details = "\n".join(f" - {label}: {e}" for label, e in errors)
|
|
288
|
+
raise WorkspaceManagementError(f"{header}\n{details}")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class _Environment(_Object, type_prefix="en"):
|
|
292
|
+
_name: Optional[str] = None
|
|
293
|
+
_settings: EnvironmentSettings
|
|
294
|
+
|
|
295
|
+
def __init__(self):
|
|
296
|
+
"""mdmd:hidden"""
|
|
297
|
+
raise RuntimeError(
|
|
298
|
+
"`Environment(...)` constructor is not allowed. "
|
|
299
|
+
"Use `Environment.from_name` or `Environment.from_context` instead."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def name(self) -> Optional[str]:
|
|
304
|
+
return self._name
|
|
305
|
+
|
|
306
|
+
@classproperty
|
|
307
|
+
@classmethod
|
|
308
|
+
def objects(cls) -> _EnvironmentManager:
|
|
309
|
+
return _EnvironmentManager()
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def members(self) -> "_EnvironmentMembersManager":
|
|
313
|
+
return _EnvironmentMembersManager(self)
|
|
314
|
+
|
|
315
|
+
# TODO(michael) Keeping this private for now until we decide what else should be in it
|
|
316
|
+
# And what the rules should be about updates / mutability
|
|
317
|
+
# @property
|
|
318
|
+
# def settings(self) -> EnvironmentSettings:
|
|
319
|
+
# return self._settings
|
|
320
|
+
|
|
321
|
+
def _hydrate_metadata(self, metadata: Message):
|
|
322
|
+
# Overridden concrete implementation of base class method
|
|
323
|
+
assert metadata and isinstance(metadata, api_pb2.EnvironmentMetadata)
|
|
324
|
+
self._name = metadata.name or None
|
|
325
|
+
|
|
326
|
+
# Is there a simpler way to go Message -> Dataclass?
|
|
327
|
+
self._settings = EnvironmentSettings(
|
|
328
|
+
image_builder_version=metadata.settings.image_builder_version,
|
|
329
|
+
webhook_suffix=metadata.settings.webhook_suffix,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def _get_or_create(
|
|
334
|
+
name: str, repr: str, create_if_missing: bool = False, client: Optional[_Client] = None
|
|
335
|
+
) -> "_Environment":
|
|
336
|
+
async def _load(
|
|
337
|
+
self: _Environment, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
338
|
+
):
|
|
339
|
+
request = api_pb2.EnvironmentGetOrCreateRequest(
|
|
340
|
+
deployment_name=name,
|
|
341
|
+
object_creation_type=(
|
|
342
|
+
api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
|
|
343
|
+
if create_if_missing
|
|
344
|
+
else api_pb2.OBJECT_CREATION_TYPE_UNSPECIFIED
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
response = await load_context.client.stub.EnvironmentGetOrCreate(request)
|
|
348
|
+
logger.debug(f"Created environment with id {response.environment_id}")
|
|
349
|
+
self._hydrate(response.environment_id, load_context.client, response.metadata)
|
|
350
|
+
|
|
351
|
+
return _Environment._from_loader(
|
|
352
|
+
_load,
|
|
353
|
+
repr,
|
|
354
|
+
is_another_app=True,
|
|
355
|
+
hydrate_lazily=True,
|
|
356
|
+
name=name,
|
|
357
|
+
load_context_overrides=LoadContext(client=client),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def from_context(*, client: Optional[_Client] = None) -> "_Environment":
|
|
362
|
+
"""Look up an Environment object using the current context.
|
|
363
|
+
|
|
364
|
+
This method returns the Environment that is defined by the local configuration
|
|
365
|
+
(i.e., your active profile or the `MODAL_ENVIRONMENT` environment variable), or
|
|
366
|
+
it fetches the default environment from the server when not defined locally.
|
|
367
|
+
If called inside a Modal container, it will return the Environment that container
|
|
368
|
+
is associated with.
|
|
369
|
+
|
|
370
|
+
"""
|
|
371
|
+
name = config.get("environment") or "" # null string falls back to server default
|
|
372
|
+
return _Environment._get_or_create(
|
|
373
|
+
name=name,
|
|
374
|
+
repr="Environment.from_context()",
|
|
375
|
+
create_if_missing=False,
|
|
376
|
+
client=client,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def from_name(
|
|
381
|
+
name: str,
|
|
382
|
+
*,
|
|
383
|
+
create_if_missing: bool = False,
|
|
384
|
+
client: Optional[_Client] = None,
|
|
385
|
+
) -> "_Environment":
|
|
386
|
+
"""Look up an Environment object using its name."""
|
|
387
|
+
check_environment_name(name)
|
|
388
|
+
return _Environment._get_or_create(
|
|
389
|
+
name=name,
|
|
390
|
+
repr=f"Environment.from_name({name!r})",
|
|
391
|
+
create_if_missing=create_if_missing,
|
|
392
|
+
client=client,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
ENVIRONMENT_CACHE: dict[str, _Environment] = {}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
async def _get_environment_cached(name: str, client: _Client) -> _Environment:
|
|
400
|
+
if name in ENVIRONMENT_CACHE:
|
|
401
|
+
return ENVIRONMENT_CACHE[name]
|
|
402
|
+
if name:
|
|
403
|
+
environment = await _Environment.from_name(name, client=client).hydrate()
|
|
404
|
+
else:
|
|
405
|
+
environment = await _Environment.from_context(client=client).hydrate()
|
|
406
|
+
ENVIRONMENT_CACHE[name] = environment
|
|
407
|
+
return environment
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# The following internal functions are functionally public API as users have come to
|
|
411
|
+
# depend on them while we did not have a proper Environment API. We can deprecate them
|
|
412
|
+
# and migrate users to the new object-oriented API, but that should happen gracefully.
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
async def _delete_environment(name: str, client: Optional[_Client] = None):
|
|
416
|
+
if client is None:
|
|
417
|
+
client = await _Client.from_env()
|
|
418
|
+
await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
async def _update_environment(
|
|
422
|
+
current_name: str,
|
|
423
|
+
*,
|
|
424
|
+
new_name: Optional[str] = None,
|
|
425
|
+
new_web_suffix: Optional[str] = None,
|
|
426
|
+
client: Optional[_Client] = None,
|
|
427
|
+
):
|
|
428
|
+
new_name_pb2 = None
|
|
429
|
+
new_web_suffix_pb2 = None
|
|
430
|
+
if new_name is not None:
|
|
431
|
+
if len(new_name) < 1:
|
|
432
|
+
raise ValueError("The new environment name cannot be empty")
|
|
433
|
+
|
|
434
|
+
new_name_pb2 = StringValue(value=new_name)
|
|
435
|
+
|
|
436
|
+
if new_web_suffix is not None:
|
|
437
|
+
new_web_suffix_pb2 = StringValue(value=new_web_suffix)
|
|
438
|
+
|
|
439
|
+
update_payload = api_pb2.EnvironmentUpdateRequest(
|
|
440
|
+
current_name=current_name, name=new_name_pb2, web_suffix=new_web_suffix_pb2
|
|
441
|
+
)
|
|
442
|
+
if client is None:
|
|
443
|
+
client = await _Client.from_env()
|
|
444
|
+
await client.stub.EnvironmentUpdate(update_payload)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
async def _create_environment(name: str, client: Optional[_Client] = None):
|
|
448
|
+
if client is None:
|
|
449
|
+
client = await _Client.from_env()
|
|
450
|
+
await client.stub.EnvironmentCreate(api_pb2.EnvironmentCreateRequest(name=name))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
async def _list_environments(client: Optional[_Client] = None) -> list[api_pb2.EnvironmentListItem]:
|
|
454
|
+
if client is None:
|
|
455
|
+
client = await _Client.from_env()
|
|
456
|
+
resp = await client.stub.EnvironmentList(Empty())
|
|
457
|
+
return list(resp.items)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def ensure_env(environment_name: Optional[str] = None) -> str:
|
|
461
|
+
"""Override config environment with environment from environment_name
|
|
462
|
+
|
|
463
|
+
This is necessary since a cli command that runs Modal code, without explicit
|
|
464
|
+
environment specification wouldn't pick up the environment specified in a
|
|
465
|
+
command line flag otherwise, e.g. when doing `modal run --env=foo`
|
|
466
|
+
"""
|
|
467
|
+
if environment_name is not None:
|
|
468
|
+
config.override_locally("environment", environment_name)
|
|
469
|
+
|
|
470
|
+
return config.get("environment")
|
|
@@ -12,6 +12,8 @@ from modal.config import config
|
|
|
12
12
|
try:
|
|
13
13
|
import cbor2 # type: ignore
|
|
14
14
|
except ImportError: # pragma: no cover - optional dependency
|
|
15
|
+
# cbor2 is a required dependency of modal as a library, but it won't
|
|
16
|
+
# be available in containers using a legacy (pre 2025.06) image builder version
|
|
15
17
|
cbor2 = None
|
|
16
18
|
|
|
17
19
|
import google.protobuf.message
|
|
@@ -374,7 +376,7 @@ def serialize_data_format(obj: Any, data_format: int) -> bytes:
|
|
|
374
376
|
raise InvalidError("CBOR support requires the 'cbor2' package to be installed.")
|
|
375
377
|
try:
|
|
376
378
|
return cbor2.dumps(obj)
|
|
377
|
-
except cbor2.
|
|
379
|
+
except cbor2.CBOREncodeError:
|
|
378
380
|
try:
|
|
379
381
|
typename = f"{type(obj).__module__}.{type(obj).__name__}"
|
|
380
382
|
except Exception:
|
|
@@ -63,7 +63,7 @@ def check_object_name(name: str, object_type: str) -> None:
|
|
|
63
63
|
|
|
64
64
|
def check_environment_name(name: str) -> None:
|
|
65
65
|
message = (
|
|
66
|
-
f"Invalid
|
|
66
|
+
f"Invalid Environment name: '{name}'."
|
|
67
67
|
"\n\nEnvironment names can only start with alphanumeric characters,"
|
|
68
68
|
" may contain only alphanumeric characters, dashes, periods, and underscores,"
|
|
69
69
|
" and must be shorter than 64 characters."
|
|
@@ -12,12 +12,12 @@ from click import UsageError
|
|
|
12
12
|
from rich.table import Column
|
|
13
13
|
from rich.text import Text
|
|
14
14
|
|
|
15
|
+
from modal._environments import ensure_env
|
|
15
16
|
from modal._object import _get_environment_name
|
|
16
17
|
from modal._traceback import print_server_warnings
|
|
17
18
|
from modal._utils.async_utils import synchronizer
|
|
18
19
|
from modal._utils.browser_utils import open_url_and_display
|
|
19
20
|
from modal.client import _Client
|
|
20
|
-
from modal.environments import ensure_env
|
|
21
21
|
from modal.exception import InvalidError, NotFoundError
|
|
22
22
|
from modal.output import OutputManager
|
|
23
23
|
from modal.runner import DEPLOYMENT_STRATEGY_TYPE, _stop_and_wait_for_containers
|
|
@@ -6,6 +6,7 @@ import click
|
|
|
6
6
|
from rich.table import Column
|
|
7
7
|
from rich.text import Text
|
|
8
8
|
|
|
9
|
+
from modal._environments import ensure_env
|
|
9
10
|
from modal._object import _get_environment_name
|
|
10
11
|
from modal._output.pty import get_pty_info
|
|
11
12
|
from modal._utils.async_utils import synchronizer
|
|
@@ -15,7 +16,6 @@ from modal.cli.utils import display_table, env_option, is_tty
|
|
|
15
16
|
from modal.client import _Client
|
|
16
17
|
from modal.config import config
|
|
17
18
|
from modal.container_process import _ContainerProcess
|
|
18
|
-
from modal.environments import ensure_env
|
|
19
19
|
from modal.exception import InvalidError
|
|
20
20
|
from modal.output import OutputManager
|
|
21
21
|
from modal.stream_type import StreamType
|
|
@@ -9,6 +9,7 @@ from click import UsageError
|
|
|
9
9
|
from rich.table import Column
|
|
10
10
|
from rich.text import Text
|
|
11
11
|
|
|
12
|
+
from modal._environments import ensure_env
|
|
12
13
|
from modal._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
|
|
13
14
|
from modal._object import _get_environment_name
|
|
14
15
|
from modal._output.pty import get_pty_info
|
|
@@ -29,7 +30,6 @@ from modal.cli.utils import (
|
|
|
29
30
|
from modal.client import _Client
|
|
30
31
|
from modal.config import config
|
|
31
32
|
from modal.container_process import _ContainerProcess
|
|
32
|
-
from modal.environments import ensure_env
|
|
33
33
|
from modal.exception import InvalidError
|
|
34
34
|
from modal.stream_type import StreamType
|
|
35
35
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
@@ -4,6 +4,7 @@ from typing import Optional
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
|
+
from modal._environments import ensure_env
|
|
7
8
|
from modal._load_context import LoadContext
|
|
8
9
|
from modal._resolver import Resolver
|
|
9
10
|
from modal._utils.async_utils import TaskContext, synchronizer
|
|
@@ -11,7 +12,6 @@ from modal._utils.time_utils import timestamp_to_localized_str
|
|
|
11
12
|
from modal.cli.utils import display_table, env_option, yes_option
|
|
12
13
|
from modal.client import _Client
|
|
13
14
|
from modal.dict import _Dict
|
|
14
|
-
from modal.environments import ensure_env
|
|
15
15
|
from modal.output import OutputManager
|
|
16
16
|
|
|
17
17
|
from ._help import ModalGroup
|