diff --git a/README.md b/README.md index 4088d80..84d32b1 100644 --- a/README.md +++ b/README.md @@ -297,10 +297,10 @@ Client.schedule_snapshot_auto_update( ```python # Watch for snapshot file changes -Client.watch_snapshot({ - 'success': lambda: print("✅ Snapshot loaded successfully"), - 'reject': lambda e: print(f"❌ Error loading snapshot: {e}") -}) +Client.watch_snapshot(WatchSnapshotCallback( + success=lambda: print("✅ Snapshot loaded successfully"), + reject=lambda e: print(f"❌ Error loading snapshot: {e}") +)) ``` ## Testing & Development diff --git a/switcher_client/__init__.py b/switcher_client/__init__.py index 7e33312..8cf2a6d 100644 --- a/switcher_client/__init__.py +++ b/switcher_client/__init__.py @@ -1,9 +1,11 @@ from .client import Client from .switcher import Switcher from .lib.globals.global_context import ContextOptions +from .lib.snapshot_watcher import WatchSnapshotCallback __all__ = [ 'Client', 'Switcher', 'ContextOptions', + 'WatchSnapshotCallback', ] \ No newline at end of file diff --git a/switcher_client/client.py b/switcher_client/client.py index 7579580..73eb9c9 100644 --- a/switcher_client/client.py +++ b/switcher_client/client.py @@ -7,7 +7,7 @@ from .lib.remote import Remote from .lib.snapshot_auto_updater import SnapshotAutoUpdater from .lib.snapshot_loader import check_switchers, load_domain, validate_snapshot, save_snapshot -from .lib.snapshot_watcher import SnapshotWatcher +from .lib.snapshot_watcher import SnapshotWatcher, WatchSnapshotCallback from .lib.utils.execution_logger import ExecutionLogger from .lib.utils.timed_match.timed_match import TimedMatch from .lib.utils import get @@ -162,14 +162,13 @@ def terminate_snapshot_auto_update(): Client._snapshot_auto_updater.terminate() @staticmethod - def watch_snapshot(callback: Optional[dict] = None) -> None: + def watch_snapshot(callback: Optional[WatchSnapshotCallback] = None) -> None: """ Watch snapshot file for changes and invoke callbacks on result """ - callback = get(callback, {}) + callback = get(callback, WatchSnapshotCallback()) snapshot_location = Client._context.options.snapshot_location if snapshot_location is None: - reject = callback.get('reject', lambda _: None) - return reject(Exception("Snapshot location is not defined in the context options")) + return callback.reject(Exception("Snapshot location is not defined in the context options")) environment = get(Client._context.environment, DEFAULT_ENVIRONMENT) Client._snapshot_watcher.watch_snapshot(snapshot_location, environment, callback) diff --git a/switcher_client/lib/snapshot_watcher.py b/switcher_client/lib/snapshot_watcher.py index 51a9224..5ab8bf2 100644 --- a/switcher_client/lib/snapshot_watcher.py +++ b/switcher_client/lib/snapshot_watcher.py @@ -1,11 +1,20 @@ import os import threading +from dataclasses import dataclass, field +from typing import Callable + from .snapshot_loader import load_domain from .globals.global_snapshot import GlobalSnapshot _POLL_INTERVAL = 1 # seconds between file stat checks +@dataclass +class WatchSnapshotCallback: + """ Typed callback contract for Client.watch_snapshot """ + success: Callable[[], None] = field(default_factory=lambda: (lambda: None)) + reject: Callable[[Exception], None] = field(default_factory=lambda: (lambda _: None)) + class SnapshotWatcher: """ Watches the snapshot file for changes and updates the switcher accordingly """ @@ -14,7 +23,7 @@ def __init__(self): self._ready_event: threading.Event = threading.Event() self._thread: threading.Thread | None = None - def watch_snapshot(self, snapshot_location: str, environment: str, callback: dict) -> None: + def watch_snapshot(self, snapshot_location: str, environment: str, callback: WatchSnapshotCallback) -> None: """ Watch snapshot file for changes and invoke callbacks on result """ self._stop_event.clear() self._ready_event.clear() @@ -34,7 +43,7 @@ def unwatch_snapshot(self) -> None: self._thread.join(timeout=5.0) self._thread = None - def _watch(self, snapshot_location: str, environment: str, callback: dict) -> None: + def _watch(self, snapshot_location: str, environment: str, callback: WatchSnapshotCallback) -> None: snapshot_file = f"{snapshot_location}/{environment}.json" last_mtime = self._get_mtime(snapshot_file) self._ready_event.set() @@ -49,13 +58,10 @@ def _watch(self, snapshot_location: str, environment: str, callback: dict) -> No def _get_mtime(self, snapshot_file: str) -> float: return os.stat(snapshot_file).st_mtime - def _on_modify_snapshot(self, snapshot_location: str, environment: str, callback: dict) -> None: - success = callback.get('success', lambda: None) - reject = callback.get('reject', lambda _: None) - + def _on_modify_snapshot(self, snapshot_location: str, environment: str, callback: WatchSnapshotCallback) -> None: try: snapshot = load_domain(snapshot_location, environment) GlobalSnapshot.init(snapshot) - success() + callback.success() except Exception as error: - reject(error) \ No newline at end of file + callback.reject(error) \ No newline at end of file diff --git a/tests/playground/index.py b/tests/playground/index.py index b8e12ea..da0358b 100644 --- a/tests/playground/index.py +++ b/tests/playground/index.py @@ -4,7 +4,7 @@ from util import monitor_run from switcher_client.lib.globals.global_context import DEFAULT_ENVIRONMENT from switcher_client.lib.globals.global_snapshot import LoadSnapshotOptions -from switcher_client import Client, ContextOptions +from switcher_client import Client, ContextOptions, WatchSnapshotCallback SWITCHER_KEY = 'CLIENT_PYTHON_FEATURE' LOOP = True @@ -101,10 +101,10 @@ def uc_watch_snapshot(): )) Client.load_snapshot() - Client.watch_snapshot({ - 'success': lambda: print("✅ Snapshot loaded successfully"), - 'reject': lambda e: print(f"❌ Error loading snapshot: {e}") - }) + Client.watch_snapshot(WatchSnapshotCallback( + success=lambda: print("✅ Snapshot loaded successfully"), + reject=lambda e: print(f"❌ Error loading snapshot: {e}") + )) switcher = Client.get_switcher('FF2FOR2030') monitor_thread = threading.Thread(target=monitor_run, args=(switcher,True), daemon=True) @@ -112,7 +112,7 @@ def uc_watch_snapshot(): try: # Replace with use case - uc_simple_api_call() + uc_watch_snapshot() while LOOP: time.sleep(1) except KeyboardInterrupt: diff --git a/tests/test_client_watch_snapshot.py b/tests/test_client_watch_snapshot.py index cbd8960..360ae32 100644 --- a/tests/test_client_watch_snapshot.py +++ b/tests/test_client_watch_snapshot.py @@ -6,7 +6,7 @@ from switcher_client.client import Client, ContextOptions from switcher_client.lib.globals.global_context import DEFAULT_ENVIRONMENT -from switcher_client.lib.snapshot_watcher import SnapshotWatcher +from switcher_client.lib.snapshot_watcher import SnapshotWatcher, WatchSnapshotCallback class TestClientWatchSnapshot: """ Test suite for Client.watch_snapshot """ @@ -38,10 +38,10 @@ def test_watch_snapshot(self): # test switcher = Client.get_switcher('FF2FOR2030') - Client.watch_snapshot({ - 'success': lambda: setattr(self, 'async_success', True), - 'reject': lambda err: setattr(self, 'async_error', err) - }) + Client.watch_snapshot(WatchSnapshotCallback( + success=lambda: setattr(self, 'async_success', True), + reject=lambda err: setattr(self, 'async_error', err) + )) assert switcher.is_on() modify_fixture_snapshot(fixture_location, fixture_env, fixture_env_file_modified) @@ -61,10 +61,10 @@ def test_watch_snapshot_err_no_snapshot_location(self): given_context() # test - Client.watch_snapshot({ - 'success': lambda: setattr(self, 'async_success', True), - 'reject': lambda err: setattr(self, 'async_error', err) - }) + Client.watch_snapshot(WatchSnapshotCallback( + success=lambda: setattr(self, 'async_success', True), + reject=lambda err: setattr(self, 'async_error', err) + )) # then assert self.async_success is None @@ -83,10 +83,10 @@ def test_watch_snapshot_err_malformed_snapshot(self): Client.load_snapshot() # test - Client.watch_snapshot({ - 'success': lambda: setattr(self, 'async_success', True), - 'reject': lambda err: setattr(self, 'async_error', err) - }) + Client.watch_snapshot(WatchSnapshotCallback( + success=lambda: setattr(self, 'async_success', True), + reject=lambda err: setattr(self, 'async_error', err) + )) modify_fixture_snapshot(fixture_location, fixture_env, fixture_env_file_modified)