File: C:/Users/fred/anaconda3/Lib/site-packages/anaconda_navigator/config/feature_flags.py
"""Module for managing feature flags"""
from __future__ import annotations
import base64
import dataclasses
import json
import sys
import typing
from urllib import parse
import anaconda_anon_usage.tokens as anon_tokens
from qtpy.QtCore import QObject, Signal, QTimer
from anaconda_navigator import __about__
from anaconda_navigator.api import cloud, conda_api
from anaconda_navigator.config import CONF, FEATURE_FLAGS_JSON_PATH
from anaconda_navigator.utils import download_manager
from anaconda_navigator.utils import singletons
def prepare_context() -> bytes:
"""Get context attributes."""
return json.dumps({
'key': anon_tokens.client_token() or 'default',
'email': cloud.CloudAPI().username,
'platform': conda_api.CondaAPI().get_platform(),
'navigator_version': __about__.__version__
}).encode()
def prepare_headers() -> typing.Mapping[str, bytes | str]:
"""Format header value of feature flag context."""
return {'X-FEATUREFLAG-CONTEXT': base64.b64encode(prepare_context()).decode('ascii')}
def normalize_key(name: str) -> str:
"""Convert launchdarkly feature key name to python format."""
return name.lower().replace('-', '_')
@dataclasses.dataclass(eq=False)
class FeatureFlags:
"""Feature flags store."""
survey_url: str = ''
known_channels: tuple[str, ...] = (
'defaults', 'anaconda', 'astropy', 'beeware', 'binstar', 'biobuilds', 'bioconda', 'conda-forge', 'esri', 'free',
'intel', 'main', 'menpo', 'msys2', 'omnia', 'pro', 'pytorch', 'qiime2', 'r', 'rapidsai', 'rapidsai-nightly',
'rdkit', 'mgoetze', 'conda-test', 'conda-canary', 'anaconda-client-test', 'navigator-test'
)
login_reminder_interval_msec: int = 86400_000
whats_new_enabled: bool = sys.platform != 'linux'
class FeatureFlagsManager(QObject):
"""Class for managing the loading and caching of feature flags."""
sig_flags_loaded = Signal()
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
"""Initialize new :class:`~FeatureFlagsManager` instance."""
super().__init__(*args, **kwargs)
self.__timer: QTimer = QTimer()
self.__loaded: bool = False
self.__base_url = CONF.get('internal', 'launchdarkly_proxy_base_url')
def __set_loaded(self, value: bool = True) -> None:
self.__loaded = value
self.sig_flags_loaded.emit()
def load(self) -> None:
"""Load feature flags from a remote source to the cache file."""
if not self.__base_url:
self.__load_cached()
return
self.__timer.singleShot(5_000, self.__load_cached)
download: download_manager.Download = download_manager.Download(
parse.urljoin(self.__base_url, 'v1/feature-flags')
).into(FEATURE_FLAGS_JSON_PATH).attach('POST', prepare_context())
download.sig_succeeded.connect(self.__load_cached)
download_manager.Manager().execute(download)
def __load_cached(self) -> None:
"""Load feature flags from the cache file."""
self.__timer.stop()
if self.__loaded:
return
try:
stream: typing.TextIO
with open(FEATURE_FLAGS_JSON_PATH, 'rt', encoding='utf-8') as stream:
cached_flags = json.load(stream)
for flag_key, value in cached_flags.items():
normal_flag_key: str = normalize_key(flag_key)
if hasattr(FEATURE_FLAGS, normal_flag_key):
setattr(FEATURE_FLAGS, normal_flag_key, value)
except (OSError, TypeError, ValueError):
pass
finally:
self.__set_loaded()
FEATURE_FLAGS: FeatureFlags = FeatureFlags()
FEATURE_FLAGS_MANAGER: typing.Final[
singletons.Singleton[FeatureFlagsManager]] = singletons.SingleInstanceOf(FeatureFlagsManager)