from __future__ import annotations import json from anydi import singleton from cachelib import BaseCache as Cache from rapidfuzz import fuzz, process from email_campaigns.config import Settings from email_campaigns.fonts.services.google_fonts import GoogleFont @singleton class GoogleFontsCache: CACHE_TTL = 7 * 24 * 3600 # 1 week def __init__( self, cache: Cache, settings: Settings, ) -> None: self.cache = cache self.prefix = settings.cache_key_prefix def is_populated(self) -> bool: return self.cache.get(f"{self.prefix}google_fonts:populated") is not None def populate(self, fonts: list[GoogleFont]) -> None: data = json.dumps( [ { "name": f.name, "fallback_font": f.fallback_font, "generic_font_family": f.generic_font_family, "css_font_family": f.css_font_family, "url": f.url, } for f in fonts ] ) self.cache.set(f"{self.prefix}google_fonts:all", data, timeout=self.CACHE_TTL) self.cache.set( f"{self.prefix}google_fonts:populated", "1", timeout=self.CACHE_TTL ) FUZZY_SCORE_CUTOFF = 80 def search(self, term: str | None, limit: int) -> list[GoogleFont]: raw = self.cache.get(f"{self.prefix}google_fonts:all") if raw is None: return [] fonts: list[GoogleFont] = [GoogleFont(**item) for item in json.loads(raw)] if not term: return fonts[:limit] substring_matches = self._substring_search(fonts, term) substring_names = {f.name for f in substring_matches} fuzzy_results = self._fuzzy_search( [f for f in fonts if f.name not in substring_names], term, limit ) return (substring_matches + fuzzy_results)[:limit] def _substring_search(self, fonts: list[GoogleFont], term: str) -> list[GoogleFont]: term_lower = term.lower() return [f for f in fonts if term_lower in f.name.lower()] def get_font(self, name: str) -> GoogleFont | None: raw = self.cache.get(f"{self.prefix}google_fonts:all") if raw is None: return None return next( (GoogleFont(**f) for f in json.loads(raw) if f["name"] == name), None ) def _fuzzy_search( self, fonts: list[GoogleFont], term: str, limit: int ) -> list[GoogleFont]: matches = process.extract( term, [f.name for f in fonts], scorer=fuzz.partial_ratio, limit=limit, score_cutoff=self.FUZZY_SCORE_CUTOFF, ) fonts_by_name = {f.name: f for f in fonts} return [fonts_by_name[name] for name, _, _ in matches]