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
17 changes: 16 additions & 1 deletion switcher_client/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Optional, Callable

from .lib.globals.global_auth import GlobalAuth
from .lib.globals.global_snapshot import GlobalSnapshot, LoadSnapshotOptions
from .lib.globals.global_context import Context, ContextOptions, DEFAULT_ENVIRONMENT
from .lib.remote_auth import RemoteAuth
from .lib.remote import Remote
from .lib.snapshot_auto_updater import SnapshotAutoUpdater
from .lib.snapshot_loader import load_domain, validate_snapshot, save_snapshot
from .lib.utils.execution_logger import ExecutionLogger
Expand Down Expand Up @@ -165,6 +167,11 @@ def snapshot_version() -> int:
return 0

return snapshot.domain.version

@staticmethod
def check_switchers(switcher_keys: list[str]) -> None:
""" Verifies if switchers are properly configured """
Client._check_switchers_remote(switcher_keys)

@staticmethod
def get_execution(switcher: Switcher) -> ExecutionLogger:
Expand Down Expand Up @@ -194,4 +201,12 @@ def subscribe_notify_error(callback: Callable[[Exception], None]) -> None:

@staticmethod
def _is_check_snapshot_available(fetch_remote = False) -> bool:
return Client.snapshot_version() == 0 and (fetch_remote or not Client._context.options.local)
return Client.snapshot_version() == 0 and (fetch_remote or not Client._context.options.local)

@staticmethod
def _check_switchers_remote(switcher_keys: list[str]) -> None:
RemoteAuth.auth()
Remote.check_switchers(
token=GlobalAuth.get_token(),
switcher_keys=switcher_keys,
context=Client._context)
5 changes: 5 additions & 0 deletions switcher_client/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class RemoteCriteriaError(RemoteError):
def __init__(self, message):
super().__init__(message)

class RemoteSwitcherError(RemoteError):
def __init__(self, not_found: list):
super().__init__(f'{", ".join(not_found)} not found')

class LocalCriteriaError(Exception):
def __init__(self, message):
self.message = message
Expand All @@ -20,5 +24,6 @@ def __init__(self, message):
'RemoteError',
'RemoteAuthError',
'RemoteCriteriaError',
'RemoteSwitcherError',
'LocalCriteriaError',
]
14 changes: 13 additions & 1 deletion switcher_client/lib/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import Optional

from ..errors import RemoteAuthError, RemoteError, RemoteCriteriaError
from ..errors import RemoteAuthError, RemoteError, RemoteCriteriaError, RemoteSwitcherError
from ..lib.globals.global_context import DEFAULT_ENVIRONMENT, Context
from ..lib.types import ResultDetail
from ..lib.utils import get, get_entry
Expand Down Expand Up @@ -53,6 +53,18 @@ def check_criteria(token: Optional[str], context: Context, switcher: SwitcherDat

raise RemoteCriteriaError(f'[check_criteria] failed with status: {response.status_code}')

@staticmethod
def check_switchers(token: Optional[str], switcher_keys: list[str], context: Context) -> None:
url = f'{context.url}/criteria/switchers_check'
response = Remote._do_post(context, url, { 'switchers': switcher_keys }, Remote._get_header(token))

if response.status_code != 200:
raise RemoteError(f'[check_switchers] failed with status: {response.status_code}')

not_found = response.json().get('not_found', [])
if len(not_found) > 0:
raise RemoteSwitcherError(not_found)

@staticmethod
def check_snapshot_version(token: Optional[str], context: Context, snapshot_version: int) -> bool:
url = f'{context.url}/criteria/snapshot_check/{snapshot_version}'
Expand Down
12 changes: 12 additions & 0 deletions tests/playground/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ def auto_update_snapshot():
)
)

def check_switchers():
""" Use case: Check switchers """
global LOOP
LOOP = False

setup_context()

try:
Client.check_switchers([SWITCHER_KEY, 'NON_EXISTENT_SWITCHER'])
except Exception as e:
print(f"❌ Configuration error: {e}")

try:
# Replace with use case
simple_api_call()
Expand Down
77 changes: 77 additions & 0 deletions tests/test_client_check_switchers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import time
from typing import Optional

from pytest_httpx import HTTPXMock

from switcher_client.client import Client
from switcher_client.errors import RemoteSwitcherError
from switcher_client.lib.globals.global_context import ContextOptions


def test_check_remote_switchers(httpx_mock):
""" Should check remote switchers with success """

# given
given_auth(httpx_mock)
given_check_switchers(httpx_mock)
given_context()

# test
Client.check_switchers(['MY_SWITCHER', 'ANOTHER_SWITCHER'])

def test_check_remote_switchers_not_found(httpx_mock):
""" Should check remote switchers and raise RemoteSwitcherError with not found switchers """

# given
given_auth(httpx_mock)
given_check_switchers(httpx_mock, not_found=['MY_SWITCHER'])
given_context()

# test
try:
Client.check_switchers(['MY_SWITCHER', 'ANOTHER_SWITCHER'])
assert False, 'Expected RemoteSwitcherError to be raised'
except RemoteSwitcherError as e:
assert str(e) == 'MY_SWITCHER not found'

def test_check_remote_switchers_api_error(httpx_mock):
""" Should check remote switchers and raise RemoteError when API returns an error """

# given
given_auth(httpx_mock)
given_check_switchers(httpx_mock, status=500)
given_context()

# test
try:
Client.check_switchers(['MY_SWITCHER', 'ANOTHER_SWITCHER'])
assert False, 'Expected RemoteError to be raised'
except Exception as e:
assert str(e) == '[check_switchers] failed with status: 500'

# Helpers

def given_context(url='https://api.switcherapi.com', api_key='[API_KEY]', options = ContextOptions()):
Client.build_context(
url=url,
api_key=api_key,
domain='Playground',
component='switcher-playground',
options=options
)

def given_auth(httpx_mock: HTTPXMock, status=200, token: Optional[str]='[token]', exp=int(round(time.time() * 1000))):
httpx_mock.add_response(
url='https://api.switcherapi.com/criteria/auth',
method='POST',
status_code=status,
json={'token': token, 'exp': exp}
)

def given_check_switchers(httpx_mock: HTTPXMock, status=200, not_found: Optional[list[str]]=None):
httpx_mock.add_response(
url='https://api.switcherapi.com/criteria/switchers_check',
method='POST',
status_code=status,
json={'not_found': not_found or []}
)