Skip to content

nskit.vcs

VCS handlers for repository infrastructure.

Repositories

nskit.vcs.repo.Repo

Bases: _Repo

Repo with namespace validator.

Source code in src/nskit/vcs/repo.py
class Repo(_Repo):
    """Repo with namespace validator."""

    namespace_validation_repo: NamespaceValidationRepo | None = None
    validation_level: ValidationEnum = ValidationEnum.none
    name: str

    @model_validator(mode="after")
    def _validate_name(self):
        value = self.name
        if self.namespace_validation_repo and self.validation_level in [ValidationEnum.strict, ValidationEnum.warn]:
            namespace_validator = self.namespace_validation_repo.validator
            result, message = namespace_validator.validate_name(value)
            if not result:
                message = f"{value} {message.format(key='<root>')}"
            value = namespace_validator.to_repo_name(value)
            if self.validation_level == ValidationEnum.strict and not result:
                raise ValueError(message)
            elif not result:
                warnings.warn(message, stacklevel=2)
        self.name = value

name instance-attribute

nskit.vcs.repo.NamespaceValidationRepo

Bases: _Repo

Repo for managing namespace validation.

Source code in src/nskit/vcs/repo.py
class NamespaceValidationRepo(_Repo):
    """Repo for managing namespace validation."""

    # # This is not ideal behaviour, but due to the issue highlighted in
    # # https://github.com/pydantic/pydantic-settings/issues/245 and the
    # # non-semver compliant versioning in pydantic-settings, we need to add this behaviour
    # # this now changes the API behaviour for these objects as they will
    # # also ignore additional inputs in the python initialisation
    # # We will pin to version < 2.1.0 instead of allowing 2.2.0+ as it requires the code below:
    # model_config = ConfigDict(extra='ignore')  noqa: E800
    name: str = ".namespaces"
    namespaces_filename: str | Path = "namespaces.yaml"
    local_dir: Annotated[Path | None, Field(validate_default=True)] = None

    _validator: NamespaceValidator = None

    @property
    def validator(self):
        """Get the namespace validator."""
        if self._validator is None:
            self._validator = self._load_namespace_validator()
        return self._validator

    def validate_name(self, proposed_name: str):
        """Validate the proposed name."""
        return self.validator.validate_name(proposed_name)

    # Validate default for local_dir
    @field_validator("local_dir", mode="before")
    @classmethod
    def _validate_local_dir(cls, value: Any, info: ValidationInfo):
        if value is None:
            value = Path(tempfile.gettempdir()) / info.data["name"]
        return value

    def _download_namespaces(self):
        # Into a .namespaces "hidden" directory that we check and pull if necessary
        self.clone()
        self.checkout(self.default_branch)

    def _load_namespace_validator(self):
        if not self.exists_locally:
            self._download_namespaces()
        self.pull()
        with (self.local_dir / self.namespaces_filename).open() as f:
            namespace_validator = NamespaceValidator(**yaml.load(f))
        return namespace_validator

    def create(
        self,
        *,
        namespace_options: NamespaceOptionsType | NamespaceValidator,
        delimiters: list[str] | None = None,
        repo_separator: str | None = None,
    ):
        """Create and populate the validator repo."""
        # Provide either namespace_validator or namespaceOptions
        kwargs = {}
        if delimiters:
            kwargs["delimiters"] = delimiters
        if repo_separator:
            kwargs["repo_separator"] = repo_separator
        if not isinstance(namespace_options, NamespaceValidator):
            namespace_validator = NamespaceValidator(options=namespace_options, **kwargs)
        else:
            # namespace_options is a NamespaceValidator
            namespace_validator = namespace_options.model_copy(update=kwargs)
        # Create the repo
        super().create()
        with ChDir(self.local_dir):
            # Write the Config
            with open(self.namespaces_filename, "w") as f:
                f.write(namespace_validator.model_dump_yaml())
            with open("README.md", "w") as f:
                f.write(_NAMESPACE_README)
            # Commit it
            self.commit("Initial Namespaces Commit", [self.namespaces_filename, "README.md"])
            # Push it
            self.push()

name = '.namespaces' class-attribute instance-attribute

namespaces_filename = 'namespaces.yaml' class-attribute instance-attribute

validator property

Get the namespace validator.

validate_name(proposed_name)

Validate the proposed name.

Source code in src/nskit/vcs/repo.py
def validate_name(self, proposed_name: str):
    """Validate the proposed name."""
    return self.validator.validate_name(proposed_name)

create(*, namespace_options, delimiters=None, repo_separator=None)

Create and populate the validator repo.

Source code in src/nskit/vcs/repo.py
def create(
    self,
    *,
    namespace_options: NamespaceOptionsType | NamespaceValidator,
    delimiters: list[str] | None = None,
    repo_separator: str | None = None,
):
    """Create and populate the validator repo."""
    # Provide either namespace_validator or namespaceOptions
    kwargs = {}
    if delimiters:
        kwargs["delimiters"] = delimiters
    if repo_separator:
        kwargs["repo_separator"] = repo_separator
    if not isinstance(namespace_options, NamespaceValidator):
        namespace_validator = NamespaceValidator(options=namespace_options, **kwargs)
    else:
        # namespace_options is a NamespaceValidator
        namespace_validator = namespace_options.model_copy(update=kwargs)
    # Create the repo
    super().create()
    with ChDir(self.local_dir):
        # Write the Config
        with open(self.namespaces_filename, "w") as f:
            f.write(namespace_validator.model_dump_yaml())
        with open("README.md", "w") as f:
            f.write(_NAMESPACE_README)
        # Commit it
        self.commit("Initial Namespaces Commit", [self.namespaces_filename, "README.md"])
        # Push it
        self.push()

Namespace Validation

nskit.vcs.namespace_validator.NamespaceValidator

Bases: BaseConfiguration

Namespace Validator object.

Source code in src/nskit/vcs/namespace_validator.py
class NamespaceValidator(BaseConfiguration):
    """Namespace Validator object."""

    options: Optional[NamespaceOptionsType]
    repo_separator: str = REPO_SEPARATOR
    delimiters: list[str] = _DELIMITERS

    __delimiters_regexp = None
    # Validate delimiters to add repo_separator

    @field_validator("delimiters", mode="after")
    @classmethod
    def _validate_repo_separator_in_delimiters(cls, v: list[str], info: ValidationInfo):
        if info.data["repo_separator"] not in v:
            v.append(info.data["repo_separator"])
        return v

    @property
    def _delimiters_regexp(self):
        if self.__delimiters_regexp is None:
            self.__delimiters_regexp = "|".join(map(re.escape, self.delimiters))
        return self.__delimiters_regexp

    def to_parts(self, name: str):
        """Break the name into the namespace parts."""
        if self.options:
            return re.split(self._delimiters_regexp, name)
        return [name]

    def to_repo_name(self, name: str):
        """Convert the name to the appropriate name with a given repo separator."""
        return self.repo_separator.join(self.to_parts(name))

    def validate_name(self, proposed_name: str):
        """Validate a proposed name."""
        name_parts = self.to_parts(proposed_name)
        if self.options:
            result, message = self._validate_level(name_parts, self.options)
            message = message.format(key="<root>")
        else:
            result = True
            message = "no constraints set"
        return result, message

    def _validate_level(self, name_parts: list[str], partial_namespace: list[Union[str, dict]]):
        not_matched = []
        for key in partial_namespace:
            # If it is a dict, then there are mappings of <section>: [<subsection 1>, <subsection 2>]
            if isinstance(key, dict):
                for sub_key, new_partial_namespace in key.items():
                    if sub_key == name_parts[0]:
                        # This maps to a section with subsections, so we need to validate those
                        result, message = self._validate_level(name_parts[1:], new_partial_namespace)
                        if not result:
                            message = message.format(key=sub_key)
                        return result, message
                    not_matched.append(sub_key)
            # Otherwise it is a string
            elif key == name_parts[0]:
                return True, "ok"
            else:
                not_matched.append(key)
        return (
            False,
            f"Does not match valid names for {{key}}: {', '.join(not_matched)}, with delimiters: {self.delimiters}",
        )

options instance-attribute

repo_separator = REPO_SEPARATOR class-attribute instance-attribute

delimiters = _DELIMITERS class-attribute instance-attribute

validate_name(proposed_name)

Validate a proposed name.

Source code in src/nskit/vcs/namespace_validator.py
def validate_name(self, proposed_name: str):
    """Validate a proposed name."""
    name_parts = self.to_parts(proposed_name)
    if self.options:
        result, message = self._validate_level(name_parts, self.options)
        message = message.format(key="<root>")
    else:
        result = True
        message = "no constraints set"
    return result, message

to_parts(name)

Break the name into the namespace parts.

Source code in src/nskit/vcs/namespace_validator.py
def to_parts(self, name: str):
    """Break the name into the namespace parts."""
    if self.options:
        return re.split(self._delimiters_regexp, name)
    return [name]

to_repo_name(name)

Convert the name to the appropriate name with a given repo separator.

Source code in src/nskit/vcs/namespace_validator.py
def to_repo_name(self, name: str):
    """Convert the name to the appropriate name with a given repo separator."""
    return self.repo_separator.join(self.to_parts(name))

nskit.vcs.namespace_validator.NamespaceOptionsType = TypeAliasType('NamespaceOptionsType', list[Union[str, dict[str, 'NamespaceOptionsType']]]) module-attribute

nskit.vcs.namespace_validator.ValidationEnum

Bases: Enum

Enum for validation level.

Source code in src/nskit/vcs/namespace_validator.py
class ValidationEnum(Enum):
    """Enum for validation level."""

    strict = "2"
    warn = "1"
    none = "0"

Installers

nskit.vcs.installer.PythonInstaller

Bases: Installer

Python language installer.

Can be enabled or disabled using the boolean flag and environment variables. The virtualenv config can be updated (to a custom dir/path relative to the codebase root)

Source code in src/nskit/vcs/installer.py
class PythonInstaller(Installer):
    """Python language installer.

    Can be enabled or disabled using the boolean flag and environment variables. The virtualenv config can be updated (to a custom dir/path relative to the codebase root)
    """

    model_config = SettingsConfigDict(env_prefix="NSKIT_PYTHON_INSTALLER_", env_file=".env")
    virtualenv_dir: Path = Path(".venv")
    # Include Azure DevOps seeder
    virtualenv_args: list[str] = []
    # For Azure Devops could set this to something like: ['--seeder', 'azdo-pip']

    def check_repo(self, path: Path):
        """Check if this is a python repo."""
        logger.debug(f"{self.__class__} enabled, checking for match.")
        result = (
            (path / "setup.py").exists() or (path / "pyproject.toml").exists() or (path / _REQUIREMENTS_TXT).exists()
        )
        logger.info(f"Matched repo to {self.__class__}.")
        return result

    def install(self, path: Path, *, executable: str = "venv", deps: bool = True):
        """Install the repo.

        executable can override the executable to use (e.g. a virtualenv)
        deps controls whether dependencies are installed or not.
        """
        executable = self._get_executable(path, executable)
        logger.info(f"Installing using {executable}.")
        args = []
        if not deps:
            args.append("--no-deps")
        with ChDir(path):
            if Path("setup.py").exists() or Path("pyproject.toml").exists():
                subprocess.check_call([str(executable), "-m", "pip", "install", "-e", ".[dev]"] + args)  # nosec B603, B607
            elif deps and Path(_REQUIREMENTS_TXT).exists():
                subprocess.check_call([str(executable), "-m", "pip", "install", "-r", _REQUIREMENTS_TXT])  # nosec B603, B607

    def _get_virtualenv(self, full_virtualenv_dir: Path):
        """Get the virtualenv executable.

        Create's it if it doesn't exist.
        """
        if not full_virtualenv_dir.exists():
            virtualenv.cli_run([str(full_virtualenv_dir)] + self.virtualenv_args)
        if sys.platform.startswith("win"):
            executable = full_virtualenv_dir / "Scripts" / "python.exe"
        else:
            executable = full_virtualenv_dir / "bin" / "python"
        return executable.absolute()

    def _get_executable(self, path: Path, executable: str | None = "venv"):
        # Install in the current environment
        if self.virtualenv_dir.is_absolute():
            full_virtualenv_dir = self.virtualenv_dir
        else:
            full_virtualenv_dir = path / self.virtualenv_dir
        if executable is None:
            executable = sys.executable
        elif executable == "venv":
            executable = self._get_virtualenv(full_virtualenv_dir=full_virtualenv_dir)
        return executable

check_repo(path)

Check if this is a python repo.

Source code in src/nskit/vcs/installer.py
def check_repo(self, path: Path):
    """Check if this is a python repo."""
    logger.debug(f"{self.__class__} enabled, checking for match.")
    result = (
        (path / "setup.py").exists() or (path / "pyproject.toml").exists() or (path / _REQUIREMENTS_TXT).exists()
    )
    logger.info(f"Matched repo to {self.__class__}.")
    return result

install(path, *, executable='venv', deps=True)

Install the repo.

executable can override the executable to use (e.g. a virtualenv) deps controls whether dependencies are installed or not.

Source code in src/nskit/vcs/installer.py
def install(self, path: Path, *, executable: str = "venv", deps: bool = True):
    """Install the repo.

    executable can override the executable to use (e.g. a virtualenv)
    deps controls whether dependencies are installed or not.
    """
    executable = self._get_executable(path, executable)
    logger.info(f"Installing using {executable}.")
    args = []
    if not deps:
        args.append("--no-deps")
    with ChDir(path):
        if Path("setup.py").exists() or Path("pyproject.toml").exists():
            subprocess.check_call([str(executable), "-m", "pip", "install", "-e", ".[dev]"] + args)  # nosec B603, B607
        elif deps and Path(_REQUIREMENTS_TXT).exists():
            subprocess.check_call([str(executable), "-m", "pip", "install", "-r", _REQUIREMENTS_TXT])  # nosec B603, B607

Provider Detection

nskit.vcs.provider_detection.get_default_repo_client()

Auto-detect and return a configured VCS provider's repo client.

Iterates through registered providers (via the nskit.vcs.providers entry point) and returns the client for the last one that initialises successfully from environment variables.

Raises:

Type Description
ValueError

If no provider could be configured.

Source code in src/nskit/vcs/provider_detection.py
def get_default_repo_client() -> RepoClient:
    """Auto-detect and return a configured VCS provider's repo client.

    Iterates through registered providers (via the ``nskit.vcs.providers``
    entry point) and returns the client for the last one that initialises
    successfully from environment variables.

    Raises:
        ValueError: If no provider could be configured.
    """
    client = None
    for provider in ProviderEnum:
        try:
            if provider.extension:
                settings = provider.extension()
                client = settings.repo_client
                logger.info(f"{provider.value} configured.")
            else:
                raise ValueError("Extension not found")
        except (ImportError, ValueError):
            logger.info(f"{provider.value} not configured.")
    if client is None:
        raise ValueError(
            "No VCS provider configured. Set appropriate environment variables (e.g. GITHUB_TOKEN for GitHub)."
        )
    return client

Providers

nskit.vcs.providers.abstract.RepoClient

Bases: ABC

Repo management client.

Source code in src/nskit/vcs/providers/abstract.py
class RepoClient(ABC):
    """Repo management client."""

    @abstractmethod
    def create(self, repo_name: str):
        """Create a repo."""
        raise NotImplementedError()

    @abstractmethod
    def get_remote_url(self, repo_name: str) -> HttpUrl:
        """Get the remote url for a repo."""
        raise NotImplementedError()

    def get_clone_url(self, repo_name: str) -> HttpUrl:
        """Get the clone URL.

        This defaults to the remote url unless specifically implemented.
        """
        return self.get_remote_url(repo_name)

    @abstractmethod
    def delete(self, repo_name: str):
        """Delete a repo."""
        raise NotImplementedError()

    @abstractmethod
    def check_exists(self, repo_name: str) -> bool:
        """Check if the repo exists on the remote."""
        raise NotImplementedError()

    @abstractmethod
    def list(self) -> list[str]:
        """List all repos on the remote."""
        raise NotImplementedError()

    def configure(self, repo_name: str, settings: Optional[dict[str, Any]] = None) -> None:
        """Apply repository-level configuration (e.g. merge options, features).

        Default is a no-op so providers that do not support remote configuration
        remain valid. Providers should override to apply ``settings`` to the repo.
        """
        return None

    def set_branch_protection(
        self,
        repo_name: str,
        branch: str,
        rules: Optional[dict[str, Any]] = None,
    ) -> None:
        """Apply classic branch protection configuration to ``branch``.

        Default is a no-op so providers without branch-protection support remain
        valid. Providers should override to apply ``rules`` to the named branch.

        Prefer the ruleset methods below where the provider supports them;
        rulesets are the successor to classic branch protection.
        """
        return None

    def create_ruleset(self, repo_name: str, ruleset: Any) -> Optional[int]:
        """Create a ruleset on the repo, returning its ID if the provider gives one.

        Default is a no-op so providers without ruleset support remain valid.
        """
        return None

    # NB: the return annotation is quoted because ``list`` is also a method on
    # this class, so an unquoted ``list[Any]`` would resolve to that method.
    def list_rulesets(self, repo_name: str) -> "list[Any]":
        """List the repo's rulesets. Default is an empty list."""
        return []

    def update_ruleset(self, repo_name: str, ruleset_id: int, **changes: Any) -> None:
        """Update fields on an existing ruleset. Default is a no-op."""
        return None

    def delete_ruleset(self, repo_name: str, ruleset_id: int) -> None:
        """Delete a ruleset. Default is a no-op."""
        return None

check_exists(repo_name) abstractmethod

Check if the repo exists on the remote.

Source code in src/nskit/vcs/providers/abstract.py
@abstractmethod
def check_exists(self, repo_name: str) -> bool:
    """Check if the repo exists on the remote."""
    raise NotImplementedError()

configure(repo_name, settings=None)

Apply repository-level configuration (e.g. merge options, features).

Default is a no-op so providers that do not support remote configuration remain valid. Providers should override to apply settings to the repo.

Source code in src/nskit/vcs/providers/abstract.py
def configure(self, repo_name: str, settings: Optional[dict[str, Any]] = None) -> None:
    """Apply repository-level configuration (e.g. merge options, features).

    Default is a no-op so providers that do not support remote configuration
    remain valid. Providers should override to apply ``settings`` to the repo.
    """
    return None

create(repo_name) abstractmethod

Create a repo.

Source code in src/nskit/vcs/providers/abstract.py
@abstractmethod
def create(self, repo_name: str):
    """Create a repo."""
    raise NotImplementedError()

create_ruleset(repo_name, ruleset)

Create a ruleset on the repo, returning its ID if the provider gives one.

Default is a no-op so providers without ruleset support remain valid.

Source code in src/nskit/vcs/providers/abstract.py
def create_ruleset(self, repo_name: str, ruleset: Any) -> Optional[int]:
    """Create a ruleset on the repo, returning its ID if the provider gives one.

    Default is a no-op so providers without ruleset support remain valid.
    """
    return None

delete(repo_name) abstractmethod

Delete a repo.

Source code in src/nskit/vcs/providers/abstract.py
@abstractmethod
def delete(self, repo_name: str):
    """Delete a repo."""
    raise NotImplementedError()

delete_ruleset(repo_name, ruleset_id)

Delete a ruleset. Default is a no-op.

Source code in src/nskit/vcs/providers/abstract.py
def delete_ruleset(self, repo_name: str, ruleset_id: int) -> None:
    """Delete a ruleset. Default is a no-op."""
    return None

get_clone_url(repo_name)

Get the clone URL.

This defaults to the remote url unless specifically implemented.

Source code in src/nskit/vcs/providers/abstract.py
def get_clone_url(self, repo_name: str) -> HttpUrl:
    """Get the clone URL.

    This defaults to the remote url unless specifically implemented.
    """
    return self.get_remote_url(repo_name)

get_remote_url(repo_name) abstractmethod

Get the remote url for a repo.

Source code in src/nskit/vcs/providers/abstract.py
@abstractmethod
def get_remote_url(self, repo_name: str) -> HttpUrl:
    """Get the remote url for a repo."""
    raise NotImplementedError()

list() abstractmethod

List all repos on the remote.

Source code in src/nskit/vcs/providers/abstract.py
@abstractmethod
def list(self) -> list[str]:
    """List all repos on the remote."""
    raise NotImplementedError()

list_rulesets(repo_name)

List the repo's rulesets. Default is an empty list.

Source code in src/nskit/vcs/providers/abstract.py
def list_rulesets(self, repo_name: str) -> "list[Any]":
    """List the repo's rulesets. Default is an empty list."""
    return []

set_branch_protection(repo_name, branch, rules=None)

Apply classic branch protection configuration to branch.

Default is a no-op so providers without branch-protection support remain valid. Providers should override to apply rules to the named branch.

Prefer the ruleset methods below where the provider supports them; rulesets are the successor to classic branch protection.

Source code in src/nskit/vcs/providers/abstract.py
def set_branch_protection(
    self,
    repo_name: str,
    branch: str,
    rules: Optional[dict[str, Any]] = None,
) -> None:
    """Apply classic branch protection configuration to ``branch``.

    Default is a no-op so providers without branch-protection support remain
    valid. Providers should override to apply ``rules`` to the named branch.

    Prefer the ruleset methods below where the provider supports them;
    rulesets are the successor to classic branch protection.
    """
    return None

update_ruleset(repo_name, ruleset_id, **changes)

Update fields on an existing ruleset. Default is a no-op.

Source code in src/nskit/vcs/providers/abstract.py
def update_ruleset(self, repo_name: str, ruleset_id: int, **changes: Any) -> None:
    """Update fields on an existing ruleset. Default is a no-op."""
    return None

nskit.vcs.providers.abstract.VCSProviderSettings

Bases: ABC, BaseConfiguration

Settings for VCS Provider.

Source code in src/nskit/vcs/providers/abstract.py
class VCSProviderSettings(ABC, BaseConfiguration):
    """Settings for VCS Provider."""

    @abstractproperty
    def repo_client(self) -> RepoClient:
        """Return the instantiated repo client object for the provider."""
        raise NotImplementedError()

repo_client()

Return the instantiated repo client object for the provider.

Source code in src/nskit/vcs/providers/abstract.py
@abstractproperty
def repo_client(self) -> RepoClient:
    """Return the instantiated repo client object for the provider."""
    raise NotImplementedError()

nskit.vcs.providers.github.GithubSettings

Bases: VCSProviderSettings

Github settings.

Token resolution order:

  1. an explicitly provided token (or GITHUB_TOKEN in the environment);
  2. the GitHub CLI (gh auth token), when use_gh_cli is set;
  3. interactive device authentication, when interactive is set.

Both fallbacks are opt-in. In particular use_gh_cli defaults to False: picking up whatever credential happens to be sitting in the developer's gh login would mean a caller that supplied no token silently acquires one, which hides misconfiguration and surprises anything that expects "no token" to mean "no access".

Source code in src/nskit/vcs/providers/github/provider.py
class GithubSettings(VCSProviderSettings):
    """Github settings.

    Token resolution order:

    1. an explicitly provided ``token`` (or ``GITHUB_TOKEN`` in the environment);
    2. the GitHub CLI (``gh auth token``), when ``use_gh_cli`` is set;
    3. interactive device authentication, when ``interactive`` is set.

    Both fallbacks are opt-in. In particular ``use_gh_cli`` defaults to
    ``False``: picking up whatever credential happens to be sitting in the
    developer's ``gh`` login would mean a caller that supplied no token silently
    acquires one, which hides misconfiguration and surprises anything that
    expects "no token" to mean "no access".
    """

    model_config = SettingsConfigDict(env_prefix="GITHUB_", env_file=".env", dotenv_extra="ignore")

    interactive: bool = Field(False, description="Use Interactive Validation for token")
    use_gh_cli: bool = Field(
        False,
        description="Fall back to the GitHub CLI (gh auth token) when no token is set",
    )
    url: HttpUrl = "https://api.github.com"
    organisation: Optional[str] = Field(
        None, description="Organisation to work in, otherwise uses the user for the token"
    )
    token: SecretStr = Field(
        None,
        validate_default=True,
        description="Token to use for authentication, falls back to the gh CLI then interactive device authentication",
    )
    repo: GithubRepoSettings = Field(default_factory=GithubRepoSettings)

    @property
    def repo_client(self) -> GithubRepoClient:
        """Get the instantiated repo client."""
        return GithubRepoClient(self)

    @field_validator("token", mode="before")
    @classmethod
    def _validate_token(cls, value, info: ValidationInfo):
        if value is not None:
            return value
        # Try the local gh CLI before prompting: on a developer machine it is
        # usually already authenticated, which avoids an interactive detour.
        if info.data.get("use_gh_cli", False):
            value = gh_cli_token()
            if value:
                logger.info("Using GitHub token from the gh CLI")
                return value
        if info.data.get("interactive", False):
            ghauth = GhDeviceAuth(_def_clientid, Scope.repo, Scope.delete_repo)
            print(ghauth.url_docs())
            ghauth.open_browser()
            value = ghauth.wait()
        return value

repo_client property

Get the instantiated repo client.