diff --git a/docs/docs/reference/api/python/index.md b/docs/docs/reference/api/python/index.md index 82cac4a5e5..5d4f7c1f49 100644 --- a/docs/docs/reference/api/python/index.md +++ b/docs/docs/reference/api/python/index.md @@ -14,7 +14,7 @@ from dstack.api import Task, GPU, Client, Resources client = Client.from_config() task = Task( - name="my-awesome-run", # If not specified, a random name is assigned + name="my-awesome-run", # If not specified, a random name is assigned image="ghcr.io/huggingface/text-generation-inference:latest", env={"MODEL_ID": "TheBloke/Llama-2-13B-chat-GPTQ"}, commands=[ @@ -42,7 +42,7 @@ finally: ``` !!! info "NOTE:" - 1. The `configuration` argument in the `apply_configuration` method can be either `dstack.api.Task`, `dstack.api.Service`, or `dstack.api.DevEnvironment`. + 1. The `configuration` argument in the `apply_configuration` method can be either `dstack.api.Task`, `dstack.api.Service`, or `dstack.api.DevEnvironment`. 2. When you create `dstack.api.Task`, `dstack.api.Service`, or `dstack.api.DevEnvironment`, you can specify the `image` argument. If `image` isn't specified, the default image will be used. For a private Docker registry, ensure you also pass the `registry_auth` argument. 3. The `repo` argument in the `apply_configuration` method allows the mounting of a local folder, a remote repo, or a programmatically created repo. In this case, the `commands` argument can refer to the files within this repo. @@ -173,15 +173,6 @@ finally: memory: dstack.api.Memory Range: dstack.api.Range -### `dstack.api.LocalRepo` { #dstack.api.LocalRepo data-toc-label="LocalRepo" } - -::: dstack.api.LocalRepo - options: - show_bases: false - show_root_heading: false - show_root_toc_entry: false - heading_level: 4 - ### `dstack.api.RemoteRepo` { #dstack.api.RemoteRepo data-toc-label="RemoteRepo" } ::: dstack.api.RemoteRepo diff --git a/src/dstack/_internal/cli/commands/init.py b/src/dstack/_internal/cli/commands/init.py index 2bbde987b1..2a5487a47a 100644 --- a/src/dstack/_internal/cli/commands/init.py +++ b/src/dstack/_internal/cli/commands/init.py @@ -6,13 +6,11 @@ from dstack._internal.cli.commands import BaseCommand from dstack._internal.cli.services.repos import ( get_repo_from_dir, + get_repo_from_url, is_git_repo_url, register_init_repo_args, ) -from dstack._internal.cli.utils.common import confirm_ask, console, warn -from dstack._internal.core.errors import ConfigurationError -from dstack._internal.core.models.repos.remote import RemoteRepo -from dstack._internal.core.services.configs import ConfigManager +from dstack._internal.cli.utils.common import console from dstack.api import Client @@ -36,20 +34,6 @@ def _register(self): dest="repo", ) register_init_repo_args(self._parser) - # Deprecated since 0.19.25, ignored - self._parser.add_argument( - "--ssh-identity", - metavar="SSH_PRIVATE_KEY", - help=argparse.SUPPRESS, - type=Path, - dest="ssh_identity_file", - ) - # A hidden mode for transitional period only, remove it with repos in `config.yml` - self._parser.add_argument( - "--remove", - help=argparse.SUPPRESS, - action="store_true", - ) def _command(self, args: argparse.Namespace): super()._command(args) @@ -65,45 +49,10 @@ def _command(self, args: argparse.Namespace): else: repo_path = Path.cwd() - if args.remove: - if repo_url is not None: - raise ConfigurationError(f"Local path expected, got URL: {repo_url}") - assert repo_path is not None - config_manager = ConfigManager() - repo_config = config_manager.get_repo_config(repo_path) - if repo_config is None: - raise ConfigurationError("Repo record not found, nothing to remove") - console.print( - f"You are about to remove the repo {repo_path}\n" - "Only the record about the repo will be removed," - " the repo files will remain intact\n" - ) - if not confirm_ask("Remove the repo?"): - return - config_manager.delete_repo_config(repo_config.repo_id) - config_manager.save() - console.print("Repo has been removed") - return - - local: bool = args.local - if local: - warn( - "Local repos are deprecated since 0.19.25 and will be removed soon. Consider" - " using [code]files[/code] instead: https://dstack.ai/docs/concepts/tasks/#files" - ) - if args.ssh_identity_file: - warn( - "[code]--ssh-identity[/code] in [code]dstack init[/code] is deprecated and ignored" - " since 0.19.25. Use this option with [code]dstack apply[/code]" - " and [code]dstack attach[/code] instead" - ) - if repo_url is not None: - # Dummy repo branch to avoid autodetection that fails on private repos. - # We don't need branch/hash for repo_id anyway. - repo = RemoteRepo.from_url(repo_url, repo_branch="master") + repo = get_repo_from_url(repo_url) elif repo_path is not None: - repo = get_repo_from_dir(repo_path, local=local) + repo = get_repo_from_dir(repo_path) else: assert False, "should not reach here" api = Client.from_config(project_name=args.project) diff --git a/src/dstack/_internal/cli/services/configurators/run.py b/src/dstack/_internal/cli/services/configurators/run.py index 090977a213..f942ca05b0 100644 --- a/src/dstack/_internal/cli/services/configurators/run.py +++ b/src/dstack/_internal/cli/services/configurators/run.py @@ -18,11 +18,12 @@ from dstack._internal.cli.services.profile import apply_profile_args, register_profile_args from dstack._internal.cli.services.repos import ( get_repo_from_dir, + get_repo_from_url, init_default_virtual_repo, is_git_repo_url, register_init_repo_args, ) -from dstack._internal.cli.utils.common import confirm_ask, console, warn +from dstack._internal.cli.utils.common import confirm_ask, console from dstack._internal.cli.utils.rich import MultiItemStatus from dstack._internal.cli.utils.run import get_runs_table, print_run_plan from dstack._internal.core.errors import ( @@ -44,17 +45,13 @@ TaskConfiguration, ) from dstack._internal.core.models.repos import RepoHeadWithCreds -from dstack._internal.core.models.repos.base import Repo -from dstack._internal.core.models.repos.local import LocalRepo from dstack._internal.core.models.repos.remote import RemoteRepo, RemoteRepoCreds from dstack._internal.core.models.resources import CPUSpec from dstack._internal.core.models.runs import JobStatus, JobSubmission, RunSpec, RunStatus -from dstack._internal.core.services.configs import ConfigManager from dstack._internal.core.services.diff import diff_models from dstack._internal.core.services.repos import ( InvalidRepoCredentialsError, get_repo_creds_and_default_branch, - load_repo, ) from dstack._internal.utils.common import local_time from dstack._internal.utils.interpolator import InterpolatorError, VariablesInterpolator @@ -96,8 +93,9 @@ def apply_configuration( if conf.working_dir is not None and not is_absolute_posix_path(conf.working_dir): raise ConfigurationError("working_dir must be absolute") - config_manager = ConfigManager() - repo = self.get_repo(conf, configuration_path, configurator_args, config_manager) + repo = self.get_repo(conf, configuration_path, configurator_args) + if repo is None: + repo = init_default_virtual_repo(api=self.api) profile = load_profile(Path.cwd(), configurator_args.profile) with console.status("Getting apply plan..."): run_plan = self.api.runs.get_run_plan( @@ -475,12 +473,11 @@ def get_repo( conf: RunConfigurationT, configuration_path: str, configurator_args: argparse.Namespace, - config_manager: ConfigManager, - ) -> Repo: + ) -> Optional[RemoteRepo]: if configurator_args.no_repo: - return init_default_virtual_repo(api=self.api) + return None - repo: Optional[Repo] = None + repo: Optional[RemoteRepo] = None repo_head: Optional[RepoHeadWithCreds] = None repo_branch: Optional[str] = configurator_args.repo_branch repo_hash: Optional[str] = configurator_args.repo_hash @@ -497,8 +494,6 @@ def get_repo( local_path: Optional[Path] = None # dummy value, safe to join with any path root_dir = Path(".") - # True if no repo specified, but we found one in `config.yml` - legacy_local_path = False if repo_arg := configurator_args.repo: if is_git_repo_url(repo_arg): url = repo_arg @@ -521,84 +516,49 @@ def get_repo( if repo_hash is None: repo_hash = repo_spec.hash else: - local_path = Path.cwd() - legacy_local_path = True + return None + if url: - repo = RemoteRepo.from_url(repo_url=url) + repo = get_repo_from_url(url) repo_head = self.api.repos.get(repo_id=repo.repo_id, with_creds=True) elif local_path: - if legacy_local_path: - if repo_config := config_manager.get_repo_config(local_path): - repo = load_repo(repo_config) - repo_head = self.api.repos.get(repo_id=repo.repo_id, with_creds=True) - if repo_head is not None: - warn( - "The repo is not specified but found and will be used in the run\n" - "Future versions will not load repos automatically\n" - "To prepare for future versions and get rid of this warning:\n" - "- If you need the repo in the run, either specify [code]repos[/code]" - " in the configuration or use [code]--repo .[/code]\n" - "- If you don't need the repo in the run, either run" - " [code]dstack init --remove[/code] once (it removes only the record" - " about the repo, the repo files will remain intact)" - " or use [code]--no-repo[/code]" - ) - else: - # ignore stale entries in `config.yml` - repo = None - init = False - else: - original_local_path = local_path - local_path = local_path.expanduser() - if not local_path.is_absolute(): - local_path = (root_dir / local_path).resolve() - if not local_path.exists(): - raise ConfigurationError( - f"Invalid repo path: {original_local_path} -> {local_path}" - ) - local: bool = configurator_args.local - repo = get_repo_from_dir(local_path, local=local) - repo_head = self.api.repos.get(repo_id=repo.repo_id, with_creds=True) - if isinstance(repo, RemoteRepo): - repo_branch = repo.run_repo_data.repo_branch - repo_hash = repo.run_repo_data.repo_hash + original_local_path = local_path + local_path = local_path.expanduser() + if not local_path.is_absolute(): + local_path = (root_dir / local_path).resolve() + if not local_path.exists(): + raise ConfigurationError( + f"Invalid repo path: {original_local_path} -> {local_path}" + ) + repo = get_repo_from_dir(local_path) + repo_head = self.api.repos.get(repo_id=repo.repo_id, with_creds=True) + repo_branch = repo.run_repo_data.repo_branch + repo_hash = repo.run_repo_data.repo_hash else: assert False, "should not reach here" - if repo is None: - return init_default_virtual_repo(api=self.api) + assert repo.repo_url is not None - if isinstance(repo, RemoteRepo): - assert repo.repo_url is not None + if repo_head is not None and repo_head.repo_creds is not None: + if git_identity_file is None and oauth_token is None: + git_private_key = repo_head.repo_creds.private_key + oauth_token = repo_head.repo_creds.oauth_token + else: + init = True - if repo_head is not None and repo_head.repo_creds is not None: - if git_identity_file is None and oauth_token is None: - git_private_key = repo_head.repo_creds.private_key - oauth_token = repo_head.repo_creds.oauth_token - else: - init = True + try: + repo_creds, _ = get_repo_creds_and_default_branch( + repo_url=repo.repo_url, + identity_file=git_identity_file, + private_key=git_private_key, + oauth_token=oauth_token, + ) + except InvalidRepoCredentialsError as e: + raise CLIError(*e.args) from e - try: - repo_creds, default_repo_branch = get_repo_creds_and_default_branch( - repo_url=repo.repo_url, - identity_file=git_identity_file, - private_key=git_private_key, - oauth_token=oauth_token, - ) - except InvalidRepoCredentialsError as e: - raise CLIError(*e.args) from e - - if repo_branch is None and repo_hash is None: - if default_repo_branch is None: - raise CLIError( - "Failed to automatically detect remote repo branch." - " Specify branch or hash." - ) - # TODO: remove in 0.20. Currently `default_repo_branch` is sent only for backward compatibility of `dstack-runner`. - repo_branch = default_repo_branch - repo.run_repo_data.repo_branch = repo_branch - if repo_hash is not None: - repo.run_repo_data.repo_hash = repo_hash + repo.run_repo_data.repo_branch = repo_branch + if repo_hash is not None: + repo.run_repo_data.repo_hash = repo_hash if init: self.api.repos.init( @@ -608,15 +568,6 @@ def get_repo( creds=repo_creds, ) - if isinstance(repo, LocalRepo): - warn( - f"{repo.repo_dir} is a local repo\n" - "Local repos are deprecated since 0.19.25 and will be removed soon\n" - "There are two options:\n" - "- Migrate to [code]files[/code]: https://dstack.ai/docs/concepts/tasks/#files\n" - "- Specify [code]--no-repo[/code] if you don't need the repo at all" - ) - return repo diff --git a/src/dstack/_internal/cli/services/repos.py b/src/dstack/_internal/cli/services/repos.py index 9f74aca8a0..9805c717fb 100644 --- a/src/dstack/_internal/cli/services/repos.py +++ b/src/dstack/_internal/cli/services/repos.py @@ -1,11 +1,9 @@ -import argparse -from typing import Literal, Union, overload +from pathlib import Path import git from dstack._internal.cli.services.configurators.base import ArgsParser from dstack._internal.core.errors import CLIError -from dstack._internal.core.models.repos.local import LocalRepo from dstack._internal.core.models.repos.remote import GitRepoURL, RemoteRepo, RepoError from dstack._internal.core.models.repos.virtual import VirtualRepo from dstack._internal.utils.path import PathLike @@ -28,12 +26,6 @@ def register_init_repo_args(parser: ArgsParser): type=str, dest="git_identity_file", ) - # Deprecated since 0.19.25 - parser.add_argument( - "--local", - action="store_true", - help=argparse.SUPPRESS, - ) def init_default_virtual_repo(api: Client) -> VirtualRepo: @@ -42,17 +34,12 @@ def init_default_virtual_repo(api: Client) -> VirtualRepo: return repo -@overload -def get_repo_from_dir(repo_dir: PathLike, local: Literal[False] = False) -> RemoteRepo: ... - - -@overload -def get_repo_from_dir(repo_dir: PathLike, local: Literal[True]) -> LocalRepo: ... - - -def get_repo_from_dir(repo_dir: PathLike, local: bool = False) -> Union[RemoteRepo, LocalRepo]: - if local: - return LocalRepo.from_dir(repo_dir) +def get_repo_from_dir(repo_dir: PathLike) -> RemoteRepo: + repo_dir = Path(repo_dir) + if not repo_dir.exists(): + raise CLIError(f"Path does not exist: {repo_dir}") + if not repo_dir.is_dir(): + raise CLIError(f"Path is not a directory: {repo_dir}") try: return RemoteRepo.from_dir(repo_dir) except git.InvalidGitRepositoryError: @@ -61,6 +48,17 @@ def get_repo_from_dir(repo_dir: PathLike, local: bool = False) -> Union[RemoteRe "Use `files` to mount an arbitrary directory:" " https://dstack.ai/docs/concepts/tasks/#files" ) + except git.GitError as e: + raise CLIError(f"{e.__class__.__name__}: {e}") from e + except RepoError as e: + raise CLIError(str(e)) from e + + +def get_repo_from_url(repo_url: str) -> RemoteRepo: + try: + return RemoteRepo.from_url(repo_url) + except git.GitError as e: + raise CLIError(f"{e.__class__.__name__}: {e}") from e except RepoError as e: raise CLIError(str(e)) from e diff --git a/src/dstack/_internal/core/models/config.py b/src/dstack/_internal/core/models/config.py index 67ea5ed2f3..c6d0916672 100644 --- a/src/dstack/_internal/core/models/config.py +++ b/src/dstack/_internal/core/models/config.py @@ -12,15 +12,17 @@ class ProjectConfig(CoreModel): default: Optional[bool] +# Not used since 0.20.0. Can be removed when most users update their `config.yml` (it's updated +# each time a project is added) class RepoConfig(CoreModel): path: str repo_id: str repo_type: RepoType - # Deprecated since 0.19.25, not used. Can be removed when most users update their `config.yml` - # (it's updated each time a project or repo is added) ssh_key_path: Annotated[Optional[str], Field(exclude=True)] = None class GlobalConfig(CoreModel): projects: Annotated[List[ProjectConfig], Field(description="The list of projects")] = [] - repos: List[RepoConfig] = [] + # Not used since 0.20.0. Can be removed when most users update their `config.yml` (it's updated + # each time a project is added) + repos: Annotated[list[RepoConfig], Field(exclude=True)] = [] diff --git a/src/dstack/_internal/core/models/repos/__init__.py b/src/dstack/_internal/core/models/repos/__init__.py index 2748ac41c4..25a3d06aca 100644 --- a/src/dstack/_internal/core/models/repos/__init__.py +++ b/src/dstack/_internal/core/models/repos/__init__.py @@ -3,6 +3,7 @@ from pydantic import Field from dstack._internal.core.models.common import CoreModel +from dstack._internal.core.models.repos.base import Repo as Repo from dstack._internal.core.models.repos.local import ( # noqa: F401 LocalRepo, LocalRepoInfo, diff --git a/src/dstack/_internal/core/models/repos/remote.py b/src/dstack/_internal/core/models/repos/remote.py index fedadee0ff..d3c3b70906 100644 --- a/src/dstack/_internal/core/models/repos/remote.py +++ b/src/dstack/_internal/core/models/repos/remote.py @@ -75,7 +75,7 @@ class RemoteRepo(Repo): Using a remote Git repo by a URL: ```python - repo=RemoteRepo.from_url( + repo = RemoteRepo.from_url( repo_url="https://github.com/dstackai/dstack-examples", repo_branch="main" ) diff --git a/src/dstack/_internal/core/services/configs/__init__.py b/src/dstack/_internal/core/services/configs/__init__.py index c6d333cf84..872c40805f 100644 --- a/src/dstack/_internal/core/services/configs/__init__.py +++ b/src/dstack/_internal/core/services/configs/__init__.py @@ -3,14 +3,11 @@ from pathlib import Path from typing import Optional -import filelock import yaml from pydantic import ValidationError from dstack._internal.cli.utils.common import confirm_ask -from dstack._internal.core.errors import DstackError -from dstack._internal.core.models.config import GlobalConfig, ProjectConfig, RepoConfig -from dstack._internal.core.models.repos.base import RepoType +from dstack._internal.core.models.config import GlobalConfig, ProjectConfig from dstack._internal.utils.common import get_dstack_dir from dstack._internal.utils.logging import get_logger from dstack._internal.utils.path import PathLike @@ -74,43 +71,6 @@ def list_project_configs(self) -> list[ProjectConfig]: def delete_project(self, name: str): self.config.projects = [p for p in self.config.projects if p.name != name] - def save_repo_config(self, repo_path: PathLike, repo_id: str, repo_type: RepoType): - self.config_filepath.parent.mkdir(parents=True, exist_ok=True) - with filelock.FileLock(str(self.config_filepath) + ".lock"): - self.load() - repo_path = os.path.abspath(repo_path) - for repo in self.config.repos: - if repo.path == repo_path: - repo.repo_id = repo_id - repo.repo_type = repo_type - break - else: - self.config.repos.append( - RepoConfig( - path=repo_path, - repo_id=repo_id, - repo_type=repo_type, - ) - ) - self.save() - - def get_repo_config(self, repo_path: PathLike) -> Optional[RepoConfig]: - repo_path = os.path.abspath(repo_path) - # TODO look at parent directories - for repo in self.config.repos: - if repo.path == repo_path: - return repo - return None - - def get_repo_config_or_error(self, repo_path: PathLike) -> RepoConfig: - repo_config = self.get_repo_config(repo_path) - if repo_config is not None: - return repo_config - raise DstackError("No repo config found") - - def delete_repo_config(self, repo_id: str): - self.config.repos = [p for p in self.config.repos if p.repo_id != repo_id] - @property def dstack_ssh_dir(self) -> Path: return self.dstack_dir / "ssh" diff --git a/src/dstack/_internal/core/services/repos.py b/src/dstack/_internal/core/services/repos.py index f68a0d0d55..f3b37443e4 100644 --- a/src/dstack/_internal/core/services/repos.py +++ b/src/dstack/_internal/core/services/repos.py @@ -2,15 +2,14 @@ from contextlib import suppress from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Optional, Union +from typing import Optional import git.cmd import yaml from git.exc import GitCommandError from dstack._internal.core.errors import DstackError -from dstack._internal.core.models.config import RepoConfig -from dstack._internal.core.models.repos import LocalRepo, RemoteRepo, RemoteRepoCreds +from dstack._internal.core.models.repos import RemoteRepoCreds from dstack._internal.core.models.repos.remote import GitRepoURL from dstack._internal.utils.logging import get_logger from dstack._internal.utils.path import PathLike @@ -235,12 +234,3 @@ def _read_private_key(identity_file: PathLike) -> str: ) with open(identity_file, "r") as file: return file.read() - - -# Used for `config.yml` only, remove it with `repos` in `config.yml` -def load_repo(config: RepoConfig) -> Union[RemoteRepo, LocalRepo]: - if config.repo_type == "remote": - return RemoteRepo(repo_id=config.repo_id, local_repo_dir=config.path) - elif config.repo_type == "local": - return LocalRepo(repo_id=config.repo_id, repo_dir=config.path) - raise TypeError(f"Unknown repo_type: {config.repo_type}") diff --git a/src/dstack/api/__init__.py b/src/dstack/api/__init__.py index 384c8be821..0e6c6ebe3c 100644 --- a/src/dstack/api/__init__.py +++ b/src/dstack/api/__init__.py @@ -1,4 +1,6 @@ # ruff: noqa: F401 +import warnings + from dstack._internal.core.errors import ClientError from dstack._internal.core.models.backends.base import BackendType from dstack._internal.core.models.common import RegistryAuth @@ -10,7 +12,6 @@ ServiceConfiguration as _ServiceConfiguration, ) from dstack._internal.core.models.configurations import TaskConfiguration as _TaskConfiguration -from dstack._internal.core.models.repos.local import LocalRepo from dstack._internal.core.models.repos.remote import RemoteRepo from dstack._internal.core.models.repos.virtual import VirtualRepo from dstack._internal.core.models.resources import ComputeCapability, Memory, Range @@ -27,3 +28,19 @@ Service = _ServiceConfiguration Task = _TaskConfiguration DevEnvironment = _DevEnvironmentConfiguration + + +def __getattr__(name): + if name == "LocalRepo": + from dstack._internal.core.models.repos.local import LocalRepo + + warnings.warn( + ( + "Local repositories are not supported since 0.20.0. Use `files` to mount" + " an arbitrary directory: https://dstack.ai/docs/concepts/tasks/#files" + ), + DeprecationWarning, + ) + return LocalRepo + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/dstack/api/_public/repos.py b/src/dstack/api/_public/repos.py index 9015bc69c0..d212201439 100644 --- a/src/dstack/api/_public/repos.py +++ b/src/dstack/api/_public/repos.py @@ -7,15 +7,13 @@ LocalRepo, RemoteRepo, RemoteRepoCreds, + Repo, RepoHead, RepoHeadWithCreds, ) -from dstack._internal.core.models.repos.base import Repo, RepoType -from dstack._internal.core.services.configs import ConfigManager from dstack._internal.core.services.repos import ( InvalidRepoCredentialsError, get_repo_creds_and_default_branch, - load_repo, ) from dstack._internal.utils.logging import get_logger from dstack._internal.utils.path import PathLike @@ -47,7 +45,7 @@ def init( Example: ```python - repo=RemoteRepo.from_url( + repo = RemoteRepo.from_url( repo_url="https://github.com/dstackai/dstack-examples", repo_branch="main", ) @@ -73,6 +71,11 @@ def init( creds: Optional prepared repo credentials. If specified, both `git_identity_file` and `oauth_token` are ignored. """ + if isinstance(repo, LocalRepo): + raise ConfigurationError( + "Local repositories are not supported since 0.20.0. Use `files` to mount" + " an arbitrary directory: https://dstack.ai/docs/concepts/tasks/#files" + ) if creds is None and isinstance(repo, RemoteRepo): assert repo.repo_url is not None try: @@ -92,56 +95,46 @@ def load( init: bool = False, git_identity_file: Optional[PathLike] = None, oauth_token: Optional[str] = None, - ) -> Union[RemoteRepo, LocalRepo]: - """ - Loads the repo from the local directory using global config - - Args: - repo_dir: Repo root directory. - local: Do not try to load `RemoteRepo` first. - init: Initialize the repo if it's not initialized. - git_identity_file: Path to an SSH private key to access the remote repo. - oauth_token: GitHub OAuth token to access the remote repo. - - Raises: - ConfigurationError: If the repo is not initialized and `init` is `False`. - - Returns: - repo: Initialized repo. - """ - config = ConfigManager() + ) -> RemoteRepo: + # """ + # Loads the repo from the local directory using global config + + # Args: + # repo_dir: Repo root directory. + # local: Do not try to load `RemoteRepo` first. + # init: Initialize the repo if it's not initialized. + # git_identity_file: Path to an SSH private key to access the remote repo. + # oauth_token: GitHub OAuth token to access the remote repo. + + # Raises: + # ConfigurationError: If the repo is not initialized and `init` is `False`. + + # Returns: + # repo: Initialized repo. + # """ + logger.warning( + "The load() method is deprecated, use RemoteRepo directly:" + " https://dstack.ai/docs/reference/api/python/#dstack.api.RemoteRepo" + ) + if local: + raise ConfigurationError( + "Local repositories are not supported since 0.20.0. Use `files` to mount" + " an arbitrary directory: https://dstack.ai/docs/concepts/tasks/#files" + ) if not init: - logger.debug("Loading repo config") - repo_config = config.get_repo_config(repo_dir) - if repo_config is None: - raise ConfigurationError( - "The repo is not initialized." - " Run `dstack init` to initialize the current directory as a repo or specify `--repo`." - ) - repo = load_repo(repo_config) - if not self.is_initialized(repo): - raise ConfigurationError( - "The repo is not initialized." - " Run `dstack init` to initialize the current directory as a repo or specify `--repo`." - ) - else: - logger.debug("Initializing repo") - if local: - repo = LocalRepo(repo_dir=repo_dir) - else: - try: - repo = RemoteRepo.from_dir(repo_dir) - except InvalidGitRepositoryError: - raise ConfigurationError( - f"Git repo not found: {repo_dir}. Use `files` to mount an arbitrary" - " directory: https://dstack.ai/docs/concepts/tasks/#files" - ) - self.init(repo, git_identity_file, oauth_token) - config.save_repo_config( - repo.get_repo_dir_or_error(), - repo.repo_id, - RepoType(repo.run_repo_data.repo_type), + raise ConfigurationError( + "Repo config has been removed in 0.20.0," + " this method can now only be used with init=True" + ) + logger.debug("Initializing repo") + try: + repo = RemoteRepo.from_dir(repo_dir) + except InvalidGitRepositoryError: + raise ConfigurationError( + f"Git repo not found: {repo_dir}. Use `files` to mount an arbitrary" + " directory: https://dstack.ai/docs/concepts/tasks/#files" ) + self.init(repo, git_identity_file, oauth_token) return repo def is_initialized( diff --git a/src/dstack/api/_public/runs.py b/src/dstack/api/_public/runs.py index 5163aa8e60..8725162ea9 100644 --- a/src/dstack/api/_public/runs.py +++ b/src/dstack/api/_public/runs.py @@ -460,7 +460,7 @@ def get_run_plan( Args: configuration (Union[Task, Service, DevEnvironment]): The run configuration. - repo (Union[LocalRepo, RemoteRepo, VirtualRepo, None]): + repo (Union[RemoteRepo, VirtualRepo, None]): The repo to use for the run. Pass `None` if repo is not needed. profile: The profile to use for the run. configuration_path: The path to the configuration file. Omit if the configuration @@ -539,7 +539,7 @@ def apply_plan( Args: run_plan: The result of `get_run_plan` call. - repo (Union[LocalRepo, RemoteRepo, VirtualRepo, None]): + repo (Union[RemoteRepo, VirtualRepo, None]): The repo to use for the run. Should be the same repo that is passed to `get_run_plan`. reserve_ports: Reserve local ports before applying. Use if you'll attach to the run. @@ -582,7 +582,7 @@ def apply_configuration( Args: configuration (Union[Task, Service, DevEnvironment]): The run configuration. - repo (Union[LocalRepo, RemoteRepo, VirtualRepo, None]): + repo (Union[RemoteRepo, VirtualRepo, None]): The repo to use for the run. Pass `None` if repo is not needed. profile: The profile to use for the run. configuration_path: The path to the configuration file. Omit if the configuration is not loaded from a file. diff --git a/src/tests/_internal/cli/commands/test_project.py b/src/tests/_internal/cli/commands/test_project.py index 1572e25740..f022b3d404 100644 --- a/src/tests/_internal/cli/commands/test_project.py +++ b/src/tests/_internal/cli/commands/test_project.py @@ -38,5 +38,4 @@ def test_adds_project(self, capsys: CaptureFixture, tmp_path: Path): "url": "http://127.0.0.1:31313", } ], - "repos": [], }