Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions switcher_client/__init__.py
Original file line number Diff line number Diff line change
@@ -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',
]
9 changes: 4 additions & 5 deletions switcher_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 14 additions & 8 deletions switcher_client/lib/snapshot_watcher.py
Original file line number Diff line number Diff line change
@@ -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 """

Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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)
callback.reject(error)
12 changes: 6 additions & 6 deletions tests/playground/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -101,18 +101,18 @@ 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)
monitor_thread.start()

try:
# Replace with use case
uc_simple_api_call()
uc_watch_snapshot()
while LOOP:
time.sleep(1)
except KeyboardInterrupt:
Expand Down
26 changes: 13 additions & 13 deletions tests/test_client_watch_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)

Expand Down