Current File : /home/inlingua/miniconda3/lib/python3.1/site-packages/conda_anaconda_tos/remote.py
# Copyright (C) 2024 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""Low-level remote (the "raw" endpoint JSON) Terms of Service metadata management."""

from __future__ import annotations

from datetime import datetime
from json import JSONDecodeError
from typing import TYPE_CHECKING

from conda.base.context import context
from conda.common.url import join_url
from conda.gateways.connection.session import get_session
from conda.models.channel import Channel
from pydantic import ValidationError
from requests.exceptions import RequestException

from .exceptions import (
    CondaToSInvalidError,
    CondaToSMissingError,
    CondaToSPermissionError,
)
from .models import RemoteToSMetadata
from .path import get_cache_path

if TYPE_CHECKING:
    from pathlib import Path
    from typing import Final

    from requests import Response

ENDPOINT: Final = "tos.json"


def get_endpoint(channel: str | Channel) -> Response:
    """Get the metadata endpoint for the given channel."""
    channel = Channel(channel)
    if not channel.base_url:
        raise ValueError(
            "`channel` must have a base URL. "
            "(hint: `conda.models.channel.MultiChannel` doesn't have an endpoint)"
        )

    session = get_session(channel.base_url)
    url = join_url(channel.base_url, ENDPOINT)

    saved_token_setting = context.add_anaconda_token
    try:
        # do not inject conda/binstar token into URL for two reasons:
        # 1. Metadata endpoint shouldn't be a protected endpoint
        # 2. CondaHttpAuth.add_binstar_token adds subdir to the URL
        #    which the metadata endpoint doesn't have
        context.add_anaconda_token = False
        response = session.get(
            url,
            headers={"Content-Type": "application/json"},
            timeout=(
                context.remote_connect_timeout_secs,
                context.remote_read_timeout_secs,
            ),
        )
        response.raise_for_status()
    except RequestException as exc:
        # RequestException: failed to get metadata endpoint
        raise CondaToSMissingError(channel) from exc
    finally:
        context.add_anaconda_token = saved_token_setting
    return response


def get_cached_endpoint(
    channel: str | Channel,
    *,
    cache_timeout: int | float | None = float("inf"),
) -> Path | None:
    """Get the path to cached payload for the given channel."""
    # early exit if cache is disabled
    if not cache_timeout:
        return None

    # argument validation/coercion
    path = get_cache_path(channel)
    if not isinstance(cache_timeout, (int, float)):
        raise TypeError("`cache_timeout` must be an integer, float, or falsy.")

    # get mtime of cache
    try:
        mtime = path.stat().st_mtime
    except FileNotFoundError:
        # FileNotFoundError: cache path doesn't exist
        return None

    # check if cache is stale
    now = datetime.now().timestamp()  # noqa: DTZ005
    if (now - mtime) >= cache_timeout:
        return None
    return path


def write_cached_endpoint(
    channel: str | Channel,
    metadata: RemoteToSMetadata | None,
) -> Path:
    """Write the metadata cache for the given channel."""
    # argument validation/coercion
    path = get_cache_path(channel)
    if metadata and not isinstance(metadata, RemoteToSMetadata):
        raise TypeError("`metadata` must be a RemoteToSMetadata.")

    # write to cache
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        if metadata:
            path.write_text(metadata.model_dump_json())
        else:
            path.touch()
    except PermissionError as exc:
        # PermissionError: can't write to cache path
        raise CondaToSPermissionError(path, channel) from exc

    return path


def get_remote_metadata(
    channel: str | Channel,
    *,
    cache_timeout: int | float | None = None,
) -> RemoteToSMetadata:
    """Get the metadata metadata for the given channel."""
    # argument validation/coercion
    cache = get_cached_endpoint(channel, cache_timeout=cache_timeout)

    # return cached metadata
    if cache:
        try:
            text = cache.read_text().strip()
            if not text:
                raise CondaToSMissingError(channel)
        except FileNotFoundError as exc:
            # FileNotFoundError: cache path doesn't exist
            raise CondaToSMissingError(channel) from exc
        except PermissionError as exc:
            # PermissionError: can't read cache path
            raise CondaToSPermissionError(cache, channel) from exc

        try:
            return RemoteToSMetadata.model_validate_json(text)
        except ValidationError as exc:
            # ValidationError: invalid JSON schema
            raise CondaToSInvalidError(channel) from exc

    # return remote metadata
    try:
        metadata = RemoteToSMetadata(**get_endpoint(channel).json())
    except CondaToSMissingError:
        # CondaToSMissingError: no Terms of Service for this channel
        # create an empty cache to prevent repeated requests
        write_cached_endpoint(channel, None)
        raise
    except (AttributeError, TypeError, JSONDecodeError, ValidationError) as exc:
        # AttributeError: response has no JSON
        # TypeError: invalid JSON
        # JSONDecodeError: invalid JSON
        # ValidationError: invalid JSON schema
        write_cached_endpoint(channel, None)
        raise CondaToSInvalidError(channel) from exc
    else:
        write_cached_endpoint(channel, metadata)
        return metadata