Current File : /home/inlingua/miniconda3/lib/python3.1/site-packages/conda_anaconda_tos/plugin.py |
# Copyright (C) 2024 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""High-level conda plugin registration."""
from __future__ import annotations
from datetime import timedelta
from functools import cache
from typing import TYPE_CHECKING
from conda.base.context import context
from conda.cli.helpers import add_parser_prefix, add_parser_verbose
from conda.cli.install import validate_prefix_exists
from conda.common.configuration import PrimitiveParameter
from conda.common.constants import NULL
from conda.plugins import (
CondaPreCommand,
CondaRequestHeader,
CondaSetting,
CondaSubcommand,
hookimpl,
)
from rich.console import Console
from . import APP_NAME, APP_VERSION
from .api import CI, get_channels
from .console import (
render_accept,
render_clean,
render_info,
render_interactive,
render_list,
render_reject,
render_view,
)
from .exceptions import CondaToSMissingError
from .local import get_local_metadata
from .path import ENV_TOS_ROOT, SITE_TOS_ROOT, SYSTEM_TOS_ROOT, USER_TOS_ROOT
from .remote import ENDPOINT
if TYPE_CHECKING:
from argparse import ArgumentParser, Namespace
from collections.abc import Iterator
from typing import Callable
#: Default metadata storage location.
DEFAULT_TOS_ROOT = USER_TOS_ROOT
#: Default cache timeout in seconds.
DEFAULT_CACHE_TIMEOUT = timedelta(hours=1).total_seconds()
#: Field separator for request header
FIELD_SEPARATOR = ";"
#: Key-value separator for request header
KEY_SEPARATOR = "="
#: Terms of Service acceptance request header
TOS_ACCEPT_HEADER = "Anaconda-ToS-Accept"
#: Hosts to which the Terms of Service header is added
HOSTS = {"repo.anaconda.com"}
def _add_channel(parser: ArgumentParser) -> None:
channel_group = parser.add_argument_group("Channel Customization")
channel_group.add_argument(
"-c",
"--channel",
action="append",
help="Additional channels to search for Terms of Service.",
)
channel_group.add_argument(
"--override-channels",
action="store_true",
help="Do not search default or .condarc channels. Requires --channel.",
)
def _add_location(parser: ArgumentParser) -> None:
location_group = parser.add_argument_group("Local Metadata Storage Location")
location_mutex = location_group.add_mutually_exclusive_group()
for flag, value, text in (
("--site", SITE_TOS_ROOT, "System-wide storage location."),
("--system", SYSTEM_TOS_ROOT, "Conda installation storage location."),
("--user", USER_TOS_ROOT, "User storage location."),
("--env", ENV_TOS_ROOT, "Conda environment storage location."),
):
location_mutex.add_argument(
flag,
dest="tos_root",
action="store_const",
const=value,
help=text,
)
location_mutex.add_argument(
"--tos-root",
action="store",
help="Custom storage location.",
)
parser.set_defaults(tos_root=DEFAULT_TOS_ROOT)
def _add_cache(parser: ArgumentParser) -> None:
cache_group = parser.add_argument_group("Cache Control")
cache_mutex = cache_group.add_mutually_exclusive_group()
cache_mutex.add_argument(
"--cache-timeout",
action="store",
type=int,
help="Cache timeout (in seconds) to check for Terms of Service updates.",
)
cache_mutex.add_argument(
"--ignore-cache",
dest="cache_timeout",
action="store_const",
const=0,
help="Ignore the cache and always check for Terms of Service updates.",
)
parser.set_defaults(cache_timeout=DEFAULT_CACHE_TIMEOUT)
def _add_json(parser: ArgumentParser) -> None:
# TODO: replace with conda.cli.helpers.add_parser_json
parser.add_argument(
"--json",
action="store_true",
default=NULL,
help="Report all output as json. Suitable for using conda programmatically.",
)
def configure_parser(parser: ArgumentParser) -> None:
"""Configure the parser for the `tos` subcommand."""
# conda tos --version
parser.add_argument(
"-V",
"--version",
action="version",
version=f"{APP_NAME} {APP_VERSION}",
help=f"Show the {APP_NAME} version number and exit.",
)
# conda tos (default behavior)
_add_channel(parser)
add_parser_prefix(parser)
_add_location(parser)
_add_cache(parser)
_add_json(parser)
add_parser_verbose(parser)
# conda tos <COMMAND>
subparsers = parser.add_subparsers(
title="subcommand",
description="The following subcommands are available.",
dest="cmd",
required=False,
)
# conda tos accept
accept_parser = subparsers.add_parser(
"accept",
help=(
"Accept the Terms of Service for all active channels "
"(default, .condarc, and/or those specified via --channel)."
),
)
_add_channel(accept_parser)
add_parser_prefix(accept_parser)
_add_location(accept_parser)
_add_cache(accept_parser)
_add_json(accept_parser)
# conda tos reject
reject_parser = subparsers.add_parser(
"reject",
help=(
"Reject the Terms of Service for all active channels "
"(default, .condarc, and/or those specified via --channel)."
),
)
_add_channel(reject_parser)
add_parser_prefix(reject_parser)
_add_location(reject_parser)
_add_cache(reject_parser)
_add_json(reject_parser)
# conda tos view
view_parser = subparsers.add_parser(
"view",
help=(
"View the Terms of Service for all active channels "
"(default, .condarc, and/or those specified via --channel)."
),
)
_add_channel(view_parser)
add_parser_prefix(view_parser)
_add_location(view_parser)
_add_cache(view_parser)
_add_json(view_parser)
# conda tos interactive
interactive_parser = subparsers.add_parser(
"interactive",
help=(
"Interactively accept/reject/view Terms of Service for all active channels "
"(default, .condarc, and/or those specified via --channel)."
),
)
_add_channel(interactive_parser)
add_parser_prefix(interactive_parser)
_add_location(interactive_parser)
_add_cache(interactive_parser)
_add_json(interactive_parser)
add_parser_verbose(interactive_parser)
# conda tos info
info_parser = subparsers.add_parser(
"info",
help=(
"Display information about the plugin "
"(e.g., search path and cache directory)."
),
)
_add_json(info_parser)
# conda tos clean
clean_parser = subparsers.add_parser(
"clean",
help="Clean the cache directories.",
)
clean_parser.add_argument(
"--cache",
action="store_true",
help="Remove all cache files.",
)
clean_parser.add_argument(
"--tos",
action="store_true",
help="Remove all acceptances/rejections.",
)
clean_parser.add_argument(
"--all",
action="store_true",
help="Invoke both `--cache` and `--tos`.",
)
_add_json(clean_parser)
def execute(args: Namespace) -> int:
"""Execute the `tos` subcommand."""
validate_prefix_exists(context.target_prefix)
console = Console()
action: Callable
kwargs = {}
if args.cmd == "accept":
action = render_accept
elif args.cmd == "reject":
action = render_reject
elif args.cmd == "view":
action = render_view
elif args.cmd == "interactive":
action = render_interactive
kwargs["auto_accept_tos"] = context.plugins.auto_accept_tos
kwargs["always_yes"] = context.always_yes
kwargs["verbose"] = context.verbose
elif args.cmd == "info":
# refactor into `conda info` plugin (when possible)
return render_info(json=context.json, console=console)
elif args.cmd == "clean":
# refactor into `conda clean` plugin (when possible)
return render_clean(
cache=args.cache,
tos=args.tos,
all=args.all,
tos_root=args.tos_root,
json=context.json,
console=console,
)
else:
# default
action = render_list
kwargs["verbose"] = context.verbose
return action(
*context.channels,
tos_root=args.tos_root,
cache_timeout=args.cache_timeout,
json=context.json,
console=console,
**kwargs,
)
@hookimpl
def conda_subcommands() -> Iterator[CondaSubcommand]:
"""Return a list of subcommands for the plugin."""
yield CondaSubcommand(
name="tos",
action=execute,
summary=(
"A subcommand for viewing, accepting, rejecting, and otherwise interacting "
"with a channel's Terms of Service (ToS). This plugin periodically checks "
"for updated Terms of Service for the active/selected channels. "
"Channels with a Terms of Service will need to be accepted or rejected "
"prior to use. Conda will only allow package installation from channels "
"without a Terms of Service or with an accepted Terms of Service. "
"Attempting to use a channel with a rejected Terms of Service will result "
"in an error."
),
configure_parser=configure_parser,
)
@hookimpl
def conda_settings() -> Iterator[CondaSetting]:
"""Return a list of settings for the plugin."""
yield CondaSetting(
name="auto_accept_tos",
description="Automatically accept Terms of Service (ToS) for all channels.",
parameter=PrimitiveParameter(False, element_type=bool),
)
def _pre_command_check_tos(_command: str) -> None:
render_interactive(
*context.channels,
tos_root=DEFAULT_TOS_ROOT,
cache_timeout=DEFAULT_CACHE_TIMEOUT,
json=None,
verbose=context.verbose,
auto_accept_tos=context.plugins.auto_accept_tos,
always_yes=context.always_yes,
)
@hookimpl(tryfirst=True)
def conda_pre_commands() -> Iterator[CondaPreCommand]:
"""Return a list of pre-commands for the plugin."""
yield CondaPreCommand(
name="check_tos",
action=_pre_command_check_tos,
run_for={
"create",
"env_create",
"env_remove",
"env_update",
"install",
"remove",
"rename",
"search",
"update",
},
)
@cache
def _get_tos_acceptance_header() -> str:
if CI:
return "CI=true"
values = []
for channel in get_channels(*context.channels):
try:
local_pair = get_local_metadata(
channel,
extend_search_path=[DEFAULT_TOS_ROOT],
)
except CondaToSMissingError:
pass
else:
values.append(
KEY_SEPARATOR.join(
(
channel.base_url,
str(int(local_pair.metadata.version.timestamp())),
"accepted" if local_pair.metadata.tos_accepted else "rejected",
str(int(local_pair.metadata.acceptance_timestamp.timestamp())),
)
)
)
return FIELD_SEPARATOR.join(values)
@hookimpl
def conda_request_headers(host: str, path: str) -> Iterator[CondaRequestHeader]:
"""Return a list of request headers for the plugin."""
if (
# only add the header to anaconda.com endpoints
host in HOSTS
# only add the Terms of Service header for non-Terms of Service endpoints
and not path.endswith(f"/{ENDPOINT}")
):
yield CondaRequestHeader(
name=TOS_ACCEPT_HEADER,
value=_get_tos_acceptance_header(),
)