From 574f8ed034ad4f9b77aca91e074924ea0a0df076 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 24 Jun 2025 03:47:15 +0545 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(utils/conversion)?= =?UTF-8?q?:=20introduce=20CurrencyCache=20for=20thread-safe=20background?= =?UTF-8?q?=20currency=20updates=20and=20caching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/json/launcher.json | 4 + modules/launcher/plugins/calculator.py | 16 ++- services/__init__.py | 2 + utils/conversion.py | 133 +++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 1 deletion(-) diff --git a/config/json/launcher.json b/config/json/launcher.json index 4d63a17..b5e5982 100644 --- a/config/json/launcher.json +++ b/config/json/launcher.json @@ -83,6 +83,10 @@ "ps": { "icon": "process", "description": "Process Manager - View and manage running processes" + }, + "weather": { + "icon": "cloud", + "description": "Weather - Get current weather information" } }, "settings": { diff --git a/modules/launcher/plugins/calculator.py b/modules/launcher/plugins/calculator.py index d0dcee5..1468154 100644 --- a/modules/launcher/plugins/calculator.py +++ b/modules/launcher/plugins/calculator.py @@ -87,16 +87,20 @@ class CalculatorPlugin(PluginBase): cache_key = f"{value}_{from_unit}_{to_unit}" if cache_key in self._conversion_cache: result = self._conversion_cache[cache_key] + subtitle = f"{value} {from_unit} = {result:.6g} {to_unit}" else: # Use the conversion utility result = self.converter.convert(value, from_unit, to_unit) # Cache the result self._conversion_cache[cache_key] = result + subtitle = f"{value} {from_unit} = {result:.6g} {to_unit}" + + return [ Result( title=f"{result:.6g} {to_unit}", - subtitle=f"{value} {from_unit} = {result:.6g} {to_unit}", + subtitle=subtitle, icon_markup=icons.calculator, action=lambda r=f"{result:.6g}": self._copy_to_clipboard(r), relevance=1.0, @@ -136,6 +140,15 @@ class CalculatorPlugin(PluginBase): return [] + def _format_cache_age(self, age_seconds: float) -> str: + """Format cache age for display.""" + if age_seconds < 60: + return f"{int(age_seconds)}s ago" + elif age_seconds < 3600: + return f"{int(age_seconds // 60)}m ago" + else: + return f"{int(age_seconds // 3600)}h ago" + def _copy_to_clipboard(self, text: str): """Copy text to clipboard using cliphist.""" try: @@ -150,3 +163,4 @@ class CalculatorPlugin(PluginBase): def cleanup(self): """Clean up resources.""" self._conversion_cache.clear() + self.converter.cleanup() diff --git a/services/__init__.py b/services/__init__.py index 1ab977c..97246c2 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -4,5 +4,7 @@ Contains background services and utilities for the shell. """ from .notification import CustomNotifications +from .weather import WeatherService notification_service = CustomNotifications() +weather_service = WeatherService() diff --git a/utils/conversion.py b/utils/conversion.py index 1cde5f2..db045bb 100644 --- a/utils/conversion.py +++ b/utils/conversion.py @@ -1,4 +1,105 @@ import requests +import time +import threading +from concurrent.futures import ThreadPoolExecutor +from typing import Dict, Optional, Tuple + + +class CurrencyCache: + """Thread-safe currency exchange rate cache with background updates.""" + + def __init__(self): + self._cache: Dict[str, Dict] = {} # {base_currency: {rates_data, timestamp}} + self._cache_lock = threading.Lock() + self._cache_ttl = 3600 # 1 hour in seconds + self._request_timeout = 2 # 2 seconds for faster response + self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="currency") + self._pending_requests: Dict[str, threading.Event] = {} + + def get_rate(self, from_code: str, to_code: str) -> Optional[Tuple[float, float]]: + """ + Get exchange rate from cache or fetch if needed. + Returns (rate, cache_age_seconds) or None if unavailable. + """ + from_lower = from_code.lower() + to_lower = to_code.lower() + + if from_lower == to_lower: + return 1.0, 0.0 + + current_time = time.time() + + with self._cache_lock: + # Check if we have fresh cached data + if from_lower in self._cache: + cache_entry = self._cache[from_lower] + cache_age = current_time - cache_entry['timestamp'] + + if cache_age < self._cache_ttl and to_lower in cache_entry['rates']: + rate = cache_entry['rates'][to_lower]['rate'] + return rate, cache_age + + # Check if we're already fetching this currency + if from_lower in self._pending_requests: + # Don't wait, return cached data if available (even if stale) + if from_lower in self._cache and to_lower in self._cache[from_lower]['rates']: + cache_entry = self._cache[from_lower] + cache_age = current_time - cache_entry['timestamp'] + rate = cache_entry['rates'][to_lower]['rate'] + return rate, cache_age + return None + + # Start background fetch + self._pending_requests[from_lower] = threading.Event() + + # Submit background task to fetch rates + self._executor.submit(self._fetch_rates_background, from_lower) + + # Return stale cached data if available + with self._cache_lock: + if from_lower in self._cache and to_lower in self._cache[from_lower]['rates']: + cache_entry = self._cache[from_lower] + cache_age = current_time - cache_entry['timestamp'] + rate = cache_entry['rates'][to_lower]['rate'] + return rate, cache_age + + return None + + def _fetch_rates_background(self, from_code: str): + """Fetch exchange rates in background thread.""" + try: + url = f"https://www.floatrates.com/daily/{from_code}.json" + response = requests.get(url, timeout=self._request_timeout) + + if response.status_code == 200: + rates_data = response.json() + current_time = time.time() + + with self._cache_lock: + self._cache[from_code] = { + 'rates': rates_data, + 'timestamp': current_time + } + + except Exception as e: + print(f"Background currency fetch failed for {from_code}: {e}") + finally: + # Mark request as complete + with self._cache_lock: + if from_code in self._pending_requests: + self._pending_requests[from_code].set() + del self._pending_requests[from_code] + + def cleanup(self): + """Cleanup resources.""" + self._executor.shutdown(wait=False) + with self._cache_lock: + self._cache.clear() + self._pending_requests.clear() + + +# Global currency cache instance +_currency_cache = CurrencyCache() class Units(): @@ -306,6 +407,7 @@ class Units(): class Conversion(): def __init__(self): self.units = Units() + self.currency_cache = _currency_cache def convert(self, value: float, from_type: str, to_type: str): """ @@ -363,11 +465,26 @@ class Conversion(): # 2) If both are currency codes (e.g. “USD”, “ARS”) # we assume they are uppercase and have 3 letters. if len(from_type) == 3 and len(to_type) == 3 and from_type.isalpha() and to_type.isalpha(): + result = self._convert_currency_fast(value, from_type, to_type) + if result is not None: + return result + # Fallback to slow method if fast method fails return self._convert_currency_via_floatrates(value, from_type, to_type) # 3) If it doesn't fall into any case, error. raise ValueError(f"Unsupported conversion: {from_type} to {to_type}") + def _convert_currency_fast(self, value: float, from_code: str, to_code: str) -> Optional[float]: + """ + Fast currency conversion using cached exchange rates. + Returns None if rate is not available in cache. + """ + rate_info = self.currency_cache.get_rate(from_code, to_code) + if rate_info is not None: + rate, _ = rate_info # cache_age not needed here + return value * rate + return None + def _convert_currency_via_floatrates(self, value: float, from_code: str, to_code: str) -> float: """ Converts using the JSON from floatrates.com: @@ -440,6 +557,22 @@ class Conversion(): return singular.lower() return type + def cleanup(self): + """Cleanup resources.""" + self.currency_cache.cleanup() + + def get_currency_cache_info(self, from_code: str, to_code: str) -> Optional[Tuple[bool, float]]: + """ + Get currency cache information for UI display. + Returns (is_fresh, cache_age_seconds) or None if not cached. + """ + rate_info = self.currency_cache.get_rate(from_code, to_code) + if rate_info is not None: + _, cache_age = rate_info # rate not needed here + is_fresh = cache_age < 300 # Consider fresh if less than 5 minutes old + return is_fresh, cache_age + return None + # Quick usage example: if __name__ == "__main__":