import abc
import json
from pathlib import Path
from typing import Any, Optional
import requests
from fcache.cache import FileCache as _FileCache
from UnleashClient.constants import FEATURES_URL, REQUEST_TIMEOUT
class BaseCache(abc.ABC):
"""
Abstract base class for caches used for UnleashClient.
If implementing your own bootstrapping methods:
- Add your custom bootstrap method.
- You must set the `bootstrapped` attribute to True after configuration is set.
"""
bootstrapped = False
@abc.abstractmethod
def set(self, key: str, value: Any):
pass
@abc.abstractmethod
def mset(self, data: dict):
pass
@abc.abstractmethod
def get(self, key: str, default: Optional[Any] = None):
pass
@abc.abstractmethod
def exists(self, key: str):
pass
@abc.abstractmethod
def destroy(self):
pass
class FileCache(BaseCache):
"""
The default cache for UnleashClient. Uses `fcache `_ behind the scenes.
You can boostrap the FileCache with initial configuration to improve resiliency on startup. To do so:
- Create a new FileCache instance.
- Bootstrap the FileCache.
- Pass your FileCache instance to UnleashClient at initialization along with `boostrap=true`.
You can bootstrap from a dictionary, a json file, or from a URL. In all cases, configuration should match the Unleash `/api/client/features `_ endpoint.
Example:
.. code-block:: python
from pathlib import Path
from UnleashClient.cache import FileCache
from UnleashClient import UnleashClient
my_cache = FileCache("HAMSTER_API")
my_cache.bootstrap_from_file(Path("/path/to/boostrap.json"))
unleash_client = UnleashClient(
"https://my.unleash.server.com",
"HAMSTER_API",
cache=my_cache
)
:param name: Name of cache.
:param directory: Location to create cache. If empty, will use filecache default.
"""
def __init__(
self,
name: str,
directory: Optional[str] = None,
request_timeout: int = REQUEST_TIMEOUT,
):
self._cache = _FileCache(name, app_cache_dir=directory)
self.request_timeout = request_timeout
def bootstrap_from_dict(self, initial_config: dict) -> None:
"""
Loads initial Unleash configuration from a dictionary.
Note: Pre-seeded configuration will only be used if UnleashClient is initialized with `bootstrap=true`.
:param initial_config: Dictionary that contains initial configuration.
"""
self.set(FEATURES_URL, json.dumps(initial_config))
self.bootstrapped = True
def bootstrap_from_file(self, initial_config_file: Path) -> None:
"""
Loads initial Unleash configuration from a file.
Note: Pre-seeded configuration will only be used if UnleashClient is initialized with `bootstrap=true`.
:param initial_configuration_file: Path to document containing initial configuration. Must be JSON.
"""
with open(initial_config_file, "r", encoding="utf8") as bootstrap_file:
self.set(FEATURES_URL, bootstrap_file.read())
self.bootstrapped = True
def bootstrap_from_url(
self,
initial_config_url: str,
headers: Optional[dict] = None,
request_timeout: Optional[int] = None,
) -> None:
"""
Loads initial Unleash configuration from a url.
Note: Pre-seeded configuration will only be used if UnleashClient is initialized with `bootstrap=true`.
:param initial_configuration_url: Url that returns document containing initial configuration. Must return JSON.
:param headers: Headers to use when GETing the initial configuration URL.
"""
timeout = request_timeout if request_timeout else self.request_timeout
response = requests.get(initial_config_url, headers=headers, timeout=timeout)
self.set(FEATURES_URL, response.text)
self.bootstrapped = True
def set(self, key: str, value: Any):
self._cache[key] = value
self._cache.sync()
def mset(self, data: dict):
self._cache.update(data)
self._cache.sync()
def get(self, key: str, default: Optional[Any] = None):
return self._cache.get(key, default)
def exists(self, key: str):
return key in self._cache
def destroy(self):
return self._cache.delete()