<gh_stars>0
import abc
from typing import List, Optional
from featureflags.evaluations.constants import SEGMENT_MATCH_OPERATOR

from featureflags.evaluations.feature import FeatureConfig
from featureflags.evaluations.segment import Segment
from featureflags.interface import Cache, Store
from featureflags.models.unset import Unset
from featureflags.util import log


class QueryInterface(metaclass=abc.ABCMeta):

    @classmethod
    def __subclasshook__(cls, subclass):
        return (hasattr(subclass, 'get_flag') and
                callable(subclass.get_flag) and
                hasattr(subclass, 'get_segment') and
                callable(subclass.get_segment) or
                hasattr(subclass, 'find_flags_by_segment') and
                callable(subclass.find_flags_by_segment) or
                NotImplemented)

    @abc.abstractmethod
    def get_flag(self, identifier: str) -> Optional[FeatureConfig]:
        """Get flag from repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def get_segment(self, identifier: str) -> Optional[Segment]:
        """Get Target group from repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def find_flags_by_segment(self, identifier: str) -> List[str]:
        """Find all flags with rule segment match"""
        raise NotImplementedError


class DataProviderInterface(QueryInterface):

    @classmethod
    def __subclasshook__(cls, subclass):
        return (hasattr(subclass, 'get_flag') and
                callable(subclass.get_flag) and
                hasattr(subclass, 'get_segment') and
                callable(subclass.get_segment) or
                hasattr(subclass, 'close') and
                callable(subclass.close) or
                NotImplemented)

    @abc.abstractmethod
    def set_flag(self, flag: FeatureConfig) -> None:
        """Put flag to the repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def set_segment(self, group: Segment) -> None:
        """Put Target group to the repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def remove_flag(self, identifier: str) -> None:
        """Remove Flag from the repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def remove_segment(self, identifier: str) -> None:
        """Remove Target group from the repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def close(self) -> None:
        """Put Target group to the repository"""
        raise NotImplementedError


class Repository(DataProviderInterface):

    def __init__(self, cache: Cache, store: Store = None) -> None:
        self.cache = cache
        self.store = store

    def get_flag(self, identifier: str,
                 cacheable: bool = None) -> Optional[FeatureConfig]:
        flag_key = format_flag_key(identifier)
        try:
            flag = self.cache.get(flag_key)
            log.debug("return flag from cache %s", identifier)
            return flag
        except KeyError:
            if self.store:
                flag = self.store.get(flag_key)
                log.debug("return flag from store %s", identifier)
                if flag and cacheable:
                    log.debug("set flag to the cache %s", identifier)
                    self.cache.set(flag_key, flag)
                return flag
        log.warning("flag not found %s", identifier)
        return None

    def get_segment(self, identifier: str,
                    cacheable: bool = None) -> Optional[Segment]:
        segment_key = format_segment_key(identifier)
        try:
            segment = self.cache.get(segment_key)
            log.debug("return segment from cache %s", identifier)
            return segment
        except KeyError:
            if self.store:
                segment = self.store.get(segment_key)
                log.debug("return segment from store %s", identifier)
                if segment and cacheable:
                    log.debug("set segment to the cache %s", identifier)
                    self.cache.set(segment_key, segment)
                return segment
        log.warning("segment not found %s", identifier)
        return None

    def set_flag(self, flag: FeatureConfig) -> None:
        if self.is_flag_outdated(flag.feature, flag):
            log.debug("Flag %s already exists", flag.feature)
            return None

        flag_key = format_flag_key(flag.feature)

        if self.store:
            self.store.set(flag_key, flag)
            self.cache.remove([flag_key])
            log.debug(
                "Flag %s successfully stored and cache invalidated",
                flag.feature)
        else:
            self.cache.set(flag_key, flag)
            log.debug("Flag %s successfully cached", flag.feature)

    def set_segment(self, segment: Segment) -> None:
        if self.is_segment_outdated(segment.identifier, segment):
            log.debug("Segment %s already exists", segment.identifier)
            return None

        segment_key = format_segment_key(segment.identifier)

        if self.store:
            self.store.set(segment_key, segment)
            self.cache.remove([segment_key])
            log.debug(
                "Segment %s successfully stored and cache invalidated",
                segment.identifier)
        else:
            self.cache.set(segment_key, segment)
            log.debug("Segment %s successfully cached", segment.identifier)

    def find_flags_by_segment(self, segment: str) -> List[str]:
        result = []
        keys = self.cache.keys()
        if self.store:
            keys = self.store.keys()
        for key in keys:
            flag = self.get_flag(key, cacheable=False)
            if not flag:
                continue
            for serving_rule in flag.rules:
                for clause in serving_rule.clauses:
                    if clause.op == SEGMENT_MATCH_OPERATOR and not next(
                        (val for val in clause.values if val == segment),
                        None
                    ):
                        log.debug("Flag %s evaluated in segments",
                                  flag.feature)
                        result.append(flag.feature)
        return result

    def remove_flag(self, identifier: str) -> None:
        """Remove Flag from the repository"""
        flag_key = format_flag_key(identifier)
        if self.store:
            self.store.remove([flag_key])
            log.debug("Flag %s successfully deleted from store", identifier)

        self.cache.remove([flag_key])
        log.debug("Flag %s successfully deleted from cache", identifier)

    def remove_segment(self, identifier: str) -> None:
        """Remove Target group from the repository"""
        segment_key = format_segment_key(identifier)
        if self.store:
            self.store.remove([segment_key])
            log.debug("Segment %s successfully deleted from store", identifier)

        self.cache.remove([segment_key])
        log.debug("Segment %s successfully deleted from cache", identifier)

    def close(self) -> None:
        if self.store:
            self.store.close()

    def is_flag_outdated(self, identifier: str,
                         new_config: FeatureConfig) -> bool:
        flag = self.get_flag(identifier, cacheable=False)
        if flag and not isinstance(flag.version, Unset) and \
                not isinstance(new_config, Unset) and \
                not isinstance(new_config.version, Unset):
            return flag.version >= new_config.version
        return False

    def is_segment_outdated(self, identifier: str,
                            new_segment: Segment) -> bool:
        segment = self.get_segment(identifier, cacheable=False)
        if segment and not isinstance(segment.version, Unset) and \
                not isinstance(new_segment, Unset) and \
                not isinstance(new_segment.version, Unset):
            return segment.version >= segment.version
        return False


def format_flag_key(identifier: str) -> str:
    return f'flags/{identifier}'


def format_segment_key(identifier: str) -> str:
    return f'segments/{identifier}'

import abc
from typing import List, Optional
from featureflags.evaluations.constants import SEGMENT_MATCH_OPERATOR

from featureflags.evaluations.feature import FeatureConfig
from featureflags.evaluations.segment import Segment
from featureflags.interface import Cache, Store
from featureflags.models.unset import Unset
from featureflags.util import log


class QueryInterface(metaclass=abc.ABCMeta):

    @classmethod
    def __subclasshook__(cls, subclass):
        return (hasattr(subclass, 'get_flag') and
                callable(subclass.get_flag) and
                hasattr(subclass, 'get_segment') and
                callable(subclass.get_segment) or
                hasattr(subclass, 'find_flags_by_segment') and
                callable(subclass.find_flags_by_segment) or
                NotImplemented)

    @abc.abstractmethod
    def get_flag(self, identifier: str) -> Optional[FeatureConfig]:
        """Get flag from repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def get_segment(self, identifier: str) -> Optional[Segment]:
        """Get Target group from repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def find_flags_by_segment(self, identifier: str) -> List[str]:
        """Find all flags with rule segment match"""
        raise NotImplementedError


class DataProviderInterface(QueryInterface):

    @classmethod
    def __subclasshook__(cls, subclass):
        return (hasattr(subclass, 'get_flag') and
                callable(subclass.get_flag) and
                hasattr(subclass, 'get_segment') and
                callable(subclass.get_segment) or
                hasattr(subclass, 'close') and
                callable(subclass.close) or
                NotImplemented)

    @abc.abstractmethod
    def set_flag(self, flag: FeatureConfig) -> None:
        """Put flag to the repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def set_segment(self, group: Segment) -> None:
        """Put Target group to the repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def remove_flag(self, identifier: str) -> None:
        """Remove Flag from the repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def remove_segment(self, identifier: str) -> None:
        """Remove Target group from the repository"""
        raise NotImplementedError

    @abc.abstractmethod
    def close(self) -> None:
        """Put Target group to the repository"""
        raise NotImplementedError


class Repository(DataProviderInterface):

    def __init__(self, cache: Cache, store: Store = None) -> None:
        self.cache = cache
        self.store = store

    def get_flag(self, identifier: str,
                 cacheable: bool = None) -> Optional[FeatureConfig]:
        flag_key = format_flag_key(identifier)
        try:
            flag = self.cache.get(flag_key)
            log.debug("return flag from cache %s", identifier)
            return flag
        except KeyError:
            if self.store:
                flag = self.store.get(flag_key)
                log.debug("return flag from store %s", identifier)
                if flag and cacheable:
                    log.debug("set flag to the cache %s", identifier)
                    self.cache.set(flag_key, flag)
                return flag
        log.warning("flag not found %s", identifier)
        return None

    def get_segment(self, identifier: str,
                    cacheable: bool = None) -> Optional[Segment]:
        segment_key = format_segment_key(identifier)
        try:
            segment = self.cache.get(segment_key)
            log.debug("return segment from cache %s", identifier)
            return segment
        except KeyError:
            if self.store:
                segment = self.store.get(segment_key)
                log.debug("return segment from store %s", identifier)
                if segment and cacheable:
                    log.debug("set segment to the cache %s", identifier)
                    self.cache.set(segment_key, segment)
                return segment
        log.warning("segment not found %s", identifier)
        return None

    def set_flag(self, flag: FeatureConfig) -> None:
        if self.is_flag_outdated(flag.feature, flag):
            log.debug("Flag %s already exists", flag.feature)
            return None

        flag_key = format_flag_key(flag.feature)

        if self.store:
            self.store.set(flag_key, flag)
            self.cache.remove([flag_key])
            log.debug(
                "Flag %s successfully stored and cache invalidated",
                flag.feature)
        else:
            self.cache.set(flag_key, flag)
            log.debug("Flag %s successfully cached", flag.feature)

    def set_segment(self, segment: Segment) -> None:
        if self.is_segment_outdated(segment.identifier, segment):
            log.debug("Segment %s already exists", segment.identifier)
            return None

        segment_key = format_segment_key(segment.identifier)

        if self.store:
            self.store.set(segment_key, segment)
            self.cache.remove([segment_key])
            log.debug(
                "Segment %s successfully stored and cache invalidated",
                segment.identifier)
        else:
            self.cache.set(segment_key, segment)
            log.debug("Segment %s successfully cached", segment.identifier)

    def find_flags_by_segment(self, segment: str) -> List[str]:
        result = []
        keys = self.cache.keys()
        if self.store:
            keys = self.store.keys()
        for key in keys:
            flag = self.get_flag(key, cacheable=False)
            if not flag:
                continue
            for serving_rule in flag.rules:
                for clause in serving_rule.clauses:
                    if clause.op == SEGMENT_MATCH_OPERATOR and not next(
                        (val for val in clause.values if val == segment),
                        None
                    ):
                        log.debug("Flag %s evaluated in segments",
                                  flag.feature)
                        result.append(flag.feature)
        return result

    def remove_flag(self, identifier: str) -> None:
        """Remove Flag from the repository"""
        flag_key = format_flag_key(identifier)
        if self.store:
            self.store.remove([flag_key])
            log.debug("Flag %s successfully deleted from store", identifier)

        self.cache.remove([flag_key])
        log.debug("Flag %s successfully deleted from cache", identifier)

    def remove_segment(self, identifier: str) -> None:
        """Remove Target group from the repository"""
        segment_key = format_segment_key(identifier)
        if self.store:
            self.store.remove([segment_key])
            log.debug("Segment %s successfully deleted from store", identifier)

        self.cache.remove([segment_key])
        log.debug("Segment %s successfully deleted from cache", identifier)

    def close(self) -> None:
        if self.store:
            self.store.close()

    def is_flag_outdated(self, identifier: str,
                         new_config: FeatureConfig) -> bool:
        flag = self.get_flag(identifier, cacheable=False)
        if flag and not isinstance(flag.version, Unset) and \
                not isinstance(new_config, Unset) and \
                not isinstance(new_config.version, Unset):
            return flag.version >= new_config.version
        return False

    def is_segment_outdated(self, identifier: str,
                            new_segment: Segment) -> bool:
        segment = self.get_segment(identifier, cacheable=False)
        if segment and not isinstance(segment.version, Unset) and \
                not isinstance(new_segment, Unset) and \
                not isinstance(new_segment.version, Unset):
            return segment.version >= segment.version
        return False


def format_flag_key(identifier: str) -> str:
    return f'flags/{identifier}'


def format_segment_key(identifier: str) -> str:
    return f'segments/{identifier}'
Get flag from repository Get Target group from repository Find all flags with rule segment match Put flag to the repository Put Target group to the repository Remove Flag from the repository Remove Target group from the repository Put Target group to the repository Remove Flag from the repository Remove Target group from the repository