Current File : /home/inlingua/miniconda3/lib/python3.1/site-packages/conda_anaconda_tos/console/render.py
# Copyright (C) 2024 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""Render functions for console output."""

from __future__ import annotations

import functools
from pathlib import Path
from typing import TYPE_CHECKING

from conda.exceptions import ArgumentError
from rich.console import Console
from rich.table import Table

from ..api import (
    CI,
    JUPYTER,
    accept_tos,
    clean_cache,
    clean_tos,
    get_all_tos,
    get_channels,
    get_one_tos,
    reject_tos,
)
from ..exceptions import (
    CondaToSMissingError,
    CondaToSNonInteractiveError,
    CondaToSRejectedError,
)
from ..path import CACHE_DIR, SEARCH_PATH
from .mappers import NULL_CHAR, accepted_mapping, location_mapping, version_mapping
from .prompt import FuzzyPrompt

if TYPE_CHECKING:
    import os
    from collections.abc import Iterable
    from typing import Any, Callable, Final

    from conda.models.channel import Channel

    from ..models import LocalPair, RemotePair


TOS_OUTDATED: Final = "* Terms of Service version(s) are outdated."


def printable(func: Callable[..., int]) -> Callable[..., int]:
    """Pass console and printer functions to the decorated function.

    This instantiates a console for the render functions if not provided and
    the console and json printers to pass them to the decorated function.
    """

    @functools.wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> int:  # noqa: ANN401
        console = kwargs.pop("console", Console())

        # json=False -> standard output, default
        # json=True -> JSON output
        # json=None -> no output
        json = kwargs.pop("json", False)
        if json is None:
            printer = json_printer = lambda *_, **__: None
            json = True  # force non-interactive
        elif json:
            printer, json_printer = lambda *_, **__: None, console.print_json
        else:
            printer, json_printer = console.print, console.print_json

        return func(
            *args,
            **kwargs,
            json=json,
            console=console,
            printer=printer,
            json_printer=json_printer,
        )

    return wrapper


@printable
def render_list(
    *channels: str | Channel,
    tos_root: str | os.PathLike[str] | Path,
    cache_timeout: int | float | None,
    json: bool = False,
    verbose: bool = False,
    console: Console | None = None,  # noqa: ARG001
    printer: Callable[..., None],
    json_printer: Callable[..., None],
) -> int:
    """Display listing of unaccepted, accepted, and rejected Terms of Service."""
    table = Table()
    table.add_column("Channel")
    table.add_column("Version")
    table.add_column("Accepted")
    table.add_column("Support")
    if verbose:
        table.add_column("Location")
        add_row = table.add_row
    else:
        add_row = lambda *args: table.add_row(*args[:-1])  # noqa: E731

    json_output: dict[str, Any] = {}
    outdated = False
    for channel, metadata_pair in get_all_tos(
        *channels,
        tos_root=tos_root,
        cache_timeout=cache_timeout,
    ):
        if not metadata_pair:
            json_output[channel.base_url] = None
            add_row(channel.base_url, NULL_CHAR, NULL_CHAR, NULL_CHAR, NULL_CHAR)
        else:
            json_output[channel.base_url] = {
                **metadata_pair.metadata.model_dump(mode="json"),
                "outdated": bool(metadata_pair.remote),
                "path": str(metadata_pair.path),
            }
            outdated = outdated or bool(metadata_pair.remote)
            add_row(
                channel.base_url,
                version_mapping(metadata_pair.metadata.version, metadata_pair.remote),
                accepted_mapping(metadata_pair.metadata),
                metadata_pair.metadata.support,
                location_mapping(metadata_pair.path),
            )

    if json:
        json_printer(data=json_output)
    else:
        printer(table)
        if outdated:
            printer(f"[bold yellow]{TOS_OUTDATED}")
    return 0


@printable
def render_view(
    *channels: str | Channel,
    tos_root: str | os.PathLike[str] | Path,
    cache_timeout: int | float | None,
    json: bool = False,
    console: Console | None = None,  # noqa: ARG001
    printer: Callable[..., None],
    json_printer: Callable[..., None],
) -> int:
    """Display the Terms of Service text for the given channels."""
    json_output: dict[str, Any] = {}
    for channel in get_channels(*channels):
        try:
            metadata = get_one_tos(
                channel,
                tos_root=tos_root,
                cache_timeout=cache_timeout,
            ).metadata
        except CondaToSMissingError:
            json_output[channel.base_url] = None
            printer(f"no Terms of Service for {channel}")
        else:
            json_output[channel.base_url] = metadata.model_dump(mode="json")
            printer(f"viewing Terms of Service for {channel}:")
            printer(metadata.text)

    if json:
        json_printer(data=json_output)
    return 0


@printable
def render_accept(
    *channels: str | Channel,
    tos_root: str | os.PathLike[str] | Path,
    cache_timeout: int | float | None,
    json: bool = False,
    console: Console | None = None,  # noqa: ARG001
    printer: Callable[..., None],
    json_printer: Callable[..., None],
) -> int:
    """Display acceptance of the Terms of Service for the given channels."""
    json_output: dict[str, Any] = {}
    for channel in get_channels(*channels):
        try:
            metadata = accept_tos(
                channel,
                tos_root=tos_root,
                cache_timeout=cache_timeout,
            ).metadata
        except CondaToSMissingError:
            json_output[channel.base_url] = None
            printer(f"Terms of Service not found for {channel}")
        else:
            json_output[channel.base_url] = metadata.model_dump(mode="json")
            printer(f"accepted Terms of Service for {channel}")

    if json:
        json_printer(data=json_output)
    return 0


@printable
def render_reject(
    *channels: str | Channel,
    tos_root: str | os.PathLike[str] | Path,
    cache_timeout: int | float | None,
    json: bool = False,
    console: Console | None = None,  # noqa: ARG001
    printer: Callable[..., None],
    json_printer: Callable[..., None],
) -> int:
    """Display rejection of the Terms of Service for the given channels."""
    json_output: dict[str, Any] = {}
    for channel in get_channels(*channels):
        try:
            metadata = reject_tos(
                channel,
                tos_root=tos_root,
                cache_timeout=cache_timeout,
            ).metadata
        except CondaToSMissingError:
            json_output[channel.base_url] = None
            printer(f"Terms of Service not found for {channel}")
        else:
            json_output[channel.base_url] = metadata.model_dump(mode="json")
            printer(f"rejected Terms of Service for {channel}")

    if json:
        json_printer(data=json_output)
    return 0


def _prompt_acceptance(
    channel: Channel,
    pair: RemotePair | LocalPair,
    console: Console,
    choices: Iterable[str] = ("(a)ccept", "(r)eject", "(v)iew"),
) -> bool:
    prologue = ""
    if pair.remote:
        state = "[bold red]rejected[/]"
        if pair.metadata.tos_accepted:
            state = "[bold green]accepted[/]"
        prologue = (
            f"The Terms of Service for {channel} was previously {state}. "
            f"An updated Terms of Service is now available.\n"
        )

    response = FuzzyPrompt.ask(
        f"{prologue}Do you accept the Terms of Service (ToS) for {channel}?",
        choices=choices,
        console=console,
    )
    if response == "accept":
        return True
    elif response == "reject":
        return False
    else:
        console.print((pair.remote or pair.metadata).text)
        return _prompt_acceptance(channel, pair, console, ("(a)ccept", "(r)eject"))


def _gather_tos(
    *channels: str | Channel,
    tos_root: str | os.PathLike[str] | Path,
    cache_timeout: int | float | None,
) -> tuple[
    dict[str, dict],
    list[Channel],
    list[tuple[Channel, RemotePair | LocalPair]],
]:
    accepted = {}
    rejected = []
    channel_pairs = []
    for channel in get_channels(*channels):
        try:
            pair = get_one_tos(channel, tos_root=tos_root, cache_timeout=cache_timeout)
        except CondaToSMissingError:
            # CondaToSMissingError: no metadata found
            continue

        if pair.remote or getattr(pair.metadata, "tos_accepted", None) is None:
            # Terms of Service has been updated or
            # Terms of Service haven't been accepted or rejected yet
            channel_pairs.append((channel, pair))
        elif pair.metadata.tos_accepted:
            accepted[channel.base_url] = pair.metadata.model_dump(mode="json")
        else:
            rejected.append(channel)
    return accepted, rejected, channel_pairs


@printable
def render_interactive(  # noqa: C901
    *channels: str | Channel,
    tos_root: str | os.PathLike[str] | Path,
    cache_timeout: int | float | None,
    json: bool = False,
    verbose: bool = False,
    auto_accept_tos: bool,
    always_yes: bool,
    console: Console | None = None,
    printer: Callable[..., None],
    json_printer: Callable[..., None],
) -> int:
    """Prompt user to accept or reject Terms of Service for channels."""
    if verbose:
        printer("[bold blue]Gathering channels...")
    accepted, rejected, channel_pairs = _gather_tos(
        *channels,
        tos_root=tos_root,
        cache_timeout=cache_timeout,
    )

    if verbose:
        printer("[bold yellow]Reviewing channels...")
    if rejected:
        printer(f"[bold red]{len(rejected)} channel Terms of Service rejected")
        raise CondaToSRejectedError(*rejected)
    elif CI:
        printer("[bold yellow]CI detected...")
    elif JUPYTER:
        printer("[bold yellow]Jupyter detected...")

    non_interactive = []
    for channel, pair in channel_pairs:
        if auto_accept_tos:
            # auto_accept_tos overrides any other setting
            printer(f"[bold yellow]ToS auto accepted for {channel}")
            accepted[channel.base_url] = accept_tos(
                channel,
                tos_root=tos_root,
                cache_timeout=cache_timeout,
            ).metadata
        elif CI:
            # CI is the same as auto_accept_tos but with a warning
            printer(f"[bold yellow]Terms of Service implicitly accepted for {channel}")
            accepted[channel.base_url] = accept_tos(
                channel,
                tos_root=tos_root,
                cache_timeout=cache_timeout,
            ).metadata
        elif json or always_yes or JUPYTER:
            # --json, --yes, and Jupyter doesn't support interactive prompts
            non_interactive.append(channel)
        elif _prompt_acceptance(channel, pair, console):
            # user manually accepted the Terms of Service
            accepted[channel.base_url] = accept_tos(
                channel,
                tos_root=tos_root,
                cache_timeout=cache_timeout,
            ).metadata
        else:
            # user manually rejected the Terms of Service
            reject_tos(channel, tos_root=tos_root, cache_timeout=cache_timeout)
            rejected.append(channel)

    if non_interactive:
        raise CondaToSNonInteractiveError(*non_interactive)
    elif rejected:
        printer(f"[bold red]{len(rejected)} channel Terms of Service rejected")
        raise CondaToSRejectedError(*rejected)

    if verbose or accepted:
        printer(f"[bold green]{len(accepted)} channel Terms of Service accepted")

    if json:
        json_printer(data=accepted)
    return 0


@printable
def render_info(
    *,
    json: bool = False,
    console: Console | None = None,  # noqa: ARG001
    printer: Callable[..., None],
    json_printer: Callable[..., None],
) -> int:
    """Display information about the Terms of Service cache."""
    data: dict[str, str | tuple[str, ...]] = {}
    data["SEARCH_PATH"] = SEARCH_PATH
    try:
        relative_dir = Path("~", CACHE_DIR.relative_to(Path.home()))
    except ValueError:
        # ValueError: CACHE_DIR is not relative to the user's home directory
        relative_dir = CACHE_DIR
    data["CACHE_DIR"] = str(relative_dir)

    if json:
        json_printer(data=data)
    else:
        table = Table(show_header=False)
        table.add_column("Key")
        table.add_column("Value")
        for key, value in data.items():
            if isinstance(value, (tuple, list)):
                value = "\n".join(map(str, value))
            else:
                value = str(value)
            table.add_row(key, value)
        printer(table)
    return 0


@printable
def render_clean(
    cache: bool,
    tos: bool,
    all: bool,  # noqa: A002
    *,
    tos_root: str | os.PathLike[str] | Path,
    json: bool = False,
    console: Console | None = None,  # noqa: ARG001
    printer: Callable[..., None],
    json_printer: Callable[..., None],
) -> int:
    """Clean the metadata cache directories."""
    if not (all or cache or tos):
        raise ArgumentError(
            "At least one removal target must be given. See 'conda tos clean --help'."
        )

    json_output: dict[str, Any] = {}
    if all or cache:
        json_output["cache"] = cache_files = list(map(str, clean_cache()))
        printer(f"Removed {len(cache_files)} cache files.")
    if all or tos:
        json_output["tos"] = tos_files = list(map(str, clean_tos(tos_root)))
        printer(f"Removed {len(tos_files)} Terms of Service files.")

    if json:
        json_printer(data=json_output)
    return 0